ProcessStartInfo pendurado em "WaitForExit"? Por quê?


187

Eu tenho o seguinte código:

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

Eu sei que a saída do processo que estou iniciando tem cerca de 7 MB. Executá-lo no console do Windows funciona bem. Infelizmente, programaticamente, isso trava indefinidamente em WaitForExit. Observe também que este código NÃO trava para saídas menores (como 3 KB).

É possível que o StandardOutput interno no ProcessStartInfo não consiga armazenar em buffer 7 MB? Se sim, o que devo fazer? Se não, o que estou fazendo de errado?


alguma solução final com código fonte completo sobre isso?
Kiquenet

2
Eu correr em mesmo problema e assim que eu era capaz de resolvê-lo stackoverflow.com/questions/2285288/...
Bedasso

6
Sim, solução final: troque as duas últimas linhas. Está no manual .
Amit Naidu

4
from msdn: o exemplo de código evita uma condição de conflito chamando p.StandardOutput.ReadToEnd antes de p.WaitForExit. Uma condição de conflito pode resultar se o processo pai chamar p.WaitForExit antes de p.StandardOutput.ReadToEnd e o processo filho gravar texto suficiente para preencher o fluxo redirecionado. O processo pai esperaria indefinidamente o processo filho sair. O processo filho aguardaria indefinidamente que o pai lesse o fluxo completo do StandardOutput.
Carlos Liu

é um pouco chato o quão complexo é fazer isso corretamente. Foi o prazer de trabalhar em torno dele com redirecionamentos de linha de comando simples> outputfile :)
eglasius

Respostas:


393

O problema é que, se você redirecionar StandardOutpute / ou StandardErroro buffer interno pode ficar cheio. Qualquer que seja a ordem usada, pode haver um problema:

  • Se você esperar o processo terminar antes de ler StandardOutputo processo, poderá bloquear a tentativa de gravar nele, para que o processo nunca termine.
  • Se você ler sobre o StandardOutputReadToEnd, seu processo poderá ser bloqueado se o processo nunca for fechado StandardOutput(por exemplo, se ele nunca for encerrado ou se estiver bloqueado para a gravação StandardError).

A solução é usar leituras assíncronas para garantir que o buffer não fique cheio. Para evitar impasses e recolher-se todas as saídas de ambos StandardOutpute StandardErrorvocê pode fazer isso:

EDIT: consulte as respostas abaixo para saber como evitar uma ObjectDisposedException se o tempo limite ocorrer.

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
Não tinha idéia de que o redirecionamento da saída estava causando o problema, mas com certeza era. Passei 4 horas batendo minha cabeça nisso e consertando-o em 5 minutos depois de ler sua postagem. Bom trabalho!
Ben Gripka

1
@AlexPeck O problema estava sendo executado como um aplicativo de console. Hans Passant identificou o problema aqui: stackoverflow.com/a/16218470/279516
Bob Horn

5
sempre que o prompt de comando é fechado, aparece: Uma exceção sem tratamento do tipo "System.ObjectDisposed" ocorreu no mscorlib.dll Informações adicionais: O identificador de segurança foi fechado
user1663380

3
Tivemos um problema semelhante ao descrito por @ user1663380 acima. Você acha que é possível que as usinginstruções para os manipuladores de eventos precisem estar acima da usinginstrução para o próprio processo?
Dan Forbes

2
Não acho que as alças de espera sejam necessárias. Conforme o msdn, termine com a versão sem tempo limite de WaitForExit: Quando a saída padrão foi redirecionada para manipuladores de eventos assíncronos, é possível que o processamento da saída não seja concluído quando esse método retornar. Para garantir que a manipulação de eventos assíncronos tenha sido concluída, chame a sobrecarga WaitForExit () que não usa parâmetro após receber um valor verdadeiro dessa sobrecarga.
Patrick

98

A documentação para Process.StandardOutputdiz para ler antes de esperar, caso contrário, você pode obter um impasse, trecho copiado abaixo:

 // 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
