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

C# 进程在等待退出时有时挂起,c#,C#,等待退出时进程挂起的原因可能是什么 此代码必须启动powershell脚本,该脚本在内部执行许多操作,例如通过MSBuild开始重新编译代码,但问题可能是它生成的输出太多,并且此代码在等待退出时被卡住,即使在power shell脚本正确执行后也是如此 这有点“奇怪”,因为有时这段代码工作得很好,有时只是卡住了 代码挂起于: process.WaitForExit(processTimeoutMilisons) Powershell脚本以1-2秒的速度执行,同时超时时间为19秒 public s

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

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

这有点“奇怪”,因为有时这段代码工作得很好,有时只是卡住了

代码挂起于:

process.WaitForExit(processTimeoutMilisons)

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构建

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的代码

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

文档页面位于相关文本中:

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

代码修改可能如下所示:

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

问题在于,如果重定向StandardOutput和/或StandardError,内部缓冲区可能会变满

要解决上述问题,您可以在单独的线程中运行该进程。我不使用WaitForExit,而是使用process exited事件,该事件将异步返回流程的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;
    }
公共异步任务RunProcessAsync(参数字符串[]args)
{
尝试
{
var tcs=new TaskCompletionSource();
var流程=新流程
{
StartInfo={
文件名='文件路径',
重定向标准输出=真,
RedirectStandardError=true,
Arguments=“shell命令”,
UseShellExecute=false,
CreateNoWindow=true
},
EnableRaisingEvents=true
};
process.exit+=(发送方,参数)=>
{
tcs.SetResult(process.ExitCode);
process.Dispose();
};
process.Start();
//在至少一个流上使用异步读取操作。
//同步读取两个流将产生另一个死锁。
process.BeginOutputReadLine();
字符串tmperroout=wait process.StandardError.ReadToEndAsync();
//process.WaitForExit();
返回等待任务;
}
捕获(异常ee){
控制台写入线(ee.Message);
}
返回-1;
}

上面的代码经过战斗测试,使用命令行参数调用FFMPEG.exe。我把mp4文件转换成mp3文件,一次完成1000多个视频,没有失败。不幸的是,我没有直接的power shell经验,但希望这能有所帮助。

让我们从相关文章中的概述开始


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

  • 如果在读取StandardOutput之前等待进程退出,进程可能会阻止尝试写入,因此进程永远不会结束
  • 如果使用ReadToEnd读取StandardOutput,则如果进程从未关闭StandardOutput(例如,如果它从未终止,或者如果它被阻止写入StandardError),则进程可能会被阻止
然而,即使是公认的答案,在某些情况下也会与执行顺序发生冲突

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

正是在这种情况下,当你想要安排好几个事件时,Rx才真正闪耀

注意,Rx的.NET实现作为System.Reactive NuGet包提供

让我们深入了解Rx如何帮助处理事件

//订阅OutputData
Observable.FromEventPattern(进程,名称(进程.OutputDataReceived))
.订阅(
eventPattern=>output.AppendLine(eventPattern.EventArgs.Data),
exception=>error.AppendLine(exception.Message)
).一次性使用(一次性使用);
FromEventPattern
允许我们将事件的不同事件映射到统一的流(也称为可观察)。这允许我们在管道中处理事件(使用类似LINQ的语义)。这里使用的
Subscribe
重载提供了
Action
Action
。无论何时引发观察到的事件,其
发送方
参数
都将被
事件模式
包装并通过
操作
推送。在管道中引发异常时,将使用
操作

事件
模式的缺点之一,在本用例中(以及参考文章中的所有解决方法)得到了明确说明,就是不清楚何时/何地取消订阅事件处理程序

使用Rx,我们在订阅时会返回一个
IDisposable
。当我们处理它时,我们实际上结束了订阅。Wi
.
.
.
    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{}

$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
$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
$path1 = """C:\....\15.0\Bin\MSBuild.exe"" ""C:\Users\John\source\Test.sln"""
$cmdOutput = cmd.exe /c $path1  '2>&1'
$cmdOutput