Процес іноді зависає під час очікування виходу


13

Що може бути причиною того, що мій процес висить під час очікування виходу?

Цей код повинен запустити скрипт powershell, який всередині виконує багато дій, наприклад, почати перекомпілювати код через MSBuild, але, ймовірно, проблема полягає в тому, що він генерує занадто багато виводу, і цей код застрягає, поки чекає виходу, навіть після того, як сценарій оболонки живлення виконаний правильно

це якось "дивно", тому що іноді цей код працює добре, а іноді просто застряє.

Код висить на:

process.WaitForExit (ProcessTimeOutMiliseconds);

Сценарій Powershell виконується приблизно як 1–2 сек., Тимчасовий час очікування - 19 секунд.

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

Сценарій:

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"

Редагувати

До рішення @ ingen я додав невелику обгортку, яка намагається виконати повішений 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;
}

Де InternalExecuteScriptкод інгена


на якій лінії насправді висить процес? і введіть свій код набагато більше
Mr.AF

@ Mr.AF ви маєте рацію.
Joelty

1
Фактичний виклик Powershell - це одне, однак те, що ви НЕ надаєте, - це фактична решта сценарію, який ви намагаєтеся обробити, в той час як в Powershell. Виклик самої повноважень - це не проблема, але всередині того, що ви намагаєтесь зробити. Відредагуйте свою публікацію та введіть явний виклик / команди, які ви намагаєтеся виконати.
DRapp

1
Це дійсно дивно, я намагався повторити помилку. Це траплялося випадковим чином двічі за 20 спроб чи щось, і я не в змозі його знову запустити.
KiKoS

1
@Joelty, ох цікаво цікаво, ти кажеш, що Rxпідхід спрацював (як і в ньому не вичерпався) навіть із бродячим процесом MSBuild, що лежить навколо, що веде до невизначеного очікування? цікаво дізнатися, як це вирішувалося
Клінт,

Відповіді:


9

Почнемо з резюме прийнятої відповіді у пов’язаному дописі.

Проблема полягає в тому, що якщо перенаправити StandardOutput та / або StandardError, внутрішній буфер може стати повним. Яке б замовлення ви не використовували, може виникнути проблема:

  • Якщо ви зачекаєте, поки процес вийде, перш ніж прочитати StandardOutput, процес може заблокувати спроби запису на нього, тому процес ніколи не закінчується.
  • Якщо ви читаєте з StandardOutput за допомогою ReadToEnd, ваш процес може блокуватися, якщо процес ніколи не закривається StandardOutput (наприклад, якщо він ніколи не припиняється, або якщо він блокується записом у StandardError).

Однак навіть прийнята відповідь бореться з порядком виконання в певних випадках.

EDIT: Дивіться відповіді нижче, як уникнути ObjectDisposedException, якщо настає час очікування.

Саме в таких ситуаціях, коли ви хочете організувати кілька подій, Rx справді світить.

Зауважте, що .NET реалізація Rx доступна у вигляді пакету System.Reactive NuGet.

Давайте зануримось, щоб побачити, як Rx полегшує роботу з подіями.

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

FromEventPatternдозволяє нам відобразити різні події події до єдиного потоку (він також можна спостерігати). Це дозволяє нам обробляти події в конвеєрі (з LINQ-подібною семантикою). SubscribeПеревантаження використовуються тут забезпечені Action<EventPattern<...>>і Action<Exception>. Щоразу, коли спостережувана подія піднімається, її senderі argsбуде обгортати EventPatternта проштовхувати через Action<EventPattern<...>>. Коли виняток піднімається в трубопроводі, Action<Exception>використовується.

Одним з недоліків Eventшаблону, чітко проілюстрованого в цьому випадку використання (і за всіма обхідними шляхами у посиланні), є те, що не видно, коли і де скасувати підписку обробників подій.

З Rx ми повертаємося назад, IDisposableколи робимо передплату. Коли ми розпоряджаємось цим, ми фактично закінчуємо підписку. Додавши DisposeWithметод розширення (запозичений у RxUI ), ми можемо додати декілька IDisposables до CompositeDisposable(названих disposablesу зразках коду). Коли ми все закінчимо, ми можемо закінчити всі підписки одним дзвінком на disposables.Dispose().

Безумовно, з Rx ми нічого не можемо зробити, що б нам не вдалося зробити з ванільним .NET. Отриманий код просто набагато простіше міркувати, коли ви адаптувались до функціонального способу мислення.

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