Não tenho 100% de certeza se isso é apenas um resultado do meu ambiente, mas descobri que se você configurou RedirectStandardOutput = true;e não usa, p.StandardOutput.ReadToEnd();obtém um impasse / travamento.
Chris S

3
Verdade. Eu estava em uma situação similar. Eu estava redirecionando o StandardError sem motivo ao converter com o ffmpeg em um processo, ele estava gravando o suficiente no fluxo do StandardError para criar um conflito.
Léon Pelletier

Isso ainda trava para mim, mesmo com o redirecionamento e a leitura da saída padrão.
user3791372

@ user3791372 Acho que isso só é aplicável se o buffer atrás do StandardOutput não estiver totalmente cheio. Aqui, o MSDN não faz sua justiça. Um grande artigo que eu recomendo que você leia está em: dzone.com/articles/async-io-and-threadpool
Cary

19

A resposta de Mark Byers é excelente, mas gostaria de acrescentar o seguinte:

Os delegados OutputDataReceivede ErrorDataReceivedprecisam ser removidos antes outputWaitHandlee errorWaitHandledescartados. Se o processo continuar produzindo dados após o tempo limite exceder e terminar, as variáveis outputWaitHandlee errorWaitHandleserão acessadas após serem descartadas.

(Para sua informação, tive que acrescentar esta advertência como resposta, pois não podia comentar sua postagem.)


2
Talvez seja melhor chamar CancelOutputRead ?
precisa

Adicionar o código editado de Mark a esta resposta seria incrível! Estou tendo exatamente o mesmo problema no momento.
ianbailey

8
@ianbailey maneira mais fácil de resolver isso é para colocar a usar (Processo p ...) dentro do usando (AutoResetEvent errorWaitHandle ...)
Didier A.

18

Esta é uma solução mais moderna e esperada, baseada em Task Parallel Library (TPL) para .NET 4.5 e superior.

Exemplo de uso

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

Implementação

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
melhor e mais completa resposta a data
TermoTux

1
Por algumas razões, essa foi a única solução que funcionou para mim, o aplicativo parou de ser pendurado.
19418 Jack

1
Parece que você não lida com a condição, onde o processo termina após o início, mas antes do evento Exited ser anexado. Minha sugestão - iniciar o processo após todos os registros.
Stas Boyarincev

@StasBoyarincev Obrigado, atualizado. Eu tinha esquecido de atualizar a resposta do StackOverflow com essa alteração.
Muhammad Rehan Saeed

1
@MuhammadRehanSaeed Mais uma coisa: parece que não é permitido chamar process.BeginOutputReadLine () ou process.BeginErrorReadLine () antes de process.Start. Nesse caso, recebo o erro: StandardOut não foi redirecionado ou o processo ainda não foi iniciado.
Stas Boyarincev 17/10/19

17

O problema com ObjectDisposedException sem tratamento ocorre quando o tempo limite do processo é excedido. Nesse caso, as outras partes da condição:

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

não são executados. Resolvi esse problema da seguinte maneira:

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
por uma questão de exaustividade, este está faltando configurar os redirecionamentos para true
knocte

e eu removi os tempos de espera em meu fim desde que o processo pode pedir a entrada do usuário (por exemplo, tipo algo) para que eu não quero exigem que o usuário a ser rápido
knocte

Por que você mudou outpute errorpara outputBuilder? Alguém pode fornecer uma resposta completa que funcione?
Marko Avlijaš

System.ObjectDisposedException: alça de segurança foi fechada ocorre nesta versão para mim também
Matt

8

Rob atendeu e me salvou mais algumas horas de testes. Leia o buffer de saída / erro antes de esperar:

// Read the output stream first and then wait.
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();

1
mas e se mais dados surgirem depois de você ligar WaitForExit()?
knocte

@knocte com base em meus testes ReadToEndou métodos semelhantes (como StandardOutput.BaseStream.CopyTo) retornarão após a leitura de TODOS os dados. nada virá depois disso.
S.Serpooshan

