ProcessStartInfo sospeso su "WaitForExit"? Perché?


187

Ho il codice seguente:

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

So che l'output del processo che sto iniziando è lungo circa 7 MB. Eseguendolo nella console di Windows funziona bene. Sfortunatamente a livello di programmazione questo si blocca indefinitamente su WaitForExit. Si noti inoltre che questo codice NON si blocca per output più piccoli (come 3 KB).

È possibile che lo StandardOutput interno in ProcessStartInfo non possa bufferizzare 7 MB? In tal caso, cosa devo fare invece? In caso contrario, cosa sto facendo di sbagliato?


qualche soluzione finale con codice sorgente completo a riguardo?
Kiquenet,

2
Mi imbatto in questo stesso problema e come mi è stato in grado di risolverlo stackoverflow.com/questions/2285288/...~~V~~singular~~1st
Bedasso

6
Sì, soluzione finale: scambia le ultime due righe. È nel manuale .
Amit Naidu,

4
da msdn: l'esempio di codice evita una condizione di deadlock chiamando p.StandardOutput.ReadToEnd prima di p.WaitForExit. Può verificarsi una condizione di deadlock se il processo padre chiama p.WaitForExit prima di p.StandardOutput.ReadToEnd e il processo figlio scrive una quantità di testo sufficiente per riempire il flusso reindirizzato. Il processo genitore aspetterebbe indefinitamente l'arresto del processo figlio. Il processo figlio aspetterebbe indefinitamente che il genitore legga dal flusso StandardOutput completo.
Carlos Liu,

è un po 'fastidioso quanto sia complesso farlo correttamente. Mi ha fatto piacere aggirarlo con reindirizzamenti della riga di comando più semplici> outputfile :)
eglasius,

Risposte:


393

Il problema è che se si reindirizza StandardOutpute / o StandardErroril buffer interno può diventare pieno. Qualunque ordine tu usi, ci può essere un problema:

  • Se aspetti che il processo termini prima di leggere StandardOutputil processo, puoi bloccare il tentativo di scrivere su di esso, quindi il processo non termina mai.
  • Se leggi da StandardOutput utilizzando ReadToEnd, il tuo processo può bloccarsi se il processo non si chiude mai StandardOutput(ad esempio se non termina mai, o se è bloccato nella scrittura StandardError).

La soluzione consiste nell'utilizzare letture asincrone per garantire che il buffer non si riempia. Per evitare eventuali deadlock e raccogliere tutto l'output da entrambi StandardOutpute StandardErrorpuoi farlo:

MODIFICA: vedere le risposte di seguito per sapere come evitare un ObjectDisposedException in caso di timeout.

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.
        }
    }
}

11
Non avevo idea di reindirizzare l'output stava causando il problema, ma sicuramente lo era. Ho trascorso 4 ore a martellarmi la testa e l'ho risolto in 5 minuti dopo aver letto il tuo post. Bel lavoro!
Ben Gripka,

1
@AlexPeck Il problema stava eseguendo questo come app console. Hans Passant ha identificato il problema qui: stackoverflow.com/a/16218470/279516
Bob Horn,

5
ogni volta che il prompt dei comandi si chiude, viene visualizzato: Un'eccezione non gestita di tipo "System.ObjectDisposed" si è verificata in mscorlib.dll Ulteriori informazioni: L'handle sicuro è stato chiuso
user1663380

3
Abbiamo avuto un problema simile come descritto da @ user1663380 sopra. Pensi che sia possibile che le usingistruzioni per i gestori di eventi debbano essere al di sopra delle usingistruzioni per il processo stesso?
Dan Forbes,

2
Non penso che siano necessarie le maniglie di attesa. Come per msdn, basta terminare con la versione non in timeout di WaitForExit: quando l'output standard è stato reindirizzato a gestori di eventi asincroni, è possibile che l'elaborazione dell'output non sia stata completata quando viene restituito questo metodo. Per assicurarsi che la gestione degli eventi asincroni sia stata completata, chiamare il sovraccarico WaitForExit () che non accetta alcun parametro dopo aver ricevuto un vero da questo sovraccarico.
Patrick,

98

La documentazione per Process.StandardOutputdice di leggere prima di attendere, altrimenti è possibile eseguire il deadlock, frammento copiato di seguito:

 // 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();

