ProcessStartInfo hängt an "WaitForExit"? Warum?


186

Ich habe folgenden Code:

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

Ich weiß, dass die Ausgabe des Prozesses, den ich starte, ungefähr 7 MB lang ist. Das Ausführen in der Windows-Konsole funktioniert einwandfrei. Leider hängt dies programmgesteuert auf unbestimmte Zeit bei WaitForExit. Beachten Sie auch, dass der Code für kleinere Ausgänge (wie 3 KB) NICHT hängt.

Ist es möglich, dass der interne StandardOutput in ProcessStartInfo 7 MB nicht puffern kann? Wenn ja, was soll ich stattdessen tun? Wenn nicht, was mache ich falsch?


Gibt es eine endgültige Lösung mit vollständigem Quellcode?
Kiquenet

2
Ich stoße
Bedasso

6
Ja, endgültige Lösung: Tauschen Sie die letzten beiden Zeilen aus. Es ist im Handbuch .
Amit Naidu

4
from msdn: Das Codebeispiel vermeidet eine Deadlock-Bedingung, indem p.StandardOutput.ReadToEnd vor p.WaitForExit aufgerufen wird. Eine Deadlock-Bedingung kann auftreten, wenn der übergeordnete Prozess p.WaitForExit vor p.StandardOutput.ReadToEnd aufruft und der untergeordnete Prozess genügend Text schreibt, um den umgeleiteten Stream zu füllen. Der übergeordnete Prozess würde unbegrenzt warten, bis der untergeordnete Prozess beendet wird. Der untergeordnete Prozess würde unbegrenzt darauf warten, dass der übergeordnete Prozess aus dem vollständigen StandardOutput-Stream liest.
Carlos Liu

Es ist ein bisschen ärgerlich, wie komplex es ist, dies richtig zu machen. War erfreut, es mit einfacheren Befehlszeilenumleitungen> Ausgabedatei zu umgehen :)
eglasius

Antworten:


392

Das Problem ist, dass wenn Sie umleiten StandardOutputund / oder StandardErrorder interne Puffer voll werden kann. Unabhängig von der Reihenfolge, die Sie verwenden, kann ein Problem auftreten:

  • Wenn Sie warten, bis der Prozess beendet ist, bevor Sie den Prozess lesen StandardOutput, kann der Versuch, darauf zu schreiben, blockiert werden, sodass der Prozess niemals endet.
  • Wenn Sie StandardOutputmit ReadToEnd lesen , kann Ihr Prozess blockieren, wenn der Prozess nie geschlossen wird StandardOutput(z. B. wenn er nie beendet wird oder wenn das Schreiben in blockiert wird StandardError).

Die Lösung besteht darin, asynchrone Lesevorgänge zu verwenden, um sicherzustellen, dass der Puffer nicht voll wird. Um jeden Deadlocks zu vermeiden und alle Ausgaben von beiden Sammeln StandardOutputund StandardErrorSie können dies tun:

BEARBEITEN: In den folgenden Antworten erfahren Sie, wie Sie eine ObjectDisposedException vermeiden, wenn das Zeitlimit auftritt.

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
Hatte keine Ahnung, dass das Umleiten der Ausgabe das Problem verursachte, aber es war sicher genug. Verbrachte 4 Stunden damit, meinen Kopf darauf zu schlagen und reparierte es in 5 Minuten, nachdem ich Ihren Beitrag gelesen hatte. Gute Arbeit!
Ben Gripka

1
@AlexPeck Das Problem wurde als Konsolen-App ausgeführt. Hans Passant identifizierte das Problem hier: stackoverflow.com/a/16218470/279516
Bob Horn

5
Jedes Mal, wenn die Eingabeaufforderung geschlossen wird, wird Folgendes angezeigt: In mscorlib.dll ist eine nicht behandelte Ausnahme vom Typ "System.ObjectDisposed" aufgetreten. Zusätzliche Informationen: Das sichere Handle wurde geschlossen
user1663380

3
Wir hatten ein ähnliches Problem wie oben von @ user1663380 beschrieben. Halten Sie es für möglich, dass usingAnweisungen für die Ereignishandler über der usingAnweisung für den Prozess selbst liegen müssen?
Dan Forbes

2
Ich glaube nicht, dass die Wartegriffe benötigt werden. Beenden Sie gemäß msdn einfach die Nicht-Timeout-Version von WaitForExit: Wenn die Standardausgabe an asynchrone Ereignishandler umgeleitet wurde, ist die Ausgabeverarbeitung möglicherweise nicht abgeschlossen, wenn diese Methode zurückgegeben wird. Um sicherzustellen, dass die asynchrone Ereignisbehandlung abgeschlossen ist, rufen Sie die WaitForExit () - Überladung auf, die nach dem Empfang eines true von dieser Überladung keinen Parameter akzeptiert.
Patrick

98

Die Dokumentation für Process.StandardOutputsagt zu lesen, bevor Sie warten, sonst können Sie Deadlock, Snippet unten kopiert:

 // 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
Ich bin nicht 100% sicher, ob dies nur ein Ergebnis meiner Umgebung ist, aber ich habe festgestellt, dass Sie einen Deadlock / Hang bekommen , wenn Sie eingestellt haben RedirectStandardOutput = true;und nicht verwenden p.StandardOutput.ReadToEnd();.
Chris S

3
Wahr. Ich war in einer ähnlichen Situation. Ich habe StandardError ohne Grund umgeleitet, als ich in einem Prozess mit ffmpeg konvertierte. Es wurde genug in den StandardError-Stream geschrieben, um einen Deadlock zu erstellen.
Léon Pelletier

Dies hängt für mich auch beim Umleiten und Lesen der Standardausgabe noch.
user3791372

@ user3791372 Ich denke, dies gilt nur, wenn der Puffer hinter dem StandardOutput nicht vollständig gefüllt ist. Hier wird die MSDN nicht gerecht. Ein großartiger Artikel, den ich Ihnen zum Lesen empfehlen würde, ist unter: dzone.com/articles/async-io-and-threadpool
Cary

19

Die Antwort von Mark Byers ist ausgezeichnet, aber ich möchte nur Folgendes hinzufügen:

Die OutputDataReceivedund ErrorDataReceived-Delegierten müssen vor dem outputWaitHandleund entfernt werdenerrorWaitHandle entsorgt werden. Wenn der Prozess zur Ausgabe von Daten fortgesetzt , nachdem die Timeout überschritten wurde und beendet dann die outputWaitHandleund errorWaitHandlewerden Variablen , nachdem sie entsorgt werden abgerufen.

(Zu Ihrer Information, ich musste diesen Vorbehalt als Antwort hinzufügen, da ich seinen Beitrag nicht kommentieren konnte.)


2
Vielleicht wäre es besser, CancelOutputRead aufzurufen ?
Mark Byers

Das Hinzufügen von Marks bearbeitetem Code zu dieser Antwort wäre ziemlich großartig! Ich habe momentan genau das gleiche Problem.
Ianbailey

8
@ianbailey Der einfachste Weg, dies zu lösen, besteht darin, das using (Process p ...) in das using (AutoResetEvent errorWaitHandle ...) zu setzen
Didier A.

17

Das Problem mit der nicht behandelten ObjectDisposedException tritt auf, wenn das Zeitlimit für den Prozess abgelaufen ist. In diesem Fall die anderen Teile der Bedingung:

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

werden nicht ausgeführt. Ich habe dieses Problem folgendermaßen gelöst:

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
Der Vollständigkeit halber fehlt dies, um die Weiterleitungen auf true
knocte

und ich habe die Zeitüberschreitungen an meinem Ende entfernt, da der Prozess möglicherweise nach Benutzereingaben fragt (z. B. etwas
eingibt

Warum hast du dich verändert outputund errorzu outputBuilder? Kann jemand bitte eine vollständige Antwort geben, die funktioniert?
Marko Avlijaš

System.ObjectDisposedException: Sicheres Handle wurde geschlossen tritt auch bei dieser Version für mich auf
Matt

17

Dies ist eine modernere, auf TPL (Based Parallel Library) basierende Lösung für .NET 4.5 und höher.

Anwendungsbeispiel

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

Implementierung

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
beste und vollständigste Antwort bis heute
TermoTux

1
Aus irgendeinem Grund war dies die einzige Lösung, die für mich funktionierte. Die Anwendung hörte auf zu hängen.
Jack

1
Es scheint, dass Sie die Bedingung nicht behandeln, bei der der Prozess nach dem Start endet, aber bevor das Ereignis Beendet angehängt wurde. Mein Vorschlag - den Prozess nach allen Registrierungen starten.
Stas Boyarincev

@StasBoyarincev Danke, aktualisiert. Ich hatte vergessen, die StackOverflow-Antwort mit dieser Änderung zu aktualisieren.
Muhammad Rehan Saeed

1
@MuhammadRehanSaeed Noch eine andere Sache - es scheint nicht erlaubt zu sein, process.BeginOutputReadLine () oder process.BeginErrorReadLine () vor process.Start aufzurufen. In diesem Fall erhalte ich die Fehlermeldung: StandardOut wurde nicht umgeleitet oder der Prozess wurde noch nicht gestartet.
Stas Boyarincev

8

Rob antwortete und ersparte mir noch ein paar Stunden Probezeit. Lesen Sie den Ausgabe- / Fehlerpuffer, bevor Sie warten:

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

1
aber was ist, wenn nach Ihrem Anruf weitere Daten eingehen WaitForExit()?
Knocte

@knocte basierend auf meinen Tests ReadToEndoder ähnlichen Methoden (wie StandardOutput.BaseStream.CopyTo) wird zurückgegeben, nachdem ALLE Daten gelesen wurden. nichts wird danach kommen
S.Serpooshan

Sie sagen, dass ReadToEnd () auch auf den Exit wartet?
Knocte

2
@knocte Sie versuchen, eine von Microsoft erstellte API zu verstehen?
aaaaaa

Das Problem der entsprechenden MSDN-Seite ist, dass nicht erklärt wurde, dass der Puffer hinter dem StandardOutput voll werden kann. In diesem Fall muss das Kind aufhören zu schreiben und warten, bis der Puffer leer ist (der Elternteil liest die Daten im Puffer weg). . ReadToEnd () kann nur synchron lesen, bis der Puffer geschlossen oder der Puffer voll ist oder das Kind mit nicht vollem Puffer beendet wird. Das ist mein Verständnis.
Cary

7

Wir haben auch dieses Problem (oder eine Variante).

Versuche Folgendes:

1) Fügen Sie p.WaitForExit (nnnn) eine Zeitüberschreitung hinzu. Dabei ist nnnn in Millisekunden.

2) Setzen Sie den ReadToEnd-Aufruf vor den WaitForExit-Aufruf. Dies ist, was wir MS empfohlen gesehen haben.


5

Gutschrift an EM0 für https://stackoverflow.com/a/17600012/4151626

Die anderen Lösungen (einschließlich EM0s) waren für meine Anwendung aufgrund interner Zeitüberschreitungen und der Verwendung von StandardOutput und StandardError durch die erzeugte Anwendung immer noch blockiert. Folgendes hat bei mir funktioniert:

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

Bearbeiten: Initialisierung von StartInfo zum Codebeispiel hinzugefügt


Dies ist, was ich benutze und hatte nie mehr Probleme mit einem Deadlock.
Roemer

3

Ich habe es so gelöst:

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

Ich habe sowohl Eingabe als auch Ausgabe und Fehler umgeleitet und das Lesen von Ausgabe- und Fehlerströmen behandelt. Diese Lösung funktioniert für SDK 7- 8.1, sowohl für Windows 7 als auch für Windows 8


2
Elina: Danke für deine Antwort. Am Ende dieses MSDN-Dokuments ( msdn.microsoft.com/en-us/library/… ) befinden sich einige Hinweise , die vor möglichen Deadlocks warnen, wenn Sie synchron zum Ende sowohl umgeleiteter stdout- als auch stderr-Streams lesen. Es ist schwer zu sagen, ob Ihre Lösung für dieses Problem anfällig ist. Es scheint auch, dass Sie die Ausgabe stdout / stderr des Prozesses direkt als Eingabe zurücksenden. Warum? :)
Matthew Piatt

3

Ich habe versucht, eine Klasse zu erstellen, die Ihr Problem mithilfe des asynchronen Stream-Lesens löst, indem ich die Antworten von Mark Byers, Rob und Stevejay berücksichtigt habe. Dabei wurde mir klar, dass es einen Fehler beim Lesen des asynchronen Prozessausgabestreams gibt.

Ich habe diesen Fehler bei Microsoft gemeldet: https://connect.microsoft.com/VisualStudio/feedback/details/3119134

Zusammenfassung:

Das kannst du nicht machen:

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

Sie erhalten die System.InvalidOperationException: StandardOut wurde nicht umgeleitet oder der Prozess wurde noch nicht gestartet.

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

Dann müssen Sie nach dem Starten des Prozesses mit dem Lesen der asynchronen Ausgabe beginnen:

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

Legen Sie dabei eine Race-Bedingung fest, da der Ausgabestream Daten empfangen kann, bevor Sie sie auf asynchron setzen:

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

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

Dann könnten einige Leute sagen, dass Sie den Stream nur lesen müssen, bevor Sie ihn auf asynchron setzen. Das gleiche Problem tritt jedoch auf. Zwischen dem synchronen Lesen und dem Versetzen des Streams in den asynchronen Modus besteht eine Race-Bedingung.

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

Es gibt keine Möglichkeit, ein sicheres asynchrones Lesen eines Ausgabestreams eines Prozesses in der tatsächlichen Weise zu erreichen, wie "Process" und "ProcessStartInfo" entworfen wurden.

Sie verwenden wahrscheinlich besser asynchrones Lesen, wie es von anderen Benutzern für Ihren Fall vorgeschlagen wurde. Sie sollten sich jedoch bewusst sein, dass Sie aufgrund der Rennbedingungen einige Informationen verpassen können.


1

Ich denke, dass dies ein einfacher und besserer Ansatz ist (wir brauchen ihn nicht 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;
    }
}

Stimmt, aber sollten Sie nicht auch .FileName = Path + @"\ggsci.exe" + @" < obeycommand.txt"Ihren Code vereinfachen? Oder vielleicht etwas Äquivalentes, "echo command | " + Path + @"\ggsci.exe"wenn Sie wirklich keine separate obeycommand.txt-Datei verwenden möchten.
Amit Naidu

3
Ihre Lösung benötigt kein AutoResetEvent, aber Sie fragen ab. Wenn Sie eine Abfrage durchführen, anstatt ein Ereignis zu verwenden (sofern verfügbar), verwenden Sie ohne Grund die CPU, was darauf hinweist, dass Sie ein schlechter Programmierer sind. Ihre Lösung ist im Vergleich zu der anderen mit AutoResetEvent wirklich schlecht. (Aber ich habe dir nicht -1 gegeben, weil du versucht hast zu helfen!).
Eric Ouellet

1

Keine der obigen Antworten macht den Job.

Die Rob-Lösung hängt und die 'Mark Byers'-Lösung erhält die entsprechende Ausnahme (ich habe die "Lösungen" der anderen Antworten ausprobiert).

Also habe ich beschlossen, eine andere Lösung vorzuschlagen:

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

Dieser Code hat debuggt und funktioniert perfekt.


1
Gut! Beachten Sie nur, dass der Token-Parameter beim Aufrufen der GetProcessOutputWithTimeoutMethode nicht angegeben wird.
Serpooshan

1

Einführung

Die derzeit akzeptierte Antwort funktioniert nicht (löst eine Ausnahme aus) und es gibt zu viele Problemumgehungen, aber keinen vollständigen Code. Dies verschwendet offensichtlich viel Zeit, da dies eine beliebte Frage ist.

Ich kombinierte die Antwort von Mark Byers und die Antwort von Karol Tyl und schrieb vollständigen Code basierend darauf, wie ich die Process.Start-Methode verwenden möchte.

Verwendung

Ich habe es verwendet, um einen Fortschrittsdialog um Git-Befehle zu erstellen. So habe ich es benutzt:

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

Theoretisch kann man auch stdout und stderr kombinieren, aber das habe ich nicht getestet.

Code

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

Erhalten Sie immer noch System.ObjectDisposedException: Das sichere Handle wurde auch für diese Version geschlossen.
Matt

1

Ich weiß, dass das Abendessen alt ist, aber nachdem ich diese ganze Seite gelesen hatte, funktionierte keine der Lösungen für mich, obwohl ich Muhammad Rehan nicht ausprobiert habe, da der Code ein wenig schwer zu befolgen war, obwohl ich denke, dass er auf dem richtigen Weg war . Wenn ich sage, dass es nicht funktioniert hat, ist das nicht ganz richtig. Manchmal funktioniert es gut. Ich denke, es hat etwas mit der Länge der Ausgabe vor einer EOF-Markierung zu tun.

Wie auch immer, die Lösung, die für mich funktioniert hat, bestand darin, verschiedene Threads zu verwenden, um StandardOutput und StandardError zu lesen und die Nachrichten zu schreiben.

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

Hoffe das hilft jemandem, der dachte das könnte so schwer sein!


Ausnahme: sw.FlushAsync(): Object is not set to an instance of an object. sw is null. Wie / wo soll swdefiniert werden?
Wallyk

1

Nachdem ich alle Beiträge hier gelesen hatte, entschied ich mich für die konsolidierte Lösung von Marko Avlijaš. Jedoch , es hat nicht alle meine Probleme zu lösen.

In unserer Umgebung haben wir einen Windows-Dienst, der Hunderte verschiedener .bat .cmd .exe, ... usw. Dateien ausführen soll, die sich im Laufe der Jahre angesammelt haben und von vielen verschiedenen Personen und in verschiedenen Stilen geschrieben wurden. Wir haben keine Kontrolle über das Schreiben der Programme und Skripte. Wir sind lediglich für die Planung, Ausführung und Berichterstattung über Erfolg / Misserfolg verantwortlich.

Also habe ich so ziemlich alle Vorschläge hier mit unterschiedlichem Erfolg ausprobiert. Markos Antwort war fast perfekt, aber wenn es als Dienst ausgeführt wurde, wurde stdout nicht immer erfasst. Ich bin nie auf den Grund gegangen, warum nicht.

Die einzige Lösung, die wir in ALLEN unseren Fällen gefunden haben, ist folgende: http://csharptest.net/319/using-the-processrunner-class/index.html


Ich werde diese Bibliothek ausprobieren. Ich habe den Code festgelegt, und es scheint, dass Delegierte sinnvoll eingesetzt werden. Es ist schön in Nuget verpackt. Es stinkt im Grunde nach Professionalität, etwas, das mir niemals vorgeworfen werden könnte. Wenn es beißt, wird es zeigen.
Steve Hibbert

Der Link zum Quellcode ist tot. Bitte kopieren Sie das nächste Mal den Code zur Antwort.
Vitaly Zdanevich

1

Problemumgehung, die ich letztendlich verwendet habe, um die Komplexität zu vermeiden:

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

Also erstelle ich eine temporäre Datei und leite sowohl die Ausgabe als auch den Fehler mithilfe von um > outputfile > 2>&1 und lese die Datei dann einfach, nachdem der Prozess abgeschlossen ist.

Die anderen Lösungen eignen sich gut für Szenarien, in denen Sie andere Aufgaben mit der Ausgabe ausführen möchten. Bei einfachen Aufgaben wird jedoch viel Komplexität vermieden.


1

Ich habe viele der Antworten gelesen und meine eigenen gemacht. Ich bin mir nicht sicher, ob dies auf jeden Fall behoben werden kann, aber es wird in meiner Umgebung behoben. Ich verwende WaitForExit einfach nicht und verwende WaitHandle.WaitAll sowohl für Ausgangs- als auch für Fehlerendesignale. Ich werde mich freuen, wenn jemand mögliche Probleme damit sieht. Oder ob es jemandem hilft. Für mich ist es besser, weil keine Timeouts verwendet werden.

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

Ich habe dies verwendet und mit Task.Run umwickelt, um das Timeout zu verarbeiten. Außerdem gebe ich processid zurück, um das Timeout zu
beenden

0

Ich denke, mit Async ist es möglich, eine elegantere Lösung zu haben und keine Deadlocks zu haben, selbst wenn sowohl standardOutput als auch standardError verwendet werden:

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

Es basiert auf der Antwort von Mark Byers. Wenn Sie sich nicht in einer asynchronen Methode befinden, können Sie string output = tStandardOutput.result;stattdessen verwendenawait



-1

