StandardOutput.ReadToEnd() 卡住了

57

我有一个经常使用外部程序并读取其输出的程序。 通常使用进程重定向输出它可以很好地工作,但是当我尝试读取一个特定的参数时却出现了挂起的情况,没有错误消息-没有异常,当它到达那一行时就会“停止”。 当然,我使用了一个集中式函数来调用和读取程序的输出,如下:

public string ADBShell(string adbInput)
{
    try
    {
        //Create Empty values
        string result = string.Empty;
        string error = string.Empty;
        string output = string.Empty;
        System.Diagnostics.ProcessStartInfo procStartInfo 
            = new System.Diagnostics.ProcessStartInfo(toolPath + "adb.exe");

        procStartInfo.Arguments = adbInput;
        procStartInfo.RedirectStandardOutput = true;
        procStartInfo.RedirectStandardError = true;
        procStartInfo.UseShellExecute = false;
        procStartInfo.CreateNoWindow = true;
        procStartInfo.WorkingDirectory = toolPath;
        System.Diagnostics.Process proc = new System.Diagnostics.Process();
        proc.StartInfo = procStartInfo;
        proc.Start();
        // Get the output into a string
        proc.WaitForExit();
        result = proc.StandardOutput.ReadToEnd();
        error = proc.StandardError.ReadToEnd();  //Some ADB outputs use this
        if (result.Length > 1)
        {
            output += result;
        }
        if (error.Length > 1)
        {
            output += error;
        }
        Return output;
    }
    catch (Exception objException)
    {
        throw objException;
    }
}

出现问题的代码行是result = proc.StandardOutput.ReadToEnd();,但并非每次都会出现问题,只有在发送特定参数("start-server")时才会出现。所有其他参数正常工作-它读取值并将其返回。

它挂起的方式也很奇怪。它不会冻结或报错,它只是停止处理。就好像是一个“返回”命令一样,但它甚至不会返回到调用函数,它只是停止了一切,接口仍然保持运行状态。

有人以前遇到过这种情况吗?有人知道我应该尝试什么吗?我认为这是流本身中的某些意外情况,但是否有办法处理/忽略它,使它仍然可以读取?


1
这是我找到解决[同样]问题的地方:https://dev59.com/bnVC5IYBdhLWcg3w9F89 - ganders
你可能会对这篇文章感兴趣:http://www.codeducky.org/process-handling-net,它解释了如何处理.NET进程流的死锁问题。此外,还有一个名为MedallionShell的库(https://github.com/madelson/MedallionShell),可以简化处理进程IO流的操作。 - ChaseMedallion
在控制台中使用相同的参数运行相同的程序。如果它提示用户进行交互,例如在警告后输入密码或确认,那么这可能是原因。例如,pgAdmin(postgress数据库管理)会在要求输入未在其配置文件中的数据库密码时挂起。但是,只有在从控制台运行时才能看到这一点。 - profimedica
如果您使用了输入流,这个答案适用于您:https://dev59.com/q2w05IYBdhLWcg3wkirX#29118547 - Mr. Squirrel.Downy
9个回答

61

BeginOutputReadLine()提供的解决方案是一个好方法,但在某些情况下不适用,因为进程(特别是使用WaitForExit())退出得比异步输出完成更早。

因此,我尝试了同步实现,并发现解决方案是使用StreamReader类的Peek()方法。我添加了Peek() > -1的检查以确保它不是流的结尾,就像MSDN文章中所描述的那样,最终它奏效并停止挂起!

这里是代码:

var process = new Process();
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.WorkingDirectory = @"C:\test\";
process.StartInfo.FileName = "test.exe";
process.StartInfo.Arguments = "your arguments here";

process.Start();
var output = new List<string>();

while (process.StandardOutput.Peek() > -1)
{
    output.Add(process.StandardOutput.ReadLine());
}

while (process.StandardError.Peek() > -1)
{
    output.Add(process.StandardError.ReadLine());
}
process.WaitForExit();

8
我刚刚实施了这个更改,但我的进程仍然停在 process.StandardError.ReadLine() 这一行代码处。 - ganders
7
我已经解决了问题,但是我实施了另一个人对不同问题的答案,这是我使用的答案链接:https://dev59.com/bnVC5IYBdhLWcg3w9F89 - ganders
1
经过重新检查,实际上它是“工作”的,但无法从进程中获取任何输出。我在这里找到了一个可行的解决方案:https://dev59.com/bnVC5IYBdhLWcg3w9F89 - Hoàng Long
2
不妨检查一下 process.StandardOutput.EndOfStream 是否为真?使用 process.StandardOutput.Peek() > -1,只会显示我多行输出的第一行。 - evandrix
2
@ganders,streamwriter实现中已知存在一个bug,当在空输出上使用Peek()时,它将永远挂起。 - RikuPotato
显示剩余7条评论

20
问题在于你正在使用同步的ReadToEnd方法来读取StandardOutputStandardError流。这可能导致你正在遇到的潜在死锁。这甚至在MSDN中有描述。解决方案也在那里说明。基本上,它是:使用异步版本的BeginOutputReadLine来读取StandardOutput流的数据:
p.BeginOutputReadLine();
string error = p.StandardError.ReadToEnd();
p.WaitForExit();

使用BeginOutputReadLine实现异步读取的实现方法见ProcessStartInfo hanging on "WaitForExit"? Why?


3
谢谢回复。 恐怕这个方法不起作用,它仍然在执行到“StandardError.ReadToEnd();”时挂起。 我甚至尝试使用“BeginErrorReadLine();”,但它也挂起了。 唯一有效的方法是在“WaitForExit”中添加超时。由于这个特定的参数几乎立即输出,我将其设置为大约3秒钟的超时时间,一切都正常工作。虽然不太优雅,但它确实有效。 再次感谢您的帮助。 - Elad Avron
这个解决方案可行,而且它是 MSDN 文档中所描述的解决方案。 - robob

6

那么,类似这样的东西怎么样:

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

process.OutputDataReceived += (sender, args) =>
{
    var outputData = args.Data;
    // ...
};
process.ErrorDataReceived += (sender, args) =>
{
    var errorData = args.Data;
    // ...
};
process.WaitForExit();

就差一点了。在打开泵之前设置事件处理程序。然后在最后移除事件处理程序。 - HackSlash
离成功已经很近了。在打开水泵之前设置事件处理程序。然后在结束时移除事件处理程序。 - undefined

4

我遇到了同样的死锁问题。这段代码片段对我有用。

ProcessStartInfo startInfo = new ProcessStartInfo("cmd")
{
    WindowStyle = ProcessWindowStyle.Hidden,
    UseShellExecute = false,
    RedirectStandardInput = true,
    RedirectStandardOutput = true,
    CreateNoWindow = true
};

Process process = new Process();
process.StartInfo = startInfo;
process.Start();
process.StandardInput.WriteLine("echo hi");
process.StandardInput.WriteLine("exit");
var output = process.StandardOutput.ReadToEnd();
process.Dispose();

3
我遇到了同样的问题,错误一直存在。
根据你对Daniel Hilgarth的回答,我甚至没有尝试使用那些代码,虽然我认为它们对我来说可能有效。
由于我想要能够进行一些更高级的输出,最终我决定将两个输出都放在后台线程中完成。
public static class RunCommands
{
    #region Outputs Property

    private static object _outputsLockObject;
    private static object OutputsLockObject
    { 
        get
        {
            if (_outputsLockObject == null)
                Interlocked.CompareExchange(ref _outputsLockObject, new object(), null);
            return _outputsLockObject;
        }
    }

    private static Dictionary<object, CommandOutput> _outputs;
    private static Dictionary<object, CommandOutput> Outputs
    {
        get
        {
            if (_outputs != null)
                return _outputs;

            lock (OutputsLockObject)
            {
                _outputs = new Dictionary<object, CommandOutput>();
            }
            return _outputs;
        }
    }

    #endregion

