是否有任何与 Process.Start 等效的异步?
Posted
技术标签:
【中文标题】是否有任何与 Process.Start 等效的异步?【英文标题】:Is there any async equivalent of Process.Start? 【发布时间】:2012-06-03 01:50:16 【问题描述】:正如标题所暗示的那样,有没有可以等待的 Process.Start
(允许您运行另一个应用程序或批处理文件)?
我正在玩一个小型控制台应用程序,这似乎是使用 async 和 await 的理想场所,但我找不到任何关于这种情况的文档。
我的想法是这样的:
void async RunCommand()
var result = await Process.RunAsync("command to run");
【问题讨论】:
为什么不对返回的 Process 对象使用 WaitForExit? 顺便说一句,听起来您更像是在寻找“同步”解决方案,而不是“异步”解决方案,因此标题具有误导性。 @YoryeNathan - 哈哈。事实上,Process.Start
是异步的,而 OP 似乎想要一个同步版本。
OP 正在讨论 C# 5 中新的 async/await 关键字
好的,我已经更新了我的帖子,使其更加清晰。为什么我想要这个的解释很简单。想象一个场景,你必须运行一个外部命令(比如 7zip)然后继续应用程序的流程。这正是 async/await 的目的,但似乎没有办法运行进程并等待它退出。
【参考方案1】:
Process.Start()
只是启动进程,它不会等到它完成,所以将它设为async
没有多大意义。如果您仍然想这样做,您可以执行await Task.Run(() => Process.Start(fileName))
之类的操作。
但是,如果你想异步等待进程完成,你可以使用the Exited
event和TaskCompletionSource
:
static Task<int> RunProcessAsync(string fileName)
var tcs = new TaskCompletionSource<int>();
var process = new Process
StartInfo = FileName = fileName ,
EnableRaisingEvents = true
;
process.Exited += (sender, args) =>
tcs.SetResult(process.ExitCode);
process.Dispose();
;
process.Start();
return tcs.Task;
【讨论】:
我终于开始在 github 上贴一些东西了——它没有任何取消/超时支持,但它至少会为你收集标准输出和标准错误。 github.com/jamesmanning/RunProcessAsTask MedallionShell NuGet 包中也提供此功能 非常重要:在process
和process.StartInfo
上设置各种属性的顺序会改变使用.Start()
运行它时发生的情况。例如,如果您在设置StartInfo
属性之前调用.EnableRaisingEvents = true
,如此处所示,事情会按预期工作。如果您稍后设置它,例如将其与.Exited
保持在一起,即使您在.Start()
之前调用它,它也无法正常工作-.Exited
立即触发而不是等待进程实际退出。不知道为什么,只是提醒一下。
@svick 在窗口窗体中,process.SynchronizingObject
应设置为窗体组件,以避免在单独的线程上调用处理事件的方法(例如 Exited、OutputDataReceived、ErrorDataReceived)。
它确实将Process.Start
包裹在Task.Run
中实际上是有意义的。例如,UNC 路径将被同步解析。此 sn-p 最多可能需要 30 秒才能完成:Process.Start(@"\\live.sysinternals.com\whatever")
【参考方案2】:
这是我的看法,基于svick's answer。它添加了输出重定向、退出代码保留和稍微更好的错误处理(即使 Process
对象无法启动也可以处理它):
public static async Task<int> RunProcessAsync(string fileName, string args)
using (var process = new Process
StartInfo =
FileName = fileName, Arguments = args,
UseShellExecute = false, CreateNoWindow = true,
RedirectStandardOutput = true, RedirectStandardError = true
,
EnableRaisingEvents = true
)
return await RunProcessAsync(process).ConfigureAwait(false);
private static Task<int> RunProcessAsync(Process process)
var tcs = new TaskCompletionSource<int>();
process.Exited += (s, ea) => tcs.SetResult(process.ExitCode);
process.OutputDataReceived += (s, ea) => Console.WriteLine(ea.Data);
process.ErrorDataReceived += (s, ea) => Console.WriteLine("ERR: " + ea.Data);
bool started = process.Start();
if (!started)
//you may allow for the process to be re-used (started = false)
//but I'm not sure about the guarantees of the Exited event in such a case
throw new InvalidOperationException("Could not start process: " + process);
process.BeginOutputReadLine();
process.BeginErrorReadLine();
return tcs.Task;
【讨论】:
刚刚找到了这个有趣的解决方案。由于我是 c# 新手,我不确定如何使用async Task<int> RunProcessAsync(string fileName, string args)
。我改编了这个例子,并一一传递了三个对象。我如何等待引发事件?例如。在我的应用程序停止之前.. 非常感谢
@marrrschine 我不明白你的意思,也许你应该用一些代码开始一个新问题,这样我们就可以看到你尝试了什么并从那里继续。
很棒的答案。感谢 svick 奠定了基础,感谢 Ohad 提供了这个非常有用的扩展。
@SuperJMN 阅读代码 (referencesource.microsoft.com/#System/services/monitoring/…) 我不相信 Dispose
会使事件处理程序无效,所以理论上如果您调用 Dispose
但保留引用,我相信那将是泄漏。但是,当没有更多对Process
对象的引用并且它被(垃圾)收集时,没有指向事件处理程序列表的人。所以它被收集了,现在没有对曾经在列表中的委托的引用,所以最后它们被垃圾收集了。
@SuperJMN:有趣的是,它比这更复杂/更强大。一方面,Dispose
清理了一些资源,但不会阻止泄露的引用保留process
。实际上,您会注意到process
引用了处理程序,但Exited
处理程序也引用了process
。在某些系统中,循环引用会阻止垃圾收集,但 .NET 中使用的算法仍然允许将其全部清理,只要所有内容都位于没有外部引用的“孤岛”上。【参考方案3】:
在.Net 5.0中,官方内置了WaitForExitAsync方法,不用自己实现。此外,Start 方法现在接受 IEnumerable<string>
形式的参数(类似于 Python/Golang 等其他编程语言)。
这是一个例子:
public static async Task YourMethod()
var p = Process.Start("bin_name", new[]"arg1", "arg2", "arg3");
await p.WaitForExitAsync().ConfigureAwait(false);
// more code;
【讨论】:
从 .net core 3.1 迁移到 .net 5【参考方案4】:我已经建立了一个类来启动一个流程,并且由于各种需求,它在过去几年中不断增长。在使用过程中,我发现 Process 类在处理甚至读取 ExitCode 方面存在一些问题。所以这一切都是我的班级解决的。
该类有多种可能性,例如读取输出,以管理员或其他用户身份启动,捕获异常并启动所有这些异步,包括。消除。很好的是,在执行期间也可以读取输出。
public class ProcessSettings
public string FileName get; set;
public string Arguments get; set; = "";
public string WorkingDirectory get; set; = "";
public string InputText get; set; = null;
public int Timeout_milliseconds get; set; = -1;
public bool ReadOutput get; set;
public bool ShowWindow get; set;
public bool KeepWindowOpen get; set;
public bool StartAsAdministrator get; set;
public string StartAsUsername get; set;
public string StartAsUsername_Password get; set;
public string StartAsUsername_Domain get; set;
public bool DontReadExitCode get; set;
public bool ThrowExceptions get; set;
public CancellationToken CancellationToken get; set;
public class ProcessOutputReader // Optional, to get the output while executing instead only as result at the end
public event TextEventHandler OutputChanged;
public event TextEventHandler OutputErrorChanged;
public void UpdateOutput(string text)
OutputChanged?.Invoke(this, new TextEventArgs(text));
public void UpdateOutputError(string text)
OutputErrorChanged?.Invoke(this, new TextEventArgs(text));
public delegate void TextEventHandler(object sender, TextEventArgs e);
public class TextEventArgs : EventArgs
public string Text get;
public TextEventArgs(string text) Text = text;
public class ProcessResult
public string Output get; set;
public string OutputError get; set;
public int ExitCode get; set;
public bool WasCancelled get; set;
public bool WasSuccessful get; set;
public class ProcessStarter
public ProcessResult Execute(ProcessSettings settings, ProcessOutputReader outputReader = null)
return Task.Run(() => ExecuteAsync(settings, outputReader)).GetAwaiter().GetResult();
public async Task<ProcessResult> ExecuteAsync(ProcessSettings settings, ProcessOutputReader outputReader = null)
if (settings.FileName == null) throw new ArgumentNullException(nameof(ProcessSettings.FileName));
if (settings.Arguments == null) throw new ArgumentNullException(nameof(ProcessSettings.Arguments));
var cmdSwitches = "/Q " + (settings.KeepWindowOpen ? "/K" : "/C");
var arguments = $"cmdSwitches settings.FileName settings.Arguments";
var startInfo = new ProcessStartInfo("cmd", arguments)
UseShellExecute = false,
RedirectStandardOutput = settings.ReadOutput,
RedirectStandardError = settings.ReadOutput,
RedirectStandardInput = settings.InputText != null,
CreateNoWindow = !(settings.ShowWindow || settings.KeepWindowOpen),
;
if (!string.IsNullOrWhiteSpace(settings.StartAsUsername))
if (string.IsNullOrWhiteSpace(settings.StartAsUsername_Password))
throw new ArgumentNullException(nameof(ProcessSettings.StartAsUsername_Password));
if (string.IsNullOrWhiteSpace(settings.StartAsUsername_Domain))
throw new ArgumentNullException(nameof(ProcessSettings.StartAsUsername_Domain));
if (string.IsNullOrWhiteSpace(settings.WorkingDirectory))
settings.WorkingDirectory = Path.GetPathRoot(Path.GetTempPath());
startInfo.UserName = settings.StartAsUsername;
startInfo.PasswordInClearText = settings.StartAsUsername_Password;
startInfo.Domain = settings.StartAsUsername_Domain;
var output = new StringBuilder();
var error = new StringBuilder();
if (!settings.ReadOutput)
output.AppendLine($"Enable nameof(ProcessSettings.ReadOutput) to get Output");
if (settings.StartAsAdministrator)
startInfo.Verb = "runas";
startInfo.UseShellExecute = true; // Verb="runas" only possible with ShellExecute=true.
startInfo.RedirectStandardOutput = startInfo.RedirectStandardError = startInfo.RedirectStandardInput = false;
output.AppendLine("Output couldn't be read when started as Administrator");
if (!string.IsNullOrWhiteSpace(settings.WorkingDirectory))
startInfo.WorkingDirectory = settings.WorkingDirectory;
var result = new ProcessResult();
var taskCompletionSourceProcess = new TaskCompletionSource<bool>();
var process = new Process StartInfo = startInfo, EnableRaisingEvents = true ;
try
process.OutputDataReceived += (sender, e) =>
if (e?.Data != null)
output.AppendLine(e.Data);
outputReader?.UpdateOutput(e.Data);
;
process.ErrorDataReceived += (sender, e) =>
if (e?.Data != null)
error.AppendLine(e.Data);
outputReader?.UpdateOutputError(e.Data);
;
process.Exited += (sender, e) =>
try (sender as Process)?.WaitForExit(); catch (InvalidOperationException)
taskCompletionSourceProcess.TrySetResult(false);
;
var success = false;
try
process.Start();
success = true;
catch (System.ComponentModel.Win32Exception ex)
if (ex.NativeErrorCode == 1223)
error.AppendLine("AdminRights request Cancelled by User!! " + ex);
if (settings.ThrowExceptions) taskCompletionSourceProcess.SetException(ex); else taskCompletionSourceProcess.TrySetResult(false);
else
error.AppendLine("Win32Exception thrown: " + ex);
if (settings.ThrowExceptions) taskCompletionSourceProcess.SetException(ex); else taskCompletionSourceProcess.TrySetResult(false);
catch (Exception ex)
error.AppendLine("Exception thrown: " + ex);
if (settings.ThrowExceptions) taskCompletionSourceProcess.SetException(ex); else taskCompletionSourceProcess.TrySetResult(false);
if (success && startInfo.RedirectStandardOutput)
process.BeginOutputReadLine();
if (success && startInfo.RedirectStandardError)
process.BeginErrorReadLine();
if (success && startInfo.RedirectStandardInput)
var writeInputTask = Task.Factory.StartNew(() => WriteInputTask());
async void WriteInputTask()
var processRunning = true;
await Task.Delay(50).ConfigureAwait(false);
try processRunning = !process.HasExited; catch
while (processRunning)
if (settings.InputText != null)
try
await process.StandardInput.WriteLineAsync(settings.InputText).ConfigureAwait(false);
await process.StandardInput.FlushAsync().ConfigureAwait(false);
settings.InputText = null;
catch
await Task.Delay(5).ConfigureAwait(false);
try processRunning = !process.HasExited; catch processRunning = false;
if (success && settings.CancellationToken != default(CancellationToken))
settings.CancellationToken.Register(() => taskCompletionSourceProcess.TrySetResult(true));
if (success && settings.Timeout_milliseconds > 0)
new CancellationTokenSource(settings.Timeout_milliseconds).Token.Register(() => taskCompletionSourceProcess.TrySetResult(true));
var taskProcess = taskCompletionSourceProcess.Task;
await taskProcess.ConfigureAwait(false);
if (taskProcess.Result == true) // process was cancelled by token or timeout
if (!process.HasExited)
result.WasCancelled = true;
error.AppendLine("Process was cancelled!");
try
process.CloseMainWindow();
await Task.Delay(30).ConfigureAwait(false);
if (!process.HasExited)
process.Kill();
catch
result.ExitCode = -1;
if (!settings.DontReadExitCode) // Reason: sometimes, like when timeout /t 30 is started, reading the ExitCode is only possible if the timeout expired, even if process.Kill was called before.
try result.ExitCode = process.ExitCode;
catch output.AppendLine("Reading ExitCode failed.");
process.Close();
finally var disposeTask = Task.Factory.StartNew(() => process.Dispose()); // start in new Task because disposing sometimes waits until the process is finished, for example while executing following command: ping -n 30 -w 1000 127.0.0.1 > nul
if (result.ExitCode == -1073741510 && !result.WasCancelled)
error.AppendLine($"Process exited by user!");
result.WasSuccessful = !result.WasCancelled && result.ExitCode == 0;
result.Output = output.ToString();
result.OutputError = error.ToString();
return result;
【讨论】:
【参考方案5】:这是另一种方法。与svick 和Ohad's 答案类似的概念,但在Process
类型上使用了扩展方法。
扩展方法:
public static Task RunAsync(this Process process)
var tcs = new TaskCompletionSource<object>();
process.EnableRaisingEvents = true;
process.Exited += (s, e) => tcs.TrySetResult(null);
// not sure on best way to handle false being returned
if (!process.Start()) tcs.SetException(new Exception("Failed to start process."));
return tcs.Task;
包含方法中的示例用例:
public async Task ExecuteAsync(string executablePath)
using (var process = new Process())
// configure process
process.StartInfo.FileName = executablePath;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
// run process asynchronously
await process.RunAsync();
// do stuff with results
Console.WriteLine($"Process finished running at process.ExitTime with exit code process.ExitCode");
;// dispose process
【讨论】:
【参考方案6】:我认为你应该使用的是这个:
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace Extensions
public static class ProcessExtensions
public static async Task<int> WaitForExitAsync(this Process process, CancellationToken cancellationToken = default)
process = process ?? throw new ArgumentNullException(nameof(process));
process.EnableRaisingEvents = true;
var completionSource = new TaskCompletionSource<int>();
process.Exited += (sender, args) =>
completionSource.TrySetResult(process.ExitCode);
;
if (process.HasExited)
return process.ExitCode;
using var registration = cancellationToken.Register(
() => completionSource.TrySetCanceled(cancellationToken));
return await completionSource.Task.ConfigureAwait(false);
使用示例:
public static async Task<int> StartProcessAsync(ProcessStartInfo info, CancellationToken cancellationToken = default)
path = path ?? throw new ArgumentNullException(nameof(path));
if (!File.Exists(path))
throw new ArgumentException(@"File is not exists", nameof(path));
using var process = Process.Start(info);
if (process == null)
throw new InvalidOperationException("Process is null");
try
return await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
catch (OperationCanceledException)
process.Kill();
throw;
【讨论】:
接受CancellationToken
有什么意义,如果取消它不会Kill
进程?
WaitForExitAsync
方法中的 CancellationToken
只是为了能够取消等待或设置超时。杀死进程可以在StartProcessAsync
: ``` try await process.WaitForExitAsync(cancellationToken); catch(OperationCanceledException) process.Kill(); ```
我的观点是,当一个方法接受CancellationToken
时,取消令牌应该导致操作取消,而不是取消等待。这是该方法的调用者通常所期望的。如果调用者只想取消等待,并让操作仍在后台运行,那么在外部很容易做到(here 是一个扩展方法AsCancelable
就是这样做的)。
我认为这个决定应该由调用者做出(特别是针对这种情况,因为这个方法以Wait开头,总的来说我同意你的观点),就像新的Usage Example一样。【参考方案7】:
在 .NET 5 中,您可以调用 WaitForExitAsync
,但在 .NET Framework 中,该方法不存在。
我建议(即使您使用的是 .NET 5+)CliWrap 库,它提供开箱即用的异步支持(并希望能处理所有竞争条件),并且可以轻松执行管道和路由输出。
我最近才发现它,我必须说到目前为止我真的很喜欢它!
愚蠢的例子:
var cmd = Cli.Wrap(@"C:\test\app.exe")
.WithArguments("-foo bar")
.WithStandardOutputPipe(PipeTarget.ToFile(@"C:\test\stdOut.txt"))
.WithStandardErrorPipe(PipeTarget.ToDelegate(s => Debug.WriteLine(s)));
var result = await cmd.ExecuteAsync(cancellationToken);
Debug.WriteLine(result.ExitCode);
【讨论】:
【参考方案8】:我真的很担心进程的处理,等待退出异步呢?这是我的建议(基于之前的):
public static class ProcessExtensions
public static Task WaitForExitAsync(this Process process)
var tcs = new TaskCompletionSource<object>();
process.EnableRaisingEvents = true;
process.Exited += (s, e) => tcs.TrySetResult(null);
return process.HasExited ? Task.CompletedTask : tcs.Task;
然后,像这样使用它:
public static async Task<int> ExecAsync(string command, string args)
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = command;
psi.Arguments = args;
using (Process proc = Process.Start(psi))
await proc.WaitForExitAsync();
return proc.ExitCode;
【讨论】:
以上是关于是否有任何与 Process.Start 等效的异步?的主要内容,如果未能解决你的问题,请参考以下文章