ProcessStartInfo zawieszone na „WaitForExit”? Czemu?


195

Mam następujący kod:

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

Wiem, że dane wyjściowe z procesu, który zaczynam, mają około 7 MB. Uruchomienie go w konsoli Windows działa dobrze. Niestety programowo zawiesza się to na czas nieokreślony w WaitForExit. Zauważ również, że kod NIE zawiesza się dla mniejszych wyjść (jak 3KB).

Czy to możliwe, że wewnętrzny StandardOutput w ProcessStartInfo nie może buforować 7 MB? Jeśli tak, co powinienem zrobić zamiast tego? Jeśli nie, co robię źle?


jakieś ostateczne rozwiązanie z pełnym kodem źródłowym?
Kiquenet,


6
Tak, ostateczne rozwiązanie: zamień ostatnie dwie linie. Jest w instrukcji .
Amit Naidu

4
z msdn: Przykładowy kod pozwala uniknąć zakleszczenia, wywołując p.StandardOutput.ReadToEnd przed p.WaitForExit. Zakleszczenie może wystąpić, jeśli proces nadrzędny wywoła p.WaitForExit przed p.StandardOutput.ReadToEnd, a proces potomny zapisze wystarczającą ilość tekstu, aby wypełnić przekierowany strumień. Proces nadrzędny czekałby w nieskończoność na zakończenie procesu potomnego. Proces potomny czekałby w nieskończoność na odczytanie przez rodzica z pełnego strumienia StandardOutput.
Carlos Liu

to trochę denerwujące, jak skomplikowane jest robienie tego poprawnie. Byłem zadowolony, mogąc obejść to z prostszymi przekierowaniami wiersza poleceń> plik wyjściowy :)
eglasius

Odpowiedzi:


406

Problem polega na tym, że jeśli przekierujesz StandardOutputi / lub StandardErrorwewnętrzny bufor może się zapełnić. Niezależnie od wybranej kolejności może wystąpić problem:

  • Jeśli poczekasz na zakończenie procesu przed odczytaniem, StandardOutputproces może zablokować próbę zapisu, więc proces nigdy się nie kończy.
  • Jeśli czytasz z StandardOutputużyciem ReadToEnd następnie Twój proces może blokować, jeśli proces nie zamyka się StandardOutput(na przykład, jeśli nigdy nie kończy, lub jeśli jest zablokowane do zapisu StandardError).

Rozwiązaniem jest użycie odczytów asynchronicznych, aby upewnić się, że bufor nie zostanie zapełniony. Aby uniknąć zakleszczenia i zebrać wszystkie dane wyjściowe z obu StandardOutputi StandardErrormożna to zrobić:

EDYCJA: Zobacz odpowiedzi poniżej, aby dowiedzieć się, jak uniknąć wyjątku ObjectDisposedException w przypadku przekroczenia limitu czasu.

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

13
Nie miałem pojęcia, że ​​przekierowanie wyjścia było przyczyną problemu, ale na pewno tak było. Spędziłem 4 godziny waląc w to głowę i naprawiłem to w 5 minut po przeczytaniu twojego postu. Dobra robota!
Ben Gripka

1
@AlexPeck Problem polegał na uruchomieniu tego jako aplikacji konsoli. Hans Passant zidentyfikował problem tutaj: stackoverflow.com/a/16218470/279516
Bob Horn

5
za każdym razem, gdy wiersz polecenia zostaje zamknięty, pojawia się następujący komunikat: W mscorlib.dll wystąpił nieobsługiwany wyjątek typu „System.ObjectDisposed” Dodatkowe informacje: Bezpieczne dojście zostało zamknięte
użytkownik1663380

3
Mieliśmy podobny problem, jak opisany powyżej przez @ user1663380. Myślisz, że to jest możliwe, że usingstwierdzenia dotyczące obsługi zdarzeń muszą być wyżej w usingwypowiedzi dla samego procesu?
Dan Forbes,