    public static string GetCommandOutputSimple(ProcessStartInfo info, bool returnErrorIfPopulated = true)
    {
        // Redirect the output stream of the child process.
        info.UseShellExecute = false;
        info.CreateNoWindow = true;
        info.RedirectStandardOutput = true;
        info.RedirectStandardError = true;
        var process = new Process();
        process.StartInfo = info;
        process.ErrorDataReceived += ErrorDataHandler;
        process.OutputDataReceived += OutputDataHandler;

        var output = new CommandOutput();
        Outputs.Add(process, output);

        process.Start();

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

        // Wait for the process to finish reading from error and output before it is finished
        process.WaitForExit();

        process.ErrorDataReceived -= ErrorDataHandler;
        process.OutputDataReceived -= OutputDataHandler;

        Outputs.Remove(process);

        if (returnErrorIfPopulated && (!String.IsNullOrWhiteSpace(output.Error)))
        {
            return output.Error.TrimEnd('\n');
        }

        return output.Output.TrimEnd('\n');
    }

    private static void ErrorDataHandler(object sendingProcess, DataReceivedEventArgs errLine)
    {
        if (errLine.Data == null)
            return;

        if (!Outputs.ContainsKey(sendingProcess))
            return;

        var commandOutput = Outputs[sendingProcess];

        commandOutput.Error = commandOutput.Error + errLine.Data + "\n";
    }

    private static void OutputDataHandler(object sendingProcess, DataReceivedEventArgs outputLine)
    {
        if (outputLine.Data == null)
            return;

        if (!Outputs.ContainsKey(sendingProcess))
            return;

        var commandOutput = Outputs[sendingProcess];

        commandOutput.Output = commandOutput.Output + outputLine.Data + "\n";
    }
}
public class CommandOutput
{
    public string Error { get; set; }
    public string Output { get; set; }

    public CommandOutput()
    {
        Error = "";
        Output = "";
    }
}

这对我很有效,让我不必使用读取超时。

非常感谢您实现了来自MSDN的参考!您唯一遗漏的是在最后没有移除事件处理程序:process.ErrorDataReceived -= ErrorDataHandler;process.OutputDataReceived -= OutputDataHandler; - HackSlash
非常感谢您根据MSDN的参考进行实施!唯一遗漏的是您没有在最后移除事件处理程序:process.ErrorDataReceived -= ErrorDataHandler;process.OutputDataReceived -= OutputDataHandler; - undefined
我认为垃圾回收已经会将它们清除,所以这并不重要。然而,为了保险起见,我已经编辑了代码,包括了你的建议。 - Matt Vukomanovic
我认为垃圾回收已经会将它们清除,所以这并不重要。不过,为了保险起见,我已经修改了代码,包含了你的建议。 - undefined
1
为了防止资源泄漏,在释放订阅者对象之前,您应该取消订阅事件。在取消订阅事件之前,发布对象中潜在的多播委托会引用封装了订阅者事件处理程序的委托。只要发布对象保持该引用,垃圾回收将不会删除您的订阅者对象。 - HackSlash
1
为了防止资源泄漏,在释放订阅对象之前,您应该取消订阅事件。在取消订阅事件之前,发布对象中潜在的多播委托将引用封装了订阅者事件处理程序的委托。只要发布对象保持该引用,垃圾回收将不会删除您的订阅对象。 - undefined

2

有一种优雅的解决方案适合我:

Process nslookup = new Process()
{
   StartInfo = new ProcessStartInfo("nslookup")
   {
      RedirectStandardInput = true,
      RedirectStandardOutput = true,
      UseShellExecute = false,
      CreateNoWindow = true,
      WindowStyle = ProcessWindowStyle.Hidden
   }
};

nslookup.Start();
nslookup.StandardInput.WriteLine("set type=srv");
nslookup.StandardInput.WriteLine("_ldap._tcp.domain.local"); 

nslookup.StandardInput.Flush();
nslookup.StandardInput.Close();

string output = nslookup.StandardOutput.ReadToEnd();

nslookup.WaitForExit();
nslookup.Close();

我在这里找到了答案(来源),窍门在于使用Flush()Close()来处理标准输入。


如果你在使用输入流时遇到了问题,这个答案是正确的方法。 - Mr. Squirrel.Downy

2

对我来说,被接受的答案的解决方案没有起作用。我必须使用任务来避免死锁:

//Code to start process here

String outputResult = GetStreamOutput(process.StandardOutput);
String errorResult = GetStreamOutput(process.StandardError);

process.WaitForExit();

以下是一个名为GetStreamOutput的函数:

private string GetStreamOutput(StreamReader stream)
{
   //Read output in separate task to avoid deadlocks
   var outputReadTask = Task.Run(() => stream.ReadToEnd());

   return outputReadTask.Result;
}

