Process.Start öğesinin eşzamansız eşdeğeri var mı?


141

Başlığın öne sürdüğü gibi, Process.Startbekleyebileceğim bir eşdeğer var mı (başka bir uygulama veya toplu iş dosyası çalıştırmanıza izin veriyor)?

Ben küçük bir konsol uygulaması ile oynuyorum ve bu zaman uyumsuz ve bekliyor kullanmak için mükemmel bir yer gibi görünüyordu ama bu senaryo için herhangi bir belge bulamıyorum.

Düşündüğüm şu çizgilerdeki bir şey:

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

2
Neden döndürülen Process nesnesinde WaitForExit kullanmıyorsunuz?
SimpleVar

2
Ve bu arada, "asenkron" bir çözümden ziyade "senkronize" bir çözüm aradığınıza benziyor, bu yüzden başlık yanıltıcı.
SimpleVar

2
@YoryeNathan - lol. Gerçekten, Process.Start zaman uyumsuz ve OP senkron bir sürüm istiyor gibi görünüyor.
Oded

10
OP, C #
5'deki

4
Tamam, yazımı biraz daha açık olacak şekilde güncelledim. Bunu neden istediğimin açıklaması basit. Harici bir komut (7zip gibi bir şey) çalıştırmanız gereken bir senaryoyu hayal edin ve ardından uygulamanın akışına devam edin. Bu, asenkron / beklemenin kolaylaştırılması anlamına geliyordu ve yine de bir süreci yürütmenin ve çıkışını beklemenin bir yolu yok gibi görünüyor.
linkerro

Yanıtlar:


196

Process.Start()sadece süreci başlatır, bitene kadar beklemez, bu yüzden bunu yapmak pek mantıklı değildir async. Hala yapmak istiyorsanız, böyle bir şey yapabilirsiniz await Task.Run(() => Process.Start(fileName)).

Ancak, işlemin tamamlanmasını eşzamansız olarak beklemek istiyorsanız , Exitedetkinliği aşağıdakilerle birlikte kullanabilirsiniz 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
Sonunda bunun için github'a bir şey yapıştıracağım - herhangi bir iptal / zaman aşımı desteği yok, ancak en azından sizin için standart çıktıyı ve standart hatayı toplayacak. github.com/jamesmanning/RunProcessAsTask
James Manning

3
Bu işlevsellik MedallionShell
NuGet

8
Gerçekten önemli: Eğer üzerinde çeşitli özelliklerini ayarlayabilirsiniz sırası processve process.StartInfosizinle çalıştırmak ne olur değiştirir .Start(). Örneğin özellikleri burada görüldüğü gibi .EnableRaisingEvents = trueayarlamadan önce ararsanız StartInfo, işler beklendiği gibi çalışır. Daha sonra ayarlarsanız, örneğin .Exiteddaha önce .Start()çağırsanız bile, onu bir arada tutmak için düzgün çalışmaz - .Exitedİşlemin gerçekten çıkmasını beklemek yerine hemen ateşler. Neden bilmiyorum, sadece bir uyarı.
Chris Moschini

2
@svick Pencere biçiminde, process.SynchronizingObjectolayları işleyen yöntemlerin (Exited, OutputDataReceived, ErrorDataReceived gibi) ayrılmış iş parçacığında çağrılmasını önlemek için form bileşenine ayarlanmalıdır.
KevinBui

4
O yok aslında sarmak için mantıklı Process.Startiçinde Task.Run. Örneğin, bir UNC yolu eşzamanlı olarak çözülecektir. Bu snippet'in tamamlanması 30 saniye kadar sürebilir:Process.Start(@"\\live.sysinternals.com\whatever")
Jabe

55

İşte svick'in cevabına dayanarak benim almam . Çıktı yeniden yönlendirme, çıkış kodu tutma ve biraz daha iyi hata işleme ekler ( Processnesneyi başlatılamasa bile elden çıkarma ):

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
bu ilginç çözümü buldum. Ben c # için yeni gibi nasıl kullanılacağından emin değilim async Task<int> RunProcessAsync(string fileName, string args). Bu örneği uyarladım ve üç nesneyi tek tek geçtim. Etkinlik yaratmayı nasıl bekleyebilirim? Örneğin. benim uygulama durmadan önce .. çok teşekkürler
marrrschine

