question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Inconsistent behavior for splatting when using Invoke-Command

See original GitHub issue

Using splatting when calling Invoke-Command has a different behavior than calling it without splatting. See my examples below.

The issue only seems to occur when $values is assigned [System.Management.Automation.Internal.AutomationNull]::Value (which is the case when there are no elements in the pipeline). If I hard code $values to be $null the code works as expected. Maybe [System.Management.Automation.Internal.AutomationNull]::Value is not converted to a normal $null when using splatting. 🤔

In the first case $Values ends up being a [pscustomobject] that foreach is able to iterate over, something that is unexpected.

Steps to reproduce

Run the following, but change some-machine to a proper remote machine.

function GetValues {

}

$values = GetValues

$invokeCommandParams = @{
    ArgumentList = $values, "dummy"
}

Invoke-Command -ComputerName "some-machine" @invokeCommandParams {
    param($Values, $Dummy)
    Write-Host "Inside the first script block"
    Write-Host "Is null: $($null -eq $Values)"
    Write-Host "Is empty string: $('' -eq $Values)"
    Write-Host "Type: $($Values.GetType())"
    foreach ($value in $Values) {
        Write-Host "Inside the loop. Value is $value"
    }
}

Invoke-Command -ComputerName "some-machine" -ArgumentList $values, "dummy" {
    param($Values, $Dummy)
    Write-Host "Inside the second script block"
    Write-Host "Is null: $($null -eq $Values)"
    Write-Host "Is empty string: $('' -eq $Values)"
    foreach ($value in $Values) {
        Write-Host "Inside the loop. Value is $value"
    }
}

Expected behavior

I expected both calls to Invoke-Command to behave in the same way, which would have yielded the following output:

Inside the first script block
Is null: True
Is empty string: False
Type:

Inside the second script block
Is null: True
Is empty string: False

Actual behavior

Inside the first script block
Is null: False
Is empty string: True
Type: System.Management.Automation.PSCustomObject
Inside the loop. Value is

Inside the second script block
Is null: True
Is empty string: False

Environment data

Name                           Value
----                           -----
PSVersion                      7.2.0-preview.2
PSEdition                      Core
GitCommitId                    7.2.0-preview.2
OS                             Microsoft Windows 10.0.19042
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

It get the same behavior on PowerShell 5 aswell.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:2
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
mklement0commented, Dec 30, 2020

Yes, the common denominator is the cross-process (whether locally or across machine boundaries) serialization infrastructure, which also applies to the $using: “scope”; again, the problem can also be demonstrated with a background job:

$automationNull = [System.Management.Automation.Internal.AutomationNull]::Value
Start-Job { ($using:automationNull).GetType().Name } | Receive-Job -Wait -AutoRemoveJob # -> 'PSCustomObject`

Splatting only surfaces the symptom, because it seems to bypass code used in direct argument binding that (surprisingly) converts [System.Management.Automation.Internal.AutomationNull]::Value to $null. Similarly, no such conversion happens with $using:.

Without having done thorough analysis, here’s my guess as to why this happens:

  • In-process it is reference equality based on the singleton [System.Management.Automation.Internal.AutomationNull]::Value that is used to detect such values (and treats them as a collection with no properties and no elements).

  • The serialization infrastructure is apparently not aware of this special value, and serializes it as a regular [psobject] instance (with a static, property-less [System.Management.Automation.PSCustomObject] instance as its base object). The rehydrated regular [pscustomobject] instance is obviously not the same as [System.Management.Automation.Internal.AutomationNull]::Value in the target process, and therefore not recognized as such.

In short: Given that the in-process [System.Management.Automation.PSCustomObject] has no distinguishing characteristics other than being a singleton, fixing this would require extending the serialization infrastructure to be aware of and correctly handle this value.

However: [System.Management.Automation.Internal.AutomationNull]::Value does differ in its serialized form from an empty [psobject] instance, so perhaps this existing differing representation is enough to unequivocally identify [System.Management.Automation.Internal.AutomationNull]::Value on rehydration, which then wouldn’t require extending the remoting protocol:

[System.Management.Automation.PSSerializer]::Serialize(@([System.Management.Automation.Internal.AutomationNull]::Value, [psobject]::new()))

Note that parameter [System.Management.Automation.Internal.AutomationNull]::Value or even @([System.Management.Automation.Internal.AutomationNull]::Value) alone wouldn’t work, because conversion to $null / no-element enumeration to an empty array would kick in.

The above yields the following; note how the [psobject] instance is simply represented as an empty <Object> element, unlike automation null:

<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <Obj RefId="0">
    <TN RefId="0">
      <T>System.Object[]</T>
      <T>System.Array</T>
      <T>System.Object</T>
    </TN>
    <LST>
      <Obj RefId="1">
        <TN RefId="1">
          <T>System.Management.Automation.PSCustomObject</T>
          <T>System.Object</T>
        </TN>
      </Obj>
      <Obj RefId="2">
        <TNRef RefId="1" />
      </Obj>
    </LST>
  </Obj>
</Objs>
1reaction
ljbcommented, Dec 30, 2020

Another similar case is when the using scope is used together with Invoke-Command:

$values = [System.Management.Automation.Internal.AutomationNull]::Value
Invoke-Command -ComputerName "some-machine" {
    $localValues = $using:values
    Write-Host "Is empty string: $('' -eq $localValues)"
    Write-Host "Type: $($localValues.GetType())"
}

This will result in the following:

Is empty string: True
Type: System.Management.Automation.PSCustomObject

This has nothing to do with splatting. But it might be caused by the same underlying circumstances as the splatting examples.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Powershell Invoke-SQLcmd Query argument inconsistent?
When using Invoke-SQLcmd in powershell I get inconsistencies. I receive SQL connection arguments at the scripts invocation from the user. Inside ...
Read more >
Unraveling the Mystery: An In-Depth Guide to Splatting in PowerShell
Here, we use both a hashtable and an array to pass different types of parameters to the Invoke-Command cmdlet. ## 6. Best Practices...
Read more >
Powershell - inconsistent output from the same code
Even weirder, the display behavior for my existing output variable changes. It goes from showing me all the members of each element in...
Read more >
Invoke-command and splatting : r/PowerShell
Hi, Not sure exactly where my issue lies, but I am trying to pass a command to run remotely. The command contains a...
Read more >
What is your #1 pain in using PowerShell?
Invoke command works fine in a pssession. If get item or get child item had that parameter, it would be much easier to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found