14
Non sono sicuro al 100% se questo è solo un risultato del mio ambiente, ma ho scoperto che se hai impostato RedirectStandardOutput = true;e non usi p.StandardOutput.ReadToEnd();ottieni un deadlock / blocco.
Chris S,

3
Vero. Sono stato in una situazione simile. Stavo reindirizzando StandardError senza motivo durante la conversione con ffmpeg in un processo, stava scrivendo abbastanza nel flusso StandardError per creare un deadlock.
Léon Pelletier,

Questo è ancora valido anche per il reindirizzamento e la lettura dell'output standard.
user3791372

@ user3791372 Immagino che questo sia applicabile solo se il buffer dietro StandardOutput non è completamente riempito. Qui MSDN non rende giustizia. Un grande articolo che ti consiglio di leggere è su: dzone.com/articles/async-io-and-threadpool
Cary

19

La risposta di Mark Byers è eccellente, ma vorrei solo aggiungere quanto segue:

I delegati OutputDataReceivede ErrorDataReceiveddevono essere rimossi prima di outputWaitHandleed errorWaitHandleeliminati. Se il processo continua a generare dati dopo che il timeout è stato superato e quindi termina, le variabili outputWaitHandlee errorWaitHandlesaranno accessibili dopo essere state eliminate.

(Cordiali saluti, ho dovuto aggiungere questo avvertimento come risposta perché non potevo commentare il suo post.)


2
Forse sarebbe meglio chiamare CancelOutputRead ?
Mark Byers,

Aggiungere il codice modificato di Mark a questa risposta sarebbe davvero fantastico! Al momento sto riscontrando lo stesso identico problema.
ianbailey,

8
@ianbailey Il modo più semplice per risolvere questo problema è mettere l'utilizzo (Process p ...) all'interno dell'utilizzo (AutoResetEvent errorWaitHandle ...)
Didier A.

18

Questa è una soluzione più moderna, attendibile, basata su Task Parallel Library (TPL) per .NET 4.5 e versioni successive.

Esempio di utilizzo

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!");
}

Implementazione

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;
}

2
risposta migliore e più completa fino ad oggi
TermoTux,

1
Per qualche ragione, questa è stata l'unica soluzione che ha funzionato per me, l'applicazione ha smesso di appendere.
Jack,

1
Sembra che non si gestisca la condizione, in cui il processo termina dopo l'avvio, ma prima che l'evento Exited fosse collegato. Il mio consiglio: iniziare il processo dopo tutte le registrazioni.
Stas Boyarincev,

@StasBoyarincev Grazie, aggiornato. Avevo dimenticato di aggiornare la risposta StackOverflow con questa modifica.
Muhammad Rehan Saeed,

1
@MuhammadRehanSaeed Ancora un'altra cosa: sembra che non sia consentito chiamare process.BeginOutputReadLine () o process.BeginErrorReadLine () prima di process.Start. In questo caso viene visualizzato l'errore: StandardOut non è stato reindirizzato o il processo non è ancora stato avviato.
Stas Boyarincev,

17

Il problema con ObjectDisposedException non gestito si verifica quando il processo è scaduto. In tal caso le altre parti della condizione:

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

non vengono eseguiti. Ho risolto questo problema nel modo seguente:

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);
        }
    }
}

1
per completezza, questo manca per impostare i reindirizzamenti su true
knocte,

e ho rimosso i timeout alla fine poiché il processo potrebbe richiedere l'input dell'utente (ad esempio digitare qualcosa), quindi non voglio richiedere all'utente di essere veloce
knocte

Perché hai cambiato outpute errorper outputBuilder? Qualcuno può fornire una risposta completa che funzioni?
Marko Avlijaš,

System.ObjectDisposedException: l'handle sicuro è stato chiuso anche in questa versione per me
Matt,

8

Rob rispose e mi risparmiò ancora qualche ora di prove. Leggere il buffer di output / errore prima di attendere:

// Read the output stream first and then wait.
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();

1
ma cosa succede se più dati arrivano dopo che hai chiamato WaitForExit()?
Knocte,

@knocte in base ai miei test ReadToEndo metodi simili (come StandardOutput.BaseStream.CopyTo) torneranno dopo aver letto TUTTI i dati. non ci sarà nulla dopo
S.Serpooshan,

