มี async ใด ๆ เทียบเท่ากับ Process.Start?


141

เช่นเดียวกับชื่อเรื่องที่แนะนำมีความเทียบเท่าProcess.Start(อนุญาตให้คุณเรียกใช้แอพพลิเคชั่นหรือไฟล์แบตช์) ที่ฉันรอได้ไหม

ฉันกำลังเล่นกับแอปคอนโซลขนาดเล็กและดูเหมือนว่าเป็นสถานที่ที่เหมาะที่จะใช้ async และรออยู่ แต่ฉันไม่พบเอกสารใด ๆ สำหรับสถานการณ์นี้

สิ่งที่ฉันคิดคือบางสิ่งบางอย่างตามบรรทัดเหล่านี้:

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

2
ทำไมคุณไม่ใช้ WaitForExit บนออบเจ็กต์ Process ที่ส่งคืน
SimpleVar

2
และดูเหมือนว่าคุณกำลังมองหาโซลูชัน "ซิงค์" มากกว่าที่จะเป็น "async" ดังนั้นชื่อเรื่องจึงทำให้เข้าใจผิด
SimpleVar

2
@YoryeNathan - lol แท้จริงแล้วProcess.Start เป็น async และ OP ดูเหมือนจะต้องการรุ่นซิงโคร
Oded

10
OP กำลังพูดถึงคำหลักแบบใหม่ / รอคำ
ศัพท์

4
ตกลงฉันได้อัปเดตโพสต์ของฉันให้ชัดเจนขึ้นแล้ว คำอธิบายว่าทำไมฉันถึงต้องการสิ่งนี้ง่าย รูปภาพสถานการณ์ที่คุณต้องเรียกใช้คำสั่งภายนอก (เช่น 7zip) แล้วดำเนินการไหลของแอปพลิเคชันต่อไป ตรงนี้เป็นสิ่งที่ async / await มีขึ้นเพื่ออำนวยความสะดวกและดูเหมือนว่าจะไม่มีทางที่จะเรียกใช้กระบวนการและรอการออกจากกระบวนการ
linkerro

คำตอบ:


196

Process.Start()เริ่มต้นกระบวนการเท่านั้นไม่ต้องรอจนกว่าจะเสร็จสิ้นดังนั้นจึงไม่สมเหตุสมผลasyncนัก await Task.Run(() => Process.Start(fileName))หากคุณยังคงต้องการที่จะทำมันคุณสามารถทำสิ่งที่ชอบ

แต่ถ้าคุณต้องการที่จะถ่ายทอดสดรอให้กระบวนการให้เสร็จคุณสามารถใช้เหตุการณ์พร้อมกับ:ExitedTaskCompletionSource

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
ในที่สุดฉันก็ถึงจุดที่จะยึดติดกับ gitub ในเรื่องนี้ - มันไม่มีการสนับสนุนการยกเลิก / หมดเวลา แต่มันจะรวบรวมเอาท์พุทมาตรฐานและข้อผิดพลาดมาตรฐานสำหรับคุณอย่างน้อยที่สุด github.com/jamesmanning/RunProcessAsTask
James Manning เมื่อ

3
ฟังก์ชั่นนี้ยังมีอยู่ในแพ็คเกจMedallionShell NuGet
ChaseMedallion

8
สิ่งที่สำคัญจริงๆ: ลำดับที่คุณตั้งค่าคุณสมบัติต่างๆprocessและการเปลี่ยนแปลงสิ่งที่เกิดขึ้นเมื่อคุณเรียกมันด้วยprocess.StartInfo .Start()หากคุณยกตัวอย่างเช่นการโทร.EnableRaisingEvents = trueก่อนตั้งค่าStartInfoคุณสมบัติตามที่เห็นที่นี่สิ่งต่าง ๆ ทำงานได้ตามที่คาดไว้ หากคุณตั้งค่าไว้ในภายหลังเช่นเพื่อให้มันเข้าด้วยกัน.Exitedแม้ว่าคุณจะเรียกมันมาก่อน.Start()มันจะไม่ทำงานอย่างถูกต้อง - .Exitedยิงทันทีแทนที่จะรอให้กระบวนการหยุดทำงานจริง ไม่ทราบว่าทำไมเพียงแค่คำเตือน
Chris Moschini

2
@svick ในรูปแบบหน้าต่างprocess.SynchronizingObjectควรตั้งค่าเป็นองค์ประกอบของแบบฟอร์มเพื่อหลีกเลี่ยงวิธีการที่จัดการเหตุการณ์ (เช่น Exited, OutputDataReceived, ErrorDataReceived) ถูกเรียกบนเธรดแยก
KevinBui