3
Nie sądzę, żeby uchwyty czekania były potrzebne. Zgodnie z msdn, po prostu zakończ z wersją WaitForExit bez limitu czasu: Gdy standardowe wyjście zostało przekierowane do asynchronicznych programów obsługi zdarzeń, możliwe jest, że przetwarzanie danych wyjściowych nie zostanie zakończone po zwróceniu tej metody. Aby upewnić się, że asynchroniczna obsługa zdarzeń została zakończona, wywołaj przeciążenie WaitForExit (), które nie przyjmuje żadnego parametru po otrzymaniu wartości true z tego przeciążenia.
Patrick,

99

Dokumentacja dla Process.StandardOutputmówi czytać zanim czekać inaczej można impasu, snippet przytoczone poniżej:

 // 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
Nie jestem w 100% pewien, czy to tylko wynik mojego środowiska, ale odkryłem, że jeśli ustawiłeś RedirectStandardOutput = true;i nie używasz p.StandardOutput.ReadToEnd();, otrzymujesz zakleszczenie / zawieszenie.
Chris S

3
Prawdziwe. Byłem w podobnej sytuacji. Przekierowywałem StandardError bez powodu podczas konwersji z ffmpeg w procesie, pisałem wystarczająco dużo w strumieniu StandardError, aby utworzyć zakleszczenie.
Léon Pelletier

To nadal się zawiesza, nawet przy przekierowywaniu i czytaniu standardowego wyjścia.
user3791372,

@ user3791372 Myślę, że ma to zastosowanie tylko wtedy, gdy bufor za StandardOutput nie jest w pełni wypełniony. Tutaj MSDN nie oddaje sprawiedliwości. Świetny artykuł, który polecam przeczytać, jest pod adresem: dzone.com/articles/async-io-and-threadpool
Cary.

19

Jest to bardziej nowoczesne i oczekiwane rozwiązanie oparte na bibliotece zadań równoległych (TPL) dla platformy .NET 4.5 i nowszych.

Przykład użycia

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

Realizacja

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
najlepsza i najpełniejsza jak dotąd odpowiedź
TermoTux

1
Z jakiegoś powodu było to jedyne rozwiązanie, które u mnie zadziałało, aplikacja przestała się zawieszać.
Jack

1
Wygląda na to, że nie obsługujesz warunku, w którym proces kończy się po rozpoczęciu, ale przed dołączeniem zdarzenia Exited. Moja sugestia - rozpoczęcie procesu po wszystkich rejestracjach.
Stas Boyarincev

@StasBoyarincev Dzięki, zaktualizowano. Zapomniałem zaktualizować odpowiedź StackOverflow tą zmianą.
Muhammad Rehan Saeed

1
@MuhammadRehanSaeed Jeszcze inna rzecz - wydaje się, że nie jest dozwolone wywołanie process.BeginOutputReadLine () lub process.BeginErrorReadLine () przed process.Start. W takim przypadku pojawia się błąd: StandardOut nie został przekierowany lub proces jeszcze się nie rozpoczął.
Stas Boyarincev

19

Odpowiedź Marka Byersa jest doskonała, ale dodałbym tylko:

Delegaty OutputDataReceivedi ErrorDataReceivedmuszą zostać usunięte przed outputWaitHandlei errorWaitHandlezostaną usunięte. Jeśli proces nadal wyprowadza dane po przekroczeniu limitu czasu, a następnie zakończy się, dostęp do zmiennych outputWaitHandlei errorWaitHandlezostanie uzyskany po ich usunięciu .

(FYI musiałem dodać to zastrzeżenie jako odpowiedź, ponieważ nie mogłem skomentować jego postu.)


2
Może lepiej byłoby wywołać CancelOutputRead ?
Mark Byers,

Dodanie edytowanego kodu Marka do tej odpowiedzi byłoby raczej niesamowite! W tej chwili mam dokładnie ten sam problem.
ianbailey,

