ProcessStartInfo colgando en "WaitForExit"? ¿Por qué?

187

Tengo el siguiente código:

info = new System.Diagnostics.ProcessStartInfo("TheProgram.exe", String.Join(" ", args));
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
System.Diagnostics.Process p = System.Diagnostics.Process.Start(info);
p.WaitForExit();
Console.WriteLine(p.StandardOutput.ReadToEnd()); //need the StandardOutput contents

Sé que el resultado del proceso que estoy iniciando es de alrededor de 7 MB. Ejecutarlo en la consola de Windows funciona bien. Desafortunadamente programáticamente esto se cuelga indefinidamente en WaitForExit. Tenga en cuenta también que esto no bloquea el código para salidas más pequeñas (como 3 KB).

¿Es posible que el StandardOutput interno en ProcessStartInfo no pueda almacenar 7MB? Si es así, ¿qué debo hacer en su lugar? Si no, ¿qué estoy haciendo mal?

Epaga
fuente
alguna solución final con el código fuente completo al respecto?
Kiquenet
2
Me encontré con el mismo problema y así pude resolverlo stackoverflow.com/questions/2285288/…
Bedasso
66
Sí, solución final: intercambie las dos últimas líneas. Está en el manual .
Amit Naidu
44
from msdn: El ejemplo de código evita una condición de punto muerto llamando a p.StandardOutput.ReadToEnd antes de p.WaitForExit. Puede producirse una condición de punto muerto si el proceso primario llama a p.WaitForExit antes de p.StandardOutput.ReadToEnd y el proceso secundario escribe suficiente texto para llenar la secuencia redirigida. El proceso padre esperaría indefinidamente a que salga el proceso hijo. El proceso secundario esperaría indefinidamente a que el padre lea de la secuencia completa de StandardOutput.
Carlos Liu
Es un poco molesto lo complejo que es hacer esto correctamente. Me complació solucionarlo con redireccionamientos de línea de comandos más simples> archivo de salida :)
eglasius

Respuestas:

393

El problema es que si redirige StandardOutputy / o StandardErrorel búfer interno puede llenarse. Cualquiera sea el orden que use, puede haber un problema:

  • Si espera a que el proceso salga antes de leer, StandardOutputel proceso puede bloquear el intento de escribir en él, por lo que el proceso nunca termina.
  • Si lees StandardOutputusando ReadToEnd, tu proceso puede bloquearse si el proceso nunca se cierra StandardOutput(por ejemplo, si nunca termina o si está bloqueado la escritura StandardError).

La solución es usar lecturas asincrónicas para garantizar que el búfer no se llene. Para evitar los puntos muertos y obtener hasta toda la salida de ambos StandardOutputy StandardErrorse puede hacer esto:

EDITAR: Vea las respuestas a continuación para saber cómo evitar una ObjectDisposedException si se produce el tiempo de espera.

using (Process process = new Process())
{
    process.StartInfo.FileName = filename;
    process.StartInfo.Arguments = arguments;
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;

    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();

    using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
    using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
    {
        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();

        if (process.WaitForExit(timeout) &&
            outputWaitHandle.WaitOne(timeout) &&
            errorWaitHandle.WaitOne(timeout))
        {
            // Process completed. Check process.ExitCode here.
        }
        else
        {
            // Timed out.
        }
    }
}
Mark Byers
fuente
11
No tenía idea de que la redirección de la salida estaba causando el problema, pero efectivamente lo fue. Pasé 4 horas golpeando mi cabeza sobre esto y lo arreglé en 5 minutos después de leer tu publicación. ¡Buen trabajo!
Ben Gripka
1
@AlexPeck El problema era ejecutar esto como una aplicación de consola. Hans Passant identificó el problema aquí: stackoverflow.com/a/16218470/279516
Bob Horn el
55
cada vez que se cierra el símbolo del sistema, aparece esto: se produjo una excepción no controlada del tipo "System.ObjectDisposed" en mscorlib.dll Información adicional: Se ha cerrado el identificador seguro
usuario1663380
3
Tuvimos un problema similar al descrito por @ user1663380 arriba. ¿Crees que es posible que las usingdeclaraciones de los controladores de eventos tengan que estar por encima de las usingdeclaraciones del proceso en sí?
Dan Forbes
2
No creo que se necesiten las manijas de espera. Según msdn, simplemente termine con la versión sin tiempo de espera de WaitForExit: cuando la salida estándar se ha redirigido a controladores de eventos asíncronos, es posible que el procesamiento de salida no se haya completado cuando este método regrese. Para asegurarse de que se haya completado el manejo de eventos asíncronos, llame a la sobrecarga WaitForExit () que no toma ningún parámetro después de recibir un verdadero de esta sobrecarga.
Patrick
98

