簡體   English   中英

使用 Start-Process 捕獲標准輸出和錯誤

[英]Capturing standard out and error with Start-Process

訪問StandardErrorStandardOutput屬性時,PowerShell 的Start-Process命令是否存在錯誤?

如果我運行以下命令,我不會得到 output:

$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait
$process.StandardOutput
$process.StandardError

但是,如果我將 output 重定向到一個文件,我會得到預期的結果:

$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait -RedirectStandardOutput stdout.txt -RedirectStandardError stderr.txt

出於某種原因,這就是Start-Process的設計方式。 這是一種無需發送到文件即可獲取它的方法:

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "ping.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "localhost"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode

在問題中給出的代碼中,我認為讀取啟動變量的 ExitCode 屬性應該可以。

$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait
$process.ExitCode

請注意(如在您的示例中),您需要添加-PassThru-Wait參數(這讓我很困惑)。

重要的:

我們一直在使用LPG提供的上述功能。

但是,這包含您在啟動生成大量輸出的進程時可能會遇到的錯誤。 因此,您在使用此功能時可能會遇到死鎖。 而是使用下面的改編版本:

Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
  Try {
    $pinfo = New-Object System.Diagnostics.ProcessStartInfo
    $pinfo.FileName = $commandPath
    $pinfo.RedirectStandardError = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute = $false
    $pinfo.Arguments = $commandArguments
    $p = New-Object System.Diagnostics.Process
    $p.StartInfo = $pinfo
    $p.Start() | Out-Null
    [pscustomobject]@{
        commandTitle = $commandTitle
        stdout = $p.StandardOutput.ReadToEnd()
        stderr = $p.StandardError.ReadToEnd()
        ExitCode = $p.ExitCode
    }
    $p.WaitForExit()
  }
  Catch {
     exit
  }
}

可以在 MSDN上找到有關此問題的更多信息:

如果父進程在 p.StandardError.ReadToEnd 之前調用 p.WaitForExit 並且子進程寫入足夠的文本來填充重定向的流,則可能導致死鎖情況。 父進程將無限期地等待子進程退出。 子進程將無限期地等待父進程從完整的 StandardError 流中讀取。

我也遇到了這個問題,最終使用Andy 的代碼創建了一個函數,以便在需要運行多個命令時進行清理。

它將標准錯誤、標准輸出和退出代碼作為對象返回。 需要注意的一件事:該函數不會接受路徑中的.\ 必須使用完整路徑。

Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
    $pinfo = New-Object System.Diagnostics.ProcessStartInfo
    $pinfo.FileName = $commandPath
    $pinfo.RedirectStandardError = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute = $false
    $pinfo.Arguments = $commandArguments
    $p = New-Object System.Diagnostics.Process
    $p.StartInfo = $pinfo
    $p.Start() | Out-Null
    $p.WaitForExit()
    [pscustomobject]@{
        commandTitle = $commandTitle
        stdout = $p.StandardOutput.ReadToEnd()
        stderr = $p.StandardError.ReadToEnd()
        ExitCode = $p.ExitCode
    }
}

以下是如何使用它:

$DisableACMonitorTimeOut = Execute-Command -commandTitle "Disable Monitor Timeout" -commandPath "C:\Windows\System32\powercfg.exe" -commandArguments " -x monitor-timeout-ac 0"

我真的對安迪·阿里斯門迪液化石油氣的這些例子感到困惑。 您應該始終使用:

$stdout = $p.StandardOutput.ReadToEnd()

打電話之前

$p.WaitForExit()

一個完整的例子是:

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "ping.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "localhost"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
$p.WaitForExit()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode

這是從另一個powershell進程(序列化)獲取輸出的一種笨拙方法:

start-process -wait -nonewwindow powershell 'ps | Export-Clixml out.xml'; import-clixml out.xml

要同時獲得標准輸出和標准錯誤,我使用:

Function GetProgramOutput([string]$exe, [string]$arguments)
{
    $process = New-Object -TypeName System.Diagnostics.Process
    $process.StartInfo.FileName = $exe
    $process.StartInfo.Arguments = $arguments

    $process.StartInfo.UseShellExecute = $false
    $process.StartInfo.RedirectStandardOutput = $true
    $process.StartInfo.RedirectStandardError = $true
    $process.Start()

    $output = $process.StandardOutput.ReadToEnd()   
    $err = $process.StandardError.ReadToEnd()

    $process.WaitForExit()

    $output
    $err
}

$exe = "cmd"
$arguments = '/c echo hello 1>&2'   #this writes 'hello' to stderr

$runResult = (GetProgramOutput $exe $arguments)
$stdout = $runResult[-2]
$stderr = $runResult[-1]

[System.Console]::WriteLine("Standard out: " + $stdout)
[System.Console]::WriteLine("Standard error: " + $stderr)

這是我根據其他人在此線程上發布的示例編寫的。 此版本將隱藏控制台窗口並提供輸出顯示選項。

function Invoke-Process {
    [CmdletBinding(SupportsShouldProcess)]
    param
        (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$FilePath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ArgumentList,

        [ValidateSet("Full","StdOut","StdErr","ExitCode","None")]
        [string]$DisplayLevel
        )

    $ErrorActionPreference = 'Stop'

    try {
        $pinfo = New-Object System.Diagnostics.ProcessStartInfo
        $pinfo.FileName = $FilePath
        $pinfo.RedirectStandardError = $true
        $pinfo.RedirectStandardOutput = $true
        $pinfo.UseShellExecute = $false
        $pinfo.WindowStyle = 'Hidden'
        $pinfo.CreateNoWindow = $true
        $pinfo.Arguments = $ArgumentList
        $p = New-Object System.Diagnostics.Process
        $p.StartInfo = $pinfo
        $p.Start() | Out-Null
        $result = [pscustomobject]@{
        Title = ($MyInvocation.MyCommand).Name
        Command = $FilePath
        Arguments = $ArgumentList
        StdOut = $p.StandardOutput.ReadToEnd()
        StdErr = $p.StandardError.ReadToEnd()
        ExitCode = $p.ExitCode
        }
        $p.WaitForExit()

        if (-not([string]::IsNullOrEmpty($DisplayLevel))) {
            switch($DisplayLevel) {
                "Full" { return $result; break }
                "StdOut" { return $result.StdOut; break }
                "StdErr" { return $result.StdErr; break }
                "ExitCode" { return $result.ExitCode; break }
                }
            }
        }
    catch {
        exit
        }
}

示例: Invoke-Process -FilePath "FQPN" -ArgumentList "ARGS" -DisplayLevel Full

改進的答案- 只要您對Start-Job而不是Start-Process問題

事實證明,STDOUT 和 STDERR 在腳本運行時累積在字符串數組$job.ChildJobs[0].Output$job.ChildJobs[0].Error .Error 中。 因此,您可以輪詢這些值並定期將它們寫出。 可能有點黑客,但它有效。

雖然它不是一個流,但您必須手動跟蹤數組的起始索引。

這段代碼比我原來的答案更簡單,最后你在$job.ChildJobs[0].Output中有整個 STDOUT。 作為這個演示的一點好處,調用腳本是 PS7,后台作業是 PS5。

$scriptBlock = {
  Param ([int]$param1, [int]$param2)
  $PSVersionTable
  Start-Sleep -Seconds 1
  $param1 + $param2
}

$parameters = @{
  ScriptBlock = $scriptBlock
  ArgumentList = 1, 2
  PSVersion = 5.1 # <-- remove this line for PS7
}

$timeoutSec = 5
$job = Start-Job @parameters
$job.ChildJobs[0].Output
$index = $job.ChildJobs[0].Output.Count

while ($job.JobStateInfo.State -eq [System.Management.Automation.JobState]::Running) {
  Start-Sleep -Milliseconds 200
  $job.ChildJobs[0].Output[$index]
  $index = $job.ChildJobs[0].Output.Count
  if (([DateTime]::Now - $job.PSBeginTime).TotalSeconds -gt $timeoutSec) {
    throw "Job timed out."
  }
}

正如所指出的,我的原始答案可以交錯輸出。 這是 PowerShell 中事件處理的一個限制。 這不是一個可以解決的問題。

