Как асинхронно захватывать выход процесса в powershell?

Я хочу записать stdout и stderr из процесса, который я запускаю в Powershell script, и выводить его асинхронно на консоль. Я нашел документацию по этому поводу через MSDN и другие блоги.

После создания и запуска приведенного ниже примера я не могу получить какой-либо вывод, который будет отображаться асинхронно. Весь вывод выводится только при завершении процесса.

$ps = new-object System.Diagnostics.Process
$ps.StartInfo.Filename = "cmd.exe"
$ps.StartInfo.UseShellExecute = $false
$ps.StartInfo.RedirectStandardOutput = $true
$ps.StartInfo.Arguments = "/c echo `"hi`" `& timeout 5"

$action = { Write-Host $EventArgs.Data  }
Register-ObjectEvent -InputObject $ps -EventName OutputDataReceived -Action $action | Out-Null

$ps.start() | Out-Null
$ps.BeginOutputReadLine()
$ps.WaitForExit()

В этом примере я ожидал увидеть вывод "hi" в командной строке до окончания выполнения программы, потому что должно было быть вызвано событие OutputDataReceived.

Я пробовал это с помощью других исполняемых файлов - java.exe, git.exe и т.д. Все они имеют одинаковый эффект, поэтому мне остается думать, что есть что-то простое, что я не понимаю или пропустили. Что еще нужно сделать для асинхронного чтения stdout?

Ответ 1

К сожалению, асинхронное чтение не так просто, если вы хотите сделать это правильно. Если вы вызываете WaitForExit() без тайм-аута, вы можете использовать что-то вроде этой функции, которую я написал (на основе кода С#):

function Invoke-Executable {
    # Runs the specified executable and captures its exit code, stdout
    # and stderr.
    # Returns: custom object.
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]$sExeFile,
        [Parameter(Mandatory=$false)]
        [String[]]$cArgs,
        [Parameter(Mandatory=$false)]
        [String]$sVerb
    )

    # Setting process invocation parameters.
    $oPsi = New-Object -TypeName System.Diagnostics.ProcessStartInfo
    $oPsi.CreateNoWindow = $true
    $oPsi.UseShellExecute = $false
    $oPsi.RedirectStandardOutput = $true
    $oPsi.RedirectStandardError = $true
    $oPsi.FileName = $sExeFile
    if (! [String]::IsNullOrEmpty($cArgs)) {
        $oPsi.Arguments = $cArgs
    }
    if (! [String]::IsNullOrEmpty($sVerb)) {
        $oPsi.Verb = $sVerb
    }

    # Creating process object.
    $oProcess = New-Object -TypeName System.Diagnostics.Process
    $oProcess.StartInfo = $oPsi

    # Creating string builders to store stdout and stderr.
    $oStdOutBuilder = New-Object -TypeName System.Text.StringBuilder
    $oStdErrBuilder = New-Object -TypeName System.Text.StringBuilder

    # Adding event handers for stdout and stderr.
    $sScripBlock = {
        if (! [String]::IsNullOrEmpty($EventArgs.Data)) {
            $Event.MessageData.AppendLine($EventArgs.Data)
        }
    }
    $oStdOutEvent = Register-ObjectEvent -InputObject $oProcess `
        -Action $sScripBlock -EventName 'OutputDataReceived' `
        -MessageData $oStdOutBuilder
    $oStdErrEvent = Register-ObjectEvent -InputObject $oProcess `
        -Action $sScripBlock -EventName 'ErrorDataReceived' `
        -MessageData $oStdErrBuilder

    # Starting process.
    [Void]$oProcess.Start()
    $oProcess.BeginOutputReadLine()
    $oProcess.BeginErrorReadLine()
    [Void]$oProcess.WaitForExit()

    # Unregistering events to retrieve process output.
    Unregister-Event -SourceIdentifier $oStdOutEvent.Name
    Unregister-Event -SourceIdentifier $oStdErrEvent.Name

    $oResult = New-Object -TypeName PSObject -Property ([Ordered]@{
        "ExeFile"  = $sExeFile;
        "Args"     = $cArgs -join " ";
        "ExitCode" = $oProcess.ExitCode;
        "StdOut"   = $oStdOutBuilder.ToString().Trim();
        "StdErr"   = $oStdErrBuilder.ToString().Trim()
    })

    return $oResult
}

Он фиксирует stdout, stderr и код выхода. Пример использования:

$oResult = Invoke-Executable -sExeFile 'ping.exe' -cArgs @('8.8.8.8', '-a')
$oResult | Format-List -Force 

