ProcessStartInfo висит на «WaitForExit»? Зачем?

187

У меня есть следующий код:

info = new System.Diagnostics.ProcessStartInfo("TheProgram.exe", String.Join(" ", args));
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
System.Diagnostics.Process p = System.Diagnostics.Process.Start(info);
p.WaitForExit();
Console.WriteLine(p.StandardOutput.ReadToEnd()); //need the StandardOutput contents

Я знаю, что вывод от запускаемого мной процесса составляет около 7 МБ. Запуск его в консоли Windows работает нормально. К сожалению, программно это висит неопределенно в WaitForExit. Также обратите внимание, что код НЕ зависает для небольших выходов (например, 3 КБ).

Возможно ли, что внутренний StandardOutput в ProcessStartInfo не может буферизовать 7 МБ? Если так, что я должен сделать вместо этого? Если нет, что я делаю не так?

Epaga
источник
какое-нибудь окончательное решение с полным исходным кодом об этом?
Kiquenet
2
Я столкнулся с той же проблемой, и вот так я смог ее решить stackoverflow.com/questions/2285288/…
Bedasso
6
Да, окончательное решение: поменяйте местами последние две строки. Это в руководстве .
Амит Найду
4
from msdn: пример кода позволяет избежать условия взаимоблокировки путем вызова p.StandardOutput.ReadToEnd перед p.WaitForExit. Условие взаимоблокировки может возникнуть, если родительский процесс вызывает p.WaitForExit перед p.StandardOutput.ReadToEnd и дочерний процесс записывает достаточно текста, чтобы заполнить перенаправленный поток. Родительский процесс будет бесконечно ждать завершения дочернего процесса. Дочерний процесс будет бесконечно ждать, пока родитель прочитает полный поток StandardOutput.
Карлос Лю
Это немного раздражает, как сложно сделать это правильно. Был рад обойти это с более простыми перенаправлениями командной строки> outputfile :)
eglasius

Ответы:

393

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

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

Решение состоит в том, чтобы использовать асинхронное чтение, чтобы гарантировать, что буфер не переполняется. Для того, чтобы избежать каких - либо тупики и собрать всю выход из обоих StandardOutputиStandardError вы можете сделать это:

РЕДАКТИРОВАТЬ: См. Ответы ниже, чтобы узнать, как избежать исключения ObjectDisposedException, если истекло время ожидания.

using (Process process = new Process())
{
    process.StartInfo.FileName = filename;
    process.StartInfo.Arguments = arguments;
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;

    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();

    using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
    using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
    {
        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();

        if (process.WaitForExit(timeout) &&
            outputWaitHandle.WaitOne(timeout) &&
            errorWaitHandle.WaitOne(timeout))
        {
            // Process completed. Check process.ExitCode here.
        }
        else
        {
            // Timed out.
        }
    }
}
Марк Байерс
источник
11
Не знал, что перенаправление вывода было причиной проблемы, но, конечно, это было так. Потратил 4 часа, стуча головой об этом, и исправил это через 5 минут после прочтения твоего поста. Хорошая работа!
Бен Грипка
1
@AlexPeck Проблема заключалась в запуске этого приложения в виде консоли. Ханс Пассант определил проблему здесь: stackoverflow.com/a/16218470/279516
Боб Хорн,
5
каждый раз, когда командная строка закрывается, появляется следующее: необработанное исключение типа «System.ObjectDisposed» возникало в mscorlib.dll Дополнительная информация: безопасный дескриптор был закрыт
user1663380
3
У нас была похожая проблема, описанная @ user1663380 выше. Как вы думаете , что это возможно , что usingзаявления для обработчиков событий должны быть выше в usingзаявлении для самого процесса?
Дэн Форбс
2
Я не думаю, что ручки ожидания необходимы. В соответствии с msdn, просто завершите работу с нережимной версией WaitForExit: когда стандартный вывод был перенаправлен на асинхронные обработчики событий, возможно, что обработка вывода не будет завершена, когда этот метод вернется. Чтобы убедиться, что асинхронная обработка событий завершена, вызовите перегрузку WaitForExit (), которая не принимает параметров после получения истины от этой перегрузки.
Патрик
98

Документация для Process.StandardOutputговорит прочитать , прежде чем ждать , в противном случае вы можете тупиковой, фрагмент кода скопирован ниже:

 // Start the child process.
 Process p = new Process();
 // Redirect the output stream of the child process.
 p.StartInfo.UseShellExecute = false;
 p.StartInfo.RedirectStandardOutput = true;
 p.StartInfo.FileName = "Write500Lines.exe";
 p.Start();
 // Do not wait for the child process to exit before
 // reading to the end of its redirected stream.
 // p.WaitForExit();
 // Read the output stream first and then wait.
 string output = p.StandardOutput.ReadToEnd();
 p.WaitForExit();
