Процесс иногда зависает в ожидании выхода

13

Что может быть причиной того, что мой процесс завис в ожидании выхода?

Этот код должен запускать сценарий powershell, который внутри выполняет много действий, например, запускает перекомпиляцию кода через MSBuild, но, вероятно, проблема в том, что он генерирует слишком много выходных данных, и этот код застревает при ожидании выхода, даже после того, как сценарий Power Shell был выполнен правильно

это немного «странно», потому что иногда этот код работает нормально, а иногда просто застревает.

Код висит на:

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находится код Ингена

Joelty
источник
на какой строчке собственно процесс зависает? и ввести ваш код гораздо больше
познакомьтесь с Mr.AF
@ Mr.AF, ты прав - готово.
Джоэлти
1
Фактическое обращение к Powershell - это одно, а то, что вы НЕ предоставляете, - это фактически оставшаяся часть сценария, который вы пытаетесь обработать во время WITHIN Powershell. Вызов PowerShell сам по себе не проблема, но в том, что вы пытаетесь сделать. Отредактируйте ваш пост и введите явный вызов / команды, которые вы пытаетесь выполнить.
DRapp
1
Это действительно странно, я пытался воспроизвести ошибку. Это случилось случайно дважды по 20 попыток или что-то, и я не могу вызвать его снова.
KiKoS
1
@Joelty, ооо, круто интересно, вы говорите, что Rxподход работал (как и в нем не было тайм-аута) даже при случайном процессе MSBuild, ведущем к неопределенному ожиданию? интересно узнать, как с этим справились
Клинт

Ответы:

9

Давайте начнем с резюме принятого ответа в соответствующем посте.

Проблема в том, что при перенаправлении StandardOutput и / или StandardError внутренний буфер может заполниться. Какой бы порядок вы ни использовали, могут возникнуть проблемы:

  • Если вы дождетесь завершения процесса перед чтением StandardOutput, процесс может заблокировать попытку записи в него, поэтому процесс никогда не завершится.
  • Если вы читаете из StandardOutput с использованием ReadToEnd, тогда ваш процесс может заблокироваться, если процесс никогда не закроет StandardOutput (например, если он никогда не завершится или если он заблокирован, запись в StandardError).

Однако даже принятый ответ в определенных случаях борется с порядком исполнения.

РЕДАКТИРОВАТЬ: см. Ответы ниже, как избежать 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 к a CompositeDisposable(названному disposablesв примерах кода). Когда мы все закончим, мы можем завершить все подписки одним вызовомdisposables.Dispose() .

Безусловно, мы ничего не можем сделать с Rx, чего бы мы не смогли сделать с vanilla .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();

Одна из проблем с OP заключается в том, что предполагается, что он process.WaitForExit(processTimeOutMiliseconds)прервет процесс, когда закончится время ожидания. Из MSDN :

Поручает процесс компонента ждать указанное количество миллисекунд для связанного процесса выхода.

Вместо этого, по истечении времени ожидания, он просто возвращает управление текущему потоку (то есть прекращает блокировку). Вы должны вручную принудительно завершить завершение, когда процесс истекает. Чтобы узнать, когда истекло время ожидания, мы можем сопоставить 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;
    }
}
Ingen
источник
4
ИМХО, определенно стоит награды. Хороший ответ и хорошее введение в тему RX.
Кецалькоатль
Спасибо!!! Ваши ExecuteScriptRxручки hangsотлично. К сожалению, зависания все еще случаются, но я просто добавил небольшую обертку над вашей, ExecuteScriptRxкоторая работает, Retryа затем она работает нормально. Причиной зависания MSBUILD может быть ответ @Clint. PS: этот код заставил меня чувствовать себя глупо <lol> Это первый раз, когда я вижуSystem.Reactive.Linq;
Джоэлти
Код Оболочки в главном посте
Джоэлти
3

Для удобства читателей я собираюсь разделить это на 2 раздела

Раздел A: Проблема и как обращаться с подобными сценариями

Раздел B: Проблема отдыха и решения

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

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

& Смотрите сценарий 4 ниже

В вашем коде:

  1. Process.WaitForExit(ProcessTimeOutMiliseconds); С этим ты ждешь Process , чтобы тайм - аут или выход , который когда - либо происходит первым .
  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)бы ждать никогда не выходить и в конечном итоге тайм-аут

Я смог решить эту проблему несколькими способами

  • Spawn 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.

Clint
источник
Итак, как я сказал выше, спасибо, "-nr:False","-m:3"похоже, это исправило поведение зависания MSBuild, которое Rx solutionсделало весь процесс несколько надёжным (время покажет). Я хотел бы принять оба ответа или дать две награды
Джоэлти
@Joelty Я просто пытался узнать, может ли Rxподход в другом решении решить проблему без применения -nr:False" ,"-m:3". В моем понимании он обрабатывает неопределенное ожидание от взаимоблокировок и других вещей, которые я рассмотрел в разделе 1. И коренная причина в Разделе 2 - это то, что я считаю основной причиной проблемы, с которой вы столкнулись;) Я могу ошибаться, поэтому Я спросил, только время покажет ... Ура!
Клинт
3

Проблема в том, что при перенаправлении StandardOutput и / или StandardError внутренний буфер может заполниться.

Для решения вышеупомянутых проблем вы можете запустить процесс в отдельных потоках. Я не использую WaitForExit, я использую событие exited процесса, которое будет возвращать 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 видео одновременно, без сбоев. К сожалению, я не имею опыта работы с Power Shell, но надеюсь, что это поможет.

Alex
источник
Это странно, этот код, как и другие решения, потерпели неудачу (зависли) при ПЕРВОЙ попытке, а затем, казалось, работали нормально (как и другие 5 попыток, я буду проверять это больше). Кстати , почему вы выполняете , BegingOutputReadlineа затем выполнить ReadToEndAsyncна StandardError?
Джоэлти
OP уже читает асинхронно, поэтому маловероятно, что проблема в блокировке буфера консоли.
Яков
0

Не уверен, что это ваша проблема, но, глядя на MSDN, вы видите некоторую странность с перегруженным WaitForExit при асинхронном перенаправлении вывода. В статье MSDN рекомендуется вызывать WaitForExit, который не принимает аргументов после вызова перегруженного метода.

Страница документов находится здесь. Соответствующий текст:

Когда стандартный вывод был перенаправлен на асинхронные обработчики событий, возможно, что обработка вывода не будет завершена, когда этот метод вернется. Чтобы убедиться, что асинхронная обработка событий завершена, вызовите перегрузку WaitForExit (), которая не принимает параметров после получения истины от этой перегрузки. Чтобы обеспечить правильную обработку события Exited в приложениях Windows Forms, установите свойство SynchronizingObject.

Модификация кода может выглядеть примерно так:

if (process.WaitForExit(ProcessTimeOutMiliseconds))
{
  process.WaitForExit();
}
Тайлер Хандли
источник
Есть некоторые сложности с использованием, process.WaitForExit()как указано в комментариях к этому ответу .
ingen