La documentación de Process.StandardOutputdice leer antes de esperar, de lo contrario puede llegar a un punto muerto, el fragmento copiado a continuación:

 // Start the child process.
 Process p = new Process();
 // Redirect the output stream of the child process.
 p.StartInfo.UseShellExecute = false;
 p.StartInfo.RedirectStandardOutput = true;
 p.StartInfo.FileName = "Write500Lines.exe";
 p.Start();
 // Do not wait for the child process to exit before
 // reading to the end of its redirected stream.
 // p.WaitForExit();
 // Read the output stream first and then wait.
 string output = p.StandardOutput.ReadToEnd();
 p.WaitForExit();
Robar
fuente
14
No estoy 100% seguro de si esto es solo el resultado de mi entorno, pero descubrí que si lo has configurado RedirectStandardOutput = true;y no lo utilizas p.StandardOutput.ReadToEnd();, obtienes un punto muerto / bloqueo.
Chris S
3
Cierto. Estaba en una situación similar. Estaba redirigiendo StandardError sin ningún motivo al convertir con ffmpeg en un proceso, estaba escribiendo lo suficiente en la secuencia StandardError para crear un punto muerto.
Léon Pelletier
Esto todavía me cuelga incluso con la redirección y la lectura de la salida estándar.
user3791372
@ user3791372 Supongo que esto solo es aplicable si el búfer detrás de StandardOutput no está completamente lleno. Aquí el MSDN no hace justicia. Un gran artículo que le recomendaría que lea está en: dzone.com/articles/async-io-and-threadpool
Cary
19

La respuesta de Mark Byers es excelente, pero solo agregaría lo siguiente:

Los delegados OutputDataReceivedy ErrorDataReceiveddeben ser eliminados antes outputWaitHandley errorWaitHandleeliminados. Si el proceso continúa enviando datos después de que se haya excedido el tiempo de espera y luego finalice, se accederá a las variables outputWaitHandley errorWaitHandledespués de eliminarlas.

(Para su información, tuve que agregar esta advertencia como respuesta, ya que no podía comentar sobre su publicación).

stevejay
fuente
2
¿Quizás sería mejor llamar a CancelOutputRead ?
Mark Byers
¡Agregar el código editado de Mark a esta respuesta sería bastante impresionante! Estoy teniendo exactamente el mismo problema en este momento.
ianbailey
8
@ianbailey La forma más fácil de resolver esto es poner el uso (Proceso p ...) dentro del uso (AutoResetEvent errorWaitHandle ...)
Didier A.
18

Esta es una solución basada en la Biblioteca de tareas paralelas (TPL) más moderna y esperable para .NET 4.5 y superior.

Ejemplo de uso

try
{
    var exitCode = await StartProcess(
        "dotnet", 
        "--version", 
        @"C:\",
        10000, 
        Console.Out, 
        Console.Out);
    Console.WriteLine($"Process Exited with Exit Code {exitCode}!");
}
catch (TaskCanceledException)
{
    Console.WriteLine("Process Timed Out!");
}

Implementación

public static async Task<int> StartProcess(
    string filename,
    string arguments,
    string workingDirectory= null,
    int? timeout = null,
    TextWriter outputTextWriter = null,
    TextWriter errorTextWriter = null)
{
    using (var process = new Process()
    {
        StartInfo = new ProcessStartInfo()
        {
            CreateNoWindow = true,
            Arguments = arguments,
            FileName = filename,
            RedirectStandardOutput = outputTextWriter != null,
            RedirectStandardError = errorTextWriter != null,
            UseShellExecute = false,
            WorkingDirectory = workingDirectory
        }
    })
    {
        var cancellationTokenSource = timeout.HasValue ?
            new CancellationTokenSource(timeout.Value) :
            new CancellationTokenSource();

        process.Start();

        var tasks = new List<Task>(3) { process.WaitForExitAsync(cancellationTokenSource.Token) };
        if (outputTextWriter != null)
        {
            tasks.Add(ReadAsync(
                x =>
                {
                    process.OutputDataReceived += x;
                    process.BeginOutputReadLine();
                },
                x => process.OutputDataReceived -= x,
                outputTextWriter,
                cancellationTokenSource.Token));
        }

        if (errorTextWriter != null)
        {
            tasks.Add(ReadAsync(
                x =>
                {
                    process.ErrorDataReceived += x;
                    process.BeginErrorReadLine();
                },
                x => process.ErrorDataReceived -= x,
                errorTextWriter,
                cancellationTokenSource.Token));
        }

        await Task.WhenAll(tasks);
        return process.ExitCode;
    }
}