обкрадывать
источник
14
Я не уверен на 100%, является ли это результатом моей среды, но я обнаружил, что если вы установили RedirectStandardOutput = true;и не используете, p.StandardOutput.ReadToEnd();вы получаете тупик / зависание.
Крис С
3
Правда. Я был в похожей ситуации. Я без причины перенаправлял StandardError, когда конвертировал с ffmpeg в процесс, он записывал достаточно в потоке StandardError, чтобы создать тупик.
Леон Пеллетье
Это все еще зависает для меня даже при перенаправлении и чтении стандартного вывода.
user3791372
@ user3791372 Я думаю, это применимо, только если буфер за StandardOutput заполнен не полностью. Здесь MSDN не делает свое дело. Отличная статья, которую я бы рекомендовал вам прочитать, находится по адресу: dzone.com/articles/async-io-and-threadpool
Кэри,
19

Ответ Марка Байерса отличный, но я бы добавил следующее:

OutputDataReceivedИ ErrorDataReceivedделегаты должны быть удалены до того , как outputWaitHandleи errorWaitHandleполучить расположены. Если процесс продолжает выводить данные после того, как тайм - аут был превышен , а затем завершается, outputWaitHandleи errorWaitHandleпеременные будут доступны после того , как расположены.

(К вашему сведению, я должен был добавить это предостережение в качестве ответа, поскольку не мог прокомментировать его сообщение.)

stevejay
источник
2
Возможно, было бы лучше вызвать CancelOutputRead ?
Марк Байерс
Добавление отредактированного кода Марка к этому ответу было бы довольно здорово! У меня точно такая же проблема в минуту.
13
8
@ianbailey Самый простой способ решить эту проблему - поместить использование (Процесс р ...) в использование (AutoResetEvent errorWaitHandle ...)
Дидье А.
18

Это более современное ожидаемое решение на основе параллельной библиотеки задач (TPL) для .NET 4.5 и выше.

Пример использования

try
{
    var exitCode = await StartProcess(
        "dotnet", 
        "--version", 
        @"C:\",
        10000, 
        Console.Out, 
        Console.Out);
    Console.WriteLine($"Process Exited with Exit Code {exitCode}!");
}
catch (TaskCanceledException)
{
    Console.WriteLine("Process Timed Out!");
}

Реализация

public static async Task<int> StartProcess(
    string filename,
    string arguments,
    string workingDirectory= null,
    int? timeout = null,
    TextWriter outputTextWriter = null,
    TextWriter errorTextWriter = null)
{
    using (var process = new Process()
    {
        StartInfo = new ProcessStartInfo()
        {
            CreateNoWindow = true,
            Arguments = arguments,
            FileName = filename,
            RedirectStandardOutput = outputTextWriter != null,
            RedirectStandardError = errorTextWriter != null,
            UseShellExecute = false,
            WorkingDirectory = workingDirectory
        }
    })
    {
        var cancellationTokenSource = timeout.HasValue ?
            new CancellationTokenSource(timeout.Value) :
            new CancellationTokenSource();

        process.Start();

        var tasks = new List<Task>(3) { process.WaitForExitAsync(cancellationTokenSource.Token) };
        if (outputTextWriter != null)
        {
            tasks.Add(ReadAsync(
                x =>
                {
                    process.OutputDataReceived += x;
                    process.BeginOutputReadLine();
                },
                x => process.OutputDataReceived -= x,
                outputTextWriter,
                cancellationTokenSource.Token));
        }

        if (errorTextWriter != null)
        {
            tasks.Add(ReadAsync(
                x =>
                {
                    process.ErrorDataReceived += x;
                    process.BeginErrorReadLine();
                },
                x => process.ErrorDataReceived -= x,
                errorTextWriter,
                cancellationTokenSource.Token));
        }

        await Task.WhenAll(tasks);
        return process.ExitCode;
    }
}

/// <summary>
/// Waits asynchronously for the process to exit.
/// </summary>
/// <param name="process">The process to wait for cancellation.</param>
/// <param name="cancellationToken">A cancellation token. If invoked, the task will return
/// immediately as cancelled.</param>
/// <returns>A Task representing waiting for the process to end.</returns>
public static Task WaitForExitAsync(
    this Process process,
    CancellationToken cancellationToken = default(CancellationToken))
{
    process.EnableRaisingEvents = true;

    var taskCompletionSource = new TaskCompletionSource<object>();

    EventHandler handler = null;
    handler = (sender, args) =>
    {
        process.Exited -= handler;
        taskCompletionSource.TrySetResult(null);
    };
    process.Exited += handler;

    if (cancellationToken != default(CancellationToken))
    {
        cancellationToken.Register(
            () =>
            {
                process.Exited -= handler;
                taskCompletionSource.TrySetCanceled();
            });
    }

    return taskCompletionSource.Task;
}

/// <summary>
/// Reads the data from the specified data recieved event and writes it to the
/// <paramref name="textWriter"/>.
/// </summary>
/// <param name="addHandler">Adds the event handler.</param>
/// <param name="removeHandler">Removes the event handler.</param>
/// <param name="textWriter">The text writer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static Task ReadAsync(
    this Action<DataReceivedEventHandler> addHandler,
    Action<DataReceivedEventHandler> removeHandler,
    TextWriter textWriter,
    CancellationToken cancellationToken = default(CancellationToken))
{
    var taskCompletionSource = new TaskCompletionSource<object>();

    DataReceivedEventHandler handler = null;
    handler = new DataReceivedEventHandler(
        (sender, e) =>
        {
            if (e.Data == null)
            {
                removeHandler(handler);
                taskCompletionSource.TrySetResult(null);
            }
            else
            {
                textWriter.WriteLine(e.Data);
            }
        });

    addHandler(handler);

    if (cancellationToken != default(CancellationToken))
    {
        cancellationToken.Register(
            () =>
            {
                removeHandler(handler);
                taskCompletionSource.TrySetCanceled();
            });
    }

    return taskCompletionSource.Task;
}
Мухаммед Рехан Саид
источник
2
лучший и самый полный ответ на сегодняшний день
TermoTux
1
По некоторым причинам, это было единственное решение, которое работало для меня, приложение перестало зависать.
Джек,
1
Кажется, вы не обрабатываете условие, когда процесс завершается после его запуска, но до прикрепления события Exited. Мое предложение - начать процесс после всех регистраций.
Стас Бояринцев
@StasBoyarincev Спасибо, обновлено. Я забыл обновить ответ StackOverflow с этим изменением.
Мухаммед Рехан Саид
1
@MuhammadRehanSaeed Еще одна вещь - кажется, не разрешается вызывать process.BeginOutputReadLine () или process.BeginErrorReadLine () перед process.Start. В этом случае я получаю сообщение об ошибке: StandardOut не был перенаправлен или процесс еще не начался.
Стас Бояринцев
17

Проблема с необработанным исключением ObjectDisposedException возникает, когда время ожидания истекло. В таком случае другие части условия:

if (process.WaitForExit(timeout) 
    && outputWaitHandle.WaitOne(timeout) 
    && errorWaitHandle.WaitOne(timeout))

не выполнены. Я решил эту проблему следующим образом:

using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
{
    using (Process process = new Process())
    {
        // preparing ProcessStartInfo

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

            process.Start();

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

            if (process.WaitForExit(timeout))
            {
                exitCode = process.ExitCode;
            }
            else
            {
                // timed out
            }

            output = outputBuilder.ToString();
        }
        finally
        {
            outputWaitHandle.WaitOne(timeout);
            errorWaitHandle.WaitOne(timeout);
        }
    }
}
Кароль Тыл
источник
1
для полноты картины не хватает настройки перенаправлений на true
knocte
и я удалил тайм-ауты в моем конце, так как процесс может запросить ввод данных пользователем (например, ввести что-то), поэтому я не хочу требовать, чтобы пользователь был быстрым
knocte
Почему ты изменился outputи errorна outputBuilder? Может кто-нибудь дать полный ответ, который работает?
Марко Авлияш,
System.ObjectDisposedException: безопасный дескриптор был закрыт, встречается и в этой версии для меня
Мэтт
8

Роб ответил на это и спас мне еще несколько часов испытаний. Прочитайте буфер вывода / ошибок перед ожиданием:

