Il processo a volte si blocca durante l'attesa di Exit


13

Quale può essere la ragione del mio processo sospeso in attesa dell'uscita?

Questo codice deve avviare lo script PowerShell che al suo interno esegue molte azioni, ad esempio avvia la ricompilazione del codice tramite MSBuild, ma probabilmente il problema è che genera troppo output e questo codice si blocca in attesa di uscire anche dopo che lo script Power Shell è stato eseguito correttamente

è un po '"strano" perché a volte questo codice funziona bene e talvolta si blocca.

Il codice si blocca su:

process.WaitForExit (ProcessTimeOutMiliseconds);

Lo script Powershell viene eseguito in 1-2 secondi, mentre il timeout è 19 secondi.

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

script:

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

dove

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

modificare

Alla soluzione di @ ingen ho aggiunto un piccolo wrapper che tenta di eseguire Hang Build di Hang

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

Dov'è InternalExecuteScriptil codice di ingen


a quale linea si blocca effettivamente il processo? e intro il tuo codice molto di più
Mr.AF

@ Mr.AF hai ragione.
Joelty,

1
La chiamata effettiva di Powershell è una cosa, tuttavia ciò che NON si fornisce è il resto effettivo dello script che si sta tentando di elaborare mentre ENTRO Powershell. Chiamare PowerShell stesso non è il problema, ma all'interno di ciò che si sta tentando di fare. Modifica il tuo post e metti la chiamata / i comandi espliciti che stai tentando di eseguire.
DRapp,

1
È davvero strano, ho provato a replicare l'errore. È successo casualmente due volte su 20 tentativi o qualcosa del genere e non sono in grado di attivarlo di nuovo.
KiKoS,

1
@Joelty, ohh interessante interessante, stai dicendo che l' Rxapproccio ha funzionato (come in esso non è scaduto) anche con il processo vagante MSBuild in giro che porta ad un'attesa indefinita? interessato a sapere come è stato gestito
Clint

Risposte:


9

Cominciamo con un riepilogo della risposta accettata in un post correlato.

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

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

Anche la risposta accettata, tuttavia, lotta con l'ordine di esecuzione in alcuni casi.

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

È in questo tipo di situazioni, in cui si desidera orchestrare diversi eventi, che Rx brilla davvero.

Si noti che l'implementazione .NET di Rx è disponibile come pacchetto NuGet System.Reactive.

Immergiamoci per vedere come Rx facilita il lavoro con gli eventi.

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

FromEventPatternci consente di mappare occorrenze distinte di un evento su un flusso unificato (noto anche come osservabile). Questo ci consente di gestire gli eventi in una pipeline (con semantica simile a LINQ). Il Subscribesovraccarico utilizzato qui è fornito con un Action<EventPattern<...>>e un Action<Exception>. Ogni volta che l'evento osservato viene generato, il suo sendere argssarà avvolto EventPatterne spinto attraverso Action<EventPattern<...>>. Quando viene generata un'eccezione nella pipeline, Action<Exception>viene utilizzata.

Uno degli svantaggi del Eventmodello, chiaramente illustrato in questo caso d'uso (e da tutte le soluzioni alternative nel post di riferimento), è che non è chiaro quando / dove annullare la sottoscrizione dei gestori di eventi.

Con Rx riceviamo una risposta IDisposablequando facciamo un abbonamento. Quando lo eliminiamo, terminiamo effettivamente l'abbonamento. Con l'aggiunta del DisposeWithmetodo di estensione (preso in prestito da RxUI ), possiamo aggiungere più IDisposables a un CompositeDisposable(indicato disposablesnegli esempi di codice). Quando abbiamo finito, possiamo terminare tutti gli abbonamenti con una chiamata a disposables.Dispose().

A dire il vero, non c'è niente che possiamo fare con Rx, che non saremmo in grado di fare con vanilla .NET. Il codice risultante è molto più facile da ragionare, una volta che ti sei adattato al modo di pensare funzionale.

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

Abbiamo già discusso della prima parte, in cui mappiamo i nostri eventi su osservabili, in modo da poter passare direttamente alla parte carnosa. Qui assegniamo il nostro osservabile alla processExitedvariabile, perché vogliamo usarlo più di una volta.

Innanzitutto, quando lo attiviamo, chiamando Subscribe. E più tardi, quando vogliamo "attendere" il suo primo valore.

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