Для получения дополнительной информации и альтернативных реализаций (на С#) прочитайте это сообщение в блоге.

Ответ 2

На основе Ответ Александра Обершта Я создал функцию, которая использует тайм-аут и асинхронные классы задач вместо обработчиков событий. Согласно Майк Адельсон

К сожалению, этот метод (обработчики событий) не дает возможности узнать когда последний бит данных был получен. Потому что все асинхронно, возможно (и я это наблюдал) для событий пожар после возвращения WaitForExit().

function Invoke-Executable {
# from /questions/44188/how-to-capture-process-output-asynchronously-in-powershell/316277#316277
    # Runs the specified executable and captures its exit code, stdout
    # and stderr.
    # Returns: custom object.
# from http://www.codeducky.org/process-handling-net/ added timeout, using tasks
param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]$sExeFile,
        [Parameter(Mandatory=$false)]
        [String[]]$cArgs,
        [Parameter(Mandatory=$false)]
        [String]$sVerb,
        [Parameter(Mandatory=$false)]
        [Int]$TimeoutMilliseconds=1800000 #30min
    )
    Write-Host $sExeFile $cArgs

    # Setting process invocation parameters.
    $oPsi = New-Object -TypeName System.Diagnostics.ProcessStartInfo
    $oPsi.CreateNoWindow = $true
    $oPsi.UseShellExecute = $false
    $oPsi.RedirectStandardOutput = $true
    $oPsi.RedirectStandardError = $true
    $oPsi.FileName = $sExeFile
    if (! [String]::IsNullOrEmpty($cArgs)) {
        $oPsi.Arguments = $cArgs
    }
    if (! [String]::IsNullOrEmpty($sVerb)) {
        $oPsi.Verb = $sVerb
    }

    # Creating process object.
    $oProcess = New-Object -TypeName System.Diagnostics.Process
    $oProcess.StartInfo = $oPsi


    # Starting process.
    [Void]$oProcess.Start()
# Tasks used based on http://www.codeducky.org/process-handling-net/    
 $outTask = $oProcess.StandardOutput.ReadToEndAsync();
 $errTask = $oProcess.StandardError.ReadToEndAsync();
 $bRet=$oProcess.WaitForExit($TimeoutMilliseconds)
    if (-Not $bRet)
    {
     $oProcess.Kill();
    #  throw [System.TimeoutException] ($sExeFile + " was killed due to timeout after " + ($TimeoutMilliseconds/1000) + " sec ") 
    }
    $outText = $outTask.Result;
    $errText = $errTask.Result;
    if (-Not $bRet)
    {
        $errText =$errText + ($sExeFile + " was killed due to timeout after " + ($TimeoutMilliseconds/1000) + " sec ") 
    }
    $oResult = New-Object -TypeName PSObject -Property ([Ordered]@{
        "ExeFile"  = $sExeFile;
        "Args"     = $cArgs -join " ";
        "ExitCode" = $oProcess.ExitCode;
        "StdOut"   = $outText;
        "StdErr"   = $errText
    })

    return $oResult
}

Ответ 3

Я не мог заставить любой из этих примеров работать с PS 4.0.

Я хотел запустить puppet apply из пакета Octopus Deploy (через Deploy.ps1) и посмотреть вывод в режиме реального времени, а не дождаться завершения процесса (через час), поэтому я придумал следующее:

# Deploy.ps1

$procTools = @"

using System;
using System.Diagnostics;

namespace Proc.Tools
{
  public static class exec
  {
    public static int runCommand(string executable, string args = "", string cwd = "", string verb = "runas") {

      //* Create your Process
      Process process = new Process();
      process.StartInfo.FileName = executable;
      process.StartInfo.UseShellExecute = false;
      process.StartInfo.CreateNoWindow = true;
      process.StartInfo.RedirectStandardOutput = true;
      process.StartInfo.RedirectStandardError = true;

      //* Optional process configuration
      if (!String.IsNullOrEmpty(args)) { process.StartInfo.Arguments = args; }
      if (!String.IsNullOrEmpty(cwd)) { process.StartInfo.WorkingDirectory = cwd; }
      if (!String.IsNullOrEmpty(verb)) { process.StartInfo.Verb = verb; }

      //* Set your output and error (asynchronous) handlers
      process.OutputDataReceived += new DataReceivedEventHandler(OutputHandler);
      process.ErrorDataReceived += new DataReceivedEventHandler(OutputHandler);

      //* Start process and handlers
      process.Start();
      process.BeginOutputReadLine();
      process.BeginErrorReadLine();
      process.WaitForExit();

      //* Return the commands exit code
      return process.ExitCode;
    }
    public static void OutputHandler(object sendingProcess, DataReceivedEventArgs outLine) {
      //* Do your stuff with the output (write to console/log/StringBuilder)
      Console.WriteLine(outLine.Data);
    }
  }
}
"@

Add-Type -TypeDefinition $procTools -Language CSharp

$puppetApplyRc = [Proc.Tools.exec]::runCommand("ruby", "-S -- puppet apply --test --color false ./manifests/site.pp", "C:\ProgramData\PuppetLabs\code\environments\production");

if ( $puppetApplyRc -eq 0 ) {
  Write-Host "The run succeeded with no changes or failures; the system was already in the desired state."
} elseif ( $puppetApplyRc -eq 1 ) {
  throw "The run failed; halt"
} elseif ( $puppetApplyRc -eq 2) {
  Write-Host "The run succeeded, and some resources were changed."
} elseif ( $puppetApplyRc -eq 4 ) {
  Write-Warning "WARNING: The run succeeded, and some resources failed."
} elseif ( $puppetApplyRc -eq 6 ) {
  Write-Warning "WARNING: The run succeeded, and included both changes and failures."
} else {
  throw "Un-recognised return code RC: $puppetApplyRc"
}

