¿Cuál puede ser la razón de que mi proceso se cuelgue mientras espero la salida?
Este código tiene que iniciar el script de PowerShell que en su interior realiza muchas acciones, por ejemplo, comenzar a recompilar el código a través de MSBuild, pero probablemente el problema es que genera demasiada salida y este código se atasca mientras espera salir incluso después de que el script de Power Shell se ejecute correctamente
es un poco "extraño" porque a veces este código funciona bien y otras simplemente se atasca.
El código se cuelga en:
process.WaitForExit (ProcessTimeOutMiliseconds);
El script de Powershell se ejecuta en 1-2 segundos, mientras que el tiempo de espera es de 19 segundos.
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);
        }
    }
}
Guión:
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;
}
dónde
$args[0] = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe"
Editar
A la solución de @ ingen agregué un pequeño contenedor que reintenta ejecutar MS Build colgado
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;
}
¿Dónde InternalExecuteScriptestá el código de ingen

Rxenfoque funcionó (ya que no se agotó el tiempo) incluso con el proceso de MSBuild perdido que condujo a una espera indefinida? interesado en saber cómo se manejó esoRespuestas:
Comencemos con un resumen de la respuesta aceptada en una publicación relacionada.
Incluso la respuesta aceptada, sin embargo, lucha con el orden de ejecución en ciertos casos.
Es en este tipo de situaciones, donde desea organizar varios eventos, que Rx realmente brilla.
Tenga en cuenta que la implementación .NET de Rx está disponible como el paquete System.Reactive NuGet.
Vamos a sumergirnos para ver cómo Rx facilita el trabajo con eventos.
FromEventPatternnos permite asignar distintas ocurrencias de un evento a una secuencia unificada (también conocida como observable). Esto nos permite manejar los eventos en una tubería (con semántica similar a LINQ). LaSubscribesobrecarga utilizada aquí se proporciona con anAction<EventPattern<...>>y anAction<Exception>. Cada vez que se produce el evento observado, susenderyargsserá envueltoEventPatterny empujado a través delAction<EventPattern<...>>. Cuando se genera una excepción en la canalización,Action<Exception>se utiliza.Uno de los inconvenientes del
Eventpatrón, claramente ilustrado en este caso de uso (y por todas las soluciones en la publicación referenciada), es que no es aparente cuándo / dónde cancelar la suscripción de los controladores de eventos.Con Rx volvemos y
IDisposablecuando hacemos una suscripción. Cuando lo desechamos, efectivamente finalizamos la suscripción. Con la adición delDisposeWithmétodo de extensión (prestado de RxUI ), podemos agregar múltiplesIDisposables a unCompositeDisposable(nombradodisposablesen los ejemplos de código). Cuando hayamos terminado, podemos finalizar todas las suscripciones con una sola llamadadisposables.Dispose().Sin duda, no hay nada que podamos hacer con Rx, que no podríamos hacer con Vanilla .NET. El código resultante es mucho más fácil de razonar, una vez que se haya adaptado a la forma funcional de pensar.
Ya discutimos la primera parte, donde asignamos nuestros eventos a observables, para que podamos saltar directamente a la parte carnosa. Aquí asignamos nuestro observable a la
processExitedvariable, porque queremos usarlo más de una vez.Primero, cuando lo activamos, llamando
Subscribe. Y más tarde cuando queremos 'esperar' su primer valor.Uno de los problemas con OP es que supone
process.WaitForExit(processTimeOutMiliseconds)que terminará el proceso cuando se agote el tiempo de espera. De MSDN :En cambio, cuando se agota el tiempo de espera, simplemente devuelve el control al subproceso actual (es decir, deja de bloquear). Debe forzar manualmente la terminación cuando el proceso agota el tiempo de espera. Para saber cuándo ha transcurrido el tiempo de espera, podemos asignar el
Process.Exitedevento a unprocessExitedobservable para su procesamiento. De esta manera podemos preparar la entrada para elDooperador.El código se explica por sí mismo. Si
exitedSuccessfullyel proceso habrá terminado con gracia. Si noexitedSuccessfully, la terminación deberá ser forzada. Tenga en cuenta queprocess.Kill()se ejecuta de forma asíncrona, comentarios de referencia . Sin embargo, llamarprocess.WaitForExit()inmediatamente después abrirá la posibilidad de puntos muertos nuevamente. Por lo tanto, incluso en el caso de terminación forzada, es mejor dejar que todos los desechables se limpien cuandousingfinalice el alcance, ya que la salida puede considerarse interrumpida / dañada de todos modos.La
try catchconstrucción está reservada para el caso excepcional (sin juego de palabras) en el que se haya alineadoprocessTimeOutMillisecondscon el tiempo real necesario para completar el proceso. En otras palabras, se produce una condición de carrera entre elProcess.Exitedevento y el temporizador. La posibilidad de que esto suceda se ve nuevamente aumentada por la naturaleza asincrónica deprocess.Kill(). Lo encontré una vez durante las pruebas.Para completar, el
DisposeWithmétodo de extensión.fuente
ExecuteScriptRxmanijashangsperfectamente. Desafortunadamente aún se producen bloqueos, pero acabo de agregar un pequeño envoltorio sobre tuExecuteScriptRxque funcionaRetryy luego se ejecuta bien. La razón de los bloqueos de MSBUILD puede ser la respuesta de @Clint. PD: Ese código me hizo sentir estúpido <lol> Esa es la primera vez que veoSystem.Reactive.Linq;Para beneficio de los lectores, voy a dividir esto en 2 secciones.
Sección A: problema y cómo manejar escenarios similares
Sección B: recreación del problema y solución
Sección A: Problema
En su código:
Process.WaitForExit(ProcessTimeOutMiliseconds);Con esto, está esperando el TiempoProcessde espera o la Salida , lo que ocurra primero .OutputWaitHandle.WaitOne(ProcessTimeOutMiliseconds)yerrorWaitHandle.WaitOne(ProcessTimeOutMiliseconds);con esto le está esperandoOutputDatayErrorDataflujo de operación de lectura para señalar su completaProcess.ExitCode == 0Obtiene el estado del proceso cuando salióDiferentes configuraciones y sus advertencias:
ObjectDisposedException()Process.ExitCodeque produce el errorSystem.InvalidOperationException: Process must exit before requested information can be determined.He probado este escenario más de una docena de veces y funciona bien, se han utilizado las siguientes configuraciones durante las pruebas
Código actualizado
EDITAR:
Después de horas de jugar con MSBuild, finalmente pude reproducir el problema en mi sistema
Sección B: Recreación de problemas y solución
Pude resolver esto de dos maneras
Genera el proceso MSBuild indirectamente a través de CMD
Continúe usando MSBuild pero asegúrese de establecer el nodo Reutilizar en Falso
Incluso si la compilación paralela no está habilitada, aún puede evitar que su proceso se bloquee
WaitForExitiniciando la compilación a través de CMD y, por lo tanto, no cree una dependencia directa en el proceso de compilaciónSe prefiere el segundo enfoque ya que no desea que haya demasiados nodos MSBuild por ahí.
fuente
"-nr:False","-m:3"parece haber solucionado el comportamiento de MSBuild hang-ish, lo que haRx solutionhecho que todo el proceso sea algo confiable (el tiempo se mostrará). Desearía poder aceptar ambas respuestas o dar dos recompensasRxenfoque en la otra solución podía resolver el problema sin aplicar-nr:False" ,"-m:3". Según tengo entendido, maneja la espera indefinida de puntos muertos y otras cosas que había cubierto en la sección 1. Y la causa raíz en la Sección 2 es la que creo que es la causa raíz del problema que enfrentaron;) Puedo estar equivocado, por eso Pregunté, solo el tiempo lo dirá ...El problema es que si redirige StandardOutput y / o StandardError, el búfer interno puede llenarse.
Para resolver los problemas mencionados anteriormente, puede ejecutar el proceso en subprocesos separados. No uso WaitForExit, utilizo el evento de proceso salido que devolverá el código de salida del proceso de forma asincrónica asegurando que se haya completado.
El código anterior se prueba en batalla llamando a FFMPEG.exe con argumentos de línea de comando. Estaba convirtiendo archivos mp4 en archivos mp3 y haciendo más de 1000 videos a la vez sin fallar. Desafortunadamente, no tengo experiencia directa en el shell de energía, pero espero que esto ayude.
fuente
BegingOutputReadliney después de realizarReadToEndAsyncelStandardError?No estoy seguro de si este es su problema, pero al mirar MSDN parece haber algo extraño con el WaitForExit sobrecargado cuando redirige la salida de forma asincrónica. El artículo de MSDN recomienda llamar a WaitForExit que no toma argumentos después de llamar al método sobrecargado.
Página de documentos ubicada aquí. Texto relevante:
La modificación del código podría verse así:
fuente
process.WaitForExit()lo indicado por los comentarios a esta respuesta .