Is there any async equivalent of Process.Start?
就像标题所暗示的,我可以等待的等效于
我正在玩一个小型控制台应用程序,这似乎是使用异步和等待的理想场所,但是我找不到这种情况的任何文档。
我在想的是以下几方面的东西:
1 2 3 4 | void async RunCommand() { var result = await Process.RunAsync("command to run"); } |
但是,如果要异步等待过程完成,则可以将
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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; } |
这是我根据svick的回答得出的结论。它增加了输出重定向,退出代码保留和稍微更好的错误处理(即使无法启动,也放置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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; } |
我建立了一个类来开始一个过程,由于各种要求,在过去的几年中它一直在增长。在使用过程中,我发现Process类在处理甚至读取ExitCode方面存在一些问题。因此,这完全由我的班级解决。
该类具有多种可能性,例如读取输出,以Admin或其他用户身份启动,捕获异常以及启动所有这些异步incl。消除。很好的是,在执行期间也可以读取输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | 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; } } |
这是另一种方法。与svick和Ohad的答案类似的概念,但对
扩展方式:
1 2 3 4 5 6 7 8 9 | 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; } |
包含方法中的示例用例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 } |
我认为您应该使用的是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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); } } } |
用法示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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; } } |
我真的很担心进程的处理,如何等待异步退出呢?这是我的建议(基于先前的建议):
1 2 3 4 5 6 7 8 9 10 | 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; } } |
然后,像这样使用它:
1 2 3 4 5 6 7 8 9 10 11 12 | 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; } } |