是否有任何与 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 包中也提供此功能 非常重要:在processprocess.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&lt;int&gt; 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&lt;string&gt; 形式的参数(类似于 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 等效的异步?的主要内容,如果未能解决你的问题,请参考以下文章

Process.Start 功能

我必须处理 Process.Start(url) 吗?

C# 中的 Process.Start 与 Process `p = new Process()`?

Process.Start() 啥都不做

使用 process.start 后如何退出主程序?

使用 Process.start 设置环境变量