stai dicendo che ReadToEnd () aspetta anche l'uscita?
Knocte,

2
@knocte stai cercando di dare un senso a un'API creata da Microsoft?
aaaaaa,

Il problema della pagina MSDN corrispondente è che non ha spiegato che il buffer dietro StandardOutput può diventare pieno e in quella situazione il bambino deve smettere di scrivere e attendere fino allo svuotamento del buffer (il genitore legge i dati nel buffer) . ReadToEnd () può solo sincronizzare fino a quando il buffer non viene chiuso o il buffer è pieno o il figlio esce con buffer non pieno. Questa è la mia comprensione.
Cary,

7

Abbiamo anche questo problema (o una variante).

Prova quanto segue:

1) Aggiungi un timeout a p.WaitForExit (nnnn); dove nnnn è espresso in millisecondi.

2) Inserire la chiamata ReadToEnd prima della chiamata WaitForExit. Questo è ciò che abbiamo visto raccomandare dalla SM.


5

Ringraziamo EM0 per https://stackoverflow.com/a/17600012/4151626

Le altre soluzioni (inclusi EM0) sono ancora bloccate per la mia applicazione, a causa dei timeout interni e dell'utilizzo di StandardOutput e StandardError da parte dell'applicazione generata. Ecco cosa ha funzionato per me:

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();

Modifica: aggiunta l'inizializzazione di StartInfo al codice di esempio


Questo è quello che uso e non ho più avuto problemi con un deadlock.
Roemer,

3

L'ho risolto in questo modo:

            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();

Ho reindirizzato sia input, output che error e ho gestito la lettura dai flussi di output e di errore. Questa soluzione funziona per SDK 7- 8.1, sia per Windows 7 che per Windows 8


2
Elina: grazie per la risposta. Ci sono alcune note in fondo a questo documento MSDN ( msdn.microsoft.com/en-us/library/… ) che avvertono di potenziali deadlock se si legge alla fine di entrambi i flussi stdout reindirizzati e stderr in modo sincrono. È difficile dire se la tua soluzione è suscettibile a questo problema. Inoltre, sembra che tu stia inviando di nuovo l'output 'stdout / stderr del processo come input. Perché? :)
Matthew Piatt,

3

Ho cercato di creare una classe in grado di risolvere il tuo problema utilizzando la lettura asincrona del flusso, tenendo conto delle risposte di Mark Byers, Rob e Stevejay. In questo modo mi sono reso conto che esiste un bug relativo al flusso di output del processo asincrono letto.

Ho segnalato quel bug su Microsoft: https://connect.microsoft.com/VisualStudio/feedback/details/3119134

Sommario:

Non puoi farlo:

process.BeginOutputReadLine (); Process.Start ();

Riceverai System.InvalidOperationException: StandardOut non è stato reindirizzato o il processo non è ancora stato avviato.

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

Quindi devi avviare la lettura dell'output asincrono dopo l'avvio del processo:

Process.Start (); process.BeginOutputReadLine ();

In questo modo, crea una condizione di competizione perché il flusso di output può ricevere dati prima di impostarlo su asincrono:

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();

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

Quindi alcune persone potrebbero dire che devi solo leggere lo stream prima di impostarlo su asincrono. Ma si verifica lo stesso problema. Ci sarà una condizione di competizione tra la lettura sincrona e l'impostazione del flusso in modalità asincrona.

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

Non è possibile ottenere la lettura asincrona sicura di un flusso di output di un processo nel modo in cui sono stati progettati "Process" e "ProcessStartInfo".

Probabilmente stai meglio usando la lettura asincrona come suggerito da altri utenti per il tuo caso. Ma dovresti essere consapevole del fatto che potresti perdere alcune informazioni a causa delle condizioni della gara.


1

Penso che questo sia un approccio semplice e migliore (non abbiamo bisogno 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;
    }
}

Vero, ma non dovresti farlo anche .FileName = Path + @"\ggsci.exe" + @" < obeycommand.txt"per semplificare il tuo codice? O forse qualcosa di equivalente a "echo command | " + Path + @"\ggsci.exe"se davvero non vuoi usare un file obeycommand.txt separato.
Amit Naidu,

