Search code examples
c#powershellperlrunspacesysinternals

Powershell generating output in the error stream instead of output stream


I have a C# application that creates a remote runspace, creates a powershell instance inside the runspace and executes a perl script using

ps.AddScript($"perl.exe {perlScriptPath} {perlScriptArgs}");

This perl script calls PsKill.exe to terminate a process.

The output I get in my C# application after ps.Invoke() is quite strange. I get the PsKill.exe logs in the error stream while everything else in the output stream. It is pretty evident from the logs that the execution went smoothly and there were no issues. The PsKill output is shown as normal if I manually execute the perl script using a cmd or powershell. Here is a comparison:

Manual execution:


PsKill v1.13 - Terminates processes on local or remote systems
Copyright (C) 1999-2009  Mark Russinovich
Sysinternals - www.sysinternals.com

The OracleServicemst1 service is not started.

More help is available by typing NET HELPMSG 3521.

C# powershell error stream:

System.Management.Automation.RemoteException
PsKill v1.13 - Terminates processes on local or remote systems
Copyright (C) 1999-2009  Mark Russinovich
Sysinternals - www.sysinternals.com
System.Management.Automation.RemoteException
The OracleServicemst1 service is not started.
System.Management.Automation.RemoteException
More help is available by typing NET HELPMSG 3521.
System.Management.Automation.RemoteException

Every blank line I see in the manual execution output is replaced by a System.Management.Automation.RemoteException. Along with that the entire output is redirected to the error stream.

I have two other perl scripts. One that uses CoreInfo and shows similar behavior, and the other one that has the exact usage of PsKill.exe but does not show this behavior. Any insight would be helpful.

Thanks!


Solution

    • When you use PowerShell remoting, the remote code is executed by the ServerRemoteHost host (as reflected in $Host.Name).

      • Note that the same host is also used in PowerShell's (child process-based) background jobs.

      • By contrast, when you use local runspaces with the PowerShell SDK (i.e. when you host PowerShell in a custom .NET (C#) application), PowerShell uses the
        Default Host host, which is also used in PowerShell's lighter-weight thread-based jobs and, in the technologically closely related, PowerShell (Core) 7-only ForEach-Object -Parallel; however, with respect to the issue at hand - how stderr output is treated - this host behave the same as ServerRemoteHost; see next point.

    • The ServerRemoteHost and Default Host hosts route stderr output from external (native) programs through PowerShell's error output stream.

      • By contrast, in a local call in a terminal (console), in interactive sessions or when executing code via the PowerShell CLI (powershell.exe for Windows PowerShell, pwsh for PowerShell (Core) 7) , the ConsoleHost host passes stderr output straight to the terminal.
        However, you can use a 2> redirection to either silence stderr output or save it to a file (which you can also do with remote commands), and you can use 2>&1 to merge it into the success output stream.

      • Caveat:

        • In Windows PowerShell, in remote calls and when a 2> redirection is involved, stderr lines are unexpectedly[1] recorded in the automatic $Error variable and are subject to the value of the $ErrorActionPreference preference variable, which, when it is set to 'Stop', causes stderr output from any external (native) program to abort execution.

        • The latter problem has been fixed in all hosts in PowerShell (Core) 7; the former problem has been fixed in the ConsoleHost host, but ServerRemoteHost and Default Host - due to routing stderr lines via the error stream - still cause these lines to be recorded in $Error / the .Streams.Error collection of the PowerShell class in SDK use.

    • While PowerShell interprets stderr output as lines of text, it also wraps each such line in a [System.Management.Automation.ErrorRecord] instance (which wraps a .NET exception) - even when you use 2>&1 - which is what you saw.

      • Note: In the ConsoleHost host, unless a 2> redirection is involved, this does not happen, as stderr output is passed directly to the terminal (this also applies to stdout output that isn't captured, piped, or redirected).
    • You can simply stringify these instances to get the original stderr text lines, i.e. call .ToString() on them.

      • To do that selectively only for instances wrapping stderr lines - to distinguish them from true PowerShell errors - check .FullyQualifiedErrorId -eq 'NativeCommandError'

      • However, you did uncover a bug: As of PowerShell 7.4.2, ErrorRecord instances wrapping empty stderr lines unexpectedly stringify to the type name of the underlying .NET exception, System.Management.Automation.RemoteException rather than to the empty string.

        • Workaround: access .Message.Exception to retrieve the wrapped stderr line's text, which also works with empty lines.
        • The bug has been reported in GitHub issue #23950.

    [1] Stderr output can not be assumed to signal an error condition; many CLIs use stderr for anything that is non-data, such as status messages. Success or failure of an external CLI call should solely be inferred from the process exit code.