Apakah ada yang setara dengan Process.Start?

141

Seperti judulnya, apakah ada yang setara dengan Process.Start(memungkinkan Anda menjalankan aplikasi lain atau file batch) yang bisa saya tunggu?

Saya bermain dengan aplikasi konsol kecil dan ini sepertinya tempat yang sempurna untuk menggunakan async dan menunggu tetapi saya tidak dapat menemukan dokumentasi untuk skenario ini.

Apa yang saya pikirkan adalah sesuatu seperti ini:

void async RunCommand()
{
    var result = await Process.RunAsync("command to run");
}
linkerro
sumber
2
Mengapa Anda tidak menggunakan WaitForExit saja pada objek Proses yang dikembalikan?
SimpleVar
2
Omong-omong, sepertinya Anda mencari solusi yang "disinkronkan", bukannya solusi "async", sehingga judulnya menyesatkan.
SimpleVar
2
@YoryeNathan - lol. Memang, Process.Start adalah async dan OP tampaknya ingin versi sinkron.
Oded
10
OP sedang berbicara tentang kata kunci async / menunggu baru di C # 5
aquinas
4
Oke, saya sudah memperbarui posting saya menjadi sedikit lebih jelas. Penjelasan mengapa saya ingin ini sederhana. Bayangkan sebuah skenario di mana Anda harus menjalankan perintah eksternal (sekitar 7zip) dan kemudian melanjutkan alur aplikasi. Inilah tepatnya yang dimaksudkan untuk memfasilitasi async / menunggu, namun tampaknya tidak ada cara untuk menjalankan proses dan menunggu keluarnya.
linkerro

Jawaban:

196

Process.Start()hanya memulai proses, tidak menunggu sampai selesai, jadi tidak masuk akal untuk membuatnya async. Jika Anda masih ingin melakukannya, Anda dapat melakukan sesuatu seperti await Task.Run(() => Process.Start(fileName)).

Tapi, jika Anda ingin asynchronous menunggu proses hingga selesai, Anda dapat menggunakan yang Exitedacara bersama-sama dengan 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
sumber
36
Saya akhirnya sempat menempelkan sesuatu pada github untuk ini - ini tidak memiliki dukungan pembatalan / batas waktu, tetapi akan mengumpulkan output standar dan kesalahan standar untuk Anda, setidaknya. github.com/jamesmanning/RunProcessAsTask
James Manning
3
Fungsionalitas ini juga tersedia dalam paket MedallionShell NuGet
ChaseMedallion
8
Sangat penting: Urutan di mana Anda mengatur berbagai properti processdan process.StartInfomengubah apa yang terjadi ketika Anda menjalankannya .Start(). Misalnya, jika Anda memanggil .EnableRaisingEvents = truesebelum menetapkan StartInfoproperti seperti yang terlihat di sini, semuanya berfungsi seperti yang diharapkan. Jika Anda mengaturnya nanti, misalnya untuk tetap bersama .Exited, meskipun Anda menyebutnya sebelumnya .Start(), ia gagal berfungsi dengan benar - .Exitedlangsung menyala daripada menunggu Proses untuk benar-benar keluar. Tidak tahu kenapa, hanya kata hati-hati.
Chris Moschini
2
@svick Dalam formulir jendela, process.SynchronizingObjectharus diatur ke komponen formulir untuk menghindari metode yang menangani peristiwa (seperti Keluar, OutputDataReceived, ErrorDataReceived) dipanggil pada utas yang terpisah.
KevinBui
4
Ini tidak benar-benar masuk akal untuk membungkus Process.Startdi Task.Run. Jalur UNC, misalnya, akan diselesaikan secara serempak. Cuplikan ini dapat memakan waktu hingga 30 detik untuk menyelesaikan:Process.Start(@"\\live.sysinternals.com\whatever")
Jabe
55

