Search code examples
powershellpowershell-remoting

Local statement output very different than invoke-command output


Logged in to the system directly, I run this statement, and get this output:

(Get-ClusterNetwork 'cluster backups').role
None

This is perfect... beautiful even, in it's simplicity.

However, when I run the exact same statement from a remote machine using invoke-command, which up until now i always just assumed was like typing this exact statement into the CLI of the machine, I get THIS output instead

Invoke-Command -Session $hi -ScriptBlock {(Get-ClusterNetwork 'cluster backups').role}
PSComputerName RunspaceId                           Value
-------------- ----------                           -----
dumdum a84b6c89-dumdum-80d3-ed43230ee8ab            None

Now here's the really funny thing. If i assign a variable to the invoke-command output, it'll have the same output shown above UNLESS - i pipe it to set-clipboard

So the variable

$hello = invoke-command -session $hi -scriptblock {(get-networkcluster 'cluster backups').role}

Now type $hello into prompt and I get:

PSComputerName RunspaceId                           Value
-------------- ----------                           -----
dumdum a84b6c89-dumdum-80d3-ed43230ee8ab            None

Which is expected. But now when I pipe that to set-clipboard and paste - the value is:

$hello | set-clipboard;
get-clipboard
None

Which is the actual value I want. Somehow piping to set-clipboard knows to only pull the property that i originally asked for. Even though the variable, has all the properties. When i run $hello.gettype() - i see the value as Int32. Which makes sense if $hello was only returning the value I wanted, but it's... not.

But if that wasn't weird enough - I'm running a few functions within the invoke-command, this is only one piece - all of the functions return a value i'm trying to report on. So:

$row = '' | select computername, ClusterNetworkRole, IP;
$row.computername = $name;
$row.clusternetworkrole = $hello;
$row.ip = dum.dum.dum.dum;
Return $row;

Do you know what the output of $row.clusternetworkrole is? Take a wild guess. It's every property EXCEPT the one I want.

$row
PSComputerName     : dumdum
RunspaceId         : b898bdad-dumdum-9eff-8a2beeefe78a
ClusterNetworkRole :
Computername       : dum
IP                 : dum.dum.dum.dum

Not only does it give me the exact properties i DON'T want - it actually adds those properties as members of $row.

$row.RunspaceID
b898bdad-dumdum-9eff-8a2beeefe78a

Now i can get the value i want by appending ".value" at the end of the statement, so this isn't so much a problem to be solved as much as it is a question of just what the hell powershell is doing. It's taken this simple, beautiful tiny statement - and wreaked havoc on my life.


Solution

  • In your specific case of an instance of an enum value (an instance of a System.Enum-derived type):

    • Use [int] $hello to get the numeric value of the original, enum (System.Enum-derived) value, without the extra NoteProperty members such as PSComputerName that the remoting infrastructure adds (see below).

    • Use $hello.Value to get the string representation of the enum value (its symbolic name rather than its number).

    • If you know the original System.Enum-derived type, and that type is also available in your local session, you can cast the deserialized object back to its original type; e.g.:
      [Microsoft.Foo.Bar.ClusterRole] $hello

    • $hello is technically an [int], but decorated with extra properties, and information about the original type recorded in the hidden .pstypenames array, which reflects the original type's inheritance hierarchy with the type names prefixed with Deserialized.; e.g. Deserialized.Microsoft.Foo.Bar.ClusterRole; PowerShell's output formatting system causes such an object to be formatted via (implicitly applied) Format-Table, which in this case shows everything but the actual [int] value - only the NoteProperty members are shown.

    Generally, you can exclude the unwanted properties as follows:

    • For [string] instances only, you can access .psobject.BaseObject to get the underlying object without any NoteProperty members.

    • For others, you can create a new object (invariably of type [pscustomobject]), by piping to Select-Object with the unwanted properties excluded, as suggested by Lee Dailey):

      • $hello | Select-Object * -Exclude PSComputerName, PSShowComputerName, RunspaceId

      • Alternatively, you can focus on selecting just the properties you do want.

    Read on for why that is necessary.


    PowerShell's remoting infrastructure decorates every object returned from a remote invocation with the following NoteProperty ETS (Extended Type System) members:

    • PSComputerName ... the name of the remote computer

    • RunspaceId ... the ID of the runspace in which the remote command executed.

    • PSShowComputerName ... a hidden property that, when set to $true on all objects returned via Invoke-Command's -HideComputerName switch, suppresses display of the PSComputerName property in the default output (but the property is still there); you can only see the PSShowComputerName itself if you pipe a remotely received object to Get-Member -Force.

    • Additionally, System.Enum-derived types, which are returned as [int] instances, are decorated with a [string]-typed Value NoteProperty that contains the enum value's symbolic name (the enum's type name can be inferred from .pstypenames[0] -replace '^Deserialized\.').

    The PSComputerName and RunspaceId properties are useful in remoting commands that target multiple computers at once: given that the order in which output is received is not guaranteed, these properties tell you where a given output object originated from.

    The PSShowComputerName property allows you to control default display behavior - though, curiously, it has no effect on whether RunspaceId is displayed.

    The Value property for System.Enum-derived types compensates for the loss of type fidelity that typically occurs in remoting commands (and background jobs) - only a limited set of known types deserialize with type fidelity - see this answer.


    While these properties always exist, whether they show by default depends on the specific types of the object returned and either what formatting data is associated with them or applied by default by PowerShell.

    Also, they may show when you pipe to Format-* cmdlets explicitly, and during serialization, such as with ConvertTo-Json.