Có bất kỳ async tương đương với Process.Start không?


141

Giống như tiêu đề gợi ý, có tương đương với Process.Start(cho phép bạn chạy một ứng dụng hoặc tệp bó khác) mà tôi có thể chờ đợi không?

Tôi đang chơi với một ứng dụng bảng điều khiển nhỏ và đây có vẻ là nơi hoàn hảo để sử dụng async và chờ đợi nhưng tôi không thể tìm thấy bất kỳ tài liệu nào cho kịch bản này.

Những gì tôi đang nghĩ là một cái gì đó dọc theo những dòng này:

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

2
Tại sao bạn không sử dụng WaitForExit trên đối tượng Process được trả về?
SimpleVar

2
Và nhân tiện, âm thanh giống như bạn đang tìm kiếm một giải pháp "được đồng bộ hóa", chứ không phải là một giải pháp "không đồng bộ", vì vậy tiêu đề gây hiểu nhầm.
SimpleVar

2
@YoryeNathan - lol. Thật vậy, Process.Start là không đồng bộ và OP dường như muốn có một phiên bản đồng bộ.
Oded

10
OP đang nói về các từ khóa async / await mới trong C # 5
aquina

4
Ok, tôi đã cập nhật bài viết của mình để rõ ràng hơn một chút. Giải thích cho lý do tại sao tôi muốn điều này là đơn giản. Hình ảnh một kịch bản trong đó bạn phải chạy một lệnh bên ngoài (giống như 7zip) và sau đó tiếp tục dòng ứng dụng. Đây chính xác là những gì async / await có nghĩa là để tạo điều kiện và dường như không có cách nào để chạy một quy trình và chờ đợi nó thoát.
linkerro

Câu trả lời:


196

Process.Start()chỉ bắt đầu quá trình, nó không đợi cho đến khi nó kết thúc, vì vậy nó không có ý nghĩa gì để thực hiện nó async. Nếu bạn vẫn muốn làm điều đó, bạn có thể làm một cái gì đó như await Task.Run(() => Process.Start(fileName)).

Nhưng, nếu bạn muốn đồng bộ chờ cho quá trình đến khi kết thúc, bạn có thể sử dụng các Exitedsự kiện cùng với 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
Cuối cùng tôi cũng đã khắc phục được điều gì đó trên github cho việc này - nó không có bất kỳ sự hỗ trợ hủy / hết thời gian nào, nhưng ít nhất nó sẽ thu thập được đầu ra tiêu chuẩn và lỗi tiêu chuẩn cho bạn. github.com/jamesmanning/RunProcessAsTask
James Manning

3
Chức năng này cũng có sẵn trong gói MedallionShell NuGet
ChaseMedallion

8
Thực sự quan trọng: Thứ tự bạn đặt các thuộc tính khác nhau processprocess.StartInfothay đổi những gì xảy ra khi bạn chạy nó .Start(). Nếu bạn gọi ví dụ .EnableRaisingEvents = truetrước khi thiết lập StartInfocác thuộc tính như được thấy ở đây, mọi thứ sẽ hoạt động như mong đợi. Nếu bạn đặt nó sau, ví dụ để giữ nó cùng với .Exited, mặc dù bạn đã gọi nó trước đó .Start(), nó không hoạt động đúng - .Exitedkích hoạt ngay lập tức thay vì chờ quá trình thực sự thoát. Không biết tại sao, chỉ là một lời cảnh báo.
Chris Moschini

2
@svick Ở dạng cửa sổ, process.SynchronizingObjectnên được đặt thành thành phần biểu mẫu để tránh các phương thức xử lý các sự kiện (như Exited, OutputDataReceured, ErrorDataReceured) được gọi trên luồng riêng biệt.
KevinBui

4
không thực sự có ý nghĩa với quấn Process.Starttrong Task.Run. Một đường dẫn UNC, ví dụ, sẽ được giải quyết đồng bộ. Đoạn mã này có thể mất tới 30 giây để hoàn thành:Process.Start(@"\\live.sysinternals.com\whatever")
Jabe

55

Đây là của tôi, dựa trên câu trả lời của Svick . Nó thêm chuyển hướng đầu ra, duy trì mã thoát và xử lý lỗi tốt hơn một chút (xử lý Processđối tượng ngay cả khi không thể khởi động):

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
chỉ tìm thấy giải pháp thú vị này. Vì tôi chưa quen với c # nên tôi không biết cách sử dụng async Task<int> RunProcessAsync(string fileName, string args). Tôi đã điều chỉnh ví dụ này và vượt qua ba đối tượng một. Làm thế nào tôi có thể chờ đợi sự kiện nâng cao? ví dụ. trước khi ứng dụng của tôi dừng lại .. cảm ơn rất nhiều
marrrschine

