深受 Stackoverflow 上其他问题的影响,我最终采用了这种从 Powershell 脚本启动进程的方法
function global:system-diagnostics-processstartinfo {
[CmdletBinding(SupportsShouldProcess=$True,ConfirmImpact='Low')]
param
(
[Parameter(Mandatory=$True,HelpMessage='Full path to exectuable')]
[Alias('exectuable')]
[string]$exe,
[Parameter(Mandatory=$True,HelpMessage='All arguments to be sent to exectuable')]
[Alias('args')]
[string]$arguments
)
if (!(Test-Path $exe)) {
$log.errorFormat("Did not find exectuable={0}, aborting script", $exe)
exit 1
}
$log.infoFormat("Start exectuable={0} with arguments='{1}'", $exe, $arguments)
$processStartInfo = New-Object System.Diagnostics.ProcessStartInfo($exe)
$processStartInfo.FileName = $exe
$processStartInfo.RedirectStandardError = $true
$processStartInfo.RedirectStandardOutput = $true
$processStartInfo.UseShellExecute = $false
$processStartInfo.Arguments = $arguments
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $processStartInfo
$log.info("Start exectuable and wait for exit")
$p.Start() | Out-Null
#$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
$log.infoFormat("exectuable={0} stdout: {1}", $exe, $stdout)
$log.debugFormat("exectuable={0} stderr: {1}", $exe,$stderr)
$global:ExitCode = $p.ExitCode
$log.debugFormat("exectuable={0} Exitcode: {1}", $exe, $p.ExitCode)
return $stdout
}
非常简单,添加了一些日志记录等。它适用于我当前的所有用例,除了一个。我创建了一个脚本,将 Confluence 生产实例的数据库转储复制到测试服务器。然后它使用上面的方法删除现有数据库,一切正常。但实际的恢复只是永远挂起。所以现在我必须退出脚本,然后手动运行以下命令
d:\postgresql\bin\pg_restore.exe -U postgres -d confluencedb -v -1 d:\temp\latest-backup.pgdump
需要一些时间,产出也相当多。这让我相信一定有以下任一原因导致了问题
有类似经历的人可以帮我解决这个问题。它将能够安排导入,而不必像今天一样手动执行。
处理后我必须立即执行以下操作。开始:
# Capture output during process execution so we don't hang
# if there is too much output.
do
{
if (!$process.StandardOutput.EndOfStream)
{
[void]$StdOut.AppendLine($process.StandardOutput.ReadLine())
}
if (!$process.StandardError.EndOfStream)
{
[void]$StdErr.AppendLine($process.StandardError.ReadLine())
}
Start-Sleep -Milliseconds 10
}
while (!$process.HasExited)
# Capture any standard output generated between our last poll and process end.
while (!$process.StandardOutput.EndOfStream)
{
[void]$StdOut.AppendLine($process.StandardOutput.ReadLine())
}
# Capture any error output generated between our last poll and process end.
while (!$process.StandardError.EndOfStream)
{
[void]$StdErr.AppendLine($process.StandardError.ReadLine())
}
# Wait for the process to exit.
$process.WaitForExit()
LogWriteFunc ("END process: " + $ProcessName)
if ($process.ExitCode -ne 0)
{
LogWriteFunc ("Error: Script execution failed: " + $process.ExitCode )
$FuncResult = 1
}
# Log and display any standard output.
if ($StdOut.Length -gt 0)
{
LogWriteFunc ($StdOut.ToString())
}
# Log and display any error output.
if ($StdErr.Length -gt 0)
{
LogWriteFunc ($StdErr.ToString())
}
Greg Prosch提供的答案可能会陷入僵局。如此处所述:
如果没有缓冲任何字符,StreamReader.EndOfStream 属性将调用 StandardOutput.Read()。如果进程没有向其输出管道发送任何内容并且没有关闭它,则 Read() 调用将阻塞。此代码采用了
Microsoft 文档 关于防止死锁的建议,以及 这篇文章 关于确保捕获所有输出的建议:
$global:stdErr = [System.Text.StringBuilder]::new()
$global:myprocessrunning = $true
Function Invoke-CustomCommand
{
Param (
$commandPath,
$commandArguments,
$workingDir = (Get-Location),
$path = @()
)
$global:stdErr.Clear()
$global:myprocessrunning = $true
$path += $env:PATH
$newPath = $path -join [IO.Path]::PathSeparator
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $commandPath
$pinfo.WorkingDirectory = $workingDir
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.StandardOutputEncoding = [System.Text.Encoding]::UTF8
$pinfo.StandardErrorEncoding = [System.Text.Encoding]::UTF8
$pinfo.UseShellExecute = $false
$pinfo.Arguments = $commandArguments
$pinfo.EnvironmentVariables["PATH"] = $newPath
$p = New-Object System.Diagnostics.Process
# https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.standardoutput
# Reading from one stream must be async
# We read the error stream, because events can be handled out of order,
# and it is better to have this happen with debug output
Register-ObjectEvent -InputObject $p -EventName "ErrorDataReceived" -Action {
$global:stdErr.AppendLine($EventArgs.Data)
} | Out-Null
# We must wait for the Exited event rather than WaitForExit()
# because WaitForExit() can result in events being missed
# https://stackoverflow.com/questions/13113624/captured-output-of-command-run-by-powershell-is-sometimes-incomplete
Register-ObjectEvent -InputObject $p -EventName "Exited" -action {
$global:myprocessrunning = $false
} | Out-Null
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.BeginErrorReadLine()
# Wait 10 minutes before forcibly killing the process
$processTimeout = 1000 * 60 * 10
while (($global:myprocessrunning -eq $true) -and ($processTimeout -gt 0))
{
# We must use lots of shorts sleeps rather than a single long one otherwise events are not processed
$processTimeout -= 50
Start-Sleep -m 50
}
$output = $p.StandardOutput.ReadToEnd()
if ($processTimeout -le 0)
{
$p.Kill()
}
$executionResults = [pscustomobject]@{
StdOut = $output
StdErr = $global:stdErr.ToString()
ExitCode = $p.ExitCode
}
return $executionResults
}