Кредит отправляется T30 и Stefan Goßner

Ответ 4

Примеры здесь полезны, но не полностью соответствуют моему варианту использования. Я не хотел вызывать команду и выйти. Я хотел открыть командную строку, отправить ввод, прочитать выход и повторить. Вот мое решение для этого.

Создать Utils.CmdManager.cs

using System;
using System.Diagnostics;
using System.Text;
using System.Threading;

namespace Utils
{
    public class CmdManager : IDisposable
    {
        const int DEFAULT_WAIT_CHECK_TIME = 100;
        const int DEFAULT_COMMAND_TIMEOUT = 3000;

        public int WaitTime { get; set; }
        public int CommandTimeout { get; set; }

        Process _process;
        StringBuilder output;

        public CmdManager() : this("cmd.exe", null, null) { }
        public CmdManager(string filename) : this(filename, null, null) { }
        public CmdManager(string filename, string arguments) : this(filename, arguments, null) { }

        public CmdManager(string filename, string arguments, string verb)
        {
            WaitTime = DEFAULT_WAIT_CHECK_TIME;
            CommandTimeout = DEFAULT_COMMAND_TIMEOUT;

            output = new StringBuilder();

            _process = new Process();
            _process.StartInfo.FileName = filename;
            _process.StartInfo.RedirectStandardInput = true;
            _process.StartInfo.RedirectStandardOutput = true;
            _process.StartInfo.RedirectStandardError = true;
            _process.StartInfo.CreateNoWindow = true;
            _process.StartInfo.UseShellExecute = false;
            _process.StartInfo.ErrorDialog = false;
            _process.StartInfo.Arguments = arguments != null ? arguments : null;
            _process.StartInfo.Verb = verb != null ? verb : null;

            _process.EnableRaisingEvents = true;
            _process.OutputDataReceived += (s, e) =>
            {
                lock (output)
                {
                    output.AppendLine(e.Data);
                };
            };
            _process.ErrorDataReceived += (s, e) =>
            {
                lock (output)
                {
                    output.AppendLine(e.Data);
                };
            };

            _process.Start();
            _process.BeginOutputReadLine();
            _process.BeginErrorReadLine();
            _process.StandardInput.AutoFlush = true;
        }

        public void RunCommand(string command)
        {
            _process.StandardInput.WriteLine(command);
        }

        public string GetOutput()
        {
            return GetOutput(null, CommandTimeout, WaitTime);
        }

        public string GetOutput(string endingOutput)
        {
            return GetOutput(endingOutput, CommandTimeout, WaitTime);
        }

        public string GetOutput(string endingOutput, int commandTimeout)
        {
            return GetOutput(endingOutput, commandTimeout, WaitTime);
        }

        public string GetOutput(string endingOutput, int commandTimeout, int waitTime)
        {
            string tempOutput = "";
            int tempOutputLength = 0;
            int amountOfTimeSlept = 0;

            // Loop until
            //  a) command timeout is reached
            //  b) some output is seen
            while (output.ToString() == "")
            {
                if (amountOfTimeSlept >= commandTimeout)
                {
                    break;
                }

                Thread.Sleep(waitTime);
                amountOfTimeSlept += waitTime;
            }

            // Loop until:
            //  a) command timeout is reached
            //  b) endingOutput is found
            //  c) OR endingOutput is null and there is no new output for at least waitTime
            while (amountOfTimeSlept < commandTimeout)
            {
                if (endingOutput != null && output.ToString().Contains(endingOutput))
                {
                    break;
                }
                else if(endingOutput == null && tempOutputLength == output.ToString().Length)
                {
                    break;
                }

                tempOutputLength = output.ToString().Length;

                Thread.Sleep(waitTime);
                amountOfTimeSlept += waitTime;
            }

            // Return the output and clear the buffer
            lock (output)
            {
                tempOutput = output.ToString();
                output.Clear();
                return tempOutput.TrimEnd();
            }
        }

        public void Dispose()
        {
            _process.Kill();
        }
    }
}

Затем из PowerShell добавьте класс и используйте его.

Add-Type -Path ".\Utils.CmdManager.cs"

$cmd = new-object Utils.CmdManager
$cmd.GetOutput() | Out-Null

$cmd.RunCommand("whoami")
$cmd.GetOutput()

$cmd.RunCommand("cd")
$cmd.GetOutput()

$cmd.RunCommand("dir")
$cmd.GetOutput()

$cmd.RunCommand("cd Desktop")
$cmd.GetOutput()

$cmd.RunCommand("cd")
$cmd.GetOutput()

$cmd.RunCommand("dir")
$cmd.GetOutput()

$cmd.Dispose()

Не забудьте вызвать функцию Dispose() в конце, чтобы очистить процесс, выполняющийся в фоновом режиме. Кроме того, вы можете закрыть этот процесс, выполнив что-то вроде $cmd.RunCommand("exit")