// Read the output stream first and then wait.
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
Джон
источник
1
но что, если после вашего звонка поступит больше данных WaitForExit()?
knocte
@knocte, основанный на моих тестах, ReadToEndили подобные методы (например StandardOutput.BaseStream.CopyTo), вернутся после того, как ВСЕ данные прочитаны. после этого ничего не
случится
Вы говорите, что ReadToEnd () также ожидает выхода?
knocte
2
@knocte вы пытаетесь понять API, созданный Microsoft?
аааааа
Проблема соответствующей страницы MSDN состоит в том, что она не объясняет, что буфер за StandardOutput может заполниться, и в этой ситуации дочерний элемент должен прекратить запись и ждать, пока буфер не будет освобожден (родительский объект считывает данные в буфере). , ReadToEnd () может только синхронно читать, пока буфер не будет закрыт или буфер заполнен, или дочерний процесс не выйдет с буфером не заполненным. Это мое понимание.
Кэри
7

У нас также есть этот вопрос (или вариант).

Попробуйте следующее:

1) Добавить тайм-аут в p.WaitForExit (nnnn); где nnnn в миллисекундах.

2) Поместите вызов ReadToEnd перед вызовом WaitForExit. Это является то , что мы уже видели MS рекомендую.

ториальную
источник
5

Кредит EM0 для https://stackoverflow.com/a/17600012/4151626

Другие решения (в том числе EM0) все еще заблокированы для моего приложения из-за внутренних тайм-аутов и использования StandardOutput и StandardError порожденным приложением. Вот что сработало для меня:

Process p = new Process()
{
  StartInfo = new ProcessStartInfo()
  {
    FileName = exe,
    Arguments = args,
    UseShellExecute = false,
    RedirectStandardOutput = true,
    RedirectStandardError = true
  }
};
p.Start();

string cv_error = null;
Thread et = new Thread(() => { cv_error = p.StandardError.ReadToEnd(); });
et.Start();

string cv_out = null;
Thread ot = new Thread(() => { cv_out = p.StandardOutput.ReadToEnd(); });
ot.Start();

p.WaitForExit();
ot.Join();
et.Join();

Редактировать: добавлена ​​инициализация StartInfo к примеру кода

ergohack
источник
Это то, чем я пользуюсь, и у меня больше не было проблем с тупиком.
Ремер
3

Я решил это так:

            Process proc = new Process();
            proc.StartInfo.FileName = batchFile;
            proc.StartInfo.UseShellExecute = false;
            proc.StartInfo.CreateNoWindow = true;
            proc.StartInfo.RedirectStandardError = true;
            proc.StartInfo.RedirectStandardInput = true;
            proc.StartInfo.RedirectStandardOutput = true;
            proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;      
            proc.Start();
            StreamWriter streamWriter = proc.StandardInput;
            StreamReader outputReader = proc.StandardOutput;
            StreamReader errorReader = proc.StandardError;
            while (!outputReader.EndOfStream)
            {
                string text = outputReader.ReadLine();                    
                streamWriter.WriteLine(text);
            }

            while (!errorReader.EndOfStream)
            {                   
                string text = errorReader.ReadLine();
                streamWriter.WriteLine(text);
            }

            streamWriter.Close();
            proc.WaitForExit();

Я перенаправил как ввод, вывод и ошибку, так и обработал чтение из потока вывода и ошибок. Это решение работает для SDK 7- 8.1, как для Windows 7, так и для Windows 8

Элина Малярская
источник
2
Элина: спасибо за ваш ответ. Внизу этого документа MSDN есть несколько заметок ( msdn.microsoft.com/en-us/library/… ), которые предупреждают о возможных тупиках, если вы одновременно читаете до конца как перенаправленные потоки stdout, так и stderr. Трудно сказать, подвержено ли ваше решение этой проблеме. Кроме того, кажется, что вы отправляете вывод процесса stdout / stderr обратно в качестве ввода. Зачем? :)
Мэтью Пайетт
3

Я попытался создать класс, который бы решал вашу проблему, используя асинхронное чтение потока, принимая во внимание ответы Марка Байерса, Роба Стивейя. При этом я понял, что существует ошибка, связанная с чтением потока вывода асинхронного процесса.

Я сообщил об этой ошибке в Microsoft: https://connect.microsoft.com/VisualStudio/feedback/details/3119134

Резюме:

Вы не можете сделать это:

process.BeginOutputReadLine (); Process.Start ();

Вы получите System.InvalidOperationException: StandardOut не был перенаправлен или процесс еще не начался.

================================================== ================================================== ========================

Затем вы должны запустить асинхронное чтение данных после запуска процесса:

Process.Start (); process.BeginOutputReadLine ();

Для этого создайте условие состязания, потому что выходной поток может получать данные, прежде чем вы установите его в асинхронный режим:

process.Start(); 
// Here the operating system could give the cpu to another thread.  
// For example, the newly created thread (Process) and it could start writing to the output
// immediately before next line would execute. 
// That create a race condition.
process.BeginOutputReadLine();

================================================== ================================================== ========================

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

================================================== ================================================== ========================

Невозможно обеспечить безопасное асинхронное чтение выходного потока процесса фактическим способом, которым были разработаны «Процесс» и «ProcessStartInfo».

Вам, вероятно, лучше использовать асинхронное чтение, как предложили другие пользователи для вашего случая. Но вы должны знать, что вы можете пропустить некоторую информацию из-за состояния гонки.

Эрик Оуэлл
источник
1

Я считаю, что это простой и лучший подход (нам не нужен AutoResetEvent):

public static string GGSCIShell(string Path, string Command)
{
    using (Process process = new Process())
    {
        process.StartInfo.WorkingDirectory = Path;
        process.StartInfo.FileName = Path + @"\ggsci.exe";
        process.StartInfo.CreateNoWindow = true;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.RedirectStandardInput = true;
        process.StartInfo.UseShellExecute = false;

        StringBuilder output = new StringBuilder();
        process.OutputDataReceived += (sender, e) =>
        {
            if (e.Data != null)
            {
                output.AppendLine(e.Data);
            }
        };

        process.Start();
        process.StandardInput.WriteLine(Command);
        process.BeginOutputReadLine();


        int timeoutParts = 10;
        int timeoutPart = (int)TIMEOUT / timeoutParts;
        do
        {
            Thread.Sleep(500);//sometimes halv scond is enough to empty output buff (therefore "exit" will be accepted without "timeoutPart" waiting)
            process.StandardInput.WriteLine("exit");
            timeoutParts--;
        }
        while (!process.WaitForExit(timeoutPart) && timeoutParts > 0);

        if (timeoutParts <= 0)
        {
            output.AppendLine("------ GGSCIShell TIMEOUT: " + TIMEOUT + "ms ------");
        }

        string result = output.ToString();
        return result;
    }
}
Кузман Маринов
источник
Верно, но разве вам не следует .FileName = Path + @"\ggsci.exe" + @" < obeycommand.txt"упростить свой код? Или, может быть, что-то эквивалентное, "echo command | " + Path + @"\ggsci.exe"если вы действительно не хотите использовать отдельный файл obeycommand.txt.
Амит Найду
3
Ваше решение не нуждается в AutoResetEvent, но вы опрашиваете. Когда вы проводите опрос вместо использования событий (когда они доступны), то вы используете ЦП без всякой причины, что говорит о том, что вы плохой программист. Ваше решение действительно плохое по сравнению с другим, использующим AutoResetEvent. (Но я не дал вам -1, потому что вы пытались помочь!).
Эрик Оуэлле
1

Ни один из ответов выше не делает работу.

Решение Роба зависает, и решение «Марк Байерс» получает исключение (я попробовал «решения» других ответов).

Поэтому я решил предложить другое решение:

public void GetProcessOutputWithTimeout(Process process, int timeoutSec, CancellationToken token, out string output, out int exitCode)
{
    string outputLocal = "";  int localExitCode = -1;
    var task = System.Threading.Tasks.Task.Factory.StartNew(() =>
    {
        outputLocal = process.StandardOutput.ReadToEnd();
        process.WaitForExit();
        localExitCode = process.ExitCode;
    }, token);

    if (task.Wait(timeoutSec, token))
    {
        output = outputLocal;
        exitCode = localExitCode;
    }
    else
    {
        exitCode = -1;
        output = "";
    }
}

using (var process = new Process())
{
    process.StartInfo = ...;
    process.Start();
    string outputUnicode; int exitCode;
    GetProcessOutputWithTimeout(process, PROCESS_TIMEOUT, out outputUnicode, out exitCode);
}

Этот код отлажен и работает отлично.

omriman12
источник
1
Хорошо! просто обратите внимание, что параметр token не предоставляется при вызове GetProcessOutputWithTimeoutметода.
С.Серпушан
1

Введение

В настоящее время принятый ответ не работает (выдает исключение), и существует слишком много обходных путей, но нет полного кода. Это, очевидно, тратит много времени людей, потому что это популярный вопрос.

Комбинируя ответ Марка Байерса и ответ Кароля Тила, я написал полный код, основанный на том, как я хочу использовать метод Process.Start.

использование

Я использовал его для создания диалога прогресса вокруг команд git. Вот как я это использовал:

    private bool Run(string fullCommand)
    {
        Error = "";
        int timeout = 5000;

        var result = ProcessNoBS.Start(
            filename: @"C:\Program Files\Git\cmd\git.exe",
            arguments: fullCommand,
            timeoutInMs: timeout,
            workingDir: @"C:\test");

        if (result.hasTimedOut)
        {
            Error = String.Format("Timeout ({0} sec)", timeout/1000);
            return false;
        }

        if (result.ExitCode != 0)
        {
            Error = (String.IsNullOrWhiteSpace(result.stderr)) 
                ? result.stdout : result.stderr;
            return false;
        }

        return true;
    }

Теоретически вы также можете комбинировать stdout и stderr, но я этого не проверял.

Код

public struct ProcessResult
{
    public string stdout;
    public string stderr;
    public bool hasTimedOut;
    private int? exitCode;

    public ProcessResult(bool hasTimedOut = true)
    {
        this.hasTimedOut = hasTimedOut;
        stdout = null;
        stderr = null;
        exitCode = null;
    }

    public int ExitCode
    {
        get 
        {
            if (hasTimedOut)
                throw new InvalidOperationException(
                    "There was no exit code - process has timed out.");

            return (int)exitCode;
        }
        set
        {
            exitCode = value;
        }
    }
}

public class ProcessNoBS
{
    public static ProcessResult Start(string filename, string arguments,
        string workingDir = null, int timeoutInMs = 5000,
        bool combineStdoutAndStderr = false)
    {
        using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
        using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
        {
            using (var process = new Process())
            {
                var info = new ProcessStartInfo();

                info.CreateNoWindow = true;
                info.FileName = filename;
                info.Arguments = arguments;
                info.UseShellExecute = false;
                info.RedirectStandardOutput = true;
                info.RedirectStandardError = true;

                if (workingDir != null)
                    info.WorkingDirectory = workingDir;

                process.StartInfo = info;

                StringBuilder stdout = new StringBuilder();
                StringBuilder stderr = combineStdoutAndStderr
                    ? stdout : new StringBuilder();

                var result = new ProcessResult();

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

                    process.Start();

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

                    if (process.WaitForExit(timeoutInMs))
                        result.ExitCode = process.ExitCode;
                    // else process has timed out 
                    // but that's already default ProcessResult

                    result.stdout = stdout.ToString();
                    if (combineStdoutAndStderr)
                        result.stderr = null;
                    else
                        result.stderr = stderr.ToString();

                    return result;
                }
                finally
                {
                    outputWaitHandle.WaitOne(timeoutInMs);
                    errorWaitHandle.WaitOne(timeoutInMs);
                }
            }
        }
    }
}
Марко Авлияш
источник
Тем не менее получить System.ObjectDisposedException: безопасный дескриптор был закрыт на этой версии тоже.
Мэтт
1

Я знаю, что это старый ужин, но после прочтения всей этой страницы ни одно из решений не сработало для меня, хотя я не пробовал Мухаммеда Рехана, так как код был немного сложным, хотя, думаю, он был на правильном пути , Когда я говорю, что это не работает, это не совсем верно, иногда это будет работать нормально, я думаю, это как-то связано с длиной вывода до знака EOF.

В любом случае, решение, которое работало для меня, состояло в том, чтобы использовать разные потоки для чтения StandardOutput и StandardError и записи сообщений.

        StreamWriter sw = null;
        var queue = new ConcurrentQueue<string>();

        var flushTask = new System.Timers.Timer(50);
        flushTask.Elapsed += (s, e) =>
        {
            while (!queue.IsEmpty)
            {
                string line = null;
                if (queue.TryDequeue(out line))
                    sw.WriteLine(line);
            }
            sw.FlushAsync();
        };
        flushTask.Start();

        using (var process = new Process())
        {
            try
            {
                process.StartInfo.FileName = @"...";
                process.StartInfo.Arguments = $"...";
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;

                process.Start();

                var outputRead = Task.Run(() =>
                {
                    while (!process.StandardOutput.EndOfStream)
                    {
                        queue.Enqueue(process.StandardOutput.ReadLine());
                    }
                });

                var errorRead = Task.Run(() =>
                {
                    while (!process.StandardError.EndOfStream)
                    {
                        queue.Enqueue(process.StandardError.ReadLine());
                    }
                });

                var timeout = new TimeSpan(hours: 0, minutes: 10, seconds: 0);

                if (Task.WaitAll(new[] { outputRead, errorRead }, timeout) &&
                    process.WaitForExit((int)timeout.TotalMilliseconds))
                {
                    if (process.ExitCode != 0)
                    {
                        throw new Exception($"Failed run... blah blah");
                    }
                }
                else
                {
                    throw new Exception($"process timed out after waiting {timeout}");
                }
            }
            catch (Exception e)
            {
                throw new Exception($"Failed to succesfully run the process.....", e);
            }
        }
    }

