我想从我在Powershell脚本中启动的进程中捕获stdout和stderr,并将其异步显示到控制台。我通过MSDN和other blogs找到了一些关于这样做的文档。
创建并运行下面的示例后,我似乎无法异步显示任何输出。所有输出仅在进程终止时显示。
$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等。所有这些都具有相同的效果,所以我还以为我有一些简单的东西可以使用#39 ; m不理解或错过。还需要做什么才能异步读取stdout?
答案 0 :(得分:30)
不幸的是,如果你想要正确地进行异步读取并不容易。如果你在没有超时的情况下调用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和退出代码。用法示例:
$oResult = Invoke-Executable -sExeFile 'ping.exe' -cArgs @('8.8.8.8', '-a')
$oResult | Format-List -Force
有关更多信息和替代实现(在C#中),请阅读this blog post。
答案 1 :(得分:9)
基于Alexander Obersht's answer我创建了一个使用超时和异步Task类而不是事件处理程序的函数。 根据{{3}}
不幸的是,这种方法(事件处理程序)无法知道 当收到最后一位数据时。因为一切都是 异步,有可能(我已观察到这一点)事件 在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
}
答案 2 :(得分:2)
我无法使用PS 4.0中的任何一个示例。
我想从Octopus Deploy软件包中运行puppet apply
(通过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
答案 3 :(得分:0)
这里的示例都很有用,但并不完全适合我的用例。我不想调用命令并退出。我想打开命令提示符,发送输入,读取输出,然后重复。这是我的解决方案。
创建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")
答案 4 :(得分:0)
我是来这里寻找解决方案的,以创建一个包装器来记录该过程并将其输出到屏幕。这些都不对我有用。我编写了这段代码,看起来不错。
PSDataCollection允许您继续执行脚本,而不必等待过程完成。
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()
答案 5 :(得分:0)
如果您只想将其动态转储到 PowerShell 控制台,请执行以下操作:
my.exe | Out-Default
我不能声称已经弄清楚了。
$LASTEXITCODE 还填充了我的 exe 中的退出代码,这也是我所需要的。