8
@ianbailey Najłatwiejszym sposobem rozwiązania tego problemu jest umieszczenie using (Process p ...) wewnątrz using (AutoResetEvent errorWaitHandle ...)
Didier A.

17

Problem z nieobsługiwanym ObjectDisposedException występuje, gdy upłynął limit czasu procesu. W takim przypadku pozostałe części warunku:

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

nie są wykonywane. Rozwiązałem ten problem w następujący sposób:

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
ze względu na kompletność brakuje w tym ustawiania przekierowań na true
knocte

i usunąłem limity czasu na moim końcu, ponieważ proces może poprosić o wprowadzenie danych przez użytkownika (np. wpisać coś), więc nie chcę wymagać, aby użytkownik był szybki
knocte

Dlaczego zmienił outputi errordo outputBuilder? Czy ktoś może udzielić pełnej odpowiedzi, która działa?
Marko Avlijaš

System.ObjectDisposedException: Bezpieczny uchwyt został zamknięty, występuje również w tej wersji dla mnie
Matt

8

Rob odpowiedział na to i zaoszczędził mi kilka godzin prób. Przeczytaj bufor wyjściowy / błąd przed czekaniem:

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

1
ale co się stanie, jeśli po wywołaniu pojawi się więcej danych WaitForExit()?
knocte

@knocte w oparciu o moje testy ReadToEndlub podobne metody (takie jak StandardOutput.BaseStream.CopyTo) powrócą po przeczytaniu WSZYSTKICH danych. nic po nim nie nastąpi
S.Serpooshan

mówisz, że ReadToEnd () również czeka na wyjście?
knocte

2
@knocte, próbujesz zrozumieć API stworzone przez Microsoft?
aaaaaa

Problem z odpowiednią stroną MSDN polega na tym, że nie wyjaśniono, że bufor za StandardOutput może się zapełnić iw takiej sytuacji dziecko musi przestać pisać i czekać, aż bufor zostanie opróżniony (rodzic odczyta dane w buforze) . ReadToEnd () może odczytywać synchronicznie tylko do momentu zamknięcia bufora lub zapełnienia bufora lub zakończenia działania elementu potomnego, gdy bufor nie jest pełny. To jest moje zrozumienie.
Cary

7

Mamy też ten problem (lub wariant).

Spróbuj wykonać następujące czynności:

1) Dodaj limit czasu do p.WaitForExit (nnnn); gdzie nnnn jest wyrażone w milisekundach.

2) Umieść wywołanie ReadToEnd przed wywołaniem WaitForExit. To jest to, co widzieliśmy MS polecam.


6

Kredyt dla EM0 za https://stackoverflow.com/a/17600012/4151626

Inne rozwiązania (w tym EM0) nadal były zakleszczone dla mojej aplikacji z powodu wewnętrznych limitów czasu i użycia zarówno StandardOutput, jak i StandardError przez uruchomioną aplikację. Oto, co zadziałało dla mnie:

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

Edycja: dodano inicjalizację StartInfo do przykładu kodu


To jest to, czego używam i nigdy nie miałem już problemów z impasem.
Roemer

3

Rozwiązałem to w ten sposób:

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

Przekierowałem zarówno dane wejściowe, wyjściowe, jak i błędy oraz obsługiwałem odczyt ze strumieni wyjściowych i strumieni błędów. To rozwiązanie działa dla SDK 7- 8.1, zarówno dla Windows 7, jak i Windows 8


2
Elina: dzięki za odpowiedź. Na dole tego dokumentu MSDN ( msdn.microsoft.com/en-us/library/… ) znajduje się kilka uwag, które ostrzegają o potencjalnych zakleszczeniach w przypadku synchronicznego odczytywania do końca przekierowanych strumieni stdout i stderr. Trudno powiedzieć, czy Twoje rozwiązanie jest podatne na ten problem. Wygląda również na to, że wysyłasz wyjście procesu stdout / stderr bezpośrednio z powrotem jako wejście. Czemu? :)
Matthew Piatt

