简体   繁体   中英

How can I get output from powershell in C# while a command is running?

I'm using powershell in C# with system.management.automation and I can access both the output and the error stream sucessfully. For most applications this is great but i'm right now in a situation where I need to get the output of a powershell command while it is running in c# and i'm lost.

I've tried subscribing to outputcollection.DataAdded, i've tried subscribing to the powershell instance verbose stream, but neither of them are getting called when powershell gives an output.

Here's the code I have so far

public async Task<string> CMD(string script)
{
    ps = PowerShell.Create();
    string errorMsg = "";
    string output;

    ps.AddScript(script);
    ps.AddCommand("Out-String");

    PSDataCollection<PSObject> outputCollection = new();
    ps.Streams.Error.DataAdded += (object sender, DataAddedEventArgs e) =>
    { errorMsg = ((PSDataCollection<ErrorRecord>)sender)[e.Index].ToString(); };

    IAsyncResult result = ps.BeginInvoke<PSObject, PSObject>(null, outputCollection);

    while (!result.IsCompleted)
    {
        await Task.Delay(100);
    }

    StringBuilder stringBuilder = new();
    foreach (PSObject outputItem in outputCollection)
    {
        stringBuilder.AppendLine(outputItem.BaseObject.ToString());
    }
    output = stringBuilder.ToString();

    //Clears commands added to runspace
    ps.Commands.Clear();
    Debug.WriteLine(output);

    if (!string.IsNullOrEmpty(errorMsg))
        MessageBox.Show(errorMsg, "Error");
    return output.Trim();
}

I've also tried checking the outputcollection in the while loop but it doesn't give me the output until the command is done.

The command i'm trying to use is Connect-ExchangeOnline -Device To simulate it in C# it would work the same as doing sleep 5;echo test;sleep 5 where I then want the program to display test after 5 seconds not after the full 10 seconds.

EDIT:

When using "Connect-ExchangeOnline -Device" powershell will deliver this output and wait for the user to complete said task. The issue being that I can't display this in C# because my C# code waits for the powershell command to be finished. And outputcollection.DataAdded never seems to be called.

To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code CDWS27A56 to authenticate.

Unfortunately, Connect-ExchangeOnline is meant only for interactive console use, and specifically gets around allowing the output to be captured (Possibly by writing directly to the $host window it was called from?).

Normally, you could try using Tee-Object or Start-Transcript / Stop-Transcript with redirection to dump all output:

Connect-ExchangeOnline -Device *>&1 | Tee-Object -FilePath C:\temp\tee.txt -Append
# Then from another process:
Get-Content C:\temp\tee.txt

Or try starting a powershell Job, which keeps all of its output in the job object's properties:

$job = Start-ThreadJob -Name Connecting -ScriptBlock { Connect-ExchangeOnline -Device }
# Wait for prompt...
$job.Output
$job.Information

However, neither of these actually grab the device authentication code.

Currently to use -Device , you need to have a visible powershell window and have the user complete their device authentication there.

You can always use one of the other authentication types:

Connect-ExchangeOnline -UserPrincipalName username@domain.tld

This version will automatically launch a Modern Authentication prompt or a browser page. Depending on your use case, it is effectively the same.

I would pipe the script outputs to a file, then have your c# code read that file, and filter out the code.

Alternatively you could use a class that exposes 2 StringBuilders as properties through which you can use to get the script output and filter out the code:

using System.Diagnostics;
using System.Text;

public sealed class ProcessOptions
{
    public bool WaitForProcessExit { get; set; }

    public bool Executing { get; internal set; } = true;
}

public class InvokePowershell
{
    public static StringBuilder stdout { get; private set; } = null;
    public static StringBuilder stderr { get; private set; } = null;

    public void string Start(string script)
    {
        var process = new Process();
        var options = new ProcessOptions()
        {
            WaitForProcessExit = true,
        };
        process.StartInfo.FileName = "powershell"; // (or pwsh for powershell core).
        process.StartInfo.Arguments = script;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.RedirectStandardError = true;
        process.StartInfo.UseShellExecute = true; // true does not always work so use caution with this.
        process.StartInfo.CreateNoWindow = true;
        process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
        process.StartInfo.WorkingDirectory = Directory.GetCurrentDirectory();
        process.Shell(options)
    }

    private static void Shell(this Process process, ProcessOptions options)
    {
        if (stdout is not null && stderr is not null)
        {
            stdout = null;
            stderr = null;
        }

        process.OutputDataReceived += (_, e) =>
        {
            if (e.Data is null)
            {
                return;
            }

            if (stdout is null)
            {
                stdout = new StringBuilder();
            }
            else
            {
                stdout.AppendLine();
            }

            stdout.Append(e.Data);
        };

        process.ErrorDataReceived += (_, e) =>
        {
            if (e.Data is null)
            {
                return;
            }

            if (stderr is null)
            {
                stderr = new StringBuilder();
            }
            else
            {
                stderr.AppendLine();
            }

            stderr.Append(e.Data);
        };
        process.Start();
        options.Executing = false;
        if (process.StartInfo.RedirectStandardError)
            process.BeginErrorReadLine();

        if (process.StartInfo.RedirectStandardOutput)
            process.BeginOutputReadLine();

        if (options.WaitForProcessExit)
            process.WaitForExit();
    }
}