/// <summary>
/// Waits asynchronously for the process to exit.
/// </summary>
/// <param name="process">The process to wait for cancellation.</param>
/// <param name="cancellationToken">A cancellation token. If invoked, the task will return
/// immediately as cancelled.</param>
/// <returns>A Task representing waiting for the process to end.</returns>
public static Task WaitForExitAsync(
    this Process process,
    CancellationToken cancellationToken = default(CancellationToken))
{
    process.EnableRaisingEvents = true;

    var taskCompletionSource = new TaskCompletionSource<object>();

    EventHandler handler = null;
    handler = (sender, args) =>
    {
        process.Exited -= handler;
        taskCompletionSource.TrySetResult(null);
    };
    process.Exited += handler;

    if (cancellationToken != default(CancellationToken))
    {
        cancellationToken.Register(
            () =>
            {
                process.Exited -= handler;
                taskCompletionSource.TrySetCanceled();
            });
    }

    return taskCompletionSource.Task;
}

/// <summary>
/// Reads the data from the specified data recieved event and writes it to the
/// <paramref name="textWriter"/>.
/// </summary>
/// <param name="addHandler">Adds the event handler.</param>
/// <param name="removeHandler">Removes the event handler.</param>
/// <param name="textWriter">The text writer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static Task ReadAsync(
    this Action<DataReceivedEventHandler> addHandler,
    Action<DataReceivedEventHandler> removeHandler,
    TextWriter textWriter,
    CancellationToken cancellationToken = default(CancellationToken))
{
    var taskCompletionSource = new TaskCompletionSource<object>();

    DataReceivedEventHandler handler = null;
    handler = new DataReceivedEventHandler(
        (sender, e) =>
        {
            if (e.Data == null)
            {
                removeHandler(handler);
                taskCompletionSource.TrySetResult(null);
            }
            else
            {
                textWriter.WriteLine(e.Data);
            }
        });

    addHandler(handler);

    if (cancellationToken != default(CancellationToken))
    {
        cancellationToken.Register(
            () =>
            {
                removeHandler(handler);
                taskCompletionSource.TrySetCanceled();
            });
    }

    return taskCompletionSource.Task;
}
Muhammad Rehan Saeed
fuente
2
mejor y más completa respuesta hasta la fecha
TermoTux
1
Por alguna razón, esta fue la única solución que funcionó para mí, la aplicación dejó de colgar.
Jack
1
Parece que no maneja la condición, donde el proceso finaliza después de que comenzó, pero antes de que se adjuntara el evento Exited. Mi sugerencia: iniciar el proceso después de todos los registros.
Stas Boyarincev
@StasBoyarincev Gracias, actualizado. Había olvidado actualizar la respuesta de StackOverflow con este cambio.
Muhammad Rehan Saeed
1
@MuhammadRehanSaeed Otra cosa más: parece que no está permitido llamar a process.BeginOutputReadLine () o process.BeginErrorReadLine () antes de process.Start. En este caso, aparece el error: StandardOut no se ha redirigido o el proceso aún no ha comenzado.
Stas Boyarincev
17

El problema con la excepción ObjectDisposedException no controlada se produce cuando se agota el tiempo de espera del proceso. En tal caso, las otras partes de la condición:

if (process.WaitForExit(timeout) 
    && outputWaitHandle.WaitOne(timeout) 
    && errorWaitHandle.WaitOne(timeout))

No se ejecutan. Resolví este problema de la siguiente manera:

using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
{
    using (Process process = new Process())
    {
        // preparing ProcessStartInfo

        try
        {
            process.OutputDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        outputWaitHandle.Set();
                    }
                    else
                    {
                        outputBuilder.AppendLine(e.Data);
                    }
                };
            process.ErrorDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        errorWaitHandle.Set();
                    }
                    else
                    {
                        errorBuilder.AppendLine(e.Data);
                    }
                };

            process.Start();

            process.BeginOutputReadLine();
            process.BeginErrorReadLine();

            if (process.WaitForExit(timeout))
            {
                exitCode = process.ExitCode;
            }
            else
            {
                // timed out
            }

            output = outputBuilder.ToString();
        }
        finally
        {
            outputWaitHandle.WaitOne(timeout);
            errorWaitHandle.WaitOne(timeout);
        }
    }
}
Karol Tyl
fuente
1
en aras de la exhaustividad, no se configuran las redirecciones a verdadero
tocará el
y yo he quitado los tiempos de espera en mi final ya que el proceso puede solicitar la entrada del usuario (por ejemplo, tipo algo) así que no quiero exigir al usuario que ser rápido
knocte
¿Por qué has cambiado outputy errorpara outputBuilder? ¿Alguien puede proporcionar una respuesta completa que funcione?
Marko Avlijaš
System.ObjectDisposedException: también se ha producido un cierre seguro en esta versión para mí
Matt
8

Rob respondió y me ahorró algunas horas más de pruebas. Lea el búfer de salida / error antes de esperar:

// Read the output stream first and then wait.
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
Jon
fuente
1
pero ¿qué pasa si llegan más datos después de haber llamado WaitForExit()?
knocte
@knocte basado en mis pruebas, ReadToEndo métodos similares (como StandardOutput.BaseStream.CopyTo) volverán después de leer TODOS los datos. nada va a venir después de él
S.Serpooshan
¿Estás diciendo que ReadToEnd () también espera la salida?
Knocte
2
@knocte, ¿estás tratando de darle sentido a una API creada por microsoft?
aaaaaa
El problema de la página correspondiente de MSDN es que no explicó que el búfer detrás de StandardOutput puede llenarse y, en esa situación, el niño debe dejar de escribir y esperar hasta que se agote el búfer (el padre lee los datos en el búfer) . ReadToEnd () solo puede sincronizar la lectura hasta que el búfer esté cerrado o el búfer esté lleno, o el hijo salga con el búfer no lleno. Ese es mi entendimiento.
Cary
7

También tenemos este problema (o una variante).

Intenta lo siguiente:

1) Agregue un tiempo de espera a p.WaitForExit (nnnn); donde nnnn está en milisegundos.

2) Coloque la llamada ReadToEnd antes de la llamada WaitForExit. Esto es lo que hemos visto recomendar MS.

torial
fuente
5

Crédito a EM0 para https://stackoverflow.com/a/17600012/4151626

Las otras soluciones (incluidas las de EM0) siguen estancadas para mi aplicación, debido a los tiempos de espera internos y al uso de StandardOutput y StandardError por la aplicación generada. Esto es lo que funcionó para mí:

Process p = new Process()
{
  StartInfo = new ProcessStartInfo()
  {
    FileName = exe,
    Arguments = args,
    UseShellExecute = false,
    RedirectStandardOutput = true,
    RedirectStandardError = true
  }
};
p.Start();

string cv_error = null;
Thread et = new Thread(() => { cv_error = p.StandardError.ReadToEnd(); });
et.Start();

string cv_out = null;
Thread ot = new Thread(() => { cv_out = p.StandardOutput.ReadToEnd(); });
ot.Start();

p.WaitForExit();
ot.Join();
et.Join();

Editar: se agregó la inicialización de StartInfo a la muestra de código

ergohack
fuente
Esto es lo que uso y nunca más tuve problemas con un punto muerto.
Roemer
3

Lo resolví de esta manera:

            Process proc = new Process();
            proc.StartInfo.FileName = batchFile;
            proc.StartInfo.UseShellExecute = false;
            proc.StartInfo.CreateNoWindow = true;
            proc.StartInfo.RedirectStandardError = true;
            proc.StartInfo.RedirectStandardInput = true;
            proc.StartInfo.RedirectStandardOutput = true;
            proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;      
            proc.Start();
            StreamWriter streamWriter = proc.StandardInput;
            StreamReader outputReader = proc.StandardOutput;
            StreamReader errorReader = proc.StandardError;
            while (!outputReader.EndOfStream)
            {
                string text = outputReader.ReadLine();                    
                streamWriter.WriteLine(text);
            }

            while (!errorReader.EndOfStream)
            {                   
                string text = errorReader.ReadLine();
                streamWriter.WriteLine(text);
            }

            streamWriter.Close();
            proc.WaitForExit();

Redirigí la entrada, la salida y el error y manejé la lectura de las secuencias de salida y error. Esta solución funciona para SDK 7- 8.1, tanto para Windows 7 como para Windows 8

Elina Maliarsky
fuente
2
Elina: gracias por tu respuesta. Hay algunas notas en la parte inferior de este documento de MSDN ( msdn.microsoft.com/en-us/library/… ) que advierten sobre posibles interbloqueos si lees al final de las transmisiones stdout y stderr redirigidas sincrónicamente. Es difícil saber si su solución es susceptible a este problema. Además, parece que está enviando la salida stdout / stderr del proceso directamente como entrada. ¿Por qué? :)
Matthew Piatt
3

Traté de hacer una clase que resolviera su problema utilizando la lectura de flujo asíncrono, teniendo en cuenta las respuestas de Mark Byers, Rob y SteveBay. Al hacerlo, me di cuenta de que hay un error relacionado con la lectura del flujo de salida del proceso asíncrono.

Informé ese error en Microsoft: https://connect.microsoft.com/VisualStudio/feedback/details/3119134

Resumen:

No puedes hacer eso:

process.BeginOutputReadLine (); proceso.Start ();

Recibirá System.InvalidOperationException: StandardOut no se ha redirigido o el proceso aún no ha comenzado.

================================================== ================================================== ========================

Luego debe iniciar la lectura de salida asíncrona después de que se inicia el proceso:

proceso.Start (); process.BeginOutputReadLine ();

Al hacerlo, cree una condición de carrera porque la secuencia de salida puede recibir datos antes de configurarla como asíncrona:

process.Start(); 
// Here the operating system could give the cpu to another thread.  
// For example, the newly created thread (Process) and it could start writing to the output
// immediately before next line would execute. 
// That create a race condition.
process.BeginOutputReadLine();

