Le processus se bloque parfois en attendant la sortie


13

Quelle peut être la raison du blocage de mon processus en attendant la sortie?

Ce code doit démarrer le script powershell qui, à l'intérieur, effectue de nombreuses actions, par exemple commencer à recompiler le code via MSBuild, mais le problème est probablement qu'il génère trop de sortie et ce code se bloque en attendant de quitter même après l'exécution correcte du script Power Shell.

c'est un peu "bizarre" parce que parfois ce code fonctionne bien et parfois il reste bloqué.

Le code se bloque à:

process.WaitForExit (ProcessTimeOutMiliseconds);

Le script Powershell s'exécute en 1 à 2 secondes, tandis que le délai d'expiration est de 19 secondes.

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

Scénario:

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

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

Éditer

À la solution de @ ingen, j'ai ajouté un petit wrapper qui réessaye d'exécuter le MS Build raccroché

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

InternalExecuteScriptest le code d'Ingen


à quelle ligne se bloque réellement le processus? et introduisez votre code beaucoup plus
Mr.AF

@ Mr.AF vous avez raison - terminé.
Joelty

1
L'appel réel de Powershell est une chose, mais ce que vous ne fournissez PAS, c'est le reste du script que vous essayez de traiter lorsque WITHIN Powershell. L'appel de PowerShell n'est pas le problème, mais dans ce que vous essayez de faire. Modifiez votre message et mettez l'appel / les commandes explicites que vous essayez d'exécuter.
DRapp

1
C'est vraiment bizarre d'avoir essayé de reproduire l'erreur. C'est arrivé deux fois au hasard sur 20 tentatives ou quelque chose et je ne peux pas le déclencher à nouveau.
KiKoS

1
@ Joelty, ohh cool intéressant, dites-vous que cette Rxapproche a fonctionné (car elle n'a pas dépassé le délai d'expiration) même avec un processus MSBuild errant qui mène à une attente indéfinie? intéressé à savoir comment cela a été géré
Clint

Réponses:


9

Commençons par un récapitulatif de la réponse acceptée dans un article connexe.

Le problème est que si vous redirigez StandardOutput et / ou StandardError, le tampon interne peut devenir plein. Quelle que soit la commande que vous utilisez, il peut y avoir un problème:

  • Si vous attendez que le processus se termine avant de lire StandardOutput, le processus peut bloquer toute tentative d'écriture, de sorte qu'il ne se termine jamais.
  • Si vous lisez à partir de StandardOutput à l'aide de ReadToEnd, votre processus peut bloquer si le processus ne ferme jamais StandardOutput (par exemple, s'il ne se termine jamais ou s'il est bloqué lors de l'écriture dans StandardError).

Cependant, même la réponse acceptée se heurte à l'ordre d'exécution dans certains cas.

EDIT: voir les réponses ci-dessous pour savoir comment éviter une exception ObjectDisposedException si le délai d'expiration se produit.

C'est dans ce genre de situations, où vous voulez orchestrer plusieurs événements, que Rx brille vraiment.

Notez que l'implémentation .NET de Rx est disponible en tant que package System.Reactive NuGet.

Plongeons-nous pour voir comment Rx facilite le travail avec les événements.

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

FromEventPatternnous permet de mapper des occurrences distinctes d'un événement à un flux unifié (aka observable). Cela nous permet de gérer les événements dans un pipeline (avec une sémantique de type LINQ). La Subscribesurcharge utilisée ici est fournie avec un Action<EventPattern<...>>et un Action<Exception>. Chaque fois que l'événement observé est déclenché, son senderet argssera enveloppé par EventPatternet poussé à travers le Action<EventPattern<...>>. Lorsqu'une exception est levée dans le pipeline, Action<Exception>est utilisée.