Uno dei problemi con OP è che presuppone process.WaitForExit(processTimeOutMiliseconds)che terminerà il processo quando scade. Da MSDN :

Indica al componente Processo di attendere il numero specificato di millisecondi per la chiusura del processo associato.

Al contrario, quando scade, restituisce semplicemente il controllo al thread corrente (ovvero interrompe il blocco). È necessario forzare manualmente la chiusura al termine del processo. Per sapere quando si è verificato il timeout, possiamo mappare l' Process.Exitedevento a un processExitedosservabile per l'elaborazione. In questo modo possiamo preparare l'input per l' Dooperatore.

Il codice è piuttosto autoesplicativo. Se exitedSuccessfullyil processo sarà terminato con grazia. In caso contrario exitedSuccessfully, la risoluzione dovrà essere forzata. Si noti che process.Kill()viene eseguito in modo asincrono, osserva ref . Tuttavia, chiamare process.WaitForExit()subito dopo aprirà nuovamente la possibilità di deadlock. Quindi, anche in caso di terminazione forzata, è meglio lasciare che tutti i prodotti monouso vengano ripuliti al termine usingdell'ambito, poiché l'output può essere considerato comunque interrotto / corrotto.

Il try catchcostrutto è riservato al caso eccezionale (nessun gioco di parole previsto) in cui ti sei allineato processTimeOutMillisecondscon il tempo effettivo necessario al completamento del processo. In altre parole, si verifica una condizione di competizione tra l' Process.Exitedevento e il timer. La possibilità che ciò accada è di nuovo amplificata dalla natura asincrona di process.Kill(). L'ho incontrato una volta durante i test.


Per completezza, il DisposeWithmetodo di estensione.

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

4
IMHO, sicuramente vale la taglia. Bella risposta e bella introduzione sull'argomento di RX.
quetzalcoatl,

Grazie!!! Le tue ExecuteScriptRxmaniglie hangsperfettamente. Sfortunatamente si verificano ancora blocchi, ma ho appena aggiunto un piccolo wrapper al tuo ExecuteScriptRxche si esibisce Retrye poi si esegue bene. Il motivo di blocco di MSBUILD potrebbe essere la risposta @Clint. PS: Quel codice mi ha fatto sentire stupido <lol> Questa è la prima volta che vedoSystem.Reactive.Linq;
Joelty il

Il codice di Wrapper è nel post principale
Joelty

3

A beneficio dei lettori, lo dividerò in 2 sezioni

Sezione A: Problema e come gestire scenari simili

Sezione B: Ricreazione e soluzione dei problemi

Sezione A: Problema

Quando si verifica questo problema: il processo viene visualizzato nel task manager, quindi dopo 2-3 secondi scompare (va bene), quindi attende il timeout e quindi viene generata l'eccezione System.InvalidOperationException: Il processo deve terminare prima di poter determinare le informazioni richieste.

E vedi lo scenario 4 di seguito

Nel tuo codice:

  1. Process.WaitForExit(ProcessTimeOutMiliseconds); Con questo si sta aspettando Processal timeout o di uscita , che prende mai posto prima .
  2. OutputWaitHandle.WaitOne(ProcessTimeOutMiliseconds)e errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds); con questo stai aspettando OutputData& ErrorDatastream read read per segnalarne il completamento
  3. Process.ExitCode == 0 Ottiene lo stato del processo quando è uscito

Impostazioni diverse e loro avvertenze:

  • Scenario 1 (Happy Path) : il processo si completa prima del timeout e quindi anche il tuo stdoutput e stderror terminano prima di tutto e tutto va bene.
  • Scenario 2 : Process, OutputWaitHandle & ErrorWaitHandle timeout tuttavia stdoutput e stderror vengono ancora letti e completati dopo il timeout WaitHandlers. Questo porta ad un'altra eccezioneObjectDisposedException()
  • Scenario 3 : prima il timeout del processo (19 sec) ma stdout e stderror sono in azione, si attende il timeout di WaitHandler (19 sec), causando un ulteriore ritardo di + 19sec.
  • Scenario 4 : timeout del processo e il codice tenta di eseguire una query prematuramente Process.ExitCodecon conseguente errore System.InvalidOperationException: Process must exit before requested information can be determined.

