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 Мб? Якщо так, то що мені робити замість цього? Якщо ні, то що я роблю неправильно?


будь-яке остаточне рішення з повним вихідним кодом про це?
Кікенет

2
Я біжу в цьому питанні , і це , як я був в змозі вирішити stackoverflow.com/questions/2285288 / ...
Bedasso

6
Так, остаточне рішення: поміняйте місцями два останніх рядки. Це в посібнику .
Аміт Найду

4
від msdn: Приклад коду дозволяє уникнути умови глухого кута виклику p.StandardOutput.ReadToEnd перед p.WaitForExit. Умова тупикового зв'язку може призвести, якщо батьківський процес викликає p.WaitForExit перед p.StandardOutput.ReadToEnd і дочірній процес записує достатньо тексту для заповнення переспрямованого потоку. Батьківський процес буде чекати нескінченно часу, щоб дочірній процес вийшов. Дочірній процес нескінченно буде чекати, коли батько прочитає з повного потоку StandardOutput.
Карлос Лю

трохи прикро, наскільки складно це зробити правильно. Був із задоволенням працювати над цим простішими переадресаціями командного рядка> outputfile :)
eglasius

Відповіді:


393

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

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

Рішення полягає у використанні асинхронних зчитувань, щоб гарантувати, що буфер не заповниться. Щоб уникнути будь-яких тупиків і зібрати весь вихід з обох, StandardOutputі StandardErrorви можете це зробити:

EDIT: Дивіться відповіді нижче, як уникнути 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
щоразу, коли командний рядок закривається, це з'являється: У
mscorlib.dll трапляється

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();ви отримаєте тупик / зависання.
Chris S

3
Правда. Я опинився в подібній ситуації. Я перенаправляв StandardError без будь-якої причини під час перетворення з ffmpeg в процесі, він писав достатньо в потоці StandardError, щоб створити тупик.
Леон Пелтьє

Це все ще висить для мене навіть при переадресації та читанні стандартного виводу.
користувач3791372

@ user3791372 Я думаю, це застосовно лише в тому випадку, якщо буфер позаду StandardOutput не заповнений повністю. Тут МСДН не виконує правосуддя. Чудова стаття, яку я рекомендую вам прочитати, знаходиться за адресою: dzone.com/articles/async-io-and-threadpool
Cary

19

Відповідь Марка Байєрса відмінна, але я просто додам наступне:

OutputDataReceivedІ ErrorDataReceivedделегати повинні бути видалені до того , як outputWaitHandleі errorWaitHandleотримати розташовані. Якщо процес продовжує виводити дані після того, як тайм - аут був перевищений , а потім завершується, outputWaitHandleі errorWaitHandleзмінні будуть доступні після того , як розташовані.

(FYI Мені довелося додати цей застереження як відповідь, оскільки я не зміг прокоментувати його повідомлення.)


2
Можливо, було б краще зателефонувати CancelOutputRead ?
Марк Байєрс

Додавання відредагованого коду Марка до цієї відповіді було б досить приголомшливо! У мене виникає саме таку проблему в хвилину.
ianbailey

8
@ianbailey Найпростіший спосіб вирішити це - помістити використання (Process p ...) всередині використовуваного (AutoResetEvent errorWaitHandle ...)
Didier A.

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 Ще одна річ - здається, не дозволений процес виклику.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
задля повноти цього не вистачає, щоб налаштувати переадресації на істинне
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?
aaaaaa

Проблема відповідної сторінки 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


Це те, що я використовую, і ніколи більше не виникало проблем із тупиком.
Румер

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

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

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

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

Немає можливості досягти безпечного асинхронного зчитування вихідного потоку процесу власне таким чином, як "Process" та "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);
}

Цей код налагоджений і працює чудово.


1
Добре! лише зауважте, що параметр токена не надається під час виклику 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, ... і т.д., які накопичувалися роками і писали багато різних людей і в різних стилях. У нас немає контролю над написанням програм та сценаріїв, ми просто відповідальні за планування, запуск та звітування про успіх / невдачу.

Тому я спробував майже всі пропозиції тут з різним рівнем успіху. Відповідь Марко була майже ідеальною, але коли вона працювала як послуга, вона завжди не захоплювала stdout. Я ніколи не дійшов до дна, чому ні.

Єдине рішення, яке ми знайшли, що працює у ВСІХ наших випадках, це таке: http://csharptest.net/319/using-the-processrunner-class/index.html


Я збираюся спробувати цю бібліотеку. Я перевірив код, і, схоже, розумно використовувати делегати. Він чудово упакований в Нугет. Це в основному смердить про професіоналізм, у чому я ніколи не можу звинуватити. Якщо вкусить, покаже.
Стів Хібберт

Посилання на вихідний код мертве. Наступного разу скопіюйте код у відповідь.
Віталій Зданевич

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а потім просто читаю файл після завершення процесу.

Інші рішення чудово підходять для сценаріїв, коли ви хочете робити інші речі з результатом, але для простих матеріалів це дозволяє уникнути великої складності.


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

Я використав це і завершив Task.Run для обробки тайм-ауту, я також повертаю
Processid,

0

Я думаю, що за допомогою асинхронізації можна отримати більш елегантне рішення і не мати тупиків навіть при використанні і стандартного виводу, і стандартуError:

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



-1

Ця публікація може бути застарілою, але я з’ясував, що головна причина, чому вона зазвичай звисає, пов’язана із переповненням стека для redirectStandardoutput або якщо у вас є redirectStandarderror.

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

щоб вирішити цю проблему:

p.StartInfo.RedirectStandardoutput = False
p.StartInfo.RedirectStandarderror = False

11
Проблема полягає в тому, що люди явно встановлюють це як істинне, оскільки вони хочуть мати доступ до цих потоків! Інакше ми можемо просто залишити їх помилковими.
користувач276648

-1

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

Так я і зробив. Для тестових даних я використовував специфікацію мови ECMA-334 C #vv; це приблизно 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 і читати дані під час запису даних, але не потрібно використовувати асинхронні зчитування. Рішення може бути досить простим. Наступне працює для мене з PDF-файлом розміром 5 Мб.

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

Інша можливість полягає у використанні програми GUI для перенаправлення. Попередній код працює у програмі WPF, за винятком явних модифікацій.


-3

У мене було те саме питання, але причина була іншою. Однак це може статися під Windows 8, але не під Windows 7. Наступний рядок, здається, спричинив проблему.

pProcess.StartInfo.UseShellExecute = False

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

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

Тепер єдине, що мене турбує, це, чому це відбувається в першу чергу під Windows 8.


1
Вам потрібно UseShellExecuteвстановити значення false, якщо ви хочете перенаправити вихід.
Бред Мур
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.