原始答案,不要使用-只是將其留在這里以引起興趣

如果有超時, ReadToEnd()不是一個選項。 你可以做一些花哨的循環,但 IMO 最“干凈”的方法是忽略流。 而是掛鈎OutputDataReceived / ErrorDataReceived事件,收集輸出。 這種方法也避免了其他人提到的線程問題。

這在 C# 中很簡單,但在 Powershell 中卻很棘手且冗長。 特別是add_OutputDataReceived由於某種原因不可用。 (不確定這是錯誤還是功能,至少在 PowerShell 5.1 中似乎是這種情況。)要解決它,您可以使用Register-ObjectEvent

$stdout = New-Object System.Text.StringBuilder
$stderr = New-Object System.Text.StringBuilder

$proc = [System.Diagnostics.Process]@{
  StartInfo = @{
    FileName = 'ping.exe'
    Arguments = 'google.com'
    RedirectStandardOutput = $true
    RedirectStandardError = $true
    UseShellExecute = $false
    WorkingDirectory = $PSScriptRoot
  }
}

$stdoutEvent = Register-ObjectEvent $proc -EventName OutputDataReceived -MessageData $stdout -Action {
  $Event.MessageData.AppendLine($Event.SourceEventArgs.Data)
}

$stderrEvent = Register-ObjectEvent $proc -EventName ErrorDataReceived -MessageData $stderr -Action {
  $Event.MessageData.AppendLine($Event.SourceEventArgs.Data)
}

$proc.Start() | Out-Null
$proc.BeginOutputReadLine()
$proc.BeginErrorReadLine()
Wait-Process -Id $proc.Id -TimeoutSec 5

if ($proc.HasExited) {
  $exitCode = $proc.ExitCode
}
else {
  Stop-Process -Force -Id $proc.Id
  $exitCode = -1
}

# Be sure to unregister.  You have been warned.
Unregister-Event $stdoutEvent.Id
Unregister-Event $stderrEvent.Id
Write-Output $stdout.ToString()
Write-Output $stderr.ToString()
Write-Output "Exit code: $exitCode"
  • 顯示的代碼是快樂的路徑(stderr 為空)
  • 要測試超時路徑,請將-TimeoutSec設置為.5
  • 要測試悲傷的路徑(stderr 有內容),請將FileName設置為'cmd'並將Arguments設置為/C asdf

這是我的函數版本,它返回具有 3 個新屬性的標准 System.Diagnostics.Process

Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
    Try {
        $pinfo = New-Object System.Diagnostics.ProcessStartInfo
        $pinfo.FileName = $commandPath
        $pinfo.RedirectStandardError = $true
        $pinfo.RedirectStandardOutput = $true
        $pinfo.UseShellExecute = $false
        $pinfo.WindowStyle = 'Hidden'
        $pinfo.CreateNoWindow = $True
        $pinfo.Arguments = $commandArguments
        $p = New-Object System.Diagnostics.Process
        $p.StartInfo = $pinfo
        $p.Start() | Out-Null
        $stdout = $p.StandardOutput.ReadToEnd()
        $stderr = $p.StandardError.ReadToEnd()
        $p.WaitForExit()
        $p | Add-Member "commandTitle" $commandTitle
        $p | Add-Member "stdout" $stdout
        $p | Add-Member "stderr" $stderr
    }
    Catch {
    }
    $p
}

您可能還想考慮將 & 運算符與--%結合使用,而不是 start-process - 這使您可以輕松地 pipe 並處理命令和/或錯誤 output。

  • 將轉義參數放入變量中
  • 將 arguments 放入變量中
$deploy= "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe"
$esc = '--%'
$arguments ="-source:package='c:\temp\pkg.zip' -verb:sync"
$output = & $deploy $esc $arguments 

這將參數無干擾地傳遞給可執行文件,讓我解決了啟動過程的問題。

將 Stderr 和 Stdout 組合成一個變量:

$output = & $deploy $esc $arguments 2>&1

為 Stderr 和 Stdout 獲取單獨的變量

$err = $( $output = & $deploy $esc $arguments) 2>&1

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM