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


13

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

此代码必须启动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);
        }
    }
}

脚本:

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

哪里

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

InternalExecuteScriptIngen的代码在哪里


进程实际挂在哪一行?并进一步介绍您的代码
AF.AF

@AF先生,您是对的-完成。
Joelty

1
实际调用Powershell是一回事,但是您未提供的是您在WITHIN Powershell中尝试处理的脚本的其余部分。调用powershell本身不是问题,而是您要尝试执行的操作。编辑您的帖子,并放置您要执行的显式调用/命令。
DRapp

1
我尝试复制错误真的很奇怪。它在20次尝试左右随机发生了两次,而我无法再次触发它。
KiKoS

1
@Joelty,哦,很有趣,您是说Rx即使杂散的MSBuild进程导致无限期等待,该方法仍然有效(因为它没有超时)?有兴趣知道如何处理
Clint

Answers:


9

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

问题是,如果您重定向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);

FromEventPattern允许我们将事件的不同事件映射到统一流(也称为可观察到的流)。这使我们能够处理管道中的事件(具有类似LINQ的语义)。Subscribe这里使用的重载带有Action<EventPattern<...>>Action<Exception>。每当观察到的事件发生时,它的senderargs将被包装EventPattern并推入Action<EventPattern<...>>。当管道中引发异常时,Action<Exception>将使用。

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

有了Rx,IDisposable当我们进行订阅时,我们将获得一个。当我们处理它时,我们实际上终止了订阅。通过添加DisposeWith扩展方法(从RxUI借用),我们可以IDisposable向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;
    }
}

我们已经讨论了第一部分,其中将事件映射到可观察对象,因此我们可以直接跳到最重要的部分。在这里,我们将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();

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

指示流程组件等待指定的毫秒数,以使关联的流程退出。

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

该代码非常不言自明。如果exitedSuccessfully该过程将正常终止。如果不是exitedSuccessfully,则必须强制终止。需要注意的是process.Kill()异步执行,裁判的言论。但是,立即调用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;
    }
}

4
恕我直言,绝对值得赏金。不错的答案,以及关于RX的不错的现场介绍。
quetzalcoatl

谢谢!!!您的ExecuteScriptRx把手hangs完美。不幸的是,挂起仍然会发生,但是我只是在ExecuteScriptRx执行的对象上添加了一个小包装Retry,然后它执行得很好。MSBUILD挂起的原因可能是@Clint答案。PS:该代码使我感到愚蠢<lol>这是我第一次看到System.Reactive.Linq;
Joelty

包装程序的代码在主要帖子中
Joelty,

3

为了读者的利益,我将其分为两个部分

A部分:问题以及如何处理类似情况

B部分:问题重现与解决方案

A部分:问题

发生此问题时-进程出现在任务管理器中,然后在2-3秒后消失(正常),然后等待超时,然后引发异常System.InvalidOperationException:必须先退出进程,然后才能确定请求的信息。

&请参阅下面的方案4

在您的代码中:

  1. Process.WaitForExit(ProcessTimeOutMiliseconds); 有了这个,你就等着Process超时退出,这曾经发生的第一次
  2. OutputWaitHandle.WaitOne(ProcessTimeOutMiliseconds)errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds); 此相关的是,您正在等待OutputDataErrorData流读取操作以指示其完成
  3. Process.ExitCode == 0 退出时获取进程状态

不同的设置及其注意事项:

  • 方案1(快乐路径):进程在超时之前完成,因此您的stdoutput和stderror也已在超时之前完成,一切都很好。
  • 方案2:进程,OutputWaitHandle和ErrorWaitHandle超时,但是仍然正在读取stdoutput和stderror,并且在WaitHandlers超时后完成。这导致另一个异常ObjectDisposedException()
  • 方案3:首先处理超时(19秒),但是stdout和stderror起作用,您等待WaitHandler超时(19秒),从而导致+ 19sec的额外延迟。
  • 方案4:进程超时,代码尝试过早查询,Process.ExitCode从而导致错误System.InvalidOperationException: Process must exit before requested information can be determined

我已经对该场景进行了十多次测试,并且在测试过程中使用了以下设置,效果很好

  • 通过启动约2-15个项目的构建,输出流的大小从5KB到198KB不等
  • 超时窗口中的过早超时和进程退出


更新的代码

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

    //First waiting for ReadOperations to Timeout and then check Process to Timeout
    if (!outputWaitHandle.WaitOne(ProcessTimeOutMiliseconds) && !errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds)
        && !process.WaitForExit(ProcessTimeOutMiliseconds)  )
    {
        //To cancel the Read operation if the process is stil reading after the timeout this will prevent ObjectDisposeException
        process.CancelOutputRead();
        process.CancelErrorRead();

        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine("Timed Out");
        Logs = output + Environment.NewLine + error;
       //To release allocated resource for the Process
        process.Close();
        return  (false, logs);
    }

    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine("Completed On Time");
    Logs = output + Environment.NewLine + error;
    ExitCode = process.ExitCode.ToString();
    // Close frees the memory allocated to the exited process
    process.Close();

    //ExitCode now accessible
    return process.ExitCode == 0 ? (true, logs) : (false, logs);
    }
}
finally{}

