就像标题所暗示的那样,Process.Start
我可以等待(允许您运行另一个应用程序或批处理文件)吗?
我正在玩一个小型控制台应用程序,这似乎是使用 async 和 await 的理想场所,但我找不到这种情况的任何文档。
我在想的是这些方面的东西:
void async RunCommand()
{
var result = await Process.RunAsync("command to run");
}
就像标题所暗示的那样,Process.Start
我可以等待(允许您运行另一个应用程序或批处理文件)吗?
我正在玩一个小型控制台应用程序,这似乎是使用 async 和 await 的理想场所,但我找不到这种情况的任何文档。
我在想的是这些方面的东西:
void async RunCommand()
{
var result = await Process.RunAsync("command to run");
}
Process.Start()
只开始这个过程,它不会等到它完成,所以它没有多大意义async
。如果你仍然想这样做,你可以做类似的事情await Task.Run(() => Process.Start(fileName))
。
但是,如果您想异步等待进程完成,您可以将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;
}
这是我的看法,基于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;
}
在.Net 5.0 中,官方内置了WaitForExitAsync方法,因此您不必自己实现。此外,Start方法现在接受参数 as 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;
}
我已经建立了一个类来启动一个流程,并且由于各种需求,它在过去几年中不断增长。在使用过程中,我发现 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;
}
}
这是另一种方法。与svick和Ohad 的答案类似的概念,但在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
}
我认为你应该使用的是:
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;
}
}
在 .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);
我真的很担心进程的处理,等待退出异步呢?这是我的建议(基于之前的):
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;
}
}