3
@marrrschine Tôi không hiểu chính xác ý bạn là gì, có lẽ bạn nên bắt đầu một câu hỏi mới với một số mã để chúng tôi có thể thấy những gì bạn đã thử và tiếp tục từ đó.
Ohad Schneider

4
Câu trả lời tuyệt vời. Cảm ơn bạn Svick đã đặt nền tảng và cảm ơn Ohad vì sự mở rộng rất hữu ích này.
Gordon Bean

1
@SuperJMN đọc mã ( referencesource.microsoft.com/#System/services/monitoring/... ) Tôi không tin Disposenull xử lý sự kiện, vì vậy về mặt lý thuyết nếu bạn gọi Disposenhưng vẫn giữ tham chiếu xung quanh, tôi tin rằng đó sẽ là một sự rò rỉ. Tuy nhiên, khi không có thêm tham chiếu đến Processđối tượng và nó được thu thập (rác), không có ai chỉ ra danh sách xử lý sự kiện. Vì vậy, nó được thu thập, và bây giờ không có tài liệu tham khảo cho các đại biểu đã từng có trong danh sách, vì vậy cuối cùng họ nhận được rác được thu thập.
Ohad Schneider

1
@SuperJMN: Thật thú vị, nó phức tạp / mạnh mẽ hơn thế. Đối với một, Disposelàm sạch một số tài nguyên, nhưng không ngăn chặn một tài liệu tham khảo bị rò rỉ giữ processxung quanh. Trong thực tế, bạn sẽ nhận thấy processđề cập đến các trình xử lý, nhưng Exitedtrình xử lý cũng có một tham chiếu đến process. Trong một số hệ thống, tham chiếu vòng tròn đó sẽ ngăn chặn việc thu gom rác, nhưng thuật toán được sử dụng trong .NET vẫn cho phép tất cả được dọn sạch miễn là mọi thứ sống trên một "hòn đảo" không có tham chiếu bên ngoài.
TheRubberDuck

4

Đây là một cách tiếp cận khác. Khái niệm tương tự với câu trả lời của SvickOhad nhưng sử dụng phương pháp mở rộng về Processloại.

Phương pháp mở rộng:

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;
}

Trường hợp sử dụng ví dụ trong một phương thức chứa:

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

Tôi đã xây dựng một lớp học để bắt đầu một quá trình và nó đã phát triển trong những năm qua vì nhiều yêu cầu khác nhau. Trong quá trình sử dụng, tôi đã phát hiện ra một số vấn đề với lớp Process với việc xử lý và thậm chí là đọc ExitCode. Vì vậy, đây là tất cả cố định bởi lớp học của tôi.

Lớp này có một số khả năng, ví dụ đọc đầu ra, bắt đầu với tư cách Quản trị viên hoặc người dùng khác, bắt Ngoại lệ và cũng bắt đầu tất cả điều này không đồng bộ. Hủy bỏ. Đẹp là đầu ra đọc cũng có thể trong quá trình thực hiện.

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

Tôi nghĩ tất cả những gì bạn nên sử dụng là đây:

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);
        }
    }
}

Ví dụ sử dụng:

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;
    }
}

Điểm chấp nhận a CancellationToken, nếu hủy bỏ nó không phải Killlà quá trình?
Theodor Zoulias

CancellationTokentrong WaitForExitAsyncphương thức là cần thiết chỉ đơn giản là có thể hủy bỏ chờ đợi hoặc đặt thời gian chờ. Giết một tiến trình có thể được thực hiện trong StartProcessAsync: `` `thử {await process.WaitForExitAsync (hủyToken); } Catch (Operation HủyedException) {process.Kill (); } `` `
Konstantin S.

Ý kiến ​​của tôi là khi một phương thức chấp nhận a CancellationToken, việc hủy mã thông báo sẽ dẫn đến việc hủy hoạt động, chứ không phải hủy bỏ việc chờ đợi. Đây là những gì người gọi phương thức thường mong đợi. Nếu người gọi muốn hủy chỉ chờ, và để hoạt động vẫn chạy trong nền, thì khá dễ thực hiện bên ngoài ( đây là một phương thức mở rộng AsCancelableđang làm điều đó).
Theodor Zoulias

Tôi nghĩ rằng quyết định này nên được đưa ra bởi người gọi (cụ thể cho trường hợp này, vì phương pháp này bắt đầu bằng Chờ, nói chung tôi đồng ý với bạn), như trong Ví dụ sử dụng mới.
Konstantin S.

0

Tôi thực sự lo lắng về việc xử lý quá trình, còn chờ gì để thoát async?, Đây là đề xuất của tôi (dựa trên trước đó):

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;
    }        
}

Sau đó, sử dụng nó như thế này:

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;
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.