And then you could then make a way to Fire and forget that code (so that way it does not get blocked until powershell exits, then simply in the code you can do the following in your normal code:

while (InvokePowershell.stdout is null || InvokePowershell.stdout.Length < /* length you expect it to be*/)
{
    // do nothing but by wait by sleeping for a few milliseconds to avoid wasting cpu cycles with this.
}

// strip the code from the stdout property then use it.

I find doing something like this is much more cleaner, plus then it could easily be ported to powershell core by changing powershell on the Start function to use pwsh which is the program name for powershell core and plus then the code would work on all platforms that powershell core supports which are Windows, Mac, and various other linux distributions out there.

Additionally for this to work powershell or even pwsh if powershell core is wanted to be used instead of powershell that the program must be in the PATH environment variable so it can be invoked inside of the terminal directly.

Also with the code above, you could theoretically not Wait for process exit, however I do not know if those events would trigger and populate the StringBuilders then, likewise the process instance would leave scope and be GC'd resulting in the events also getting GC'd and then the StringBuilders never getting assigned to.

As such that is why I recommend calling InvokePowershell.Start(script); as a delegate as a fire-and-forget call. Then doing a loop that checks if null or is smaller than the expected length of the string outputs then sleep for a few cpu clockcycles (each clockcycle is less than a second), and then filtering out the results from there after that while loop ensures that it is populated for the preprocessing that comes after the loop.

Edit: Instead of having to call InvokePowershell.Start(script); in a fire-and-forget, you can replace the call to process.WaitForExit() and the if check for it entirely with the while loop shown above, pass in the length you expect to the method, and to the (Shell method by adding it as a parameter to it and remove the options argument, instantiation, and type entirely), and then after the while loop breaks (to allow time for the event handlers to add what you need to the property's stringbuilders, you can call process.Kill(); to kill powershell or powershell core.

You can use the code below to get the output of a PowerShell command in real time from a C# application.

This uses a PowerShell Pipeline, which allows you to call a notification handler whenever the PowerShell command/script writes output into the Pipeline. I've implemented the solution below as an async enumerable but if you wanted something non-async you can also just use the Pipeline.Output.DataReady handler to trigger some code to read from the pipeline.

https://gist.github.com/OnKey/83cf98e6adafe5a2b4aaf561b138087b

static async Task Main(string[] args)
        {
            var script = @"
For ($i=0; $i -le 5; $i++) {
    $i
    Start-Sleep -s 1 
}
";
            var p = new Program();
            await foreach (var item in p.PowerShellAsyncEnumerable(script))
            {
                Console.WriteLine(item);
            }
        }

        private IAsyncEnumerable<PSObject> PowerShellAsyncEnumerable(string script)
        {
            var rs = RunspaceFactory.CreateRunspace();
            rs.Open();
            var pipeline = rs.CreatePipeline();
            pipeline.Commands.AddScript(script);

            return new PsAsyncEnumerable(pipeline);
        }

        internal class PsAsyncEnumerable : IAsyncEnumerable<PSObject>
        {
            private readonly Pipeline pipe;
            public PsAsyncEnumerable(Pipeline pipe) => this.pipe = pipe;

            public IAsyncEnumerator<PSObject> GetAsyncEnumerator(CancellationToken cancellationToken = new()) 
                => new PsAsyncEnumerator(this.pipe);
        }

        internal class PsAsyncEnumerator : IAsyncEnumerator<PSObject>
        {
            private readonly Pipeline pipe;
            private TaskCompletionSource dataReady = new();

            public PsAsyncEnumerator(Pipeline pipe)
            {
                this.pipe = pipe;
                this.pipe.Output.DataReady += NotificationHandler;
                this.pipe.Error.DataReady += NotificationHandler;
                this.pipe.InvokeAsync();
            }
            
            private void NotificationHandler(object sender, EventArgs e)
            {
                this.dataReady.SetResult();
            }
            
            public ValueTask DisposeAsync()
            {
                this.pipe.Dispose();
                return ValueTask.CompletedTask;
            }

            public async ValueTask<bool> MoveNextAsync()
            {
                while (!this.pipe.Output.EndOfPipeline)
                {
                    var item = this.pipe.Output.NonBlockingRead(1).FirstOrDefault();
                    if (item != null)
                    {
                        this.Current = item;
                        return true;
                    }
                
                    await this.dataReady.Task;
                    this.dataReady = new TaskCompletionSource();
                }

                return false;
            }

            public PSObject Current { get; private set; }
        }
1. In C# Start BackgroundWorker bw;
2. In bw.DoWork(...) Start PowerShell.
3. In PowerShell write to a File and close it.
4. In the Main thread of C# read the File.
===
using System.ComponentModel;
BackgroundWorker bw = new();
bw.DoWork += Bw_DoWork; 
private void Bw_DoWork(object sender, DoWorkEventArgs e)
{
    <Start ps>
}
=== in ps
Set obj=CreateObject("Scripting.FileSystemObject")
outFile="C:\File.txt"
Set objFile = obj.CreateTextFile(outFile,True)
objFile.Write "test string"
objFile.Close # it makes file accessible outside ps
=== In the Main thread
<read the C:\File.txt>
===

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM