Proces czasami zawiesza się podczas oczekiwania na wyjście

13

Co może być przyczyną zawieszania się procesu podczas oczekiwania na wyjście?

Ten kod musi uruchomić skrypt PowerShell, który wykonuje wiele akcji, np. Rozpocząć rekompilację kodu za pomocą MSBuild, ale prawdopodobnie problem polega na tym, że generuje on zbyt wiele danych wyjściowych i kod ten blokuje się podczas oczekiwania na zakończenie, nawet po prawidłowym wykonaniu skryptu powłoki

to trochę „dziwne”, ponieważ czasami ten kod działa dobrze, a czasem po prostu się zacina.

Kod zawiesza się w:

process.WaitForExit (ProcessTimeOutMiliseconds);

Skrypt PowerShell wykonuje się w ciągu 1–2 sekund, a limit czasu wynosi 19 sekund.

public static (bool Success, string Logs) ExecuteScript(string path, int ProcessTimeOutMiliseconds, params string[] args)
{
    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();

    using (var outputWaitHandle = new AutoResetEvent(false))
    using (var errorWaitHandle = new AutoResetEvent(false))
    {
        try
        {
            using (var process = new Process())
            {
                process.StartInfo = new ProcessStartInfo
                {
                    WindowStyle = ProcessWindowStyle.Hidden,
                    FileName = "powershell.exe",
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    UseShellExecute = false,
                    Arguments = $"-ExecutionPolicy Bypass -File \"{path}\"",
                    WorkingDirectory = Path.GetDirectoryName(path)
                };

                if (args.Length > 0)
                {
                    var arguments = string.Join(" ", args.Select(x => $"\"{x}\""));
                    process.StartInfo.Arguments += $" {arguments}";
                }

                output.AppendLine($"args:'{process.StartInfo.Arguments}'");

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

                process.Start();

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

                process.WaitForExit(ProcessTimeOutMiliseconds);

                var logs = output + Environment.NewLine + error;

                return process.ExitCode == 0 ? (true, logs) : (false, logs);
            }
        }
        finally
        {
            outputWaitHandle.WaitOne(ProcessTimeOutMiliseconds);
            errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds);
        }
    }
}

Scenariusz:

start-process $args[0] App.csproj -Wait -NoNewWindow

[string]$sourceDirectory  = "\bin\Debug\*"
[int]$count = (dir $sourceDirectory | measure).Count;

If ($count -eq 0)
{
    exit 1;
}
Else
{
    exit 0;
}

gdzie

$args[0] = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe"

Edytować

Do rozwiązania @ ingen dodałem małe opakowanie, które próbuje wykonać powieszony MS Build

public static void ExecuteScriptRx(string path, int processTimeOutMilliseconds, out string logs, out bool success, params string[] args)
{
    var current = 0;
    int attempts_count = 5;
    bool _local_success = false;
    string _local_logs = "";

    while (attempts_count > 0 && _local_success == false)
    {
        Console.WriteLine($"Attempt: {++current}");
        InternalExecuteScript(path, processTimeOutMilliseconds, out _local_logs, out _local_success, args);
        attempts_count--;
    }

    success = _local_success;
    logs = _local_logs;
}

Gdzie InternalExecuteScriptjest kod ingen

Joelty
źródło
na której linii faktycznie zawiesza się proces? i znacznie więcej wprowadzaj swój kod
Mr.AF
@ Mr.AF masz rację - gotowe.
Joelty
1
Rzeczywiste wywołanie Powershell jest jedną rzeczą, jednak NIE zapewniasz realnej części skryptu, którą próbujesz przetworzyć podczas korzystania z PENShell. Samo wywoływanie programu PowerShell nie stanowi problemu, ale dotyczy tego, co próbujesz zrobić. Edytuj swój post i umieść wyraźne wywołanie / polecenia, które próbujesz wykonać.
DRapp
1
To naprawdę dziwne, że próbowałem powtórzyć błąd. Zdarzyło się to losowo dwukrotnie przy 20 próbach lub czymś innym i nie jestem w stanie ponownie go uruchomić.
KiKoS
1
@Joelty, och fajnie ciekawe, czy mówisz, że to Rxpodejście zadziałało (bo w nim nie przekroczyło limitu czasu) nawet w przypadku nieudanego procesu MSBuild, który prowadzi do nieokreślonego oczekiwania? zainteresowany wiedzieć, jak to było obsługiwane
Clint

Odpowiedzi:

9

Zacznijmy od podsumowania zaakceptowanej odpowiedzi w powiązanym poście.

Problem polega na tym, że jeśli przekierujesz StandardOutput i / lub StandardError, bufor wewnętrzny może się zapełnić. Niezależnie od użytej kolejności może wystąpić problem:

  • Jeśli czekasz na zakończenie procesu przed odczytaniem StandardOutput, proces może zablokować próbę zapisu do niego, więc proces nigdy się nie kończy.
  • Jeśli czytasz z StandardOutput za pomocą ReadToEnd, wówczas proces może zostać zablokowany, jeśli proces nigdy nie zamknie StandardOutput (na przykład, jeśli nigdy się nie zakończy lub jeśli zostanie zablokowany zapis do StandardError).

Jednak nawet zaakceptowana odpowiedź w niektórych przypadkach zmaga się z kolejnością wykonania.

EDYCJA: Zobacz odpowiedzi poniżej, jak uniknąć wyjątku ObjectDisposedException, jeśli nastąpi przekroczenie limitu czasu.

To w takich sytuacjach, w których chcesz zorganizować kilka wydarzeń, Rx naprawdę błyszczy.

Uwaga: implementacja Rx .NET jest dostępna jako pakiet System.Reactive NuGet.

Zanurzmy się, aby zobaczyć, jak Rx ułatwia pracę ze zdarzeniami.

// Subscribe to OutputData
Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.OutputDataReceived))
    .Subscribe(
        eventPattern => output.AppendLine(eventPattern.EventArgs.Data),
        exception => error.AppendLine(exception.Message)
    ).DisposeWith(disposables);

FromEventPatternpozwala nam mapować różne wystąpienia zdarzenia na zunifikowany strumień (znany również jako obserwowalny). To pozwala nam obsługiwać zdarzenia w potoku (z semantyką podobną do LINQ). SubscribePrzeciążenie stosowane tutaj jest zaopatrzona w Action<EventPattern<...>>i Action<Exception>. Za każdym razem, gdy obserwowane zdarzenie zostanie podniesione, jego senderi argszostanie owinięte EventPatterni przepchnięte przez Action<EventPattern<...>>. Kiedy wyjątek jest zgłaszany w potoku, Action<Exception>jest używany.

Jedną z wad Eventwzorca, wyraźnie zilustrowaną w tym przypadku użycia (i wszystkimi obejściami w odnośnym poście), jest to, że nie jest oczywiste, kiedy / gdzie wypisać procedury obsługi zdarzeń.

Z Rx wrócimy IDisposablepo dokonaniu subskrypcji. Gdy go pozbędziemy, skutecznie zakończymy subskrypcję. Dzięki dodaniu DisposeWithmetody rozszerzenia (zapożyczonej z RxUI ) możemy dodać wiele IDisposables do CompositeDisposable(nazwanych disposablesw przykładach kodu). Kiedy skończymy, możemy zakończyć wszystkie subskrypcje za pomocą jednego połączenia z disposables.Dispose().

Oczywiście, nic nie możemy zrobić z Rx, czego nie moglibyśmy zrobić z waniliowym .NET. Wynikowy kod jest o wiele łatwiejszy do zrozumienia, gdy dostosujesz się do funkcjonalnego sposobu myślenia.

public static void ExecuteScriptRx(string path, int processTimeOutMilliseconds, out string logs, out bool success, params string[] args)
{
    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();

    using (var process = new Process())
    using (var disposables = new CompositeDisposable())
    {
        process.StartInfo = new ProcessStartInfo
        {
            WindowStyle = ProcessWindowStyle.Hidden,
            FileName = "powershell.exe",
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            Arguments = $"-ExecutionPolicy Bypass -File \"{path}\"",
            WorkingDirectory = Path.GetDirectoryName(path)
        };

        if (args.Length > 0)
        {
            var arguments = string.Join(" ", args.Select(x => $"\"{x}\""));
            process.StartInfo.Arguments += $" {arguments}";
        }

        output.AppendLine($"args:'{process.StartInfo.Arguments}'");

        // Raise the Process.Exited event when the process terminates.
        process.EnableRaisingEvents = true;

        // Subscribe to OutputData
        Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.OutputDataReceived))
            .Subscribe(
                eventPattern => output.AppendLine(eventPattern.EventArgs.Data),
                exception => error.AppendLine(exception.Message)
            ).DisposeWith(disposables);

        // Subscribe to ErrorData
        Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.ErrorDataReceived))
            .Subscribe(
                eventPattern => error.AppendLine(eventPattern.EventArgs.Data),
                exception => error.AppendLine(exception.Message)
            ).DisposeWith(disposables);

        var processExited =
            // Observable will tick when the process has gracefully exited.
            Observable.FromEventPattern<EventArgs>(process, nameof(Process.Exited))
                // First two lines to tick true when the process has gracefully exited and false when it has timed out.
                .Select(_ => true)
                .Timeout(TimeSpan.FromMilliseconds(processTimeOutMilliseconds), Observable.Return(false))
                // Force termination when the process timed out
                .Do(exitedSuccessfully => { if (!exitedSuccessfully) { try { process.Kill(); } catch {} } } );

        // Subscribe to the Process.Exited event.
        processExited
            .Subscribe()
            .DisposeWith(disposables);

        // Start process(ing)
        process.Start();

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

        // Wait for the process to terminate (gracefully or forced)
        processExited.Take(1).Wait();

        logs = output + Environment.NewLine + error;
        success = process.ExitCode == 0;
    }
}

Omówiliśmy już pierwszą część, w której mapujemy nasze wydarzenia do obserwowalnych, abyśmy mogli przejść bezpośrednio do mięsnej części. Tutaj przypisujemy naszą obserwowalność do processExitedzmiennej, ponieważ chcemy jej użyć więcej niż raz.

Po pierwsze, kiedy go aktywujemy, dzwoniąc Subscribe. A później, kiedy chcemy „poczekać” na jego pierwszą wartość.

var processExited =
    // Observable will tick when the process has gracefully exited.
    Observable.FromEventPattern<EventArgs>(process, nameof(Process.Exited))
        // First two lines to tick true when the process has gracefully exited and false when it has timed out.
        .Select(_ => true)
        .Timeout(TimeSpan.FromMilliseconds(processTimeOutMilliseconds), Observable.Return(false))
        // Force termination when the process timed out
        .Do(exitedSuccessfully => { if (!exitedSuccessfully) { try { process.Kill(); } catch {} } } );

// Subscribe to the Process.Exited event.
processExited
    .Subscribe()
    .DisposeWith(disposables);

// Start process(ing)
...

// Wait for the process to terminate (gracefully or forced)
processExited.Take(1).Wait();

Jednym z problemów związanych z OP jest to, że zakłada on, process.WaitForExit(processTimeOutMiliseconds)że zakończy proces po przekroczeniu limitu czasu. Z MSDN :

Nakazuje komponentowi procesu czekać określoną liczbę milisekund na zakończenie powiązanego procesu.

Zamiast tego, gdy upłynie limit czasu, po prostu przywraca kontrolę nad bieżącym wątkiem (tzn. Przestaje blokować). Musisz ręcznie wymusić zakończenie, gdy proces przekroczy limit czasu. Aby wiedzieć, kiedy upłynął limit czasu, możemy zmapować Process.Exitedzdarzenie na processExitedobserwowalne w celu przetworzenia. W ten sposób możemy przygotować dane wejściowe dla Dooperatora.

Kod jest dość zrozumiały. Jeśli exitedSuccessfullyproces zakończy się z gracją. Jeśli nie exitedSuccessfully, wypowiedzenie będzie musiało zostać wymuszone. Uwaga: process.Kill()jest wykonywany asynchronicznie, patrz uwagi . Jednak process.WaitForExit()natychmiastowe wywołanie otworzy możliwość impasu. Dlatego nawet w przypadku wymuszonego zakończenia lepiej jest pozwolić, aby wszystkie artykuły jednorazowego użytku zostały wyczyszczone po zakończeniu usingzakresu, ponieważ dane wyjściowe można uznać za przerwane / uszkodzone.

try catchKonstrukt jest zarezerwowane dla wyjątkowych przypadkach (gra słów nie przeznaczonych), gdzie już wyrównany processTimeOutMillisecondsz rzeczywistego czasu potrzebnego do zakończenia procesu. Innymi słowy, między wyścigiem Process.Exiteda licznikiem występuje warunek wyścigu . Możliwość takiego zdarzenia ponownie zwiększa asynchroniczna natura process.Kill(). Spotkałem go raz podczas testów.


Dla kompletności DisposeWithmetoda rozszerzenia.

/// <summary>
/// Extension methods associated with the IDisposable interface.
/// </summary>
public static class DisposableExtensions
{
    /// <summary>
    /// Ensures the provided disposable is disposed with the specified <see cref="CompositeDisposable"/>.
    /// </summary>
    public static T DisposeWith<T>(this T item, CompositeDisposable compositeDisposable)
        where T : IDisposable
    {
        if (compositeDisposable == null)
        {
            throw new ArgumentNullException(nameof(compositeDisposable));
        }

        compositeDisposable.Add(item);
        return item;
    }
}
ingen
źródło
4
IMHO, zdecydowanie warte nagrody. Ładna odpowiedź i miłe wprowadzenie do RX na temat.
quetzalcoatl
Dzięki!!! Twoje ExecuteScriptRxuchwyty hangsdoskonale. Niestety nadal się zawiesza, ale właśnie dodałem małe opakowanie, ExecuteScriptRxktóre działa, Retrya następnie działa dobrze. Przyczyną zawieszania się MSBUILD może być odpowiedź @Clint. PS: Ten kod sprawił, że poczułam się głupio <lol> Po raz pierwszy widzęSystem.Reactive.Linq;
Joelty
Kod opakowania jest w głównym poście
Joelty
3

Z korzyścią dla czytelników podzielę to na 2 sekcje

Sekcja A: Problem i sposób radzenia sobie z podobnymi scenariuszami

Część B: Rekreacja i rozwiązanie problemu

Część A: Problem

Kiedy wystąpi ten problem - proces pojawia się w menedżerze zadań, a następnie po 2-3 sekundach znika (jest w porządku), następnie czeka na przekroczenie limitu czasu, a następnie zgłaszany jest wyjątek System.InvalidOperationException: Proces musi zostać zakończony, aby można było określić żądane informacje.

& Zobacz scenariusz 4 poniżej

W twoim kodzie:

  1. Process.WaitForExit(ProcessTimeOutMiliseconds); Z tego czekasz Processna Timeout lub Wyjdź , które kiedykolwiek ma miejsce pierwszy .
  2. OutputWaitHandle.WaitOne(ProcessTimeOutMiliseconds)i errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds); dzięki temu czekasz na operację odczytu OutputDatai ErrorDataprzesyłania strumieniowego, aby zasygnalizować jej zakończenie
  3. Process.ExitCode == 0 Pobiera status procesu po jego wyjściu

Różne ustawienia i ich zastrzeżenia:

  • Scenariusz 1 (Happy Path) : Proces kończy się przed upływem limitu czasu, a zatem twój stdoutput i stderror również kończą się przed nim i wszystko jest w porządku.
  • Scenariusz 2 : Przekroczono limit czasu procesu, OutputWaitHandle i ErrorWaitHandle, jednak wartość stdoutput i stderror jest nadal odczytywana i kończy się po upływie limitu czasu WaitHandlers. To prowadzi do kolejnego wyjątkuObjectDisposedException()
  • Scenariusz 3 : Najpierw przekroczono limit czasu procesu (19 sekund), ale działa opóźnienie standardowe i standardowe, czekasz na przekroczenie limitu czasu WaitHandler (19 sekund), co powoduje dodatkowe opóźnienie o + 19 sekund.
  • Scenariusz 4 : Przekroczono limit czasu i próby przedwczesnego zapytania kodu Process.ExitCodepowodowały błąd System.InvalidOperationException: Process must exit before requested information can be determined.

Testowałem ten scenariusz kilkanaście razy i działa dobrze, podczas testowania użyto następujących ustawień

  • Rozmiar strumienia wyjściowego od 5 KB do 198 KB poprzez zainicjowanie budowy około 2–15 projektów
  • Przedwczesne limity czasu i wyjścia z procesu w oknie limitu czasu


Zaktualizowany kod

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

    //First waiting for ReadOperations to Timeout and then check Process to Timeout
    if (!outputWaitHandle.WaitOne(ProcessTimeOutMiliseconds) && !errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds)
        && !process.WaitForExit(ProcessTimeOutMiliseconds)  )
    {
        //To cancel the Read operation if the process is stil reading after the timeout this will prevent ObjectDisposeException
        process.CancelOutputRead();
        process.CancelErrorRead();

        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine("Timed Out");
        Logs = output + Environment.NewLine + error;
       //To release allocated resource for the Process
        process.Close();
        return  (false, logs);
    }

    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine("Completed On Time");
    Logs = output + Environment.NewLine + error;
    ExitCode = process.ExitCode.ToString();
    // Close frees the memory allocated to the exited process
    process.Close();

    //ExitCode now accessible
    return process.ExitCode == 0 ? (true, logs) : (false, logs);
    }
}
finally{}

EDYTOWAĆ:

Po wielu godzinach zabawy z MSBuild mogłem w końcu odtworzyć problem w moim systemie


Część B: Rekreacja i rozwiązanie problemu

MSBuild ma-m[:number]przełącznik, który służy do określania maksymalnej liczby równoczesnych procesów używanych podczas budowania.

Gdy ta opcja jest włączona, MSBuild odradza wiele węzłów, które żyją nawet po zakończeniu kompilacji. Teraz Process.WaitForExit(milliseconds)czekałbym nigdy nie wychodzić i ostatecznie przekroczył limit czasu

Byłem w stanie rozwiązać to na kilka sposobów

  • Odradzaj proces MSBuild pośrednio poprzez CMD

    $path1 = """C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\MSBuild.exe"" ""C:\Users\John\source\repos\Test\Test.sln"" -maxcpucount:3"
    $cmdOutput = cmd.exe /c $path1  '2>&1'
    $cmdOutput
    
  • Kontynuuj korzystanie z MSBuild, ale pamiętaj, aby ustawić wartość węzła Reuse na False

    $filepath = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\MSBuild.exe"
    $arg1 = "C:\Users\John\source\repos\Test\Test.sln"
    $arg2 = "-m:3"
    $arg3 = "-nr:False"
    
    Start-Process -FilePath $filepath -ArgumentList $arg1,$arg2,$arg3 -Wait -NoNewWindow
    
  • Nawet jeśli kompilacja równoległa nie jest włączona, nadal możesz uniemożliwić zawieszenie się procesu, WaitForExituruchamiając kompilację za pośrednictwem CMD, a zatem nie tworzysz bezpośredniej zależności od procesu kompilacji

    $path1 = """C:\....\15.0\Bin\MSBuild.exe"" ""C:\Users\John\source\Test.sln"""
    $cmdOutput = cmd.exe /c $path1  '2>&1'
    $cmdOutput
    

Preferowane jest drugie podejście, ponieważ nie chcesz, aby leżało zbyt wiele węzłów MSBuild.

Clint
źródło
Tak więc, jak powiedziałem powyżej, dzięki, "-nr:False","-m:3"wydaje się, że to poprawiło zachowanie zawieszania MSBuild, które Rx solutionsprawiło, że cały proces stał się w pewnym stopniu niezawodny (czas pokaże). Chciałbym zaakceptować obie odpowiedzi lub dać dwie nagrody
Joelty
@Joelty Właśnie próbowałem się dowiedzieć, czy Rxpodejście w innym rozwiązaniu jest w stanie rozwiązać problem bez zastosowania -nr:False" ,"-m:3". W moim rozumieniu radzi sobie z nieokreślonym oczekiwaniem od impasu i innymi rzeczami, które omówiłem w części 1. A przyczyną problemu w części 2 jest, jak sądzę, podstawowa przyczyna problemu, z którym się spotkałeś;) Mogę się mylić i dlatego Zapytałem, tylko czas pokaże ... Pozdrawiam !!
Clint
3

Problem polega na tym, że jeśli przekierujesz StandardOutput i / lub StandardError, bufor wewnętrzny może się zapełnić.

Aby rozwiązać wyżej wymienione problemy, możesz uruchomić proces w osobnych wątkach. Nie używam WaitForExit, wykorzystuję zdarzenie zakończenia procesu, które zwróci kod wyjścia procesu asynchronicznie, zapewniając jego zakończenie.

public async Task<int> RunProcessAsync(params string[] args)
    {
        try
        {
            var tcs = new TaskCompletionSource<int>();

            var process = new Process
            {
                StartInfo = {
                    FileName = 'file path',
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    Arguments = "shell command",
                    UseShellExecute = false,
                    CreateNoWindow = true
                },
                EnableRaisingEvents = true
            };


            process.Exited += (sender, args) =>
            {
                tcs.SetResult(process.ExitCode);
                process.Dispose();
            };

            process.Start();
            // Use asynchronous read operations on at least one of the streams.
            // Reading both streams synchronously would generate another deadlock.
            process.BeginOutputReadLine();
            string tmpErrorOut = await process.StandardError.ReadToEndAsync();
            //process.WaitForExit();


            return await tcs.Task;
        }
        catch (Exception ee) {
            Console.WriteLine(ee.Message);
        }
        return -1;
    }

Powyższy kod jest testowany w bitwie pod nazwą FFMPEG.exe z argumentami wiersza poleceń. Konwertowałem pliki mp4 do plików mp3 i robiłem ponad 1000 filmów naraz. Niestety nie mam bezpośredniego doświadczenia z powłoką mocy, ale mam nadzieję, że to pomoże.

Alex
źródło
Dziwny jest ten kod, podobnie jak inne rozwiązania, które zawiodły (utknęły) przy PIERWSZEJ próbie, a następnie wydawały się działać dobrze (podobnie jak w przypadku innych 5 prób, przetestuję go bardziej). Btw dlaczego wykonać BegingOutputReadline, a następnie wykonać ReadToEndAsyncna StandardError?
Joelty
OP już odczytuje asynchronicznie, więc jest mało prawdopodobne, aby zakleszczenie bufora konsoli było tutaj problemem.
yaakov
0

Nie jestem pewien, czy to jest twój problem, ale patrząc na MSDN wydaje się, że jest trochę dziwności z przeciążonym WaitForExit, gdy przekierowujesz wyjście asynchronicznie. Artykuł MSDN zaleca wywołanie WaitForExit, który nie przyjmuje żadnych argumentów po wywołaniu przeciążonej metody.

Strona Dokumentów znajduje się tutaj. Odpowiedni tekst:

Gdy standardowe dane wyjściowe zostaną przekierowane do asynchronicznych programów obsługi zdarzeń, możliwe jest, że przetwarzanie danych wyjściowych nie zostanie zakończone po powrocie tej metody. Aby upewnić się, że asynchroniczna obsługa zdarzeń została zakończona, wywołaj przeciążenie WaitForExit (), które nie przyjmuje parametru po otrzymaniu wartości true z tego przeciążenia. Aby upewnić się, że zdarzenie Exited jest poprawnie obsługiwane w aplikacjach Windows Forms, ustaw właściwość SynchronizingObject.

Modyfikacja kodu może wyglądać mniej więcej tak:

if (process.WaitForExit(ProcessTimeOutMiliseconds))
{
  process.WaitForExit();
}
Tyler Hundley
źródło
Istnieją pewne zawiłości z użyciem, process.WaitForExit()jak wskazano w komentarzach do tej odpowiedzi .
ingen