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



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

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

这有点"奇怪",因为有时这段代码工作正常,有时它只是卡住了。

代码在以下位置挂起:

过程。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  = "binDebug*"
[int]$count = (dir $sourceDirectory | measure).Count;
If ($count -eq 0)
{
exit 1;
}
Else
{
exit 0;
}

哪里

$args[0] = "C:Program Files (x86)Microsoft Visual Studio2019ProfessionalMSBuildCurrentBinMSBuild.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;
}

InternalExecuteScript是英根的代码

让我们从相关文章中接受的答案的回顾开始。

问题是,如果重定向标准输出和/或标准错误,内部缓冲区可能会变满。无论您使用什么顺序,都可能存在问题:

  • 如果在读取 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),我们可以向CompositeDisposable(在代码示例中名为disposables)添加多个IDisposable。完成后,我们可以通过一次调用来结束所有订阅disposables.Dispose().

可以肯定的是,我们用Rx做不到什么,用vanilla .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;
}
}

我们已经讨论了第一部分,我们将事件映射到可观察量,因此我们可以直接跳到肉质部分。在这里,我们将可观察量分配给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 组件等待指定的毫秒数,以便关联的进程退出。

相反,当它超时时,它只是将控制权返回给当前线程(即它停止阻塞)。您需要在进程超时时手动强制终止。要知道何时发生超时,我们可以将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;
}
}

问题是,如果重定向 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多个视频而不会失败。不幸的是,我没有直接的电源外壳经验,但希望这有所帮助。

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

A部分:问题和如何处理类似场景

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

A节:问题

发生此问题时 - 进程出现在任务管理器中,然后 2-3秒后消失(很好),然后等待超时,然后 引发异常系统。无效操作异常:进程必须 在确定请求的信息之前退出。

&请参阅下面的场景 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 秒),但标准输出和标准错误正在运行,您等待 WaitHandler 超时(19 秒),导致额外的延迟 + 19 秒。
  • 方案 4:进程超时,代码尝试过早查询Process.ExitCode导致错误System.InvalidOperationException: Process must exit before requested information can be determined

我已经测试了十几次这种情况并且工作正常,测试时使用了以下设置

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


更新的代码

.
.
.
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 Studio2017CommunityMSBuild15.0BinMSBuild.exe"" ""C:UsersJohnsourcereposTestTest.sln"" -maxcpucount:3"
    $cmdOutput = cmd.exe /c $path1  '2>&1'
    $cmdOutput
    
  • 继续使用 MSBuild,但请确保将节点重用设置为 False

    $filepath = "C:Program Files (x86)Microsoft Visual Studio2017CommunityMSBuild15.0BinMSBuild.exe"
    $arg1 = "C:UsersJohnsourcereposTestTest.sln"
    $arg2 = "-m:3"
    $arg3 = "-nr:False"
    Start-Process -FilePath $filepath -ArgumentList $arg1,$arg2,$arg3 -Wait -NoNewWindow
    
  • 即使未启用并行构建,您仍然可以通过CMD启动构建来防止进程挂起WaitForExit,因此您不会创建对构建过程的直接依赖

    $path1 = """C:....15.0BinMSBuild.exe"" ""C:UsersJohnsourceTest.sln"""
    $cmdOutput = cmd.exe /c $path1  '2>&1'
    $cmdOutput
    

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

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

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

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

代码修改可能如下所示:

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

最新更新