3

Próbowałem stworzyć klasę, która rozwiąże Twój problem za pomocą asynchronicznego odczytu strumienia, biorąc pod uwagę odpowiedzi Mark Byers, Rob, stevejay. Robiąc to, zdałem sobie sprawę, że jest błąd związany z odczytem strumienia wyjściowego procesu asynchronicznego.

Zgłosiłem ten błąd w Microsoft: https://connect.microsoft.com/VisualStudio/feedback/details/3119134

Podsumowanie:

Nie możesz tego zrobić:

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

Otrzymasz System.InvalidOperationException: StandardOut nie został przekierowany lub proces jeszcze się nie rozpoczął.

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

Następnie należy rozpocząć asynchroniczne odczytywanie wyjścia po uruchomieniu procesu:

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

Robiąc to, stwórz warunek wyścigu, ponieważ strumień wyjściowy może odbierać dane przed ustawieniem go na asynchroniczny:

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

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

Wtedy niektórzy mogą powiedzieć, że wystarczy przeczytać strumień, zanim ustawisz go na asynchroniczny. Ale pojawia się ten sam problem. Między synchronicznym odczytem a ustawieniem strumienia w tryb asynchroniczny wystąpi sytuacja wyścigu.

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

Nie ma sposobu na osiągnięcie bezpiecznego asynchronicznego odczytu strumienia wyjściowego procesu w rzeczywistym sposobie, w jaki zaprojektowano „Process” i „ProcessStartInfo”.

Prawdopodobnie lepiej będzie korzystać z odczytu asynchronicznego, jak sugerowali inni użytkownicy w Twoim przypadku. Ale powinieneś być świadomy, że możesz przegapić niektóre informacje z powodu stanu wyścigu.


1

Uważam, że jest to proste i lepsze podejście (nie potrzebujemy 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;
    }
}

To prawda, ale czy nie powinieneś też starać .FileName = Path + @"\ggsci.exe" + @" < obeycommand.txt"się uprościć kodu? A może coś równoważnego, "echo command | " + Path + @"\ggsci.exe"jeśli naprawdę nie chcesz używać oddzielnego pliku obeycommand.txt.
Amit Naidu

3
Twoje rozwiązanie nie potrzebuje AutoResetEvent, ale sondujesz. Kiedy odpytujesz zamiast używać zdarzeń (jeśli są dostępne), to używasz procesora bez powodu, co wskazuje, że jesteś złym programistą. Twoje rozwiązanie jest naprawdę złe w porównaniu z innymi używającymi AutoResetEvent. (Ale nie dałem Ci -1, ponieważ próbowałeś pomóc!).
Eric Ouellet

1

Żadna z powyższych odpowiedzi nie działa.

Rozwiązanie Roba zawiesza się, a rozwiązanie „Mark Byers” otrzymuje usunięty wyjątek (wypróbowałem „rozwiązania” innych odpowiedzi).

Postanowiłem więc zasugerować inne rozwiązanie:

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

Ten kod został debugowany i działa doskonale.


1
Dobry! pamiętaj tylko, że parametr token nie jest podawany podczas wywoływania GetProcessOutputWithTimeoutmetody.
S.Serpooshan,

1

Wprowadzenie

Obecnie zaakceptowana odpowiedź nie działa (zgłasza wyjątek) i istnieje zbyt wiele obejść, ale nie ma pełnego kodu. To oczywiście marnowanie czasu wielu ludzi, ponieważ jest to popularne pytanie.

Łącząc odpowiedź Marka Byersa i odpowiedź Karola Tyle'a, napisałem pełny kod w oparciu o to, jak chcę używać metody Process.Start.

Stosowanie

Użyłem go do stworzenia okna dialogowego postępu wokół poleceń git. Oto jak to wykorzystałem:

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

Teoretycznie można również łączyć stdout i stderr, ale nie testowałem tego.

Kod

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