Dieser Beitrag ist möglicherweise veraltet, aber ich habe herausgefunden, dass die Hauptursache dafür, dass er normalerweise hängt, auf einen Stapelüberlauf für den redirectStandardoutput zurückzuführen ist oder wenn Sie redirectStandarderror haben.

Da die Ausgabedaten oder die Fehlerdaten groß sind, führt dies zu einer Wartezeit, da sie noch auf unbestimmte Zeit verarbeitet werden.

So beheben Sie dieses Problem:

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

11
Das Problem ist, dass die Leute diese explizit auf true setzen, weil sie auf diese Streams zugreifen wollen! Sonst können wir sie einfach falsch lassen.
user276648

-1

Nennen wir den hier veröffentlichten Beispielcode den Redirector und das andere Programm den Redirector. Wenn ich es wäre, würde ich wahrscheinlich ein Testumleitungsprogramm schreiben, mit dem das Problem dupliziert werden kann.

So tat ich. Für Testdaten habe ich die PDF-Datei ECMA-334 C # verwendet. es ist ungefähr 5 MB. Das Folgende ist der wichtige Teil davon.

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

Der Datengrößenwert stimmt nicht mit der tatsächlichen Dateigröße überein, dies spielt jedoch keine Rolle. Es ist nicht klar, ob eine PDF-Datei am Ende der Zeilen immer sowohl CR als auch LF verwendet, aber das spielt dabei keine Rolle. Sie können jede andere große Textdatei zum Testen verwenden.

Dabei hängt der Beispiel-Redirector-Code, wenn ich die große Datenmenge schreibe, aber nicht, wenn ich eine kleine Menge schreibe.

Ich habe sehr viel versucht, die Ausführung dieses Codes irgendwie zu verfolgen, und ich konnte es nicht. Ich habe die Zeilen des umgeleiteten Programms auskommentiert, die die Erstellung einer Konsole für das umgeleitete Programm deaktiviert haben, um zu versuchen, ein separates Konsolenfenster zu erhalten, konnte dies jedoch nicht.

Dann fand ich heraus, wie man eine Konsolen-App in einem neuen Fenster, im Fenster der Eltern oder in keinem Fenster startet . Anscheinend können wir keine (einfache) separate Konsole haben, wenn ein Konsolenprogramm ein anderes Konsolenprogramm ohne ShellExecute startet. Da ShellExecute die Umleitung nicht unterstützt, müssen wir eine Konsole freigeben, auch wenn wir kein Fenster für den anderen Prozess angeben.

Ich gehe davon aus, dass das umgeleitete Programm, wenn es irgendwo einen Puffer füllt, auf das Lesen der Daten warten muss. Wenn zu diesem Zeitpunkt keine Daten vom Redirector gelesen werden, ist dies ein Deadlock.

Die Lösung besteht darin, ReadToEnd nicht zu verwenden und die Daten zu lesen, während die Daten geschrieben werden. Es ist jedoch nicht erforderlich, asynchrone Lesevorgänge zu verwenden. Die Lösung kann recht einfach sein. Das Folgende funktioniert für mich mit dem 5 MB PDF.

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

Eine andere Möglichkeit besteht darin, ein GUI-Programm zu verwenden, um die Umleitung durchzuführen. Der vorhergehende Code funktioniert in einer WPF-Anwendung, außer mit offensichtlichen Änderungen.


-3

Ich hatte das gleiche Problem, aber der Grund war anders. Dies würde jedoch unter Windows 8 geschehen, jedoch nicht unter Windows 7. Die folgende Zeile scheint das Problem verursacht zu haben.

pProcess.StartInfo.UseShellExecute = False

Die Lösung bestand darin, UseShellExecute NICHT zu deaktivieren. Ich habe jetzt ein Shell-Popup-Fenster erhalten, das unerwünscht ist, aber viel besser als das Programm, das darauf wartet, dass nichts Besonderes passiert. Deshalb habe ich die folgende Problemumgehung hinzugefügt:

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

Jetzt stört mich nur noch, warum dies überhaupt unter Windows 8 geschieht.


1
Sie müssen UseShellExecuteauf false gesetzt sein, wenn Sie die Ausgabe umleiten möchten.
Brad Moore
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.