================================================== ================================================== ========================

Entonces, algunas personas podrían decir que solo tiene que leer la secuencia antes de configurarla como asíncrona. Pero ocurre el mismo problema. Habrá una condición de carrera entre la lectura síncrona y establecer la transmisión en modo asíncrono.

================================================== ================================================== ========================

No hay forma de lograr una lectura asincrónica segura de un flujo de salida de un proceso en la forma en que se ha diseñado "Process" y "ProcessStartInfo".

Probablemente sea mejor usar una lectura asincrónica como la sugerida por otros usuarios para su caso. Pero debe tener en cuenta que podría perder alguna información debido a la condición de la carrera.

Eric Ouellet
fuente
1

Creo que este es un enfoque simple y mejor (no necesitamos AutoResetEvent):

public static string GGSCIShell(string Path, string Command)
{
    using (Process process = new Process())
    {
        process.StartInfo.WorkingDirectory = Path;
        process.StartInfo.FileName = Path + @"\ggsci.exe";
        process.StartInfo.CreateNoWindow = true;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.RedirectStandardInput = true;
        process.StartInfo.UseShellExecute = false;

        StringBuilder output = new StringBuilder();
        process.OutputDataReceived += (sender, e) =>
        {
            if (e.Data != null)
            {
                output.AppendLine(e.Data);
            }
        };

        process.Start();
        process.StandardInput.WriteLine(Command);
        process.BeginOutputReadLine();


        int timeoutParts = 10;
        int timeoutPart = (int)TIMEOUT / timeoutParts;
        do
        {
            Thread.Sleep(500);//sometimes halv scond is enough to empty output buff (therefore "exit" will be accepted without "timeoutPart" waiting)
            process.StandardInput.WriteLine("exit");
            timeoutParts--;
        }
        while (!process.WaitForExit(timeoutPart) && timeoutParts > 0);

        if (timeoutParts <= 0)
        {
            output.AppendLine("------ GGSCIShell TIMEOUT: " + TIMEOUT + "ms ------");
        }

        string result = output.ToString();
        return result;
    }
}
Kuzman Marinov
fuente
Es cierto, pero ¿no deberías estar haciendo .FileName = Path + @"\ggsci.exe" + @" < obeycommand.txt"para simplificar tu código también? O tal vez algo equivalente a "echo command | " + Path + @"\ggsci.exe"si realmente no desea utilizar un archivo obeycommand.txt separado.
Amit Naidu
3
Su solución no necesita AutoResetEvent pero usted sondea. Cuando realiza una encuesta en lugar de utilizar un evento (cuando están disponibles), está utilizando la CPU sin ningún motivo, lo que indica que es un mal programador. Su solución es realmente mala en comparación con la otra que usa AutoResetEvent. (¡Pero no te di -1 porque intentaste ayudar!).
Eric Ouellet
1

Ninguna de las respuestas anteriores está haciendo el trabajo.

La solución Rob se cuelga y la solución 'Mark Byers' obtiene la excepción eliminada (probé las "soluciones" de las otras respuestas).

Entonces decidí sugerir otra solución:

public void GetProcessOutputWithTimeout(Process process, int timeoutSec, CancellationToken token, out string output, out int exitCode)
{
    string outputLocal = "";  int localExitCode = -1;
    var task = System.Threading.Tasks.Task.Factory.StartNew(() =>
    {
        outputLocal = process.StandardOutput.ReadToEnd();
        process.WaitForExit();
        localExitCode = process.ExitCode;
    }, token);

    if (task.Wait(timeoutSec, token))
    {
        output = outputLocal;
        exitCode = localExitCode;
    }
    else
    {
        exitCode = -1;
        output = "";
    }
}

using (var process = new Process())
{
    process.StartInfo = ...;
    process.Start();
    string outputUnicode; int exitCode;
    GetProcessOutputWithTimeout(process, PROCESS_TIMEOUT, out outputUnicode, out exitCode);
}

Este código se depuró y funciona perfectamente.

omriman12
fuente
1
¡Bueno! solo tenga en cuenta que el parámetro token no se proporciona al llamar al GetProcessOutputWithTimeoutmétodo.
S.Serpooshan
1

Introducción

La respuesta actualmente aceptada no funciona (arroja una excepción) y hay demasiadas soluciones pero no hay código completo. Obviamente, esto está perdiendo el tiempo de muchas personas porque esta es una pregunta popular.

Combinando la respuesta de Mark Byers y la respuesta de Karol Tyl, escribí un código completo basado en cómo quiero usar el método Process.Start.

Uso

