Как следует из названия, есть ли эквивалент Process.Start
(позволяющий запускать другое приложение или пакетный файл), которого я могу ждать?
Я играю с небольшим консольным приложением, и это показалось мне идеальным местом для использования async и await, но я не могу найти никакой документации для этого сценария.
Я думаю о чем-то в этом роде:
void async RunCommand()
{
var result = await Process.RunAsync("command to run");
}
c#
async-await
c#-5.0
линкерро
источник
источник
Process.Start
является асинхронным и ОП , как представляется , хотят синхронную версию.Ответы:
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; }
источник
process
иprocess.StartInfo
меняет то, что происходит при его запуске.Start()
. Если вы, например, позвоните.EnableRaisingEvents = true
перед установкойStartInfo
свойств, как показано здесь, все будет работать должным образом. Если вы установите его позже, например, чтобы сохранить его вместе.Exited
, даже если вы вызываете его раньше.Start()
, он не работает должным образом -.Exited
запускается немедленно, а не дожидается фактического завершения процесса. Не знаю почему, просто предостережение.process.SynchronizingObject
должен быть установлен компонент формы, чтобы методы, обрабатывающие события (такие как Exited, OutputDataReceived, ErrorDataReceived), не вызывались в отдельном потоке.Process.Start
вTask.Run
. Например, путь UNC будет разрешен синхронно. Этот фрагмент может занять до 30 секунд:Process.Start(@"\\live.sysinternals.com\whatever")
Вот мое мнение , основанное на ответе Свика . Он добавляет перенаправление вывода, сохранение кода выхода и немного лучшую обработку ошибок (удаление
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; }
источник
async Task<int> RunProcessAsync(string fileName, string args)
. Я адаптировал этот пример и передал три объекта один за другим. Как я могу дождаться повышения событий? например. прежде, чем мое приложение остановится .. большое спасибоDispose
обнуляет обработчик события, так что теоретически , если вы назвали ,Dispose
но сохранили ссылку вокруг, я считаю , что будет утечка. Однако, когда больше нет ссылок наProcess
объект и он собирается (мусор), нет никого, кто указывает на список обработчиков событий. Итак, он собирается, и теперь нет ссылок на делегатов, которые раньше были в списке, поэтому, наконец, они собирают мусор.Dispose
первых , очищает некоторые ресурсы, но не препятствует сохранению просочившейся ссылкиprocess
. Фактически, вы заметите, что этоprocess
относится к обработчикам, ноExited
обработчик также имеет ссылку наprocess
. В некоторых системах эта циклическая ссылка предотвратит сборку мусора, но алгоритм, используемый в .NET, по-прежнему позволит очистить его, пока все живет на «острове» без внешних ссылок.Вот еще один подход. Концепция, аналогичная ответам 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 }
источник
Я создал класс, чтобы начать процесс, и в последние годы он рос из-за различных требований. Во время использования я обнаружил несколько проблем с классом 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; } }
источник
Я думаю, все, что вам следует использовать, это следующее:
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
процесс?CancellationToken
вWaitForExitAsync
методе нужен просто, чтобы иметь возможность отменить ожидание или установить тайм-аут. Завершить процесс можно с помощьюStartProcessAsync
: `` try {await process.WaitForExitAsync (cancellationToken); } catch (OperationCanceledException) {process.Kill (); } `` `CancellationToken
, отмена токена должна приводить к отмене операции, а не к отмене ожидания. Это то, что обычно ожидает вызывающий метод. Если вызывающий хочет отменить только ожидание и позволить операции по-прежнему работать в фоновом режиме, это довольно легко сделать извне ( вот метод расширения,AsCancelable
который делает именно это).Я действительно беспокоюсь об удалении процесса, а как насчет ожидания выхода async? Это мое предложение (основанное на предыдущем):
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; } }
источник