Есть ли асинхронный эквивалент Process.Start?

146

Как следует из названия, есть ли эквивалент Process.Start(позволяющий запускать другое приложение или пакетный файл), которого я могу ждать?

Я играю с небольшим консольным приложением, и это показалось мне идеальным местом для использования async и await, но я не могу найти никакой документации для этого сценария.

Я думаю о чем-то в этом роде:

void async RunCommand()
{
    var result = await Process.RunAsync("command to run");
}
линкерро
источник
2
Почему бы вам просто не использовать WaitForExit для возвращаемого объекта Process?
SimpleVar
2
И, кстати, это больше похоже на то, что вы ищете «синхронизированное» решение, а не «асинхронное», поэтому название вводит в заблуждение.
SimpleVar
2
@YoryeNathan - смеется. В самом деле, Process.Start является асинхронным и ОП , как представляется , хотят синхронную версию.
Oded
12
OP говорит о новых ключевых словах async / await в C # 5
aquinas
4
Хорошо, я обновил свой пост, чтобы он стал более понятным. Объяснение того, почему я хочу это, простое. Представьте сценарий, в котором вам нужно запустить внешнюю команду (что-то вроде 7zip), а затем продолжить выполнение приложения. Это именно то, что должно было облегчить async / await, и все же, похоже, нет способа запустить процесс и дождаться его выхода.
linkerro

Ответы:

203

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;
}
Свик
источник
36
Наконец-то я нашел время прикрепить что-то для этого на github - у него нет поддержки отмены / тайм-аута, но он, по крайней мере, соберет стандартный вывод и стандартную ошибку для вас. github.com/jamesmanning/RunProcessAsTask
Джеймс Мэннинг,
3
Эта функция также доступна в пакете NuGet MedallionShell
ChaseMedallion
9
Действительно важно: порядок, в котором вы устанавливаете различные свойства, processи process.StartInfoменяет то, что происходит при его запуске .Start(). Если вы, например, позвоните .EnableRaisingEvents = trueперед установкой StartInfoсвойств, как показано здесь, все будет работать должным образом. Если вы установите его позже, например, чтобы сохранить его вместе .Exited, даже если вы вызываете его раньше .Start(), он не работает должным образом - .Exitedзапускается немедленно, а не дожидается фактического завершения процесса. Не знаю почему, просто предостережение.
Крис Москини
2
@svick В оконной форме process.SynchronizingObjectдолжен быть установлен компонент формы, чтобы методы, обрабатывающие события (такие как Exited, OutputDataReceived, ErrorDataReceived), не вызывались в отдельном потоке.
KevinBui
4
Это делает на самом деле имеет смысл , чтобы обернуть Process.Startв Task.Run. Например, путь UNC будет разрешен синхронно. Этот фрагмент может занять до 30 секунд:Process.Start(@"\\live.sysinternals.com\whatever")
Jabe
57

Вот мое мнение , основанное на ответе Свика . Он добавляет перенаправление вывода, сохранение кода выхода и немного лучшую обработку ошибок (удаление 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;
}
Охад Шнайдер
источник
1
только что нашел это интересное решение. Поскольку я новичок в C #, я не уверен, как использовать async Task<int> RunProcessAsync(string fileName, string args). Я адаптировал этот пример и передал три объекта один за другим. Как я могу дождаться повышения событий? например. прежде, чем мое приложение остановится .. большое спасибо
marrrschine
3
@marrrschine Я не совсем понимаю, что вы имеете в виду, возможно, вам следует начать новый вопрос с какого-то кода, чтобы мы могли увидеть, что вы пробовали, и продолжить оттуда.
Охад Шнайдер
4
Фантастический ответ. Спасибо, svick, за основу, и спасибо, Охад, за это очень полезное расширение.
Гордон Бин
1
@SuperJMN считывания кода ( referencesource.microsoft.com/#System/services/monitoring/... ) Я не верю , что Disposeобнуляет обработчик события, так что теоретически , если вы назвали , Disposeно сохранили ссылку вокруг, я считаю , что будет утечка. Однако, когда больше нет ссылок на Processобъект и он собирается (мусор), нет никого, кто указывает на список обработчиков событий. Итак, он собирается, и теперь нет ссылок на делегатов, которые раньше были в списке, поэтому, наконец, они собирают мусор.
Охад Шнайдер
1
@SuperJMN: Интересно, что это более сложно / мощно, чем это. Во- Disposeпервых , очищает некоторые ресурсы, но не препятствует сохранению просочившейся ссылки process. Фактически, вы заметите, что это processотносится к обработчикам, но Exitedобработчик также имеет ссылку на process. В некоторых системах эта циклическая ссылка предотвратит сборку мусора, но алгоритм, используемый в .NET, по-прежнему позволит очистить его, пока все живет на «острове» без внешних ссылок.
TheRubberDuck
4

Вот еще один подход. Концепция, аналогичная ответам 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
}
Брэндон
источник
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;
    }
}
Апфелькуача
источник
1

Я думаю, все, что вам следует использовать, это следующее:

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;
    }
}
Константин С.
источник
1
Какой смысл принимать CancellationToken, если отмена - не Killпроцесс?
Теодор Зулиас,
CancellationTokenв WaitForExitAsyncметоде нужен просто, чтобы иметь возможность отменить ожидание или установить тайм-аут. Завершить процесс можно с помощью StartProcessAsync: `` try {await process.WaitForExitAsync (cancellationToken); } catch (OperationCanceledException) {process.Kill (); } `` `
Константин С.
Я считаю, что когда метод принимает a CancellationToken, отмена токена должна приводить к отмене операции, а не к отмене ожидания. Это то, что обычно ожидает вызывающий метод. Если вызывающий хочет отменить только ожидание и позволить операции по-прежнему работать в фоновом режиме, это довольно легко сделать извне ( вот метод расширения, AsCancelableкоторый делает именно это).
Теодор Зулиас
Я думаю, что это решение должен принимать вызывающий (специально для этого случая, поскольку этот метод начинается с Wait, в целом я с вами согласен), как в новом примере использования.
Константин С.
0

Я действительно беспокоюсь об удалении процесса, а как насчет ожидания выхода 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;
    }
}
Иоганн Медина
источник