1
我最喜欢这个答案,即使MSDN推荐使用单独的线程/任务,但它仍然会死锁。 - ergohack
即使是这个变体也会死锁:string cvout = (Task<string>.Run(async () => { return await p.StandardOutput.ReadToEndAsync(); })).Result; - ergohack
即使是这个变量也会死锁:string cvout = (Task<string>.Run(async () => { return await p.StandardOutput.ReadToEndAsync().ConfigureAwait(false); })).Result; - ergohack
实际上,上述情况会死锁几分钟,直到一些底层线程超时,然后才会继续执行。这种延迟是不可接受的,但给了我一线希望。 - ergohack
这是我的最终成果:https://dev59.com/bnVC5IYBdhLWcg3w9F89#47213952 - ergohack

0

如果有人想要使用Windows Forms和TextBox(或RichTextBox)实时显示进程返回的错误和输出结果(因为它们被写入process.StandardOutput / process.StandardError),请注意以下内容。

您需要使用OutputDataReceived() / ErrorDataReceived()来读取两个流,以避免死锁。否则,我所知道的没有其他方法可以避免死锁,即使是Fedor的答案,它现在拥有“Answer”标签并且是最受欢迎的答案,对我也不起作用。

然而,当您使用RichTextBox(或TextBox)输出数据时,您会遇到另一个问题,即如何实时将数据写入文本框中。您只能从主线程中的AppendText()访问后台线程OutputDataReceived() / ErrorDataReceived()中的数据。

我最初尝试的是在后台线程中调用process.Start(),然后在主线程执行process.WaitForExit()的同时,在OutputDataReceived() / ErrorDataReceived()线程中调用BeginInvoke() => AppendText()
然而,这导致我的窗体冻结并最终永久挂起。经过几天的尝试,我最终得到了下面的解决方案,似乎效果不错。
简而言之,您需要在OutputDataReceived() / ErrorDataReceived()线程中将消息添加到并发集合中,而主线程应不断尝试从该集合中提取消息并将其附加到文本框中:
            ProcessStartInfo startInfo
                = new ProcessStartInfo(File, mysqldumpCommand);

            process.StartInfo.FileName = File;
            process.StartInfo.Arguments = mysqldumpCommand;
            process.StartInfo.CreateNoWindow = true;
            process.StartInfo.UseShellExecute = false;
            process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
            process.StartInfo.RedirectStandardInput = false;
            process.StartInfo.RedirectStandardError = true;
            process.StartInfo.RedirectStandardOutput = true;
            process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
            process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
            process.EnableRaisingEvents = true;

            ConcurrentQueue<string> messages = new ConcurrentQueue<string>();

            process.ErrorDataReceived += (object se, DataReceivedEventArgs ar) =>
            {
                string data = ar.Data;
                if (!string.IsNullOrWhiteSpace(data))
                    messages.Enqueue(data);
            };
            process.OutputDataReceived += (object se, DataReceivedEventArgs ar) =>
            {
                string data = ar.Data;
                if (!string.IsNullOrWhiteSpace(data))
                    messages.Enqueue(data);
            };

            process.Start();
            process.BeginErrorReadLine();
            process.BeginOutputReadLine();
            while (!process.HasExited)
            {
                string data = null;
                if (messages.TryDequeue(out data))
                    UpdateOutputText(data, tbOutput);
                Thread.Sleep(5);
            }

            process.WaitForExit();

这种方法唯一的缺点是在进程开始写入消息之间process.Start()process.BeginErrorReadLine() / process.BeginOutputReadLine()的极少数情况下,您可能会丢失消息,请记住这一点。避免这种情况的唯一方法是在进程完成后读取完整的流并(或)仅在获得对它们的访问权限时才读取它们。

-1

第一

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

第二个

 // Do not perform a synchronous read to the end of both 
 // redirected streams.
 // string output = p.StandardOutput.ReadToEnd();
 // string error = p.StandardError.ReadToEnd();
 // p.WaitForExit();
 // Use asynchronous read operations on at least one of the streams.
 p.BeginOutputReadLine();
 string error = p.StandardError.ReadToEnd();
 p.WaitForExit();

这是来自MSDN的内容


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接