ProcessStartInfo挂在“ WaitForExit”上吗?为什么?


186

我有以下代码:

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

我知道我正在启动的进程的输出大约7MB长。在Windows控制台中运行它可以正常工作。不幸的是,它以编程方式无限期地挂在WaitForExit上。还要注意,对于较小的输出(如3KB),此代码不会挂起。

ProcessStartInfo中的内部StandardOutput是否可能无法缓冲7MB?如果是这样,我应该怎么做呢?如果没有,我在做什么错?


任何具有完整源代码的最终解决方案?
Kiquenet

2
我遇到同样的问题,这我怎么能解决这个问题stackoverflow.com/questions/2285288/...
Bedasso

6
是的,最终解决方案:交换最后两行。在手册中
阿米特·奈杜

4
来自msdn:代码示例通过在p.WaitForExit之前调用p.StandardOutput.ReadToEnd来避免死锁情况。如果父进程在p.StandardOutput.ReadToEnd之前调用p.WaitForExit,并且子进程写入足够的文本以填充重定向的流,则可能导致死锁。父进程将无限期地等待子进程退出。子进程将无限期等待父进程从完整的StandardOutput流中读取。
卡洛斯·刘

正确执行此操作有多烦人。很高兴通过更简单的命令行重定向> outputfile解决它:)
eglasius

Answers:


392

问题是,如果您重定向StandardOutput和/或StandardError内部缓冲区可能变满。无论您使用什么顺序,都可能出现问题:

  • 如果您在读取之前等待进程退出,则StandardOutput该进程可能会阻止尝试对其进行写入,因此该进程永远不会结束。
  • 如果从阅读StandardOutput使用ReadToEnd的,然后你的过程可以阻止如果过程永远不会关闭StandardOutput(例如,如果它永远不会终止,或者如果它被阻止写入StandardError)。

解决方案是使用异步读取来确保缓冲区未满。为了避免任何死锁并收集两者的所有输出StandardOutputStandardError您可以执行以下操作:

编辑:请参阅下面的答案,以了解如果发生超时,如何避免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分钟内将其修复。干得好!
Ben Gripka

1
@AlexPeck问题是作为控制台应用程序运行。汉斯·帕桑特(Hans Passant)在这里发现了问题:stackoverflow.com/a/16218470/279516
鲍勃·霍恩

5
每次命令提示符关闭时,都会出现:mscorlib.dll中发生“ System.ObjectDisposed”类型的未处理异常附加信息:安全句柄已关闭
user1663380 2014年

3
我们遇到了与上述@ user1663380类似的问题。您是否认为using事件处理程序的语句可能需要高于using流程本身的语句?
丹·福布斯

2
我认为不需要等待句柄。按照msdn,仅使用非超时版本的WaitForExit结束:当标准输出已重定向到异步事件处理程序时,此方法返回时,输出处理可能未完成。为确保异步事件处理已完成,请在从此重载收到true后,调用不带任何参数的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();则会出现死锁/挂起。
克里斯S

3
真正。我也有类似情况。在进程中使用ffmpeg进行转换时,我无缘无故地重定向StandardError,它在StandardError流中写入了足够的内容以创建死锁。
莱昂佩尔蒂埃

即使重定向和读取标准输出,这对于我来说仍然挂起。
user3791372 '17

@ user3791372我猜这仅在StandardOutput后面的缓冲区未完全填充时才适用。在这里,MSDN不能做到公正。我建议您阅读的一篇很棒的文章在:dzone.com/articles/async-io-and-threadpool
Cary

19

马克·拜尔斯(Mark Byers)的回答很好,但我只补充以下内容:

OutputDataReceivedErrorDataReceived代表们需要之前被删除outputWaitHandle,并errorWaitHandle得到安置。如果在超过超时时间后该过程继续输出数据然后终止,则outputWaitHandleerrorWaitHandle变量将在被处置后被访问。

(仅供参考,因为我无法评论他的帖子,因此我不得不将此警告添加为答案。)


2
也许最好调用CancelOutputRead吗?
Mark Byers 2012年

将Mark的编辑代码添加到此答案中将非常棒!我现在有完全相同的问题。
ianbailey 2013年