4
มันไม่จริงให้ความรู้สึกที่จะห่อในProcess.Start Task.Runตัวอย่างเช่นเส้นทาง UNC จะได้รับการแก้ไขพร้อมกัน ตัวอย่างนี้อาจใช้เวลาถึง 30 วินาทีจึงจะเสร็จสมบูรณ์:Process.Start(@"\\live.sysinternals.com\whatever")
Jabe

55

ที่นี่ใช้เวลาของฉันขึ้นอยู่กับคำตอบของ svick มันเพิ่มการเปลี่ยนเส้นทางออกการเก็บรหัสออกและการจัดการข้อผิดพลาดที่ดีขึ้นเล็กน้อย (การทิ้ง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 ฉันไม่เข้าใจสิ่งที่คุณหมายถึงบางทีคุณควรเริ่มต้นคำถามใหม่ด้วยรหัสบางอย่างเพื่อให้เราสามารถเห็นสิ่งที่คุณพยายามและดำเนินการต่อจากที่นั่น
Ohad Schneider

4
คำตอบที่ยอดเยี่ยม ขอบคุณ svick สำหรับการวางรากฐานและขอบคุณ Ohad สำหรับการขยายตัวที่มีประโยชน์มากนี้
Gordon Bean

1
@SuperJMN กำลังอ่านโค้ด ( referenceource.microsoft.com/#System/services/monitoring/ ...... ) ฉันไม่เชื่อว่าDisposeตัวจัดการเหตุการณ์เป็นโมฆะดังนั้นในทางทฤษฎีถ้าคุณเรียกDisposeแต่เก็บการอ้างอิงไว้ฉันเชื่อว่ามันจะรั่วไหล อย่างไรก็ตามเมื่อไม่มีการอ้างอิงไปยังProcessวัตถุอีกต่อไปและจะได้รับ (ขยะ) จะไม่มีใครที่ชี้ไปยังรายการตัวจัดการเหตุการณ์ ดังนั้นจึงได้รับการรวบรวมและตอนนี้ไม่มีการอ้างอิงถึงตัวแทนที่เคยอยู่ในรายการดังนั้นในที่สุดพวกเขาก็เก็บขยะ
Ohad Schneider

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 ดังนั้นทั้งหมดนี้ถูกกำหนดโดยชั้นเรียนของฉัน

คลาสมีความเป็นไปได้หลายอย่างเช่นการอ่านเอาต์พุตเริ่มต้นเป็นผู้ดูแลระบบหรือผู้ใช้ที่แตกต่างกันจับข้อยกเว้นและเริ่มรวมทั้งหมดนี้แบบอะซิงโครนัส การยกเลิก Nice คือเอาต์พุตการอ่านยังเป็นไปได้ระหว่างการประมวลผล

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กระบวนการ
Theodor Zoulias

CancellationTokenในWaitForExitAsyncวิธีการนั้นจำเป็นต้องมีเพื่อให้สามารถยกเลิกการรอหรือตั้งค่าการหมดเวลา การฆ่ากระบวนการสามารถทำได้ในStartProcessAsync: `` `ลอง {รอกระบวนการกำลังรอให้การส่งออก Async } catch (OperationCanceledException) {process.Kill (); } `` `
Konstantin S.

ความคิดเห็นของฉันคือเมื่อวิธีการยอมรับ a การCancellationTokenยกเลิกโทเค็นควรส่งผลให้การยกเลิกการดำเนินการไม่ใช่การยกเลิกการรอคอย นี่คือสิ่งที่ผู้เรียกวิธีการปกติคาดไว้ หากผู้โทรต้องการยกเลิกเพียงแค่การรอและปล่อยให้การดำเนินการยังคงทำงานในพื้นหลังมันค่อนข้างง่ายที่จะทำจากภายนอก ( นี่คือวิธีการขยายAsCancelableที่ทำเช่นนั้น)
Theodor Zoulias

ฉันคิดว่าผู้โทรควรทำการตัดสินใจนี้ (โดยเฉพาะสำหรับกรณีนี้เนื่องจากวิธีนี้เริ่มต้นด้วยการรอโดยทั่วไปฉันเห็นด้วยกับคุณ) เช่นเดียวกับในตัวอย่างการใช้งานใหม่
Konstantin S.

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;
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.