Inilah pendapat saya, berdasarkan jawaban svick . Itu menambah redirection output, retensi kode keluar, dan penanganan kesalahan sedikit lebih baik (membuang Processobjek bahkan jika itu tidak dapat dimulai):

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;
}
Ohad Schneider
sumber
1
baru saja menemukan solusi yang menarik ini. Karena saya baru di c # Saya tidak yakin bagaimana cara menggunakan async Task<int> RunProcessAsync(string fileName, string args). Saya mengadaptasi contoh ini dan memberikan tiga objek satu per satu. Bagaimana saya bisa menunggu acara peningkatan? misalnya. sebelum aplikasi saya berhenti .. terima kasih banyak
marrrschine
3
@marrrschine Saya tidak mengerti apa yang Anda maksud, mungkin Anda harus memulai pertanyaan baru dengan beberapa kode sehingga kita dapat melihat apa yang Anda coba dan lanjutkan dari sana.
Ohad Schneider
4
Jawaban yang fantastis. Terima kasih svick untuk meletakkan dasar dan terima kasih Ohad untuk ekspansi yang sangat berguna ini.
Gordon Bean
1
@SuperJMN membaca kode ( referenceource.microsoft.com/#System/services/monitoring/... ) Saya tidak percaya Disposenulls event handler, jadi secara teoritis jika Anda menelepon Disposetetapi menyimpan referensi di sekitar, saya percaya itu akan menjadi kebocoran. Namun, ketika tidak ada lagi referensi ke Processobjek dan mendapat (sampah) dikumpulkan, tidak ada yang menunjuk ke daftar event handler. Jadi dikumpulkan, dan sekarang tidak ada referensi untuk delegasi yang dulu ada dalam daftar, jadi akhirnya mereka mendapatkan sampah yang dikumpulkan.
Ohad Schneider
1
@ SupupJMN: Menariknya, ini lebih rumit / kuat dari itu. Pertama, Disposemembersihkan beberapa sumber daya, tetapi tidak mencegah referensi yang bocor tetap processada. Bahkan, Anda akan melihat bahwa itu processmerujuk pada penangan, tetapi Exitedpenangan juga memiliki referensi process. Dalam beberapa sistem, referensi melingkar itu akan mencegah pengumpulan sampah, tetapi algoritma yang digunakan dalam. NET masih akan memungkinkan semuanya dibersihkan selama semuanya hidup di "pulau" tanpa referensi luar.
TheRubberDuck
4

Ini pendekatan lain. Konsep serupa dengan jawaban svick dan Ohad tetapi menggunakan metode ekstensi padaProcess jenisnya.

Metode ekstensi:

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

Contoh use case dalam metode yang mengandung:

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
}
Brandon
sumber
4

Saya telah membangun kelas untuk memulai proses dan itu tumbuh selama beberapa tahun terakhir karena berbagai persyaratan. Selama penggunaan saya menemukan beberapa masalah dengan kelas Proses dengan membuang dan bahkan membaca ExitCode. Jadi ini semua diperbaiki oleh kelas saya.

Kelas memiliki beberapa kemungkinan, misalnya membaca keluaran, mulai sebagai Admin atau pengguna yang berbeda, menangkap Pengecualian dan juga memulai semua termasuk asinkron ini. Pembatalan. Bagusnya, hasil bacaan juga dimungkinkan selama eksekusi.

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;
    }
}
Apfelkuacha
sumber
1

Saya pikir semua yang harus Anda gunakan adalah ini:

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

Contoh penggunaan:

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;
    }
}
Konstantin S.
sumber
Apa gunanya menerima CancellationToken, jika membatalkan itu bukan Killproses?
Theodor Zoulias
CancellationTokendalam WaitForExitAsyncmetode ini diperlukan hanya untuk dapat membatalkan menunggu atau mengatur batas waktu. Membunuh suatu proses dapat dilakukan di StartProcessAsync: `` `coba {menunggu proses. TungguForExitAsync (cancellationToken); } catch (OperationCanceledException) {process.Kill (); } `` `
Konstantin S.
Pendapat saya adalah bahwa ketika suatu metode menerima a CancellationToken, membatalkan token harus mengakibatkan pembatalan operasi, bukan untuk membatalkan menunggu. Inilah yang biasanya diharapkan oleh penelepon metode ini. Jika penelepon ingin membatalkan hanya menunggu, dan membiarkan operasi masih berjalan di latar belakang, cukup mudah dilakukan secara eksternal (di sini adalah metode ekstensi AsCancelableyang melakukan hal itu).
Theodor Zoulias
Saya pikir keputusan ini harus dibuat oleh penelepon (khusus untuk kasus ini, karena metode ini dimulai dengan Tunggu, secara umum saya setuju dengan Anda), seperti dalam Contoh Penggunaan yang baru.
Konstantin S.
0

Saya benar-benar khawatir tentang pembuangan proses, bagaimana dengan menunggu untuk keluar dari async ?, ini proposal saya (berdasarkan sebelumnya):

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

Lalu, gunakan seperti ini:

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;
    }
}
Johann Medina
sumber