programing

파워셸에서 프로세스 출력을 비동기식으로 캡처하는 방법은 무엇입니까?

batch 2023. 9. 3. 12:25
반응형

파워셸에서 프로세스 출력을 비동기식으로 캡처하는 방법은 무엇입니까?

Powershell 스크립트에서 시작하는 프로세스에서 stdout 및 stderr을 캡처하여 콘솔에 비동기식으로 표시합니다.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()

이 예에서는 OutputDataReceived 이벤트가 트리거되어야 하므로 프로그램 실행이 종료되기 전에 명령줄에서 "hi"의 출력을 볼 수 있을 것으로 예상했습니다.

java.exe, git.exe 등 다른 실행 파일을 사용해 보았습니다.모두 같은 효과를 가지고 있기 때문에 제가 이해하지 못하거나 놓친 단순한 것이 있다고 생각하게 됩니다.stdout을 비동기식으로 읽으려면 또 무엇을 해야 합니까?

불행히도 제대로 하려면 비동기식 읽기가 그렇게 쉽지 않습니다.타임아웃 없이 WaitForExit()을 호출하면 다음과 같은 기능을 사용할 수 있습니다(C# 코드 기준).

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 및 exit 코드를 캡처합니다.사용 예:

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

자세한 내용 및 대안적 구현(C#)은 이 블로그 게시물을 참조하십시오.

Alexander Obersht의 답변을 기반으로 이벤트 핸들러 대신 시간 초과 및 비동기 작업 클래스를 사용하는 함수를 만들었습니다.Mike Adelson에 따르면

안타깝게도 이 메서드(이벤트 핸들러)는 마지막 데이터 비트가 언제 수신되었는지 알 수 있는 방법을 제공하지 않습니다.모든 것이 비동기식이기 때문에 WaitForExit()이 반환된 후 이벤트가 발생할 수 있습니다.

function Invoke-Executable {
# from https://stackoverflow.com/a/24371479/52277
    # 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
}

PS 4.0에서 사용할 수 있는 예를 찾을 수 없었습니다.

달리고 싶었습니다.puppet applyOctopus Deploy 패키지에서(를 통해)Deploy.ps1(1시간 후) 프로세스가 완료될 때까지 기다리는 대신 "실시간"으로 출력을 확인할 수 있습니다. 그래서 다음과 같은 방법을 생각해 냈습니다.

# 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과 스테판 고너에게 돌아갑니다.

여기 예시들은 모두 유용하지만, 제 사용 사례에 완전히 맞지는 않았습니다.명령을 실행하고 종료하고 싶지 않았습니다.명령 프롬프트를 열고, 입력을 보내고, 출력을 읽고, 반복하고 싶었습니다.여기 그것에 대한 저의 해결책이 있습니다.

유틸리티를 만듭니다.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")

저는 이 과정을 기록하고 스크린에 출력하는 래퍼를 만드는 해결책을 찾으러 왔습니다.이것들 중 어느 것도 저에게 효과가 없었습니다.제가 만든 코드인데, 작동이 잘 되는 것 같았습니다.

PSData Collection을 사용하면 프로세스가 완료될 때까지 기다리지 않고 스크립트를 계속 진행할 수 있습니다.

Using namespace System.Diagnostics;
Using namespace System.Management.Automation;

$Global:Dir = Convert-Path "."
$Global:LogPath = "$global:Dir\logs\mylog.log"
[Process]$Process = [Process]::New();
[ProcessStartInfo]$info = [ProcessStartInfo]::New();
$info.UseShellExecute = $false
$info.Verb = "runas"
$info.WorkingDirectory = "$Global:Dir\process.exe"
$info.FileName = "$Global:Dir\folder\process.exe"
$info.Arguments = "-myarg yes -another_arg no"
$info.RedirectStandardOutput = $true
$info.RedirectStandardError  = $true
$Process.StartInfo = $info;
$Process.EnableRaisingEvents = $true
$Global:DataStream = [PSDataCollection[string]]::New()
$Global:DataStream.add_DataAdded(
    {
        $line = $this[0];
        [IO.File]::AppendAllLines($LogPath, [string[]]$line);
        [Console]::WriteLine($line)
        $this.Remove($line);
    }
)
$script = {
    param([Object]$sender, [DataReceivedEventArgs]$e) 
    $global:Datastream.Add($e.Data)
}
Register-ObjectEvent -InputObject $Process -Action $script -EventName 'OutputDataReceived' | Out-Null
Register-ObjectEvent -InputObject $Process -Action $script -EventName 'ErrorDataReceived' | Out-Null
$Process.Start()
$Process.BeginOutputReadLine()
$Process.BeginErrorReadLine()

PowerShell 콘솔에 동적으로 덤프하려는 경우 다음을 수행합니다.

my.exe | Out-Default

그걸 알아냈다고 주장할 수는 없습니다.

이 기술 게시물의 하단을 참조하십시오. https://social.technet.microsoft.com/Forums/windowsserver/en-US/b6691fba-0e92-4e9d-aec2-47f3d5a17419/start-process-and-redirect-output-to-powershell-window?forum=winserverpowershell

스택 오버플로 게시물도 참조합니다.

$LASTEXITCODE는 또한 제 exe의 종료 코드로 채워졌고, 이것은 제가 필요로 했던 것이기도 했습니다.

저는 이 스레드를 우연히 발견했고 미래에 필요한 사람들을 위해 제 솔루션을 공유하고 싶습니다.이 작업은 PowerShell Core 7.3.4에서 수행되었습니다.

<#
.Synopsis
    This function will run a provided command and arguments.
.DESCRIPTION
    This function was created due to the inconsistencies of running Start-Process in Linux. This function provides a 
    consistent way of running non-PowerShell commands that require many parameters/arguments to run (e.g., docker).
    
    PowerShell commands or aliases will NOT work with this function. For example commands such as: echo, history, or cp
    will NOT work. Use the build-in PowerShell commands for those.
.PARAMETER Name
    The path or name of the command to be ran.
.PARAMETER Arguments
    The optional parameters/arguments to be added with your command.
.PARAMETER WorkingDirectory
    The current WorkingDirectory to run said Command. If you are not using the full path to files, you should probably
    use this parameter. 
.PARAMETER LoadUserProfile
    Gets or sets a value that indicates whether the Windows user profile is to be loaded from the registry.

    This will NOT work on Unix/Linux.
.PARAMETER Timer
    Provide a timer (in ms) for how long you want to wait for the process to exit/end.
.PARAMETER Verb
    Specifies a verb to use when this cmdlet starts the process. The verbs that are available are determined by the filename extension of the file that runs in the process.

    The following table shows the verbs for some common process file types.

    File type   Verbs
    .cmd    Edit, Open, Print, RunAs, RunAsUser
    .exe    Open, RunAs, RunAsUser
    .txt    Open, Print, PrintTo
    .wav    Open, Play
    To find the verbs that can be used with the file that runs in a process, use the New-Object cmdlet to create a System.Diagnostics.ProcessStartInfo object for the file. The available verbs are in the Verbs property of the ProcessStartInfo object. For details, see the examples.

    This will NOT work on Unix/Linux.
.PARAMETER Passthru
    Pass the object into the pipeline. Using -Passthru will ignore error-handling.
.NOTES
    Author - Zack Flowers
.LINK
    GitHub: https://github.com/zackshomelab
.EXAMPLE
    Start-Command -Name 'docker' -CommandArguments "container ls --all"
    
    Example #1:
    This example executes command 'docker' and passes arguments 'container ls --all' to display the offline/online containers.
.EXAMPLE
    Start-Command -Name 'docker' -CommandArguments "container", "ls", "--all"

    Example #2:
    This example is simular to Example #1, except it accepts comma-separated arguments.
.EXAMPLE
    $whoami = Start-Command -Name 'whoami' -Passthru

    $whoami

    Title        : whoami
    OutputStream : System.Management.Automation.PSEventJob
    OutputData   : zac
    ErrorStream  : 
    ErrorData    : 
    ExitCode     : 0

    Example #3:
    This example utilizes the -Passthru feature of this script.
.INPUTS
    None
.OUTPUTS
    System.String
    System.Management.Automation.PSCustomObject
#>
function Start-Command {
    [cmdletbinding(DefaultParameterSetName="default")]
    param (
        [parameter(Mandatory,
            Position=0,
            ValueFromPipelineByPropertyName)]
            [ValidateNotNullOrEmpty()]
        [string]$Name,

        [parameter(Mandatory=$false,
            Position=1,
            ValueFromPipelineByPropertyName)]
            [ValidateNotNullOrEmpty()]
        [object]$Arguments,

        [parameter(Mandatory=$false,
            ValueFromPipelineByPropertyName)]
            [ValidateScript({Test-Path $_})]
        [string]$WorkingDirectory,

        [parameter(Mandatory=$false)]
            [ValidateScript({
                if ($PSVersionTable.Platform -eq "Unix") {
                    Throw "-LoadUserProfile cannot be used on Unix/Linux."
                }
            })]
        [switch]$LoadUserProfile,

        [parameter(Mandatory,
            ValueFromPipelineByPropertyName,
            ParameterSetName="timer")]
            [ValidateRange(1, 600000)]
        [int]$Timer,

        [parameter(Mandatory=$false,
            ValueFromPipelineByPropertyName)]
            [ValidateScript({
                if ($PSVersionTable.Platform -eq "Unix") {
                    Throw "-Verb cannot be used on Unix/Linux."
                }
            })]
        [string]$Verb,

        [parameter(Mandatory=$false)]
        [switch]$Passthru
    )

    begin {
        $FileName = (Get-Command -Name $Name -ErrorAction SilentlyContinue).Source

        # If we cannot find the provided FileName, this could be due to the user providing..
        # ..a command that is a PowerShell Alias (e.g., echo, history, cp)
        if ($null -eq $FileName -or $FileName -eq "") {
            
            # Source doesn't exist. Let's see if the provided command is a PowerShell command
            $getPSCommand = (Get-Command -Name $Name -ErrorAction SilentlyContinue)

            if ($null -eq $getPSCommand -or $getPSCommand -eq "") {
                Throw "Start-Command: Could not find command $Name nor could we find its PowerShell equivalent."
            }

            # Stop the script if the command was found but it returned an alias. 
            # Sometimes, a command may not return a source but WILL return an alias. This will cause issues with incompatibility with..
            # ..parameters for said commands.
            #
            # Example commands that will not work: echo, history, and cd
            if ($getPSCommand.CommandType -eq 'Alias') {
                Throw "Start-Command: This function does not support Aliases. Command $Name matches $($getPSCommand.ResolvedCommand.Name)."
            }

            # This function does not support Microsoft PowerShell commands.
            if ($getPSCommand.Source -like "Microsoft.PowerShell*") {
                Throw "Start-Command: This function should only be used for Non-PowerShell commands (e.g., wget, touch, mkdir, etc.)"
            }

            # Retrieve the version of PowerShell and its location and replace $FileName with it
            $FileName = $PSVersionTable.PSEdition -eq 'Core' ? (Get-Command -Name 'pwsh').Source : (Get-Command -Name 'powershell').Source
            
            # Reconfigure Arguments to execute PowerShell
            $Arguments = "-noprofile -Command `"& {$($getPSCommand.ReferencedCommand.Name) $Arguments}`""
        }

        # Data Object will store all streams of data from our command
        $dataObject = [pscustomobject]@{
            Title        = $Name
            OutputStream = ''
            OutputData   = ''
            ErrorData    = ''
            ExitCode     = 0
        }
    }
    process {

        $processStartInfoProps = @{
            Arguments               = $null -ne $Arguments ? $Arguments : $null
            CreateNoWindow          = $true
            ErrorDialog             = $false
            FileName                = $FileName
            RedirectStandardError   = $true
            RedirectStandardInput   = $true
            RedirectStandardOutput  = $true
            UseShellExecute         = $false
            WindowStyle             = [System.Diagnostics.ProcessWindowStyle]::Hidden
            WorkingDirectory        = $PSBoundParameters.ContainsKey('WorkingDirectory') ? $WorkingDirectory : $PSScriptRoot
            Verb                    = $PSBoundParameters.ContainsKey('Verb') ? $Verb : $null
        }

        # This will Error on Unix/Linux Systems if property LoadUserProfile is added regardless if it's null or false.
        if ($PSBoundParameters.ContainsKey('LoadUserProfile')) {
            $processStartInfoProps.Add('LoadUserProfile', $LoadUserProfile)
        }

        try {

            $process = New-Object System.Diagnostics.Process
            $process.EnableRaisingEvents = $true

            $processStartInfo = New-Object System.Diagnostics.ProcessStartInfo -Property $processStartInfoProps
            $process.StartInfo = $processStartInfo

            # Register Process OutputDataReceived:
            #   This will create a background job to capture output data
            #   Reference: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.standardoutput?redirectedfrom=MSDN&view=net-7.0#System_Diagnostics_Process_StandardOutput
            $outputEventParams = @{
                InputObject = $process
                SourceIdentifier = 'OnOutputDataReceived '
                EventName = 'OutputDataReceived'
                Action = {
                    param (
                        [System.Object]$sender,
                        [System.Diagnostics.DataReceivedEventArgs]$e
                    )

                    foreach ($data in $e.Data) { 
                        if ($null -ne $data -and $data -ne "") { 
                            $($data).Trim()
                        } 
                    }
                }
            }
            $dataObject.OutputStream = Register-ObjectEvent @outputEventParams

            # Start the process/command
            if ($process.Start()) {
                $process.BeginOutputReadLine()
                $dataObject.ErrorData = $process.StandardError.ReadToEnd()

                if ($PSCmdlet.ParameterSetName -eq 'timer') {
                    $process.WaitForExit($Timer) | Out-Null
                } else {
                    $process.WaitForExit()
                }
            }
            
            # Retrieve the exit code and the OutputStream Job
            $dataObject.ExitCode = $process.ExitCode
            $dataObject.OutputData = Receive-Job -id $($dataObject.OutputStream.id)

            [bool]$hasError = ($null -ne $($dataObject.ErrorData) -and $($dataObject.ErrorData) -ne "" -and $($dataObject.ExitCode) -ne 0) ? $true : $false
            [bool]$hasOutput = ($null -ne $($dataObject.OutputData) -and $($dataObject.OutputData) -ne "") ? $true : $false

            # Output the PSCustomObject if -Passthru is provided.
            if ($Passthru) {
                if ($hasError) {
                    $dataObject.ErrorData = $($dataObject.ErrorData.Trim())
                }
                $dataObject
            } else {

                if ($hasError) {
                    if ($($ErrorActionPreference) -ne 'Stop') {
                        Write-Error "Exit Code $($dataObject.ExitCode): $($dataObject.ErrorData.Trim())"
                    } else {
                        Throw "Exit Code $($dataObject.ExitCode): $($dataObject.ErrorData.Trim())"
                    }
                }

                if ($hasOutput) {
                    $($dataObject.OutputData)
                }
            }
        }
        finally {

            # Cleanup
            $process.Close()
            Unregister-Event -SourceIdentifier $($dataObject.OutputStream.Name) -Force | Out-Null
            Remove-Job -Id $($dataObject.OutputStream.Id) -Force
        }
    }
}

예 1: 정기적인 사용

Start-Command -Name 'docker' -Arguments 'container ls --all'

예 2: 쉼표로 구분된 인수

Start-Command -Name 'docker' -Arguments 'container', 'ls', '--all'

예 3: 경유 사용

$whoami = Start-Command -Name 'whoami' -Passthru

$whoami

Title        : whoami
OutputStream : System.Management.Automation.PSEventJob
OutputData   : zac
ErrorStream  : 
ErrorData    : 
ExitCode     : 0

예 4: 오류 예

Start-Command -Name 'docker' -Arguments 'force' -ErrorAction Stop

Output: 
Line |
 245 |  …             Throw "Exit Code $($dataObject.ExitCode): $($dataObject.E …
     |                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Exit Code 1: docker: 'force' is not a docker command. See 'docker --help'

언급URL : https://stackoverflow.com/questions/24370814/how-to-capture-process-output-asynchronously-in-powershell

반응형