进程有时在等待退出时挂起

Joe*_*lty 15 c#

我的进程在等待退出时挂起的原因可能是什么?

此代码必须启动 powershell 脚本,该脚本在内部执行许多操作,例如通过 MSBuild 开始重新编译代码,但问题可能是它生成了太多输出,并且即使在正确执行 power shell 脚本后,此代码在等待退出时也会卡住

这有点“奇怪”,因为有时这段代码运行良好,有时却卡住了。

代码挂在:

process.WaitForExit(ProcessTimeOutMiliseconds);

Powershell 脚本在 1-2 秒内执行,同时超时为 19 秒。

public static (bool Success, string Logs) ExecuteScript(string path, int ProcessTimeOutMiliseconds, params string[] args)
{
    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();

    using (var outputWaitHandle = new AutoResetEvent(false))
    using (var errorWaitHandle = new AutoResetEvent(false))
    {
        try
        {
            using (var process = new Process())
            {
                process.StartInfo = new ProcessStartInfo
                {
                    WindowStyle = ProcessWindowStyle.Hidden,
                    FileName = "powershell.exe",
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    UseShellExecute = false,
                    Arguments = $"-ExecutionPolicy Bypass -File \"{path}\"",
                    WorkingDirectory = Path.GetDirectoryName(path)
                };

                if (args.Length > 0)
                {
                    var arguments = string.Join(" ", args.Select(x => $"\"{x}\""));
                    process.StartInfo.Arguments += $" {arguments}";
                }

                output.AppendLine($"args:'{process.StartInfo.Arguments}'");

                process.OutputDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        outputWaitHandle.Set();
                    }
                    else
                    {
                        output.AppendLine(e.Data);
                    }
                };
                process.ErrorDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        errorWaitHandle.Set();
                    }
                    else
                    {
                        error.AppendLine(e.Data);
                    }
                };

                process.Start();

                process.BeginOutputReadLine();
                process.BeginErrorReadLine();

                process.WaitForExit(ProcessTimeOutMiliseconds);

                var logs = output + Environment.NewLine + error;

                return process.ExitCode == 0 ? (true, logs) : (false, logs);
            }
        }
        finally
        {
            outputWaitHandle.WaitOne(ProcessTimeOutMiliseconds);
            errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

脚本:

start-process $args[0] App.csproj -Wait -NoNewWindow

[string]$sourceDirectory  = "\bin\Debug\*"
[int]$count = (dir $sourceDirectory | measure).Count;

If ($count -eq 0)
{
    exit 1;
}
Else
{
    exit 0;
}
Run Code Online (Sandbox Code Playgroud)

在哪里

$args[0] = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe"

编辑

对于@ingen 的解决方案,我添加了一个小包装器,它重试执行挂起的 MS Build

public static void ExecuteScriptRx(string path, int processTimeOutMilliseconds, out string logs, out bool success, params string[] args)
{
    var current = 0;
    int attempts_count = 5;
    bool _local_success = false;
    string _local_logs = "";

    while (attempts_count > 0 && _local_success == false)
    {
        Console.WriteLine($"Attempt: {++current}");
        InternalExecuteScript(path, processTimeOutMilliseconds, out _local_logs, out _local_success, args);
        attempts_count--;
    }

    success = _local_success;
    logs = _local_logs;
}
Run Code Online (Sandbox Code Playgroud)

InternalExecuteScriptingen的代码在哪里

ing*_*gen 13

让我们先回顾一下相关帖子中已接受的答案

问题是,如果您重定向 StandardOutput 和/或 StandardError,内部缓冲区可能会变满。无论您使用何种顺序,都可能存在问题:

  • 如果在读取 StandardOutput 之前等待进程退出,进程可能会阻止尝试写入它,因此进程永远不会结束。
  • 如果您使用 ReadToEnd 从 StandardOutput 读取数据,那么如果进程从不关闭 StandardOutput(例如,如果它从不终止,或者如果它被阻止写入 StandardError),则您的进程可能会阻塞。

然而,即使是公认的答案,在某些情况下也会与执行顺序作斗争。

编辑:如果发生超时,请参阅下面的答案以了解如何避免ObjectDisposedException

正是在这种情况下,您想要编排多个事件,Rx 才真正发挥作用。

请注意,Rx 的 .NET 实现可用作 System.Reactive NuGet 包。

让我们深入了解 Rx 如何促进处理事件。

// Subscribe to OutputData
Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.OutputDataReceived))
    .Subscribe(
        eventPattern => output.AppendLine(eventPattern.EventArgs.Data),
        exception => error.AppendLine(exception.Message)
    ).DisposeWith(disposables);
Run Code Online (Sandbox Code Playgroud)

FromEventPattern允许我们将一个事件的不同发生映射到一个统一的流(又名 observable)。这允许我们处理管道中的事件(使用类似 LINQ 的语义)。Subscribe此处使用的重载由 anAction<EventPattern<...>>和 an 提供Action<Exception>。每当观察到的事件上升,其senderargs将被包裹EventPattern,并通过推压Action<EventPattern<...>>。当管道中引发异常时,Action<Exception>使用。

Event模式的缺点之一,在这个用例中(以及参考文章中的所有变通方法)清楚地说明,是何时/何地取消订阅事件处理程序并不明显。

使用 Rx IDisposable,我们在订阅时会返回一个。当我们处理它时,我们实际上结束了订阅。通过添加DisposeWith扩展方法(从RxUI 中借用),我们可以将多个IDisposables添加到 a CompositeDisposabledisposables在代码示例中命名)。完成后,我们可以通过一次调用来结束所有订阅disposables.Dispose()