Nadal pobierz System.ObjectDisposedException: Bezpieczne dojście zostało również zamknięte w tej wersji.
Matt

1

Wiem, że to kolacja stara, ale po przeczytaniu całej tej strony żadne z rozwiązań nie działało dla mnie, chociaż nie próbowałem Muhammada Rehana, ponieważ kod był trochę trudny do naśladowania, chociaż myślę, że był na dobrej drodze . Kiedy mówię, że to nie zadziałało, to nie do końca prawda, czasami działało dobrze, myślę, że ma to coś wspólnego z długością wyjścia przed znakiem EOF.

W każdym razie rozwiązaniem, które zadziałało, było użycie różnych wątków do odczytywania StandardOutput i StandardError oraz do pisania wiadomości.

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

Mam nadzieję, że pomoże to komuś, kto pomyślał, że to może być takie trudne!


Wyjątek: sw.FlushAsync(): Object is not set to an instance of an object. sw is null. jak / gdzie należy swzdefiniować?
wallyk

1

Po przeczytaniu wszystkich postów tutaj zdecydowałem się na skonsolidowane rozwiązanie Marko Avlijaša. Jednak nie rozwiązało to wszystkich moich problemów.

W naszym środowisku mamy usługę Windows, która ma uruchamiać setki różnych plików .bat .cmd .exe, ... itp., Które gromadziły się przez lata i zostały napisane przez wielu różnych ludzi i w różnych stylach. Nie mamy kontroli nad pisaniem programów i skryptów, jesteśmy tylko odpowiedzialni za planowanie, uruchamianie i raportowanie o sukcesach / niepowodzeniach.

Wypróbowałem więc prawie wszystkie sugestie tutaj z różnymi poziomami sukcesu. Odpowiedź Marko była prawie idealna, ale uruchamiany jako usługa nie zawsze przechwytywał standardowe wyjście. Nigdy nie doszedłem do sedna, dlaczego nie.

Jedyne znalezione przez nas rozwiązanie, które działa we WSZYSTKICH naszych przypadkach, to: http://csharptest.net/319/using-the-processrunner-class/index.html


Mam zamiar wypróbować tę bibliotekę. Ograniczyłem kod i wygląda na to, że rozsądnie używałem delegatów. Jest ładnie zapakowany w Nuget. W zasadzie śmierdzi profesjonalizmem, o co nigdy nie mógłbym zostać zarzucony. Jeśli gryzie, powie.
Steve Hibbert

Link do kodu źródłowego jest martwy. Następnym razem skopiuj kod do odpowiedzi.
Witalij Zdanevich

1

Obejście, którego użyłem, aby uniknąć całej złożoności:

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

Więc tworzę plik tymczasowy, przekierowuję zarówno wyjście, jak i błąd do niego za pomocą, > outputfile > 2>&1a następnie po prostu czytam plik po zakończeniu procesu.

Inne rozwiązania są dobre w scenariuszach, w których chcesz zrobić inne rzeczy z wyjściem, ale w przypadku prostych rzeczy pozwala to uniknąć dużej złożoności.


1

Przeczytałem wiele odpowiedzi i stworzyłem własne. Nie jestem pewien, czy to naprawi w każdym przypadku, ale naprawia się w moim środowisku. Po prostu nie używam WaitForExit i używam WaitHandle.WaitAll na obu sygnałach wyjściowych i końcowych błędów. Będzie mi miło, jeśli ktoś zobaczy z tym możliwe problemy. Albo jeśli to komuś pomoże. Dla mnie to lepsze, ponieważ nie używa timeoutów.

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

Użyłem tego i zapakowałem w Task.Run do obsługi limitu czasu, zwracam również processid, aby zabijać po
przekroczeniu

0

Myślę, że dzięki async można mieć bardziej eleganckie rozwiązanie i nie mieć zakleszczeń, nawet jeśli używa się zarówno standardOutput, jak i 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.
    }
}