8
@ianbailey解决此问题的最简单方法是将using(进程p ...)放在using(AutoResetEvent errorWaitHandle ...)内
Didier A.

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,并erroroutputBuilder?有人可以提供有效的完整答案吗?
MarkoAvlijaš17年

System.ObjectDisposedException:对于我来说,此版本上的安全句柄也已关闭
Matt

17

这是针对.NET 4.5及更高版本的,更现代的,基于任务并行库(TPL)的解决方案。

使用范例

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事件之前结束。我的建议-在所有注册后开始该过程。
Stas Boyarincev,

@StasBoyarincev谢谢,更新。我忘记了使用此更改来更新StackOverflow答案。
穆罕默德·雷汉

1
@MuhammadRehanSaeed另一件事-似乎不允许在process.Start之前调用process.BeginOutputReadLine()或process.BeginErrorReadLine()。在这种情况下,我会收到错误消息:StandardOut尚未重定向或该进程尚未开始。
Stas Boyarincev,

8

罗布回答了这个问题,为我节省了几个小时的审判。在等待之前读取输出/错误缓冲区:

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

1
但是如果您致电后又收到更多数据WaitForExit()怎么办?
knocte

基于我的测试的@knocte ReadToEnd或类似的方法(如StandardOutput.BaseStream.CopyTo)将在读取所有数据后返回。此后什么也没发生
S.Serpooshan

您是说ReadToEnd()也等待退出吗?
knocte

2
@knocte您试图理解由Microsoft创建的API?
aaaaaa

相应的MSDN页面的问题是,它没有说明StandardOutput后面的缓冲区可能已满,并且在这种情况下,子级必须停止写入并等待缓冲区耗尽(父级读取了缓冲区中的数据)。 。ReadToEnd()只能同步读取,直到关闭缓冲区或缓冲区已满,或者子级退出且缓冲区未满为止。那是我的理解。
卡里

7

我们也有这个问题(或一个变体)。

尝试以下方法:

1)将超时添加到p.WaitForExit(nnnn); 其中nnnn以毫秒为单位。

2)将ReadToEnd调用放在WaitForExit调用之前。这就是我们推荐的MS。


5

感谢EM0https://stackoverflow.com/a/17600012/4151626

由于内部超时以及所产生的应用程序同时使用StandardOutput和StandardError,其他解决方案(包括EM0)仍然对我的应用程序僵持。这是对我有用的东西:

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

我重定向了输入,输出和错误,并处理了来自输出和错误流的读取。此解决方案适用于Windows 7和Windows 8的SDK 7- 8.1。


2
埃琳娜:谢谢你的回答。此MSDN文档(msdn.microsoft.com/zh-cn/library/…)的底部有一些注释,如果您同时读到重定向的stdout和stderr流的末尾,它们会警告潜在的死锁。很难确定您的解决方案是否容易受到此问题的影响。同样,您似乎正在将流程的stdout / stderr输出直接作为输入发送回去。为什么?:)
马修·皮亚特

3

我尝试通过考虑Mark Byers,Rob,stevejay的回答,尝试制作一个使用异步流读取解决您的问题的类。这样做我意识到存在与异步过程输出流读取相关的错误。

我在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,但可以轮询。当您轮询而不是使用事件(当事件可用时)时,则您无缘无故地在使用CPU,这表明您是一个不好的程序员。与使用AutoResetEvent的解决方案相比,您的解决方案确实很糟糕。(但是我没有给您-1,因为您试图提供帮助!)。
Eric Ouellet 2014年

1

上面的答案都没有完成这项工作。

Rob解决方案挂起,“ Mark Byers”解决方案获得处置异常。(我尝试了其他答案的“解决方案”)。

因此,我决定建议另一种解决方案:

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方法时未提供token参数。
S.Serpooshan

1

介绍

当前接受的答案不起作用(引发异常),解决方法太多,但没有完整的代码。显然,这是在浪费很多人的时间,因为这是一个很普遍的问题。

结合Mark Byers的答案和Karol Tyl的答案,我根据想要使用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