编辑:

经过数小时的MSBuild测试,我终于能够在系统上重现该问题


B部分:问题解决与解决

MSBuild具有-m[:number]开关,用于指定构建时要使用的最大并发进程数。

启用此功能后,即使在构建完成后,MSBuild也会生成许多存活的节点。现在, Process.WaitForExit(milliseconds)将等待永不退出,最终超时

我可以通过几种方式解决这个问题

  • 通过CMD间接生成MSBuild过程

    $path1 = """C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\MSBuild.exe"" ""C:\Users\John\source\repos\Test\Test.sln"" -maxcpucount:3"
    $cmdOutput = cmd.exe /c $path1  '2>&1'
    $cmdOutput
    
  • 继续使用MSBuild,但请确保将nodeReuse设置为False

    $filepath = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\MSBuild.exe"
    $arg1 = "C:\Users\John\source\repos\Test\Test.sln"
    $arg2 = "-m:3"
    $arg3 = "-nr:False"
    
    Start-Process -FilePath $filepath -ArgumentList $arg1,$arg2,$arg3 -Wait -NoNewWindow
    
  • 即使未启用并行构建,您仍然可以WaitForExit通过CMD启动Build来防止进程挂起,因此,您不会直接依赖Build进程

    $path1 = """C:\....\15.0\Bin\MSBuild.exe"" ""C:\Users\John\source\Test.sln"""
    $cmdOutput = cmd.exe /c $path1  '2>&1'
    $cmdOutput
    

首选第二种方法,因为您不希望放置太多的MSBuild节点。


因此,就像我在上面说的,谢谢,这"-nr:False","-m:3"似乎已经解决了MSBuild的挂起行为,Rx solution使整个过程变得可靠了(时间将会显示)。我希望我可以接受两个答案,也可以给予两个赏金
Joelty

@Joelty我只是想知道Rx其他解决方案中的方法是否可以解决问题而无需申请-nr:False" ,"-m:3"。以我的理解,它可以处理无限期等待的死锁和我在第1节中介绍的其他内容。第2节中的根本原因是我认为是您所遇到问题的根本原因;)我可能是错的,这就是为什么我问,只有时间会告诉...欢呼!
克林特

3

问题是,如果您重定向StandardOutput和/或StandardError,则内部缓冲区可能已满。

要解决上述问题,您可以在单独的线程中运行该过程。我不使用WaitForExit,而是利用流程退出事件,该事件将异步返回流程的ExitCode,以确保流程已完成。

public async Task<int> RunProcessAsync(params string[] args)
    {
        try
        {
            var tcs = new TaskCompletionSource<int>();

            var process = new Process
            {
                StartInfo = {
                    FileName = 'file path',
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    Arguments = "shell command",
                    UseShellExecute = false,
                    CreateNoWindow = true
                },
                EnableRaisingEvents = true
            };


            process.Exited += (sender, args) =>
            {
                tcs.SetResult(process.ExitCode);
                process.Dispose();
            };

            process.Start();
            // Use asynchronous read operations on at least one of the streams.
            // Reading both streams synchronously would generate another deadlock.
            process.BeginOutputReadLine();
            string tmpErrorOut = await process.StandardError.ReadToEndAsync();
            //process.WaitForExit();


            return await tcs.Task;
        }
        catch (Exception ee) {
            Console.WriteLine(ee.Message);
        }
        return -1;
    }

上面的代码已经过实战测试,并使用命令行参数调用FFMPEG.exe。我将mp4文件转换为mp3文件,一次完成了1000多个视频,而没有失败。不幸的是,我没有直接的电源外壳经验,但希望能有所帮助。


这段代码很奇怪,类似于其他解决方案在第一次尝试时失败(卡住),然后似乎可以正常工作(像其他5次尝试一样,我将对其进行更多测试)。顺便说一下你为什么执行BegingOutputReadline,然后执行ReadToEndAsyncStandardError
Joelty

OP已经开始异步读取,因此控制台缓冲区死锁的可能性不大。
yaakov

0

不确定这是否是您的问题,但是在异步重定向输出时,在MSDN上看,过载的WaitForExit似乎有些怪异。MSDN文章建议在调用重载方法后,调用不带任何参数的WaitForExit。

文档页面位于此处。相关文字:

当标准输出已重定向到异步事件处理程序时,此方法返回时,输出处理可能尚未完成。为确保异步事件处理已完成,请在从此重载收到true后,调用不带任何参数的WaitForExit()重载。为确保Windows窗体应用程序中正确处理了Exited事件,请设置SynchronizingObject属性。

代码修改可能看起来像这样:

if (process.WaitForExit(ProcessTimeOutMiliseconds))
{
  process.WaitForExit();
}

process.WaitForExit()如对这个答案的评论所指出的那样,在使用上有些复杂。
ingen
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.