可以肯定的是,我们不能用 Rx 做任何事情,我们不能用普通的 .NET 做。一旦您适应了函数式思维方式,生成的代码就会更容易推理。

public static void ExecuteScriptRx(string path, int processTimeOutMilliseconds, out string logs, out bool success, params string[] args)
{
    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();

    using (var process = new Process())
    using (var disposables = new CompositeDisposable())
    {
        process.StartInfo = new ProcessStartInfo
        {
            WindowStyle = ProcessWindowStyle.Hidden,
            FileName = "powershell.exe",
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            Arguments = $"-ExecutionPolicy Bypass -File \"{path}\"",
            WorkingDirectory = Path.GetDirectoryName(path)
        };

        if (args.Length > 0)
        {
            var arguments = string.Join(" ", args.Select(x => $"\"{x}\""));
            process.StartInfo.Arguments += $" {arguments}";
        }

        output.AppendLine($"args:'{process.StartInfo.Arguments}'");

        // Raise the Process.Exited event when the process terminates.
        process.EnableRaisingEvents = true;

        // Subscribe to OutputData
        Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.OutputDataReceived))
            .Subscribe(
                eventPattern => output.AppendLine(eventPattern.EventArgs.Data),
                exception => error.AppendLine(exception.Message)
            ).DisposeWith(disposables);

        // Subscribe to ErrorData
        Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.ErrorDataReceived))
            .Subscribe(
                eventPattern => error.AppendLine(eventPattern.EventArgs.Data),
                exception => error.AppendLine(exception.Message)
            ).DisposeWith(disposables);

        var processExited =
            // Observable will tick when the process has gracefully exited.
            Observable.FromEventPattern<EventArgs>(process, nameof(Process.Exited))
                // First two lines to tick true when the process has gracefully exited and false when it has timed out.
                .Select(_ => true)
                .Timeout(TimeSpan.FromMilliseconds(processTimeOutMilliseconds), Observable.Return(false))
                // Force termination when the process timed out
                .Do(exitedSuccessfully => { if (!exitedSuccessfully) { try { process.Kill(); } catch {} } } );

        // Subscribe to the Process.Exited event.
        processExited
            .Subscribe()
            .DisposeWith(disposables);

        // Start process(ing)
        process.Start();

        process.BeginOutputReadLine();
        process.BeginErrorReadLine();

        // Wait for the process to terminate (gracefully or forced)
        processExited.Take(1).Wait();

        logs = output + Environment.NewLine + error;
        success = process.ExitCode == 0;
    }
}
Run Code Online (Sandbox Code Playgroud)

我们已经讨论了第一部分,我们将事件映射到可观察对象,因此我们可以直接跳到内容丰富的部分。在这里,我们将 observable 分配给processExited变量,因为我们想多次使用它。

首先,当我们激活它时,通过调用Subscribe. 稍后当我们想要“等待”它的第一个值时。

var processExited =
    // Observable will tick when the process has gracefully exited.
    Observable.FromEventPattern<EventArgs>(process, nameof(Process.Exited))
        // First two lines to tick true when the process has gracefully exited and false when it has timed out.
        .Select(_ => true)
        .Timeout(TimeSpan.FromMilliseconds(processTimeOutMilliseconds), Observable.Return(false))
        // Force termination when the process timed out
        .Do(exitedSuccessfully => { if (!exitedSuccessfully) { try { process.Kill(); } catch {} } } );

// Subscribe to the Process.Exited event.
processExited
    .Subscribe()
    .DisposeWith(disposables);

// Start process(ing)
...

// Wait for the process to terminate (gracefully or forced)
processExited.Take(1).Wait();
Run Code Online (Sandbox Code Playgroud)

OP 的问题之一是它假设process.WaitForExit(processTimeOutMiliseconds)超时时会终止进程。从MSDN

指示Process组件等待指定的毫秒数,以便关联的进程退出。

相反,当它超时时,它只是将控制权返回给当前线程(即它停止阻塞)。当进程超时时,您需要手动强制终止。要知道何时发生超时,我们可以将Process.Exited事件映射到processExited可观察对象进行处理。这样我们就可以为Do操作员准备输入。

代码是不言自明的。如果exitedSuccessfully进程正常终止。如果不是exitedSuccessfully,则需要强制终止。注意process.Kill()是异步执行的,ref备注。但是,立即调用process.WaitForExit()将再次打开死锁的可能性。因此,即使在强制终止的情况下,最好在using范围结束时清理所有一次性物品,因为无论如何输出都可能被视为中断/损坏。

try catch构造保留用于特殊情况(无双关语),在这种情况下,您已与processTimeOutMilliseconds完成流程所需的实际时间保持一致。换句话说,在Process.Exited事件和计时器之间发生了竞争条件。发生这种情况的可能性再次被 的异步性质放大process.Kill()。我在测试中遇到过一次。


为了完整起见,DisposeWith扩展方法。

/// <summary>
/// Extension methods associated with the IDisposable interface.
/// </summary>
public static class DisposableExtensions
{
    /// <summary>
    /// Ensures the provided disposable is disposed with the specified <see cref="CompositeDisposable"/>.
    /// </summary>
    public static T DisposeWith<T>(this T item, CompositeDisposable compositeDisposable)
        where T : IDisposable
    {
        if (compositeDisposable == null)
        {
            throw new ArgumentNullException(nameof(compositeDisposable));
        }

        compositeDisposable.Add(item);
        return item;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 恕我直言,绝对值得赏金。很好的答案,以及对 RX 的精彩主题介绍。 (4认同)