3
La tua soluzione non ha bisogno di AutoResetEvent ma fai il polling. Quando esegui il polling invece di utilizzare l'evento (quando sono disponibili), stai usando la CPU senza motivo e questo indica che sei un programmatore cattivo. La tua soluzione è davvero pessima se confrontata con l'altra usando AutoResetEvent. (Ma non ti ho dato -1 perché hai provato ad aiutare!).
Eric Ouellet,

1

Nessuna delle risposte sopra sta facendo il lavoro.

La soluzione Rob si blocca e la soluzione "Mark Byers" ottiene l'eccezione eliminata (ho provato le "soluzioni" delle altre risposte).

Quindi ho deciso di suggerire un'altra soluzione:

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);
}

Questo codice ha eseguito il debug e funziona perfettamente.


1
Buona! basta notare che il parametro token non viene fornito quando si chiama il GetProcessOutputWithTimeoutmetodo.
S. Serpooshan,

1

introduzione

La risposta attualmente accettata non funziona (genera un'eccezione) e ci sono troppe soluzioni alternative ma nessun codice completo. Questo ovviamente sta sprecando un sacco di tempo perché questa è una domanda popolare.

Combinando la risposta di Mark Byers e la risposta di Karol Tyl, ho scritto un codice completo basato su come voglio usare il metodo Process.Start.

uso

L'ho usato per creare una finestra di dialogo sullo stato di avanzamento dei comandi git. Ecco come l'ho usato:

    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;
    }

In teoria puoi anche combinare stdout e stderr, ma non l'ho provato.

Codice

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);
                }
            }
        }
    }
}

Ottieni ancora System.ObjectDisposedException: l'handle sicuro è stato chiuso anche su questa versione.
Matt,

1

So che questa è una cena vecchia ma, dopo aver letto l'intera pagina, nessuna delle soluzioni funzionava per me, anche se non ho provato Muhammad Rehan poiché il codice era un po 'difficile da seguire, anche se immagino che fosse sulla buona strada . Quando dico che non ha funzionato, non è del tutto vero, a volte funzionerebbe bene, suppongo che abbia a che fare con la lunghezza dell'output prima di un segno EOF.

Ad ogni modo, la soluzione che ha funzionato per me era usare thread diversi per leggere StandardOutput e StandardError e scrivere i messaggi.

        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);
            }
        }
    }

Spero che questo aiuti qualcuno, che ha pensato che potesse essere così difficile!


Eccezione: sw.FlushAsync(): Object is not set to an instance of an object. sw is null. come / dove dovrebbe swessere definita?
Wallyk,

1

Dopo aver letto tutti i post qui, ho optato per la soluzione consolidata di Marko Avlijaš. però , non ha risolto tutti i miei problemi.

Nel nostro ambiente abbiamo un servizio Windows che è programmato per eseguire centinaia di diversi file .bat .cmd .exe, ... ecc. Che si sono accumulati nel corso degli anni e sono stati scritti da molte persone diverse e con stili diversi. Non abbiamo alcun controllo sulla scrittura di programmi e script, siamo solo responsabili della pianificazione, dell'esecuzione e della segnalazione di esiti positivi / negativi.

Quindi ho provato praticamente tutti i suggerimenti qui con diversi livelli di successo. La risposta di Marko era quasi perfetta, ma quando veniva eseguita come servizio, non sempre catturava stdout. Non sono mai arrivato al fondo del perché no.

L'unica soluzione che abbiamo trovato che funziona in TUTTI i nostri casi è questa: http://csharptest.net/319/using-the-processrunner-class/index.html


Ho intenzione di provare questa libreria. Ho individuato il codice e sembra usare i delegati in modo ragionevole. È ben confezionato in Nuget. In pratica puzza di professionalità, qualcosa di cui non potrei mai essere accusato. Se morde, lo dirà.
Steve Hibbert,

Il link al codice sorgente è morto. La prossima volta copia il codice nella risposta.
Vitaly Zdanevich,

1

Soluzione che ho usato per evitare tutta la complessità:

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

Quindi creo un file temporaneo, reindirizzando sia l'output che l'errore ad esso utilizzando > outputfile > 2>&1 e quindi leggo il file al termine del processo.

Le altre soluzioni vanno bene per scenari in cui si desidera fare altre cose con l'output, ma per cose semplici questo evita molta complessità.


1