Надеюсь, что это поможет кому-то, кто думал, что это может быть так сложно!

Алексис Коулз
источник
Исключение: sw.FlushAsync(): Object is not set to an instance of an object. sw is null. как / где должно swбыть определено?
Wallyk
1

Прочитав все посты здесь, я остановился на консолидированном решении Марко Авлияша. тем не мение , это не решило все мои проблемы.

В нашей среде у нас есть служба Windows, которая планирует запускать сотни различных файлов .bat .cmd .exe, ... и т. Д., Которые накапливались годами и были написаны многими разными людьми и в разных стилях. Мы не контролируем написание программ и сценариев, мы просто несем ответственность за планирование, запуск и отчетность об успехах / неудачах.

Итак, я попробовал почти все предложения здесь с разным уровнем успеха. Ответ Марко был почти идеальным, но при запуске в качестве службы он не всегда захватывал стандартный вывод. Я так и не понял, почему нет.

Единственное найденное нами решение, которое работает во ВСЕХ наших случаях, это: http://csharptest.net/319/using-the-processrunner-class/index.html

flapster
источник
Я собираюсь попробовать эту библиотеку. Я определил область действия кода, и похоже, что он разумно использует делегатов. Это красиво упаковано в Nuget. Это в основном воняет профессионализмом, в чем я никогда не мог быть обвинен. Если он кусается, скажу.
Стив Хибберт,
Ссылка на исходный код не работает. Пожалуйста, в следующий раз скопируйте код в ответ.
Виталий Зданевич
1

Обходной путь, который я использовал, чтобы избежать всей сложности:

var outputFile = Path.GetTempFileName();
info = new System.Diagnostics.ProcessStartInfo("TheProgram.exe", String.Join(" ", args) + " > " + outputFile + " 2>&1");
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.UseShellExecute = false;
System.Diagnostics.Process p = System.Diagnostics.Process.Start(info);
p.WaitForExit();
Console.WriteLine(File.ReadAllText(outputFile)); //need the StandardOutput contents

Поэтому я создаю временный файл, перенаправляю на него вывод и ошибку с помощью, > outputfile > 2>&1а затем просто читаю файл после завершения процесса.

Другие решения хороши для сценариев, в которых вы хотите делать другие вещи с выводом, но для простых вещей это позволяет избежать большой сложности.

eglasius
источник
1

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

private static int DoProcess(string workingDir, string fileName, string arguments)
{
    int exitCode;
    using (var process = new Process
    {
        StartInfo =
        {
            WorkingDirectory = workingDir,
            WindowStyle = ProcessWindowStyle.Hidden,
            CreateNoWindow = true,
            UseShellExecute = false,
            FileName = fileName,
            Arguments = arguments,
            RedirectStandardError = true,
            RedirectStandardOutput = true
        },
        EnableRaisingEvents = true
    })
    {
        using (var outputWaitHandle = new AutoResetEvent(false))
        using (var errorWaitHandle = new AutoResetEvent(false))
        {
            process.OutputDataReceived += (sender, args) =>
            {
                // ReSharper disable once AccessToDisposedClosure
                if (args.Data != null) Debug.Log(args.Data);
                else outputWaitHandle.Set();
            };
            process.ErrorDataReceived += (sender, args) =>
            {
                // ReSharper disable once AccessToDisposedClosure
                if (args.Data != null) Debug.LogError(args.Data);
                else errorWaitHandle.Set();
            };

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

            WaitHandle.WaitAll(new WaitHandle[] { outputWaitHandle, errorWaitHandle });

            exitCode = process.ExitCode;
        }
    }
    return exitCode;
}
Deepscorn
источник
Я использовал это и обернул Task.Run для обработки тайм-аута, я также возвращаю processid для уничтожения по тайм-
ауту
0

Я думаю, что с асинхронным, возможно иметь более элегантное решение и не иметь взаимоблокировок даже при использовании как standardOutput, так и standardError:

using (Process process = new Process())
{
    process.StartInfo.FileName = filename;
    process.StartInfo.Arguments = arguments;
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;

    process.Start();

    var tStandardOutput = process.StandardOutput.ReadToEndAsync();
    var tStandardError = process.StandardError.ReadToEndAsync();

    if (process.WaitForExit(timeout))
    {
        string output = await tStandardOutput;
        string errors = await tStandardError;

        // Process completed. Check process.ExitCode here.
    }
    else
    {
        // Timed out.
    }
}