você está dizendo que ReadToEnd () também espera pela saída?
knocte

2
@knocte você está tentando entender uma API criada pela microsoft?
aaaaaa 25/02

O problema da página correspondente do MSDN é que ele não explicou que o buffer atrás do StandardOutput pode ficar cheio e, nessa situação, o filho deve parar de escrever e aguardar até que o buffer seja drenado (o pai lê os dados no buffer) . ReadToEnd () só pode ler de forma sincronizada até que o buffer esteja fechado ou cheio ou que o filho saia com o buffer não cheio. Esse é o meu entendimento.
Cary

7

Também temos esse problema (ou uma variante).

Tente o seguinte:

1) Adicione um tempo limite a p.WaitForExit (nnnn); onde nnnn está em milissegundos.

2) Coloque a chamada ReadToEnd antes da chamada WaitForExit. Isso é o que temos visto MS recomendar.


5

Crédito para EM0 por https://stackoverflow.com/a/17600012/4151626

As outras soluções (incluindo EM0) ainda estão bloqueadas para o meu aplicativo, devido a tempos limites internos e ao uso de StandardOutput e StandardError pelo aplicativo gerado. Aqui está o que funcionou para mim:

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

Editar: adicionada inicialização do StartInfo ao exemplo de código


É isso que eu uso e nunca mais tive problemas com um impasse.
Roemer

3

Eu resolvi assim:

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

Redirecionei a entrada, a saída e o erro e lidei com a leitura dos fluxos de saída e erro. Esta solução funciona para o SDK 7- 8.1, tanto no Windows 7 quanto no Windows 8


2
Elina: obrigado pela sua resposta. Existem algumas notas na parte inferior deste documento do MSDN ( msdn.microsoft.com/en-us/library/… ) que alertam sobre possíveis bloqueios se você ler o final dos fluxos stdout e stderr redirecionados de forma síncrona. É difícil dizer se sua solução é suscetível a esse problema. Além disso, parece que você está enviando a saída stdout / stderr do processo de volta como entrada. Por quê? :)
Matthew Piatt

3

Tentei fazer uma classe que resolvesse seu problema usando a leitura assíncrona do fluxo, levando em consideração as respostas de Mark Byers, Rob e stevejay. Ao fazer isso, percebi que havia um erro relacionado à leitura assíncrona do fluxo de saída do processo.

Eu relatei esse bug na Microsoft: https://connect.microsoft.com/VisualStudio/feedback/details/3119134

Resumo:

Você não pode fazer isso:

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

Você receberá System.InvalidOperationException: StandardOut não foi redirecionado ou o processo ainda não foi iniciado.

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

Então você deve iniciar a leitura assíncrona da saída após o início do processo:

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

Fazendo isso, crie uma condição de corrida porque o fluxo de saída pode receber dados antes de configurá-lo como assíncrono:

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

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

Algumas pessoas podem dizer que você só precisa ler o fluxo antes de defini-lo como assíncrono. Mas o mesmo problema ocorre. Haverá uma condição de corrida entre a leitura síncrona e definir o fluxo no modo assíncrono.

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

Não há como obter a leitura assíncrona segura de um fluxo de saída de um processo da maneira real "Process" e "ProcessStartInfo" foram projetados.

Provavelmente é melhor usar a leitura assíncrona, como sugerido por outros usuários para o seu caso. Mas você deve estar ciente de que poderá perder algumas informações devido às condições da corrida.


1

Eu acho que essa é uma abordagem simples e melhor (não precisamos 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;
    }
}

É verdade, mas você não deveria estar fazendo .FileName = Path + @"\ggsci.exe" + @" < obeycommand.txt"para simplificar seu código também? Ou talvez algo equivalente a "echo command | " + Path + @"\ggsci.exe"se você realmente não deseja usar um arquivo obeycommand.txt separado.
Amit Naidu

