Process.Start에 해당하는 비동기가 있습니까?


141

제목에서 알 수 있듯이 Process.Start내가 기다릴 수 있는 것과 다른 것이 있습니까 (다른 응용 프로그램이나 배치 파일을 실행할 수 있습니까)?

작은 콘솔 응용 프로그램을 사용하고 있는데 비동기를 사용하고 기다리는 완벽한 장소 인 것처럼 보였지만이 시나리오에 대한 문서를 찾을 수 없습니다.

내가 생각하는 것은 다음과 같은 내용입니다.

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

2
왜 반환 된 Process 객체에 WaitForExit를 사용하지 않습니까?
SimpleVar

2
그런데 "비동기"솔루션보다는 "동기화 된"솔루션을 찾는 것처럼 들리므로 제목이 잘못되었습니다.
SimpleVar

2
@YoryeNathan-롤. 실제로 Process.Start 이며 비동기은과 영업 이익은 동기 버전을 원하는 것으로 보인다.
Oded

10
OP는 C # 5의 새로운 async / await 키워드에 대해 이야기하고 있습니다
aquinas

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
나는 마침내 이것을 위해 github에 무언가를 고수했다. 그것은 취소 / 타임 아웃 지원이 없지만 적어도 표준 출력과 표준 오류를 모을 것이다. github.com/jamesmanning/RunProcessAsTask
James Manning

3
이 기능은 MedallionShell NuGet 패키지
ChaseMedallion에서

8
정말 중요한 : 당신이 다양한 속성을 설정하는 순서 processprocess.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 나는 당신이 의미하는 바를 정확히 이해하지 못합니다. 어쩌면 당신은 어떤 코드로 새로운 질문을 시작해야 우리가 당신이 시도한 것을 볼 수 있고 거기서부터 계속할 수 있습니다.
하드 슈나이더

4
환상적인 답변. 토대를 마련해 주신 svick에게 감사 드리며이 유용한 확장에 대해 Ohad에게 감사드립니다.
Gordon Bean

1
@SuperJMN 코드 읽기 ( referencesource.microsoft.com/#System/services/monitoring/… ) Dispose이벤트 핸들러가 null 이라고 생각하지 않으므로 이론적으로 호출 Dispose했지만 참조를 유지하면 누출이 될 것입니다. 그러나 Process오브젝트에 대한 참조가 더 이상없고 (가비지) 수집되면 이벤트 핸들러 목록을 가리키는 것이 없습니다. 따라서 수집되었으며 이제는 목록에 있던 델리게이트에 대한 참조가 없으므로 결국 가비지 수집됩니다.
하드 슈나이더

1
@SuperJMN : 흥미롭게도 그보다 더 복잡하고 강력합니다. 우선, Dispose일부 리소스를 정리하지만 유출 된 참조가 유지되는 것을 막지는 않습니다 process. 실제로 process는 핸들러 를 참조하지만 Exited핸들러에도 참조가 있음을 알 수 process있습니다. 일부 시스템에서이 순환 참조는 가비지 수집을 방지하지만 .NET에 사용 된 알고리즘은 모든 것이 외부 참조없이 "섬"에있는 한 여전히 정리할 수 있습니다.
TheRubberDuck

4

다른 접근법이 있습니다. 유사 개념 svick오핫의 답변 만에 확장 방법을 사용하여 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

프로세스를 시작하기 위해 수업을 만들었으며 다양한 요구 사항으로 인해 지난 몇 년 동안 성장했습니다. 사용하는 동안 ProcessCode에서 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프로세스 가 아닌 경우 수락하는 요점은 무엇입니까 ?
Theodor Zoulias

CancellationTokenWaitForExitAsync방법에서는 단순히 대기를 취소하거나 시간 초과를 설정할 수 있어야합니다. 프로세스 종료는 다음과 같이 할 수 있습니다 StartProcessAsync:```try {await process.WaitForExitAsync (cancellationToken); } catch (OperationCanceledException) {process.Kill (); }```
Konstantin S.

내 의견은 메소드가을 수락 CancellationToken하면 토큰을 취소하면 대기중인 취소가 아닌 작업이 취소되어야한다는 것입니다. 이것이 메소드 호출자가 일반적으로 기대하는 것입니다. 호출자가 대기중인 것을 취소하고 작업이 백그라운드에서 계속 실행되도록하려는 경우 외부에서 수행하는 것이 매우 쉽습니다 ( 여기서는 확장 방법 AsCancelable이 있습니다).
Theodor Zoulias

이 결정은 새로운 사용 예에서와 같이 호출자가 (이 경우에는이 방법은 일반적으로 대기로 시작하기 때문에 일반적으로 동의합니다) 결정해야한다고 생각합니다.
Konstantin S.

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;
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.