Ho testato questo scenario più di una dozzina di volte e funziona bene, durante i test sono state utilizzate le seguenti impostazioni

  • Le dimensioni del flusso di output vanno da 5 KB a 198 KB avviando la creazione di circa 2-15 progetti
  • Timeout prematuri ed uscite di processo all'interno della finestra di timeout


Codice aggiornato

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

MODIFICARE:

Dopo ore di gioco con MSBuild sono stato finalmente in grado di riprodurre il problema sul mio sistema


Sezione B: Problem Recreation & Solution

MSBuild ha un-m[:number]interruttore che viene utilizzato per specificare il numero massimo di processi simultanei da utilizzare durante la creazione.

Quando è abilitato, MSBuild genera un numero di nodi che sopravvive anche dopo il completamento della compilazione. Ora, Process.WaitForExit(milliseconds)aspetterebbe mai uscire e alla fine il timeout

Sono stato in grado di risolverlo in un paio di modi

  • Genera il processo MSBuild indirettamente tramite 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
  • Continuare a utilizzare MSBuild ma assicurarsi di impostare nodeReuse su 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
  • Anche se la compilazione parallela non è abilitata, è comunque possibile impedire il blocco del processo WaitForExitavviando Build via CMD e pertanto non si crea una dipendenza diretta dal processo Build

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

Il secondo approccio è preferito poiché non si desidera che ci siano troppi nodi MSBuild in giro.


Quindi, come ho detto sopra, grazie, questo "-nr:False","-m:3"sembra aver corretto il comportamento di MSBuild, che ha Rx solutionreso l'intero processo in qualche modo affidabile (il tempo lo mostrerà). Vorrei poter accettare entrambe le risposte o dare due
taglie

@Joelty Stavo solo cercando di sapere se l' Rxapproccio nell'altra soluzione è in grado di risolvere il problema senza applicare -nr:False" ,"-m:3". Nella mia comprensione gestisce l'attesa indefinita da deadlock e altre cose che avrei trattato nella sezione 1. E la causa principale nella Sezione 2 è quella che credo sia la causa principale del problema che hai affrontato;) Potrei sbagliarmi, motivo per cui Ho chiesto, solo il tempo lo dirà ... Saluti !!
Clint

3

Il problema è che se si reindirizza StandardOutput e / o StandardError il buffer interno può diventare pieno.

Per risolvere i problemi di cui sopra, è possibile eseguire il processo in thread separati. Non utilizzo WaitForExit, utilizzo l'evento di processo uscito che restituirà il ExitCode del processo in modo asincrono assicurandone il completamento.

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

Il codice sopra è testato in battaglia chiamando FFMPEG.exe con argomenti della riga di comando. Stavo convertendo i file mp4 in file mp3 e facendo oltre 1000 video alla volta senza fallire. Sfortunatamente non ho esperienza diretta di power shell ma spero che questo aiuti.


È strano questo codice, in modo simile ad altre soluzioni fallite (bloccate) al PRIMO tentativo e poi sembrava funzionare bene (come altri 5 tentativi, lo testerò di più). Btw perché si esegue BegingOutputReadlinee quindi eseguire ReadToEndAsyncsu StandardError?
Joelty,

OP sta già leggendo in modo asincrono, quindi è improbabile che un deadlock sul buffer della console sia il problema qui.
Yaakov,

0

Non sono sicuro se questo è il tuo problema, ma guardando MSDN sembra esserci qualche stranezza con WaitForExit sovraccarico quando si reindirizza l'output in modo asincrono. L'articolo MSDN consiglia di chiamare WaitForExit che non accetta argomenti dopo aver chiamato il metodo sovraccaricato.

Pagina dei documenti situata qui. Testo pertinente:

Quando l'output standard è stato reindirizzato a gestori di eventi asincroni, è possibile che l'elaborazione dell'output non sia stata completata quando viene restituito questo metodo. Per assicurarsi che la gestione degli eventi asincroni sia stata completata, chiamare il sovraccarico WaitForExit () che non accetta alcun parametro dopo aver ricevuto un vero da questo sovraccarico. Per garantire che l'evento Exited sia gestito correttamente nelle applicazioni Windows Form, impostare la proprietà SynchronizingObject.

La modifica del codice potrebbe essere simile a questa:

if (process.WaitForExit(ProcessTimeOutMiliseconds))
{
  process.WaitForExit();
}

Ci sono alcune complessità con l'uso di process.WaitForExit()come indicato dai commenti a questa risposta .
ingen
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.