我的进程在等待退出时挂起的原因可能是什么?
此代码必须启动 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>。每当观察到的事件上升,其sender与args将被包裹EventPattern,并通过推压Action<EventPattern<...>>。当管道中引发异常时,Action<Exception>使用。
该Event模式的缺点之一,在这个用例中(以及参考文章中的所有变通方法)清楚地说明,是何时/何地取消订阅事件处理程序并不明显。
使用 Rx IDisposable,我们在订阅时会返回一个。当我们处理它时,我们实际上结束了订阅。通过添加DisposeWith扩展方法(从RxUI 中借用),我们可以将多个IDisposables添加到 a CompositeDisposable(disposables在代码示例中命名)。完成后,我们可以通过一次调用来结束所有订阅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)
| 归档时间: |
|
| 查看次数: |
2199 次 |
| 最近记录: |