Opiera się na odpowiedzi Marka Byersa. Jeśli nie korzystasz z metody asynchronicznej, możesz użyć string output = tStandardOutput.result;zamiastawait



-1

Ten post może być nieaktualny, ale odkryłem, że główną przyczyną, dla której zwykle się zawiesza, jest przepełnienie stosu dla wyjścia redirectStandardoutput lub błąd redirectStandarderror.

Ponieważ dane wyjściowe lub dane o błędach są duże, spowoduje to zawieszenie, ponieważ nadal przetwarza przez nieokreślony czas.

aby rozwiązać ten problem:

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

11
Problem polega na tym, że ludzie jawnie ustawiają je jako prawdziwe, ponieważ chcą mieć dostęp do tych strumieni! W przeciwnym razie możemy po prostu pozostawić je fałszywym.
user276648,

-1

Nazwijmy zamieszczony tutaj przykładowy kod redirectorem, a drugi program przekierowanym. Gdybym to był ja, prawdopodobnie napisałbym program przekierowany testowo, który można wykorzystać do powielenia problemu.

Więc zrobiłem. Do danych testowych użyłem specyfikacji języka ECMA-334 C # v PDF; jest to około 5 MB. Poniżej znajduje się ważna część tego.

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

Wartość rozmiaru danych nie odpowiada rzeczywistemu rozmiarowi pliku, ale to nie ma znaczenia. Nie jest jasne, czy plik PDF zawsze używa na końcu linii zarówno CR, jak i LF, ale nie ma to znaczenia. Możesz użyć dowolnego innego dużego pliku tekstowego do przetestowania.

Korzystając z tego, przykładowy kod readresatora zawiesza się, gdy piszę dużą ilość danych, ale nie podczas pisania małej ilości.

Bardzo starałem się jakoś prześledzić wykonanie tego kodu i nie mogłem. Skomentowałem wiersze przekierowanego programu, który wyłączał tworzenie konsoli dla przekierowanego programu, aby spróbować uzyskać oddzielne okno konsoli, ale nie mogłem.

Potem znalazłem Jak uruchomić aplikację konsolową w nowym oknie, w oknie rodzica lub bez okna . Tak więc najwyraźniej nie możemy (łatwo) mieć oddzielnej konsoli, gdy jeden program konsoli uruchamia inny program konsoli bez ShellExecute, a ponieważ ShellExecute nie obsługuje przekierowywania, musimy udostępniać konsolę, nawet jeśli nie określimy okna dla drugiego procesu.

Zakładam, że jeśli przekierowany program zapełnia gdzieś bufor, to musi czekać na odczytanie danych i jeśli w tym momencie żadne dane nie są odczytywane przez readresator, to jest to zakleszczenie.

Rozwiązaniem jest nieużywanie ReadToEnd i odczytywanie danych podczas ich zapisywania, ale nie jest konieczne stosowanie odczytów asynchronicznych. Rozwiązanie może być całkiem proste. Poniższe działa dla mnie z 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();

Inną możliwością jest użycie programu GUI do wykonania przekierowania. Poprzedni kod działa w aplikacji WPF z wyjątkiem oczywistych modyfikacji.


-3

Miałem ten sam problem, ale powód był inny. Może się to jednak zdarzyć pod Windows 8, ale nie pod Windows 7. Wydaje się, że problem spowodował następujący wiersz.

pProcess.StartInfo.UseShellExecute = False

Rozwiązaniem było NIE wyłączenie UseShellExecute. Otrzymałem teraz wyskakujące okienko Shell, które jest niepożądane, ale znacznie lepsze niż program czekający na nic szczególnego. Dlatego dodałem następujące obejście:

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

Teraz jedyne, co mnie niepokoi, to dlaczego tak się dzieje w systemie Windows 8 w pierwszej kolejności.


1
Musisz UseShellExecuteustawić wartość false, jeśli chcesz przekierować dane wyjściowe.
Brad Moore,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.