Это основано на ответе Марк Байерс. Если вы не используете асинхронный метод, вы можете использовать string output = tStandardOutput.result;вместоawait

Yepeekai
источник
-1

Это сообщение может быть устаревшим, но я выяснил, что основная причина, почему он обычно зависает, связана с переполнением стека для redirectStandardoutput или если у вас есть redirectStandarderror.

Поскольку выходные данные или данные об ошибках велики, это приведет к зависанию, так как оно все еще обрабатывается в течение неопределенного времени.

Итак, чтобы решить эту проблему:

p.StartInfo.RedirectStandardoutput = False
p.StartInfo.RedirectStandarderror = False
песня
источник
11
Проблема в том, что люди явно устанавливают для них значение true, потому что они хотят иметь доступ к этим потокам! Иначе мы можем просто оставить их ложными.
user276648
-1

Давайте назовем пример кода, размещенный здесь, перенаправителем, а другую программу - перенаправленной. Если бы это был я, я бы написал тестовую перенаправленную программу, которую можно использовать для дублирования проблемы.

Так я и сделал. Для тестовых данных я использовал спецификацию языка ECMA-334 C # v PDF; это около 5 МБ. Следующее является важной частью этого.

StreamReader stream = null;
try { stream = new StreamReader(Path); }
catch (Exception ex)
{
    Console.Error.WriteLine("Input open error: " + ex.Message);
    return;
}
Console.SetIn(stream);
int datasize = 0;
try
{
    string record = Console.ReadLine();
    while (record != null)
    {
        datasize += record.Length + 2;
        record = Console.ReadLine();
        Console.WriteLine(record);
    }
}
catch (Exception ex)
{
    Console.Error.WriteLine($"Error: {ex.Message}");
    return;
}

Значение размера данных не соответствует фактическому размеру файла, но это не имеет значения. Неясно, всегда ли PDF-файл использует CR и LF в конце строк, но это не имеет значения для этого. Вы можете использовать любой другой большой текстовый файл для тестирования.

Использование этого примера кода перенаправителя зависает, когда я пишу большой объем данных, но не когда я пишу небольшой объем.

Я очень старался как-то отследить выполнение этого кода и не смог. Я закомментировал строки перенаправленной программы, которая отключила создание консоли для перенаправленной программы, чтобы попытаться получить отдельное окно консоли, но я не смог.

Затем я нашел, как запустить консольное приложение в новом окне, родительском окне или без окна . Таким образом, очевидно, что мы не можем (легко) иметь отдельную консоль, когда одна консольная программа запускает другую консольную программу без ShellExecute, и поскольку ShellExecute не поддерживает перенаправление, мы должны совместно использовать консоль, даже если мы не указываем окно для другого процесса.

Я предполагаю, что если перенаправленная программа заполняет буфер где-то, то она должна ждать, пока данные будут прочитаны, и если в этот момент данные не будут прочитаны перенаправителем, то это тупик.

Решение состоит в том, чтобы не использовать ReadToEnd и читать данные во время записи данных, но нет необходимости использовать асинхронное чтение. Решение может быть довольно простым. Следующее работает для меня с 5 МБ PDF.

ProcessStartInfo info = new ProcessStartInfo(TheProgram);
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
Process p = Process.Start(info);
string record = p.StandardOutput.ReadLine();
while (record != null)
{
    Console.WriteLine(record);
    record = p.StandardOutput.ReadLine();
}
p.WaitForExit();

Другая возможность - использовать программу с графическим интерфейсом для перенаправления. Предыдущий код работает в приложении WPF, за исключением явных изменений.

user34660
источник
-3

У меня была та же проблема, но причина была в другом. Однако это произошло бы в Windows 8, но не в Windows 7. Кажется, следующая строка вызвала проблему.

pProcess.StartInfo.UseShellExecute = False

Решением было НЕ отключить UseShellExecute. Теперь я получил всплывающее окно Shell, которое нежелательно, но гораздо лучше, чем программа, ожидающая ничего особенного. Поэтому я добавил следующие обходные пути для этого:

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

Теперь единственное, что меня беспокоит, это то, почему это происходит в первую очередь под Windows 8.

ohgodnotanotherone
источник
1
Вам нужно UseShellExecuteустановить значение false, если вы хотите перенаправить вывод.
Брэд Мур