Lo he usado para crear un diálogo de progreso alrededor de los comandos de git. Así es como lo he usado:

    private bool Run(string fullCommand)
    {
        Error = "";
        int timeout = 5000;

        var result = ProcessNoBS.Start(
            filename: @"C:\Program Files\Git\cmd\git.exe",
            arguments: fullCommand,
            timeoutInMs: timeout,
            workingDir: @"C:\test");

        if (result.hasTimedOut)
        {
            Error = String.Format("Timeout ({0} sec)", timeout/1000);
            return false;
        }

        if (result.ExitCode != 0)
        {
            Error = (String.IsNullOrWhiteSpace(result.stderr)) 
                ? result.stdout : result.stderr;
            return false;
        }

        return true;
    }

En teoría, también puedes combinar stdout y stderr, pero no lo he probado.

Código

public struct ProcessResult
{
    public string stdout;
    public string stderr;
    public bool hasTimedOut;
    private int? exitCode;

    public ProcessResult(bool hasTimedOut = true)
    {
        this.hasTimedOut = hasTimedOut;
        stdout = null;
        stderr = null;
        exitCode = null;
    }

    public int ExitCode
    {
        get 
        {
            if (hasTimedOut)
                throw new InvalidOperationException(
                    "There was no exit code - process has timed out.");

            return (int)exitCode;
        }
        set
        {
            exitCode = value;
        }
    }
}

public class ProcessNoBS
{
    public static ProcessResult Start(string filename, string arguments,
        string workingDir = null, int timeoutInMs = 5000,
        bool combineStdoutAndStderr = false)
    {
        using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
        using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
        {
            using (var process = new Process())
            {
                var info = new ProcessStartInfo();

                info.CreateNoWindow = true;
                info.FileName = filename;
                info.Arguments = arguments;
                info.UseShellExecute = false;
                info.RedirectStandardOutput = true;
                info.RedirectStandardError = true;

                if (workingDir != null)
                    info.WorkingDirectory = workingDir;

                process.StartInfo = info;

                StringBuilder stdout = new StringBuilder();
                StringBuilder stderr = combineStdoutAndStderr
                    ? stdout : new StringBuilder();

                var result = new ProcessResult();

                try
                {
                    process.OutputDataReceived += (sender, e) =>
                    {
                        if (e.Data == null)
                            outputWaitHandle.Set();
                        else
                            stdout.AppendLine(e.Data);
                    };
                    process.ErrorDataReceived += (sender, e) =>
                    {
                        if (e.Data == null)
                            errorWaitHandle.Set();
                        else
                            stderr.AppendLine(e.Data);
                    };

                    process.Start();

                    process.BeginOutputReadLine();
                    process.BeginErrorReadLine();

                    if (process.WaitForExit(timeoutInMs))
                        result.ExitCode = process.ExitCode;
                    // else process has timed out 
                    // but that's already default ProcessResult

                    result.stdout = stdout.ToString();
                    if (combineStdoutAndStderr)
                        result.stderr = null;
                    else
                        result.stderr = stderr.ToString();

                    return result;
                }
                finally
                {
                    outputWaitHandle.WaitOne(timeoutInMs);
                    errorWaitHandle.WaitOne(timeoutInMs);
                }
            }
        }
    }
}
Marko Avlijaš
fuente
Todavía obtengo System.ObjectDisposedException: el controlador seguro también se ha cerrado en esta versión.
Matt
1

Sé que esta es una cena antigua pero, después de leer toda esta página, ninguna de las soluciones funcionaba para mí, aunque no intenté con Muhammad Rehan ya que el código era un poco difícil de seguir, aunque supongo que estaba en el camino correcto. . Cuando digo que no funcionó, eso no es del todo cierto, a veces funcionaría bien, supongo que tiene algo que ver con la longitud de la salida antes de una marca EOF.

De todos modos, la solución que funcionó para mí fue usar diferentes hilos para leer StandardOutput y StandardError y escribir los mensajes.

        StreamWriter sw = null;
        var queue = new ConcurrentQueue<string>();

        var flushTask = new System.Timers.Timer(50);
        flushTask.Elapsed += (s, e) =>
        {
            while (!queue.IsEmpty)
            {
                string line = null;
                if (queue.TryDequeue(out line))
                    sw.WriteLine(line);
            }
            sw.FlushAsync();
        };
        flushTask.Start();

        using (var process = new Process())
        {
            try
            {
                process.StartInfo.FileName = @"...";
                process.StartInfo.Arguments = $"...";
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;

                process.Start();

                var outputRead = Task.Run(() =>
                {
                    while (!process.StandardOutput.EndOfStream)
                    {
                        queue.Enqueue(process.StandardOutput.ReadLine());
                    }
                });

                var errorRead = Task.Run(() =>
                {
                    while (!process.StandardError.EndOfStream)
                    {
                        queue.Enqueue(process.StandardError.ReadLine());
                    }
                });

                var timeout = new TimeSpan(hours: 0, minutes: 10, seconds: 0);

                if (Task.WaitAll(new[] { outputRead, errorRead }, timeout) &&
                    process.WaitForExit((int)timeout.TotalMilliseconds))
                {
                    if (process.ExitCode != 0)
                    {
                        throw new Exception($"Failed run... blah blah");
                    }
                }
                else
                {
                    throw new Exception($"process timed out after waiting {timeout}");
                }
            }
            catch (Exception e)
            {
                throw new Exception($"Failed to succesfully run the process.....", e);
            }
        }
    }