3
Sua solução não precisa do AutoResetEvent, mas você faz uma pesquisa. Quando você pesquisa em vez de usar o evento (quando eles estão disponíveis), usa a CPU sem motivo e isso indica que você é um programador ruim. Sua solução é muito ruim quando comparada com a outra usando o AutoResetEvent. (Mas eu não lhe dei -1 porque você tentou ajudar!).
22678 Eric Clan,

1

Nenhuma das respostas acima está fazendo o trabalho.

A solução Rob trava e a solução 'Mark Byers' recebe a exceção descartada (tentei as "soluções" das outras respostas).

Então decidi sugerir outra solução:

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

Este código depurou e funciona perfeitamente.


1
Boa! observe que o parâmetro token não é fornecido ao chamar o GetProcessOutputWithTimeoutmétodo.
S.Serpooshan

1

Introdução

A resposta atualmente aceita não funciona (lança exceção) e há muitas soluções alternativas, mas nenhum código completo. Obviamente, isso está desperdiçando muito tempo das pessoas, porque esta é uma pergunta popular.

Combinando a resposta de Mark Byers e a resposta de Karol Tyl, escrevi o código completo com base em como desejo usar o método Process.Start.

Uso

Eu o usei para criar uma caixa de diálogo de progresso em torno dos comandos git. É assim que eu o usei:

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

Em teoria, você também pode combinar stdout e stderr, mas eu não testei isso.

Código

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

Ainda obtém System.ObjectDisposedException: o identificador seguro também foi fechado nesta versão.
27518 Matt

1

Eu sei que esse é o jantar antigo, mas, depois de ler esta página inteira, nenhuma das soluções funcionou para mim, embora eu não tenha experimentado Muhammad Rehan, pois o código era um pouco difícil de seguir, embora eu ache que ele estava no caminho certo . Quando digo que não funcionou, isso não é inteiramente verdade, às vezes funcionaria bem, acho que tem algo a ver com o comprimento da saída antes de uma marca EOF.

De qualquer forma, a solução que funcionou para mim foi usar threads diferentes para ler o StandardOutput e o StandardError e escrever as mensagens.

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

Espero que isso ajude alguém que pensou que isso poderia ser tão difícil!


Exceção: sw.FlushAsync(): Object is not set to an instance of an object. sw is null. Como / onde deve swser definido?
Wallyk # 03/18

1

Depois de ler todas as postagens aqui, decidi pela solução consolidada de Marko Avlijaš. No entanto , não resolveu todos os meus problemas.

Em nosso ambiente, temos um serviço do Windows que está programado para executar centenas de arquivos .bat .cmd .exe, ... etc. diferentes que se acumularam ao longo dos anos e foram escritos por muitas pessoas diferentes e em estilos diferentes. Não temos controle sobre a gravação dos programas e scripts, somos apenas responsáveis ​​pelo agendamento, execução e relatório de sucesso / falha.

Então, eu tentei praticamente todas as sugestões aqui com diferentes níveis de sucesso. A resposta de Marko foi quase perfeita, mas quando executada como um serviço, ela nem sempre capturava o padrão. Eu nunca cheguei ao fundo do porque não.

A única solução que encontramos que funciona em TODOS os nossos casos é a seguinte: http://csharptest.net/319/using-the-processrunner-class/index.html


Vou tentar esta biblioteca. Eu defini o escopo do código e parece estar usando delegados de maneira sensata. É bem embalado em Nuget. Basicamente, cheira a profissionalismo, algo que eu nunca poderia ser acusado. Se morder, dirá.
9788 Steve Hibbert #

O link para o código fonte está morto. Da próxima vez, copie o código para a resposta.
Vitaly Zdanevich

1

Solução alternativa que acabei usando para evitar toda a complexidade:

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

Então, eu crio um arquivo temporário, redireciono a saída e o erro para ele usando > outputfile > 2>&1e depois leio o arquivo após o término do processo.

As outras soluções são adequadas para cenários em que você deseja fazer outras coisas com a saída, mas para coisas simples, isso evita muita complexidade.


1

