0
votes

TLDW: To support a specific process I need to go from a powershell instances/module to kick off a known safe executable for some c# magic, which then in turn needs to kick and execute a powershell script before exiting.

Powershell Entry point { Known Good Exe { Powershell Work To Do } }

Now ideally, this would all run from a single console instance so that all of the output is simple to look at. The exe -> powershell logging all works fine and as expected when using powershell.Streams.... The write-hosts in the powershell work all show up in the console and I get all the info I want.

powerShell.Streams.Information.DataAdded += LogMessage;

The problem comes when the outer powershell module is introduced. This one is needed because the parent process and execution environment this is running from is powershell. Once this whole stack is started from within a powershell instance, I get console logging from the outer powershell, and from the exe. BUT all of the write-hosts from the inner powershell modules disappear.

I've tried disabling the stream redirects, and a few other things, but this isn't resolving in the manner I would hope. I'm hoping someone knows if there is a way to get this to work as it solves so many problems if it just would.

PowerShell Outer:

$C#Exe = Join-Path $PSScriptRoot $C#ExePath
$C#Args = @()


Write-Host "Hello world" # will show up

& $C#Exe  $C#Args

C# Exe Code:

public static void Main(string[] args)
{
    Console.WriteLine("Hello world"); #Will show up

    var powerShell = PowerShell.Create().AddScript("PowerShellInner.ps1");

    powerShell.Streams.Information.DataAdded += LogMessage<InformationRecord>;
    powerShell.Streams.Warning.DataAdded += LogMessage<WarningRecord>;
    powerShell.Streams.Error.DataAdded += LogMessage<ErrorRecord>;
    powerShell.Streams.Verbose.DataAdded += LogMessage<VerboseRecord>;
    powerShell.Streams.Debug.DataAdded += LogMessage<DebugRecord>;


    StringBuilder resultString = new StringBuilder();

    foreach (dynamic item in powerShell.Invoke().ToList())
    {
        resultString.AppendLine(item.ToString());
    }

    Console.WriteLine(resultString.ToString());

}

private static void LogMessage<T>(object sender, DataAddedEventArgs e)
{
    var data = (sender as PSDataCollection<T>)[e.Index];
    Console.WriteLine($"[{typeof(T).Name}] {Convert.ToString(data)}");
}

PowerShell Inner:

Write-Host "Hello world" #Wont show up
1
No, and maybe to clarify, my real question is why does running this with the outer powershell cause the c# ps stream redirection to fail? - Ethan Criss
So, you have a powershell script invoking an exe (written in C#) and the C# program is, in turn, executing a powershell script? I'm getting confused because when you say "Powershell", I'm not sure if you mean the PS that's executing the exe, or if you mean the PS that the exe is executing - William Charlton
@Nova you are precisely correct in your summary of the call stack. - Ethan Criss
So, the PS you're showing in your question is the "outer" PS, yes? - William Charlton

1 Answers

1
votes

UPDATE on 1/22/2020

I can't fully explain why you're experiencing what you're seeing, but the following code works.

Output from executing PowerShellOuter.ps1

enter image description here

Code

Notes:

  • Your c# program doesn't show any code manipulating input arguments,
    so I didn't model any input args

  • You mis-used the AddScript method. It needs the text of the script, not the script name

  • The code below assumes that the c# exe and the two PS scripts are in the same folder

  • In PowerShellInner.ps1, use write-output. write-host does not output data to PowerShell Objectflow Engine but rather, as the name implies, writes directly to the host and sends nothing to the PowerShell engine to be forwarded to commands later in the pipeline. See Write-Output or Write-Host in PowerShell

PowerShellOuter.ps1

cls
$CSharpExe = Join-Path $PSScriptRoot "PowerShellExecutionSample.exe"
Write-Host "Hello from PowerShellOuter.ps1"
&$CSharpExe
pause

PowerShellInner.ps1

Write-Output "Hello from PowerShellInner.ps1"

Code for PowerShellExecutionSample.exe

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Management.Automation;

namespace PowerShellExecutionSample
{
    class Program
    {
        static void Main(string[] args)
        {
            PowerShellExecutor t = new PowerShellExecutor();
            t.ExecuteSynchronously();
        }
    }

    /// <summary>
    /// Provides PowerShell script execution examples
    /// </summary>
    class PowerShellExecutor
    {
        public void ExecuteSynchronously()
        {
            using (PowerShell PowerShellInstance = PowerShell.Create())
            {
                //You mis-used the AddScript method. It needs the text of the script, not the script name
                string scriptText = File.ReadAllText(string.Format(@"{0}\PowerShellInner.ps1", Environment.CurrentDirectory));

                PowerShellInstance.AddScript(scriptText);
                Collection <PSObject> PSOutput = PowerShellInstance.Invoke();

                // loop through each output object item
                foreach (PSObject outputItem in PSOutput)
                {
                    // if null object was dumped to the pipeline during the script then a null
                    // object may be present here. check for null to prevent potential NRE.
                    if (outputItem != null)
                    {
                        Console.WriteLine(outputItem.BaseObject.ToString() + "\n");
                    }
                }
            }
        }
    }
}

Original Answer on 1/21/2020

Updating your question to break out your code out helped - thanks.

I think you have two issues:

1) Modify your c# program to obtain the streams coming from PowerShell Inner and make your c# program re-emit the data from the PowerShell Inner output streams. Here is a Microsoft blog entry I used to crack the same nut: Executing PowerShell scripts from C#

2) Modify your PowerShell Outer to obtain the streams coming from the c# program. Here is a blog entry that seems to crack that nut: How to redirect output of console program to a file in PowerShell. The heart of this is to execute the following from your PowerShell Outer:

cmd /c XXX.exe ^>log.txt 2^>^&1

Note: The ^ are really backticks