C# 是否有与 Process.Start 等效的异步?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/10788982/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Is there any async equivalent of Process.Start?
提问by linkerro
Like the title suggests, is there an equivalent to Process.Start(allows you run another application or batch file) that I can await?
就像标题所暗示的那样,是否有Process.Start我可以等待的等价物(允许您运行另一个应用程序或批处理文件)?
I'm playing with a small console app and this seemed like the perfect place to be using async and await but I can't find any documentation for this scenario.
我正在玩一个小型控制台应用程序,这似乎是使用 async 和 await 的理想场所,但我找不到有关此场景的任何文档。
What I'm thinking is something along these lines:
我在想的是这样的事情:
void async RunCommand()
{
var result = await Process.RunAsync("command to run");
}
采纳答案by svick
Process.Start()only starts the process, it doesn't wait until it finishes, so it doesn't make much sense to make it async. If you still want to do it, you can do something like await Task.Run(() => Process.Start(fileName)).
Process.Start()只启动进程,不会等到完成,所以 make it 没有多大意义async。如果你仍然想这样做,你可以做类似的事情await Task.Run(() => Process.Start(fileName))。
But, if you want to asynchronously wait for the process to finish, you can use the Exitedeventtogether with TaskCompletionSource:
但是,如果您想异步等待进程完成,您可以将Exited事件与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;
}
回答by Ohad Schneider
Here's my take, based on svick's answer. It adds output redirection, exit code retention, and slightly better error handling (disposing the Processobject even if it could not be started):
这是我的看法,基于svick 的回答。它添加了输出重定向、退出代码保留和稍微更好的错误处理(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;
}
回答by Brandon
Here's another approach. Similar concept to svickand Ohad'sanswers but using an extension method on the Processtype.
这是另一种方法。与svick和Ohad 的答案相似的概念,但在Process类型上使用了扩展方法。
Extension method:
扩展方法:
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;
}
Example use case in a containing method:
包含方法中的示例用例:
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
}
回答by Johann Medina
Im really worried about disposal of process, what about wait for exit async?, this is my proposal (based on previous):
我真的很担心进程的处理,等待退出异步怎么办?这是我的建议(基于以前的):
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;
}
}
Then, use it like this:
然后,像这样使用它:
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;
}
}
回答by Apfelkuacha
I have built a class to start a process and it was growing over the last years because of various requirements. During the usage I found out several issues with the Process class with disposing and even reading the ExitCode. So this is all fixed by my class.
我已经建立了一个类来启动一个过程,并且由于各种要求,它在过去几年中不断增长。在使用过程中,我发现了 Process 类的几个问题,包括处理甚至读取 ExitCode。所以这一切都由我的班级解决了。
The class has several possibilities, for example reading output, start as Admin or different user, catch Exceptions and also start all this asynchronous incl. Cancellation. Nice is that reading output is also possible during execution.
该类有多种可能性,例如读取输出、以管理员或不同用户身份启动、捕获异常并启动所有这些异步包括。消除。不错的是,在执行期间也可以读取输出。
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;
}
}
回答by Konstantin S.
I think all you should use is this:
我认为你应该使用的是:
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);
}
}
}
Usage example:
用法示例:
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;
}
}