Eu li muitas das respostas e fiz as minhas. Não tenho certeza se este será corrigido em qualquer caso, mas no meu ambiente. Eu apenas não estou usando WaitForExit e uso WaitHandle.WaitAll nos sinais de saída e erro final. Ficarei feliz se alguém perceber possíveis problemas com isso. Ou se isso vai ajudar alguém. Para mim, é melhor porque não usa tempos limite.

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

Eu usei isso e embrulhei com Task.Run para lidar com o tempo limite, também retorno processid para matar com o tempo limite
plus5volt

0

Eu acho que com o assíncrono, é possível ter uma solução mais elegante e não ter impasses, mesmo ao usar o standardOutput e o 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.
    }
}

É baseado na resposta de Mark Byers. Se você não estiver em um método assíncrono, poderá usar em string output = tStandardOutput.result;vez deawait



-1

Esta postagem pode estar desatualizada, mas descobri a principal causa pela qual geralmente travar devido ao estouro de pilha do redirectStandardoutput ou se você tiver redirectStandarderror.

Como os dados de saída ou os dados de erro são grandes, isso causa um tempo de espera, pois ainda está sendo processado por tempo indeterminado.

para resolver esse problema:

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

11
O problema é que as pessoas explicitamente as definem como verdadeiras porque desejam poder acessar essas transmissões! Caso contrário, podemos simplesmente deixá-los falsos.
user276648

-1

Vamos chamar o código de exemplo postado aqui como redirecionador e o outro programa como redirecionado. Se fosse eu, provavelmente escreveria um programa de teste redirecionado que pode ser usado para duplicar o problema.

Então eu fiz. Para dados de teste, usei o PDF de Especificação de Linguagem CMA ECMA-334; é cerca de 5MB. A seguir, é a parte importante disso.

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

O valor do tamanho dos dados não corresponde ao tamanho real do arquivo, mas isso não importa. Não está claro se um arquivo PDF sempre usa CR e LF no final das linhas, mas isso não importa. Você pode usar qualquer outro arquivo de texto grande para testar.

Usando isso, o código de redirecionador de amostra trava quando eu escrevo a grande quantidade de dados, mas não quando eu escrevo uma pequena quantidade.

Tentei muito rastrear a execução desse código e não consegui. Comentei as linhas do programa redirecionado que desabilitaram a criação de um console para o programa redirecionado para tentar obter uma janela separada do console, mas não consegui.

Então eu encontrei Como iniciar um aplicativo de console em uma nova janela, na janela dos pais ou em nenhuma janela . Então, aparentemente, não podemos (facilmente) ter um console separado quando um programa de console inicia outro programa de console sem o ShellExecute e, como o ShellExecute não suporta redirecionamento, devemos compartilhar um console, mesmo se não especificarmos nenhuma janela para o outro processo.

Presumo que, se o programa redirecionado preenche um buffer em algum lugar, ele deve aguardar a leitura dos dados e, se nesse ponto, nenhum dado for lido pelo redirecionador, será um impasse.

A solução é não usar ReadToEnd e ler os dados enquanto os dados estão sendo gravados, mas não é necessário usar leituras assíncronas. A solução pode ser bastante simples. O seguinte funciona para mim com o PDF de 5 MB.

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

Outra possibilidade é usar um programa GUI para fazer o redirecionamento. O código anterior funciona em um aplicativo WPF, exceto com modificações óbvias.


-3

Eu estava tendo o mesmo problema, mas o motivo era diferente. No entanto, isso aconteceria no Windows 8, mas não no Windows 7. A seguinte linha parece ter causado o problema.

pProcess.StartInfo.UseShellExecute = False

A solução foi NÃO desativar o UseShellExecute. Agora recebi uma janela pop-up do Shell, que é indesejável, mas muito melhor do que o programa esperando que nada de especial aconteça. Então, adicionei a seguinte solução alternativa para isso:

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

Agora, a única coisa que me incomoda é por que isso está acontecendo no Windows 8 em primeiro lugar.


1
Você precisa UseShellExecuteser definido como false se desejar redirecionar a saída.
Brad Moore
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.