Чи є який-небудь еквівалент асинхроніки Process.Start?


141

Як підказує заголовок, чи є еквівалент Process.Start(дозволяє запустити інший додаток або пакетний файл), якого я можу чекати?

Я граю з невеликим консольним додатком, і це здавалося ідеальним місцем для використання асинхронізації та очікування, але я не можу знайти жодної документації для цього сценарію.

Що я думаю, є щось таке:

void async RunCommand()
{
    var result = await Process.RunAsync("command to run");
}

2
Чому ви просто не будете використовувати WaitForExit для повернутого об'єкта Process?
SimpleVar

2
І, до речі, звучить більше, ніж ви шукаєте "синхронізоване" рішення, а не рішення "асинхронізація", тому назва вводить в оману.
SimpleVar

2
@YoryeNathan - lol Дійсно, Process.Start є асинхронізація, і ОП, схоже, хоче синхронну версію.
Одід

10
ОП розповідає про нові ключові слова асинхронізування / очікування в C # 5
aquinas

4
Гаразд, я оновив свою публікацію, щоб бути трохи більш зрозумілою. Пояснення, чому я хочу цього, просте. Створіть сценарій, коли потрібно запустити зовнішню команду (щось на зразок 7zip), а потім продовжити потік програми. Це саме те, що повинно було сприяти асинхронізація / очікування, але, здається, немає способу запустити процес і чекати його виходу.
linkerro

Відповіді:


196

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
Ця функція також доступна в пакеті MedallionShell NuGet
ChaseMedallion

8
Дійсно важливо: порядок, коли ви встановлюєте різні властивості processта process.StartInfoзмінює те, що відбувається під час запуску .Start(). Якщо ви, наприклад, зателефонували .EnableRaisingEvents = trueперед тим, як встановити StartInfoвластивості, як показано тут, все працює як очікувалося. Якщо ви встановите його пізніше, наприклад, щоб зберегти його разом із ним .Exited, навіть якщо ви його раніше зателефонували .Start(), він не спрацює належним чином - .Exitedзапускається негайно, а не чекає, поки процес дійсно завершиться. Не знаю чому, лише слово обережності.
Кріс Москіні

2
@svick У віконній формі process.SynchronizingObjectслід встановити компонент форм, щоб уникнути методів, які обробляють події (наприклад, Вихід, OutputDataReceived, ErrorDataReceived), що викликаються в окремому потоці.
KevinBui

4
Це робить на насправді має сенс , щоб обернути Process.Startв Task.Run. Наприклад, шлях UNC буде вирішено синхронно. Цей фрагмент може зайняти до 30 секунд для завершення:Process.Start(@"\\live.sysinternals.com\whatever")
Jabe

55

Ось мій погляд, заснований на відповіді Свикка . Він додає перенаправлення виводу, збереження коду виходу та трохи кращу обробку помилок (розпорядження 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
щойно знайшов це цікаве рішення. Оскільки я новачок в # я не знаю, як користуватися async Task<int> RunProcessAsync(string fileName, string args). Я адаптував цей приклад і передав три об’єкти по одному. Як я можу чекати підвищення подій? напр. перед тим, як моя заявка зупиняється .. велике спасибі
marrrschine

3
@marrrschine Я не розумію, що саме ви маєте на увазі, можливо, ви повинні почати нове запитання з деяким кодом, щоб ми могли побачити, що ви спробували, і продовжувати звідти.
Охад Шнайдер

4
Фантастична відповідь. Дякуємо вам, що ви заклали основу, і дякую Охад за це дуже корисне розширення.
Гордон Бін

1
@SuperJMN, читаючи код ( referenceource.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;
    }
}

Який сенс приймати документ CancellationToken, якщо його скасування не Killвідбувається?
Теодор Зуліяс

CancellationTokenу WaitForExitAsyncметоді потрібен просто, щоб мати можливість скасувати очікування або встановити тайм-аут. Убити процес можна в StartProcessAsync: `` спробувати {await process.WaitForExitAsync (cancellationToken); } улов (OperationCanceledException) {process.Kill (); } `` `
Костянтин С.

На мою думку, коли метод приймає a CancellationToken, скасування маркера повинно призвести до скасування операції, а не до скасування очікування. Ось що зазвичай очікує абонент методу. Якщо абонент хоче скасувати просто очікуване і нехай операція все ще працює у фоновому режимі, це зробити досить просто зовні ( ось метод розширення, AsCancelableякий робить саме це).
Теодор Зуліяс

Я думаю, що це рішення повинен прийняти абонент (спеціально для цього випадку, оскільки цей метод починається з Wait, загалом я згоден з вами), як у новому прикладі використання.
Костянтин С.

0

Я дуже переживаю з приводу утилізації процесу, а як чекати виходу з асинхронізацією? Це моя пропозиція (на основі попереднього):

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;
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.