等待退出时我的进程挂起的原因可能是什么?
此代码必须启动powershell脚本,该脚本在内部执行许多操作,例如通过MSBuild启动重新编译代码,但是可能的问题是,即使生成了正确的power shell脚本,它也会生成过多的输出,并且在等待退出时该代码会卡住< / p>
有点“怪异”,因为有时此代码可以正常工作,有时甚至会卡住。
代码挂在:
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;
}
InternalExecuteScript
是ingen的代码
答案 0 :(得分:11)
让我们从相关帖子中的the accepted answer回顾开始。
问题是,如果您重定向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>
。每当引发观察到的事件时,其事件sender
和args
就会被EventPattern
包裹起来并通过Action<EventPattern<...>>
推送。在管道中引发异常时,将使用Action<Exception>
。
在此用例中(以及所引用的文章中的所有变通方法)清楚地说明了Event
模式的缺点之一是,不清楚何时/何处取消订阅事件处理程序。
使用Rx进行订阅时,我们会返回IDisposable
。当我们处理它时,我们实际上终止了订阅。通过添加DisposeWith
扩展方法(从RxUI借用),我们可以将多个IDisposable
添加到CompositeDisposable
(在代码示例中命名为disposables
)。完成所有操作后,我们可以通过一次致电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()
是异步执行的,参考remarks。但是,立即调用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;
}
}
答案 1 :(得分: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多个视频,而没有失败。不幸的是,我没有直接的电源外壳经验,但希望能对您有所帮助。
答案 2 :(得分:3)
对于读者的优点,我将其分为2部分
部分A:问题以及如何处理类似情况
B部分:问题重现&解决方案
发生此问题时-进程出现在任务管理器中,然后 2-3秒消失后(可以),然后等待超时,然后 引发System.InvalidOperationException:进程必须 在确定请求的信息之前退出。
&参见下面的方案4
在您的代码中:
Process.WaitForExit(ProcessTimeOutMiliseconds);
为此,您正在等待Process
超时 或 Exit ,该事件最早发生在 。 OutputWaitHandle.WaitOne(ProcessTimeOutMiliseconds)
和errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds);
这样,您正在等待OutputData
和ErrorData
流读取操作以表明其已完成 Process.ExitCode == 0
获取退出状态的进程 不同的设置及其注意事项:
ObjectDisposedException()
Process.ExitCode
,导致错误System.InvalidOperationException: Process must exit before requested information can be determined
。我已经对该场景进行了十多次测试,并且在测试时使用了以下设置,效果很好
更新代码
.
.
.
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测试,我终于能够在系统上重现该问题
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
即使未启用并行构建,您仍然可以通过 CMD 启动构建,从而防止进程挂在WaitForExit
上,因此您不会创建直接依赖项在构建过程中
$path1 = """C:\....\15.0\Bin\MSBuild.exe"" ""C:\Users\John\source\Test.sln"""
$cmdOutput = cmd.exe /c $path1 '2>&1'
$cmdOutput
首选第二种方法,因为您不希望周围有太多MSBuild节点。
答案 3 :(得分:0)
不确定这是否是您的问题,但是在异步重定向输出时,在MSDN上看,过载的WaitForExit似乎有些怪异。 MSDN文章建议在调用重载方法后,调用不带任何参数的WaitForExit。
位于here.相关文档的文档页面:
当标准输出已重定向到异步事件处理程序时,此方法返回时,输出处理可能未完成。为确保异步事件处理已完成,请在从此重载收到true后,调用不带任何参数的WaitForExit()重载。为了确保Windows窗体应用程序中正确处理了Exited事件,请设置SynchronizingObject属性。
代码修改可能看起来像这样:
if (process.WaitForExit(ProcessTimeOutMiliseconds))
{
process.WaitForExit();
}