L'un des inconvénients du Eventmodèle, clairement illustré dans ce cas d'utilisation (et par toutes les solutions de contournement dans l'article référencé), est qu'il n'est pas évident quand / où désinscrire les gestionnaires d'événements.

Avec Rx, nous récupérons un IDisposablelorsque nous souscrivons. Lorsque nous nous en débarrassons, nous mettons effectivement fin à l'abonnement. Avec l'ajout de la DisposeWithméthode d'extension (empruntée à RxUI ), nous pouvons ajouter plusieurs IDisposables à un CompositeDisposable(nommé disposablesdans les exemples de code). Lorsque nous avons tous terminé, nous pouvons mettre fin à tous les abonnements en un seul appel à disposables.Dispose().

Pour être sûr, nous ne pouvons rien faire avec Rx, que nous ne pourrions pas faire avec vanilla .NET. Le code résultant est juste beaucoup plus facile à raisonner, une fois que vous vous êtes adapté à la manière fonctionnelle de penser.

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

Nous avons déjà discuté de la première partie, où nous mappons nos événements à des observables, afin que nous puissions passer directement à la partie charnue. Ici, nous affectons notre observable à la processExitedvariable, car nous voulons l'utiliser plus d'une fois.

Tout d'abord, lorsque nous l'activons, en appelant Subscribe. Et plus tard, quand nous voulons «attendre» sa première valeur.

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

L'un des problèmes avec OP est qu'il suppose qu'il process.WaitForExit(processTimeOutMiliseconds)mettra fin au processus lorsqu'il expirera. Depuis MSDN :

Demande au composant Process d'attendre le nombre de millisecondes spécifié pour que le processus associé se termine.

Au lieu de cela, lorsqu'il expire, il renvoie simplement le contrôle au thread actuel (c'est-à-dire qu'il arrête de bloquer). Vous devez forcer manuellement l'arrêt lorsque le processus arrive à expiration. Pour savoir quand le temps mort s'est produit, nous pouvons mapper l' Process.Exitedévénement à un processExitedobservable pour le traitement. De cette façon, nous pouvons préparer l'entrée pour l' Doopérateur.

Le code est assez explicite. Si exitedSuccessfullyle processus s'est terminé avec élégance. Sinon exitedSuccessfully, la résiliation devra être forcée. Notez qu'il process.Kill()est exécuté de manière asynchrone, ref remarques . Cependant, appeler process.WaitForExit()juste après ouvrira à nouveau la possibilité de blocages. Ainsi, même en cas d'arrêt forcé, il est préférable de laisser tous les produits jetables nettoyés à la usingfin de la portée, car la sortie peut être considérée comme interrompue / corrompue de toute façon.

La try catchconstruction est réservée au cas exceptionnel (sans jeu de mots) où vous vous êtes aligné processTimeOutMillisecondssur le temps réel nécessaire au processus pour terminer. En d'autres termes, une condition de concurrence critique se produit entre l' Process.Exitedévénement et le chronomètre. La possibilité que cela se produise est encore amplifiée par la nature asynchrone de process.Kill(). Je l'ai rencontré une fois lors des tests.


Pour être complet, la DisposeWithméthode d'extension.

/// <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
À mon humble avis, ça vaut vraiment la peine. Belle réponse et belle introduction sur le sujet à RX.
quetzalcoatl

Merci!!! Vos ExecuteScriptRxpoignées hangsparfaitement. Malheureusement, les blocages se produisent encore, mais je viens d'ajouter un petit wrapper sur votre ExecuteScriptRxqui fonctionne Retry, puis il s'exécute correctement. La raison du blocage de MSBUILD peut être la réponse @Clint. PS: Ce code m'a fait me sentir stupide <lol> C'est la première fois que je voisSystem.Reactive.Linq;
Joelty

Le code de Wrapper dans le post principal
Joelty

3

Pour le bénéfice des lecteurs, je vais diviser cela en 2 sections

Section A: Problème et comment gérer des scénarios similaires

Section B: Récréation du problème et solution

Section A: Problème

Lorsque ce problème se produit - le processus apparaît dans le gestionnaire de tâches, puis après 2-3 secondes disparaît (son amende), puis il attend le délai d'expiration et puis l'exception est levée System.InvalidOperationException: le processus doit se terminer avant que les informations demandées puissent être déterminées.

& Voir le scénario 4 ci-dessous

Dans votre code:

  1. Process.WaitForExit(ProcessTimeOutMiliseconds); Avec cela, vous attendez le délaiProcess d' expiration ou la sortie , qui a lieu en premier .
  2. OutputWaitHandle.WaitOne(ProcessTimeOutMiliseconds)et errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds); avec ce que vous attendez OutputDataet ErrorDataflux opération de lecture pour signaler sa complète
  3. Process.ExitCode == 0 Obtient le statut du processus à sa sortie

Différents paramètres et leurs mises en garde:

  • Scénario 1 (Happy Path) : le processus se termine avant la temporisation, et donc votre sortie standard et votre erreur de navigation se terminent également avant et tout va bien.
  • Scénario 2 : Délai d'attente des processus, OutputWaitHandle et ErrorWaitHandle, mais stdoutput et stderror sont toujours en cours de lecture et se terminent après l'expiration du délai d'attente WaitHandlers. Cela conduit à une autre exceptionObjectDisposedException()
  • Scénario 3 : Délai d'expiration du processus en premier (19 s) mais stdout et stderror sont en action, vous attendez que WaitHandler expire (19 s), provoquant un délai supplémentaire de + 19 s.
  • Scénario 4 : le processus expire et le code tente d'interroger prématurément, Process.ExitCodece qui entraîne l'erreur System.InvalidOperationException: Process must exit before requested information can be determined.

J'ai testé ce scénario plus d'une douzaine de fois et fonctionne correctement, les paramètres suivants ont été utilisés lors des tests

  • Taille du flux de sortie allant de 5 Ko à 198 Ko en lançant la construction d'environ 2 à 15 projets
  • Délais d'attente prématurés et sorties de processus dans la fenêtre de délai d'attente


Code mis à jour

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

ÉDITER:

Après des heures de jeu avec MSBuild, j'ai finalement pu reproduire le problème sur mon système


Section B: Récréation et solution du problème

MSBuild a un-m[:number]commutateur qui est utilisé pour spécifier le nombre maximum de processus simultanés à utiliser lors de la construction.

Lorsque cette option est activée, MSBuild génère un certain nombre de nœuds qui subsistent même après la fin de la génération. Maintenant, Process.WaitForExit(milliseconds)j'attendrais jamais de sortir et éventuellement de temporiser

J'ai pu résoudre cela de deux manières

  • Génération indirecte du processus MSBuild via 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
  • Continuez à utiliser MSBuild mais assurez-vous de définir le nodeReuse sur 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
  • Même si la construction parallèle n'est pas activée, vous pouvez toujours empêcher votre processus de se bloquer WaitForExiten lançant la construction via CMD et donc vous ne créez pas de dépendance directe sur le processus de construction

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

La 2ème approche est préférable car vous ne voulez pas que trop de nœuds MSBuild traînent.


Donc, comme je l'ai dit ci-dessus, merci, cela "-nr:False","-m:3"semble avoir corrigé le comportement de blocage de MSBuild, ce qui a Rx solutionrendu l'ensemble du processus assez fiable (le temps va s'afficher). Je souhaite pouvoir accepter les deux réponses ou donner deux primes
Joelty

@ Joelty J'essayais juste de savoir si l' Rxapproche de l'autre solution était en mesure de résoudre le problème sans postuler -nr:False" ,"-m:3". À ma connaissance, il gère l'attente indéfinie des blocages et d'autres choses que j'avais couvertes dans la section 1. Et la cause première de la section 2 est ce que je crois être la cause profonde du problème auquel vous avez été confronté;) Je peux me tromper, c'est pourquoi J'ai demandé, seul le temps nous le dira ... A bientôt !!
Clint

3

Le problème est que si vous redirigez StandardOutput et / ou StandardError, le tampon interne peut devenir plein.

Pour résoudre les problèmes susmentionnés, vous pouvez exécuter le processus dans des threads séparés. Je n'utilise pas WaitForExit, j'utilise l'événement exit de processus qui retournera le code de sortie du processus de manière asynchrone en s'assurant qu'il est terminé.

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

Le code ci-dessus est testé au combat en appelant FFMPEG.exe avec des arguments de ligne de commande. Je convertissais des fichiers mp4 en fichiers mp3 et faisais plus de 1000 vidéos à la fois sans échec. Malheureusement, je n'ai pas d'expérience directe sur le Power Shell, mais j'espère que cela vous aidera.


C'est bizarre ce code, de la même manière que d'autres solutions ont échoué (bloquées) lors de la PREMIÈRE tentative et ont ensuite semblé fonctionner correctement (comme les 5 autres tentatives, je le testerai plus). BTW pourquoi vous effectuez BegingOutputReadlinepuis effectuer ReadToEndAsyncsur StandardError?
Joelty

OP lit déjà de manière asynchrone, il est donc peu probable qu'un blocage sur le tampon de la console soit le problème ici.
yaakov

0

Je ne sais pas si c'est votre problème, mais en regardant MSDN, il semble y avoir une certaine bizarrerie avec WaitForExit surchargé lorsque vous redirigez la sortie de manière asynchrone. L'article MSDN recommande d'appeler WaitForExit qui ne prend aucun argument après avoir appelé la méthode surchargée.

Page de documents située ici. Texte pertinent:

Lorsque la sortie standard a été redirigée vers des gestionnaires d'événements asynchrones, il est possible que le traitement de sortie ne soit pas terminé lorsque cette méthode revient. Pour vous assurer que la gestion des événements asynchrones est terminée, appelez la surcharge WaitForExit () qui ne prend aucun paramètre après avoir reçu un vrai de cette surcharge. Pour vous assurer que l'événement Exited est géré correctement dans les applications Windows Forms, définissez la propriété SynchronizingObject.

La modification du code pourrait ressembler à ceci:

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

Il y a quelques subtilités avec l'utilisation de process.WaitForExit()comme indiqué par les commentaires de cette réponse .
ingen
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.