我知道这已经晚了,但是在阅读了整页之后,没有一种解决方案对我有用,尽管我没有尝试使用Muhammad Rehan,因为代码很难理解,尽管我猜他是在正确的轨道上。当我说这并不完全正确时,有时它会很好,我想这与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

在阅读完所有此处的帖子后,我决定使用MarkoAvlijaš的合并解决方案。 但是,它并不能解决我的所有问题。

在我们的环境中,我们有一个Windows服务,该服务计划运行数百个不同的.bat .cmd .exe等文件,这些文件已经积累了多年,并由许多不同的人以不同的风格编写。我们无法控制程序和脚本的编写,我们仅负责计划,运行和报告成功/失败。

因此,我在这里尝试了几乎所有建议,但都获得了不同程度的成功。Marko的回答几乎是完美的,但是当作为服务运行时,它并不总是捕获stdout。我从来没有深入了解为什么不这样做。

我们发现适用于所有情况的唯一解决方案是:http : //csharptest.net/319/using-the-processrunner-class/index.html


我要尝试这个图书馆。我已经确定了代码的范围,并且看起来明智地使用了委托。它很好地包装在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,然后在过程完成后才读取该文件。

对于要对输出执行其他操作的方案,其他解决方案也很好,但是对于简单的操作,这可以避免很多复杂性。


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

我认为,使用async时,即使同时使用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.
    }
}

它基于Mark Byers的答案。如果您不在异步方法中,则可以使用string output = tStandardOutput.result;代替await



-1

这篇文章也许已经过时,但我发现它通常挂起的主要原因是由于redirectStandardoutput的堆栈溢出或您是否具有redirectStandarderror。

由于输出数据或错误数据很大,将导致挂起时间,因为它仍在无限期地进行处理。

因此要解决此问题:

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

11
问题在于人们明确地将其设置为true,因为他们希望能够访问这些流!否则,我们确实可以将它们设为假。
user276648 2013年

-1

让我们称这里发布的示例代码为重定向器,将另一个程序称为重定向。如果是我,那么我可能会写一个测试重定向程序,该程序可以用来重现该问题。

所以我做了。对于测试数据,我使用了ECMA-334 C#语言规范v PDF;它约为5MB。以下是其中的重要部分。

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

datasize值与实际文件大小不匹配,但这无关紧要。目前尚不清楚PDF文件是否始终在行尾使用CR和LF,但这无关紧要。您可以使用任何其他大型文本文件进行测试。

使用该示例重定向器代码会在我写入大量数据时挂起,而在写入少量数据时不会挂起。

我做了很多尝试以某种方式跟踪该代码的执行,但我做不到。我注释了重定向程序的各行内容,这些行禁用了为重定向程序创建控制台的尝试,以尝试获取单独的控制台窗口,但我做不到。

然后,我找到了如何在新窗口,父窗口或无窗口中启动控制台应用程序。因此,很明显,当一个控制台程序在没有ShellExecute的情况下启动另一个控制台程序时,我们不能(轻松地)拥有一个单独的控制台,并且由于ShellExecute不支持重定向,即使我们未为其他进程指定窗口,我们也必须共享一个控制台。

我假设,如果重定向的程序在某个地方填满了缓冲区,则它必须等待数据被读取,并且如果此时重定向程序没有读取任何数据,则它是一个死锁。

解决方案是不使用ReadToEnd并在写入数据时读取数据,但是不必使用异步读取。解决方案可能非常简单。以下内容适用于5 MB 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();

另一种可能性是使用GUI程序进行重定向。前面的代码在WPF应用程序中有效,除非进行了明显的修改。


-3

我遇到了同样的问题,但原因有所不同。但是,这将在Windows 8下发生,但在Windows 7下不会发生。以下行似乎引起了问题。

pProcess.StartInfo.UseShellExecute = False

解决方案是不禁用UseShellExecute。现在,我收到了一个Shell弹出窗口,这是不需要的,但是比等待没有特别事情发生的程序好得多。因此,我为此添加了以下解决方法:

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

现在唯一令我困扰的是,为什么这首先在Windows 8下发生。


1
UseShellExecute如果要重定向输出,则需要将其设置为false。
布拉德·摩尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.