3
@marrrschine Ne demek istediğinizi tam olarak anlamadım, belki bazı kodlarla yeni bir soru başlatmalısınız, böylece denediklerinizi görebilir ve oradan devam edebiliriz.
Ohad Schneider

4
Harika bir cevap. Zemin döşemesinden dolayı teşekkür ederiz ve bu çok faydalı genişleme için Ohad'a teşekkür ederiz.
Gordon Bean

1
@SuperJMN kodu okuma ( referenceource.microsoft.com/#System/services/monitoring/… ) DisposeOlay işleyiciyi geçersiz kılmıyorum , bu yüzden teorik olarak referansı aradıysanız ancak referansı tuttuysanız Dispose, bunun bir sızıntı olacağına inanıyorum. Bununla birlikte, Processnesneye daha fazla referans olmadığında ve nesne (çöp) toplandığında, olay işleyici listesine işaret eden kimse yoktur. Bu yüzden toplanır ve şimdi listede bulunan delegelere referans yoktur, bu yüzden sonunda çöp toplanırlar.
Ohad Schneider

1
@SuperJMN: İlginçtir, bundan daha karmaşık / güçlü. Birincisi, Disposebazı kaynakları temizler, ancak sızan bir referansın processetrafta kalmasını engellemez . Aslında, processbunun işleyicileri ifade ettiğini fark edersiniz , ancak Exitedişleyicinin de bir referansı vardır process. Bazı sistemlerde, bu döngüsel referans çöp toplamayı engelleyecektir, ancak .NET'te kullanılan algoritma, her şey dış referans olmadan bir "adada" yaşadığı sürece tümünün temizlenmesine izin verecektir.
TheRubberDuck

4

İşte başka bir yaklaşım. Benzer kavram svick ve Ohad en cevapları fakat üzerinde bir uzantısı yöntemi kullanılarak Processtip.

Genişletme yöntemi:

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

İçerme yönteminde örnek kullanım örneği:

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

Bir süreci başlatmak için bir sınıf oluşturdum ve son yıllarda çeşitli gereksinimler nedeniyle büyüyordu. Kullanım sırasında, ExitCode öğesinin okunması ve okunması ile Process sınıfıyla ilgili birkaç sorun buldum. Yani bütün bunlar benim sınıfım tarafından düzeltildi.

Sınıfın okuma çıktısı, Yönetici veya farklı kullanıcı olarak başlatma, İstisnaları yakalama ve tüm bu eşzamansız dahil etme gibi çeşitli olasılıkları vardır. İptal. Güzel, yürütme sırasında okuma çıktısının da mümkün olmasıdır.

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

Sanırım tüm kullanman gereken bu:

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

Kullanım örneği:

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

Kabul etmenin anlamı nedir CancellationToken, iptal edilirse Killişlem olmaz mı?
Theodor Zoulias

CancellationTokeniçinde WaitForExitAsyncbir metoda ihtiyaç duyulmaktadır basit bir bekleme iptal etmek veya bir zaman aşımı ayarlamak mümkün. Bir işlemi öldürmek şu işlemlerde yapılabilir StartProcessAsync: `` try {işlem bekliyor.WaitForExitAsync (cancellationToken); } catch (OperationCanceledException) {process.Kill (); } ``
Konstantin S.18

Kanımca bir yöntem kabul ettiğinde CancellationToken, jetonun iptal edilmesi, işlemin iptal edilmesiyle değil, beklemenin iptal edilmesiyle sonuçlanmalıdır. Bu yöntem arayan kişinin normalde beklediği şeydir. Arayan, sadece bekleyen işlemi iptal etmek ve işlemin arka planda çalışmasına izin vermek istiyorsa, harici olarak yapmak oldukça kolaydır ( buradaAsCancelable sadece bunu yapan bir uzantı yöntemi vardır).
Theodor Zoulias

Bu kararın arayan tarafından verilmesi gerektiğini düşünüyorum (özellikle bu durum için, çünkü bu yöntem genel olarak Bekleyin ile başlar, size katılıyorum), yeni Kullanım Örneği'nde olduğu gibi.
Konstantin S.18

0

Sürecin elden çıkarılmasından gerçekten endişeleniyorum, çıkış zaman uyumsuzluğunu beklemeye ne dersiniz?

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

Ardından, şu şekilde kullanın:

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;
    }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.