是否存在Process.Start的异步等价物?

lin*_*rro 124 c# async-await c#-5.0

就像标题所示,是否有等价的Process.Start(允许你运行另一个应用程序或批处理文件)我可以等待?

我正在玩一个小型控制台应用程序,这似乎是使用异步和等待的完美的地方,但我找不到这种情况的任何文档.

我在想的是这些方面:

void async RunCommand()
{
    var result = await Process.RunAsync("command to run");
}
Run Code Online (Sandbox Code Playgroud)

svi*_*ick 173

Process.Start()只是启动过程,它不会等到它完成,所以它没有多大意义async.如果你还想这样做,你可以做类似的事情await Task.Run(() => Process.Start(fileName)).

但是,如果您想异步等待进程完成,可以将该Exited事件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;
}
Run Code Online (Sandbox Code Playgroud)

  • 我终于在github上坚持了一些东西 - 它没有任何取消/超时支持,但它会为你收集标准输出和标准错误,至少.https://github.com/jamesmanning/RunProcessAsTask (34认同)
  • 非常重要:在`process`和`process.StartInfo`上设置各种属性的顺序会改变使用`.Start()`运行它时发生的情况.例如,如果您在设置`StartInfo`属性之前调用`.EnableRaisingEvents = true`,那么事情就会按预期工作.如果你稍后设置它,例如将它与`.Exited`保持在一起,即使你在`.Start()`之前调用它,它也无法正常工作 - `.Exited`立即触发而不是等待进程到实际上是退出 不知道为什么,只是一句谨慎的话. (5认同)
  • 此功能也可在[MedallionShell](https://github.com/madelson/MedallionShell)NuGet包中找到 (3认同)
  • @svick在窗口形式中,`process.SynchronizingObject`应设置为表单组件,以避免在分离的线程上调用处理事件的方法(如Exited,OutputDataReceived,ErrorDataReceived). (2认同)
  • 将`Process.Start`包装在`Task.Run`中实际上是有意义的。例如,UNC路径将被同步解析。此代码段最多可能需要30秒才能完成:`Process.Start(@“ \\ live.sysinternals.com \ whatever”)` (2认同)
  • 根据[这个答案]的评论(/sf/ask/4527251151/#64676891) ,可以在“Process”的其他事件(如“OutputDataReceived”)的所有处理程序完成之前引发“Exited”事件。另一方面,“WaitForExit”保证在等待之后,所有“Process”的活动都将结束。在 [`WaitForExitAsync`](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexitasync) 出现之前,似乎根本没有好的解决方案来解决这个问题。 (2认同)

Oha*_*der 52

根据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;
}
Run Code Online (Sandbox Code Playgroud)

  • 很棒的答案.谢谢你为奠定基础而感谢svick,感谢Ohad这次非常有用的扩展. (4认同)
  • @marrrschine我不明白你的意思,也许你应该用一些代码开始一个新的问题,这样我们就可以看到你尝试过的东西并从那里继续. (3认同)

Hie*_*ieu 14

在.Net 5.0中,有官方内置的WaitForExitAsync方法,所以你不必自己实现。此外,Start方法现在接受参数 as IEnumerable<string>(类似于 Python/Golang 等其他编程语言)。

下面是一个例子:

public static async Task YourMethod() {
    var p = Process.Start("bin_name", new[]{"arg1", "arg2", "arg3"});
    await p.WaitForExitAsync().ConfigureAwait(false);
    // more code;
}
Run Code Online (Sandbox Code Playgroud)

  • 你如何得到这个的输出? (2认同)

Apf*_*cha 5

我已经建立了一个课程来启动一个流程,并且由于各种需求,它在过去几年中不断增长。在使用过程中,我发现 Process 类在处理甚至读取 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;
    }
}
Run Code Online (Sandbox Code Playgroud)