¡Espero que esto ayude a alguien, que pensó que esto podría ser tan difícil!

Alexis Coles
fuente
Excepción: sw.FlushAsync(): Object is not set to an instance of an object. sw is null. ¿Cómo / dónde se debe swdefinir?
wallyk
1

Después de leer todas las publicaciones aquí, me decidí por la solución consolidada de Marko Avlijaš. sin embargo , no resolvió todos mis problemas.

En nuestro entorno tenemos un Servicio de Windows que está programado para ejecutar cientos de archivos .bat .cmd .exe, ... etc. diferentes que se han acumulado a lo largo de los años y fueron escritos por diferentes personas y en diferentes estilos. No tenemos control sobre la escritura de los programas y scripts, solo somos responsables de programar, ejecutar e informar sobre el éxito / el fracaso.

Así que probé casi todas las sugerencias aquí con diferentes niveles de éxito. La respuesta de Marko fue casi perfecta, pero cuando se ejecutó como un servicio, no siempre captó stdout. Nunca llegué al fondo de por qué no.

La única solución que encontramos que funciona en TODOS nuestros casos es esta: http://csharptest.net/319/using-the-processrunner-class/index.html

flapster
fuente
Voy a probar esta biblioteca. He analizado el código, y parece estar usando delegados con sensatez. Está bien empaquetado en Nuget. Básicamente apesta a profesionalismo, algo de lo que nunca podría ser acusado. Si muerde, lo dirá.
Steve Hibbert
El enlace al código fuente está muerto. Por favor, la próxima vez copie el código a la respuesta.
Vitaly Zdanevich
1

Solución que terminé usando para evitar toda la complejidad:

var outputFile = Path.GetTempFileName();
info = new System.Diagnostics.ProcessStartInfo("TheProgram.exe", String.Join(" ", args) + " > " + outputFile + " 2>&1");
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.UseShellExecute = false;
System.Diagnostics.Process p = System.Diagnostics.Process.Start(info);
p.WaitForExit();
Console.WriteLine(File.ReadAllText(outputFile)); //need the StandardOutput contents

Así que creo un archivo temporal, redirijo tanto el resultado como el error al usarlo > outputfile > 2>&1y luego solo leo el archivo una vez que el proceso ha finalizado.

Las otras soluciones están bien para escenarios en los que desea hacer otras cosas con la salida, pero para cosas simples esto evita mucha complejidad.

eglasius
fuente
1

He leído muchas de las respuestas e hice la mía. No estoy seguro de que este se arregle en cualquier caso, pero se soluciona en mi entorno. Simplemente no estoy usando WaitForExit y uso WaitHandle.WaitAll en las señales de salida y error de finalización. Me alegrará si alguien ve posibles problemas con eso. O si ayudará a alguien. Para mí es mejor porque no usa tiempos de espera.

private static int DoProcess(string workingDir, string fileName, string arguments)
{
    int exitCode;
    using (var process = new Process
    {
        StartInfo =
        {
            WorkingDirectory = workingDir,
            WindowStyle = ProcessWindowStyle.Hidden,
            CreateNoWindow = true,
            UseShellExecute = false,
            FileName = fileName,
            Arguments = arguments,
            RedirectStandardError = true,
            RedirectStandardOutput = true
        },
        EnableRaisingEvents = true
    })
    {
        using (var outputWaitHandle = new AutoResetEvent(false))
        using (var errorWaitHandle = new AutoResetEvent(false))
        {
            process.OutputDataReceived += (sender, args) =>
            {
                // ReSharper disable once AccessToDisposedClosure
                if (args.Data != null) Debug.Log(args.Data);
                else outputWaitHandle.Set();
            };
            process.ErrorDataReceived += (sender, args) =>
            {
                // ReSharper disable once AccessToDisposedClosure
                if (args.Data != null) Debug.LogError(args.Data);
                else errorWaitHandle.Set();
            };

            process.Start();
            process.BeginOutputReadLine();
            process.BeginErrorReadLine();

            WaitHandle.WaitAll(new WaitHandle[] { outputWaitHandle, errorWaitHandle });

            exitCode = process.ExitCode;
        }
    }
    return exitCode;
}
Mazorca de maíz
fuente
Usé esto y lo envolví con Task.Run para manejar el tiempo de espera, también devuelvo processid para matar al tiempo de espera
plus5volt
0

Creo que con async, es posible tener una solución más elegante y no tener puntos muertos incluso cuando se usa standardOutput y standardError:

using (Process process = new Process())
{
    process.StartInfo.FileName = filename;
    process.StartInfo.Arguments = arguments;
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;

    process.Start();

    var tStandardOutput = process.StandardOutput.ReadToEndAsync();
    var tStandardError = process.StandardError.ReadToEndAsync();

    if (process.WaitForExit(timeout))
    {
        string output = await tStandardOutput;
        string errors = await tStandardError;

        // Process completed. Check process.ExitCode here.
    }
    else
    {
        // Timed out.
    }
}

Se basa en la respuesta de Mark Byers. Si no está en un método asíncrono, puede usar en string output = tStandardOutput.result;lugar deawait

Yepeekai
fuente
-1

Esta publicación puede estar desactualizada, pero descubrí que la causa principal por la que generalmente se bloquea se debe al desbordamiento de la pila para el redirectStandardoutput o si tiene redirectStandarderror.

Como los datos de salida o los datos de error son grandes, provocará un tiempo de bloqueo ya que todavía se está procesando por una duración indefinida.

para resolver este problema:

p.StartInfo.RedirectStandardoutput = False
p.StartInfo.RedirectStandarderror = False
canción
fuente
11
¡El problema es que las personas las establecen explícitamente como verdaderas porque quieren poder acceder a esas transmisiones! De lo contrario, podemos dejarlos en falso.
user276648
-1

Llamemos al código de muestra publicado aquí el redirector y el otro programa redirigido. Si fuera yo, probablemente escribiría un programa redirigido de prueba que pueda usarse para duplicar el problema.

Así que lo hice. Para los datos de prueba utilicé la especificación de lenguaje ECMA-334 C # v PDF; Es de unos 5 MB. La siguiente es la parte importante de eso.

StreamReader stream = null;
try { stream = new StreamReader(Path); }
catch (Exception ex)
{
    Console.Error.WriteLine("Input open error: " + ex.Message);
    return;
}
Console.SetIn(stream);
int datasize = 0;
try
{
    string record = Console.ReadLine();
    while (record != null)
    {
        datasize += record.Length + 2;
        record = Console.ReadLine();
        Console.WriteLine(record);
    }
}
catch (Exception ex)
{
    Console.Error.WriteLine($"Error: {ex.Message}");
    return;
}

El valor del tamaño de datos no coincide con el tamaño real del archivo, pero eso no importa. No está claro si un archivo PDF siempre usa CR y LF al final de las líneas, pero eso no importa. Puede usar cualquier otro archivo de texto grande para probar.

Usando eso, el código del redirector de muestra se cuelga cuando escribo la gran cantidad de datos pero no cuando escribo una pequeña cantidad.

Intenté mucho rastrear de alguna manera la ejecución de ese código y no pude. Comenté las líneas del programa redirigido que desactivaron la creación de una consola para que el programa redirigido intentara obtener una ventana de consola separada pero no pude.

Luego encontré Cómo iniciar una aplicación de consola en una nueva ventana, la ventana de los padres o ninguna ventana . Aparentemente, no podemos (fácilmente) tener una consola separada cuando un programa de consola inicia otro programa de consola sin ShellExecute y dado que ShellExecute no admite la redirección, debemos compartir una consola, incluso si no especificamos ninguna ventana para el otro proceso.

Supongo que si el programa redirigido llena un búfer en algún lugar, entonces debe esperar a que se lean los datos y si en ese momento el redirector no lee ningún dato, entonces es un punto muerto.

La solución es no usar ReadToEnd y leer los datos mientras se escriben, pero no es necesario usar lecturas asincrónicas. La solución puede ser bastante simple. Lo siguiente funciona para mí con el PDF de 5 MB.

ProcessStartInfo info = new ProcessStartInfo(TheProgram);
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
Process p = Process.Start(info);
string record = p.StandardOutput.ReadLine();
while (record != null)
{
    Console.WriteLine(record);
    record = p.StandardOutput.ReadLine();
}
p.WaitForExit();

Otra posibilidad es usar un programa GUI para hacer la redirección. El código anterior funciona en una aplicación WPF excepto con modificaciones obvias.

usuario34660
fuente
-3

Estaba teniendo el mismo problema, pero la razón era diferente. Sin embargo, ocurriría en Windows 8, pero no en Windows 7. La siguiente línea parece haber causado el problema.

pProcess.StartInfo.UseShellExecute = False

La solución fue NO deshabilitar UseShellExecute. Ahora recibí una ventana emergente de Shell, que no es deseada, pero mucho mejor que el programa esperando que no suceda nada en particular. Así que agregué la siguiente solución para eso:

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

Ahora, lo único que me molesta es por qué esto está sucediendo en Windows 8 en primer lugar.

oh dios no otra
fuente
1
Debe UseShellExecuteestablecerlo en falso si desea redirigir la salida.
Brad Moore