Ми вже обговорили першу частину, де відображаємо наші події на спостережувані, щоб ми могли перейти прямо до м'ясної частини. Тут ми призначаємо нашу спостережувану processExitedзмінну, оскільки ми хочемо її використовувати не один раз.

По-перше, коли ми його активуємо, зателефонувавши Subscribe. І пізніше, коли ми хочемо «чекати» його першого значення.

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

Однією з проблем з ОП є те, що він припускає, що він process.WaitForExit(processTimeOutMiliseconds)припинить процес, коли він вичерпається. Від MSDN :

Інструкція компонента Process чекати задану кількість мілісекунд для виходу пов'язаного процесу.

Натомість, коли він вичерпується, він просто повертає контроль поточному потоку (тобто він перестає блокувати). Вам потрібно вручну примусити припинення, коли процес закінчується. Щоб дізнатися, коли настав час вичерпання, ми можемо відобразити Process.Exitedподію до processExitedспостережуваної для обробки. Таким чином ми можемо підготувати дані для Doоператора.

Код досить зрозумілий. Якщо exitedSuccessfullyпроцес закінчиться витончено. Якщо ні exitedSuccessfully, припинення потрібно буде примусити. Зверніть увагу , що process.Kill()виконується асинхронно, реф зауваження . Однак виклик process.WaitForExit()відразу після цього відкриє можливість для тупикових ситуацій знову. Тож навіть у разі примусового припинення, краще дозволити очищенню всіх одноразових матеріалів, коли usingобласть закінчується, оскільки вихід може бути вважаний перерваним / пошкодженим у будь-якому випадку.

try catchКонструкція зарезервований для виняткового випадку (НЕ каламбур) , де ви вирівняні processTimeOutMillisecondsз реальним часом , необхідним в процесі завершення. Іншими словами, між Process.Exitedподією та таймером відбувається умова гонки . Можливість цього трапляється знову посилюється асинхронним характером process.Kill(). Я стикався з ним один раз під час тестування.


Для повноти DisposeWithметод розширення.

/// <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
ІМХО, безумовно, варто вигравати. Приємна відповідь та приємне вступне в тему RX.
quetzalcoatl

Дякую!!! Ваші ExecuteScriptRxручки hangsідеально. До нещасних випадків висячі все ще трапляються, але я просто додав маленьку обгортку над вашою, ExecuteScriptRxяка виконує, Retryі потім це добре виконує. Причиною висівок MSBUILD може бути відповідь @Clint. PS: Цей код змусив мене почуватися дурним <lol> Це вперше я бачуSystem.Reactive.Linq;
Джоелті

Код Wrapper в головному дописі
Joelty

3

Для користі читачів я поділю це на 2 розділи

Розділ A: Проблема та способи поводження з подібними сценаріями

Розділ B: Проблеми відпочинку та вирішення

Розділ А: Проблема

Коли ця проблема трапляється - процес з'являється в диспетчері завдань, після 2-3сек зникає (його штраф), потім він чекає таймауту, а потім викидається виняток System.InvalidOperationException: Процес повинен вийти, перш ніж запитувана інформація може бути визначена.

& Дивіться сценарій 4 нижче

У вашому коді:

  1. Process.WaitForExit(ProcessTimeOutMiliseconds); З цим ви чекаєте Processна Timeout або Exit , який коли-небудь відбудеться першим .
  2. OutputWaitHandle.WaitOne(ProcessTimeOutMiliseconds)і errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds); з цим ви чекаєте OutputDataі ErrorDataпотоку операцій зчитування сигналу його повної
  3. Process.ExitCode == 0 Отримує статус процесу під час його виходу

Різні параметри та їх застереження:

  • Сценарій 1 (Щасливий шлях) : процес завершується до тайм-ауту, і, таким чином, ваш stdoutput та stderror також закінчуються перед цим, і все добре.
  • Сценарій 2 : Тайм-аут процесу, OutputWaitHandle та ErrorWaitHandle, однак stdoutput та stderror все ще читаються та завершуються після закінчення часу WaitHandlers. Це призводить до іншого виняткуObjectDisposedException()
  • Сценарій 3 : Перший тайм-аут обробки (19 сек), але stdout та stderror в дії, ви чекаєте, поки WaitHandler вимкне час (19 сек), що спричинить додаткову затримку + 19 сек.
  • Сценарій 4 : Опрацюйте тайм-аути та спроби коду передчасного запиту, що Process.ExitCodeпризводить до помилки System.InvalidOperationException: Process must exit before requested information can be determined.