Ho letto molte delle risposte e le ho fatte mie. Non sono sicuro che questo risolverà in ogni caso, ma si risolve nel mio ambiente. Non sto solo usando WaitForExit e uso WaitHandle.WaitAll su entrambi i segnali di uscita e di fine errore. Sarò contento se qualcuno vedrà possibili problemi con questo. O se aiuterà qualcuno. Per me è meglio perché non usa i timeout.

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;
}

L'ho usato e completato con Task.Run per gestire il timeout, restituisco anche processid per uccidere in timeout
plus5volt

0

Penso che con asincrono, è possibile avere una soluzione più elegante e non avere deadlock anche quando si utilizzano sia standardOutput che 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.
    }
}

È basato sulla risposta di Mark Byers. Se non si utilizza un metodo asincrono, è possibile utilizzare string output = tStandardOutput.result;invece diawait



-1

Questo post potrebbe essere obsoleto, ma ho scoperto che la causa principale per cui si blocca di solito è dovuta allo overflow dello stack per il redirectStandardoutput o se hai redirectStandarderror.

Poiché i dati di output o i dati di errore sono di grandi dimensioni, causerà un tempo di blocco poiché viene ancora elaborato per una durata indefinita.

quindi per risolvere questo problema:

p.StartInfo.RedirectStandardoutput = False
p.StartInfo.RedirectStandarderror = False

11
Il problema è che le persone impostano esplicitamente quelle vere perché vogliono poter accedere a quei flussi! Altrimenti, possiamo semplicemente lasciarli falsi.
user276648

-1

Chiamiamo il codice di esempio pubblicato qui il redirector e l'altro programma il reindirizzato. Se fossi in me, probabilmente scriverei un programma di reindirizzamento di prova che può essere utilizzato per duplicare il problema.

Così ho fatto. Per i dati di test ho usato l'ECMA-334 C # Specifica del linguaggio PDF; è di circa 5 MB. Quella che segue è la parte importante di ciò.

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;
}

Il valore del datasize non corrisponde alla dimensione effettiva del file ma non ha importanza. Non è chiaro se un file PDF utilizza sempre sia CR che LF alla fine delle righe, ma non importa per questo. È possibile utilizzare qualsiasi altro file di testo di grandi dimensioni con cui eseguire il test.

Usando quello il codice redirector di esempio si blocca quando scrivo la grande quantità di dati ma non quando scrivo una piccola quantità.

Ho provato moltissimo a rintracciare in qualche modo l'esecuzione di quel codice e non ci sono riuscito. Ho commentato le righe del programma reindirizzato che ha disabilitato la creazione di una console per il programma reindirizzato per tentare di ottenere una finestra console separata ma non ci sono riuscito.

Poi ho trovato Come avviare un'app console in una nuova finestra, la finestra del genitore o nessuna finestra . Quindi apparentemente non possiamo (facilmente) avere una console separata quando un programma console avvia un altro programma console senza ShellExecute e poiché ShellExecute non supporta il reindirizzamento, dobbiamo condividere una console, anche se non specifichiamo alcuna finestra per l'altro processo.

Suppongo che se il programma reindirizzato riempie un buffer da qualche parte, allora deve attendere che i dati vengano letti e se a quel punto nessun dato viene letto dal redirector allora è un deadlock.

La soluzione è non usare ReadToEnd e leggere i dati mentre i dati vengono scritti ma non è necessario utilizzare le letture asincrone. La soluzione può essere abbastanza semplice. Quanto segue funziona per me con il PDF da 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();

Un'altra possibilità è utilizzare un programma GUI per eseguire il reindirizzamento. Il codice precedente funziona in un'applicazione WPF tranne che con ovvie modifiche.


-3

Avevo lo stesso problema, ma il motivo era diverso. Ciò accadrebbe comunque in Windows 8, ma non in Windows 7. La seguente riga sembra aver causato il problema.

pProcess.StartInfo.UseShellExecute = False

La soluzione era NON disabilitare UseShellExecute. Ora ho ricevuto una finestra popup Shell, che è indesiderata, ma molto meglio del programma in attesa che non accada nulla di particolare. Quindi ho aggiunto la seguente soluzione per questo:

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

Ora l'unica cosa che mi dà fastidio è il motivo per cui questo sta accadendo sotto Windows 8 in primo luogo.


1
Devi UseShellExecuteessere impostato su false se desideri reindirizzare l'output.
Brad Moore,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.