Question

The Background

I'm writing an application that programatically executes PowerShell scripts. This application has a custom PSHost implementation to allow scripts to output logging statements. Currently, the behavior I'm seeing is that some requests are properly forwarded to my custom PSHost and others are flat out ignored.

Things get even stranger when I started inspecting the $Host variable in my scripts, which seem to suggest that my custom PSHost isn't even being used.

The Code

I have some code that's executing PowerShell within a .NET application:

var state = InitialSessionState.CreateDefault();
state.AuthorizationManager = new AuthorizationManager("dummy"); // Disable execution policy

var host = new CustomPsHost(new CustomPsHostUI());

using (var runspace = RunspaceFactory.CreateRunspace(host, state))
{
    runspace.Open();

    using (var powershell = PowerShell.Create())
    {
        powershell.Runspace = runspace;

        var command = new Command(filepath);

        powershell.Invoke(command);
    }
}

The implementation for CustomPsHost is very minimal, only containing what's needed to forward the PSHostUserInterface:

public class CustomPsHost : PSHost
{
    private readonly PSHostUserInterface _hostUserInterface;

    public CustomPsHost(PSHostUserInterface hostUserInterface)
    {
        _hostUserInterface = hostUserInterface;
    }

    public override PSHostUserInterface UI
    {
        get { return _hostUserInterface; }
    }

    // Methods omitted for brevity
}

The CustomPsHostUI is used as a wrapper for logging:

public class CustomPsHostUI : PSHostUserInterface
{
    public override void Write(string value) { Debug.WriteLine(value); }
    public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value){ Debug.WriteLine(value); }
    public override void WriteLine(string value) { Debug.WriteLine(value); }
    public override void WriteErrorLine(string value) { Debug.WriteLinevalue); }
    public override void WriteDebugLine(string message) { Debug.WriteLine(message); }
    public override void WriteProgress(long sourceId, ProgressRecord record) {}
    public override void WriteVerboseLine(string message) { Debug.WriteLine(message); }

    // Other methods omitted for brevity
}

In my PowerShell script, I am trying to write information to the host:

Write-Warning "This gets outputted to my CustomPSHostUI"
Write-Host "This does not get outputted to the CustomPSHostUI"

Write-Warning $Host.GetType().FullName # Says System.Management.Automation.Internal.Host.InternalHost
Write-Warning $Host.UI.GetType().FullName # Says System.Management.Automation.Internal.Host.InternalHostUserInterface

Why am I getting the strange behavior with my CustomPSHostUI?

Was it helpful?

Solution

You need to provide an implementation for PSHostRawUserInterface.

Write-Host ends up calling your version of Write(ConsoleColor, ConsoleColor, string). PowerShell relies on the raw ui implementation for the foreground and background colors.

I have verified this with sample code. Instead of calling out to a ps1 file, I invoked Write-Host directly:

powershell.AddCommand("Write-Host").AddParameter("Testing...")

By running a script, PowerShell was handling the exceptions for you. By invoking the command directly, you can more easily see the exceptions. If you had inspected $error in your original example, you would have seen a helpful error.

Note that the value of $host is never the actual implementation. PowerShell hides the actual implementation by wrapping it. I forget the exact details of why it's wrapped.

OTHER TIPS

For anyone else still struggling after implementing PSHostUserInterface and PSHostRawUserInterface and finding that WriteErrorLine() is being completely ignored when you call Write-Error, even though Warning, Debug, and Verbose make it to the PSHostUserInterface, here's how to get your errors:

Pay close attention to https://msdn.microsoft.com/en-us/library/ee706570%28v=vs.85%29.aspx and add these two lines right before your .Invoke() call, like so:

powershell.AddCommand("out-default");
powershell.Commands.Commands[0].MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output);

powershell.Invoke() // you had this already

This will merge the Errors stream into your console output, otherwise it apparently doesn't go there. I don't have a detailed understanding of why (so perhaps I shouldn't be implementing a custom PSHost to begin with) but there is some further explanation to be had out there:

http://mshforfun.blogspot.com/2006/07/why-there-is-out-default-cmdlet.html

https://msdn.microsoft.com/en-us/library/system.management.automation.runspaces.command.mergemyresults%28v=vs.85%29.aspx

Also, assuming your host is not a console app, and you're not implementing your own cmd-style character-mode display, you'll need to give it a fake buffer size, because it needs to consult this before giving you the Write-Error output. (Don't give it 0,0, otherwise you get a never-ending torrent of blank lines as it struggles to fit the output into a nothing-sized buffer.) I'm using:

class Whatever : PSHostRawUserInterface
{
    public override Size BufferSize
    {
        get { return new Size(300, 5000); }
        set { }
    }
    
    ...
}

If you ARE a console app, just use Console.BufferWidth and Console.BufferHeight.

Update: If you'd rather get your errors in ErrorRecord objects rather than lines of pre-formatted error text going to your WriteErrorLine override, hook the PowerShell.Streams.Error.DataAdding event and get the ItemAdded property on the event args. Way less unruly to work with if you're doing something other than simple line-by-line output in your GUI.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top