Я перевіряв цей сценарій вже десяток разів і працює чудово, наступні налаштування були використані під час тестування

  • Розмір вихідного потоку - від 5 КБ до 198 КБ за рахунок ініціювання створення приблизно 2-15 проектів
  • Передчасні очікування та процес закінчуються у вікні очікування


Оновлений код

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

Редагувати:

Після годинних розмов з MSBuild я нарешті зміг відтворити проблему в моїй системі


Розділ B: Відпочинок та вирішення проблем

MSBuild має-m[:number]перемикач, який використовується для визначення максимальної кількості одночасних процесів, які слід використовувати при побудові.

Коли це ввімкнено, MSBuild породжує ряд вузлів, які існують навіть після завершення збірки. Тепер, я Process.WaitForExit(milliseconds)б не зачекав, що ніколи не вийде і, зрештою, очікується час

Мені вдалося вирішити це двома способами

  • Нерестовий MSBuild процес опосередковано здійснюється через 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
  • Продовжуйте використовувати MSBuild, але обов'язково встановіть для параметра nodeReuse значення 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
  • Навіть якщо паралельна збірка не ввімкнена, ви все одно можете запобігти зависанню вашого процесу WaitForExit, запустивши збірку за допомогою CMD, і тому ви не створюєте прямої залежності від процесу збирання

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

Другий підхід є кращим, оскільки ви не хочете, щоб занадто багато вузлів MSBuild лежало навколо.


Отже, як я вже говорив вище, спасибі, "-nr:False","-m:3"схоже , це зафіксувало поведінку MSBuild притулок, що Rx solutionзробило весь процес дещо надійним (час покаже). Я б хотів, щоб я міг прийняти обидві відповіді або дати два
подяки

@Joelty Я просто намагався знати, чи Rxпідхід в іншому рішенні здатний вирішити проблему без застосування -nr:False" ,"-m:3". Наскільки я розумію, він вирішує нескінченне очікування від тупиків та інших речей, які я висвітлював у розділі 1. І першопричиною в розділі 2 є те, що я вважаю, що є першопричиною проблеми, з якою ви стикалися;) Я можу помилятися, тому Я запитав, тільки час підкаже ... Ура !!
Клінт

3

Проблема полягає в тому, що якщо перенаправити StandardOutput та / або StandardError, внутрішній буфер може стати повним.

Щоб вирішити вищезазначені проблеми, ви можете запустити процес окремими потоками. Я не використовую WaitForExit, я використовую подія, що вийшов із процесу, що поверне ExitCode процесу асинхронно, забезпечивши його завершення.

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

Вищевказаний код перевіряється бойовим викликом FFMPEG.exe з аргументами командного рядка. Я перетворював mp4 файли в mp3-файли і робив одночасно понад 1000 відео, не виходячи з ладу. На жаль, я не маю прямого досвіду роботи живлення, але сподіваюся, що це допомагає.


Це дивний цей код, як і інші рішення не вдалося (застряг) у першій спробі, а потім, здається, працює нормально (як і інші 5 спроб, я перевіряю його більше). Btw, чому ти виступаєш, BegingOutputReadlineа потім виступаєш ReadToEndAsyncдалі StandardError?
Joelty

OP вже читає асинхронно, тому навряд чи проблема в тупіку консольного буфера.
yakov

0

Не впевнений, чи це ваша проблема, але дивлячись на MSDN, здається, є деяка дивність із перевантаженим WaitForExit, коли ви перенаправляєте вихід асинхронно. Стаття MSDN рекомендує викликати WaitForExit, який не приймає аргументів після виклику перевантаженого методу.

Сторінка Документів, розташована тут. Відповідний текст:

Коли стандартний вихід був перенаправлений на асинхронні обробники подій, можливо, обробка виводу не буде завершена, коли цей метод повернеться. Щоб переконатися, що обробка асинхронних подій завершена, викликайте перевантаження WaitForExit (), яка не приймає жодного параметра після отримання істинної від цієї перевантаження. Щоб переконатися, що подія Exited правильно обробляється у програмах Windows Forms, встановіть властивість SynchroisingObject.

Модифікація коду може виглядати приблизно так:

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

Є деякі тонкощі використання, process.WaitForExit()як зазначено в коментарях до цієї відповіді .
ingen
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.