从进程中获取实时输出

24

我在我的项目中遇到了一个问题。我想要启动一个名为7z.exe(控制台版本)的进程。 我尝试了三种不同的方法:

  • Process.StandardOutput.ReadToEnd();
  • OutputDataReceived和BeginOutputReadLine
  • StreamWriter

没有一种方法能够正常工作。它总是等待进程结束后才显示我想要的内容。 我没有代码可以提供,只有如果您需要的话,可以用上述方法之一来编写代码。谢谢。

编辑: 我的代码:

        process.StartInfo.UseShellExecute = false;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.CreateNoWindow = true;
        process.Start();

        this.sr = process.StandardOutput;
        while (!sr.EndOfStream)
        {
            String s = sr.ReadLine();
            if (s != "")
            {
                System.Console.WriteLine(DateTime.Now + " - " + s);
            }
        }

或者

process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.OutputDataReceived += new DataReceivedEventHandler(recieve);
process.StartInfo.CreateNoWindow = true;
process.Start();
process.BeginOutputReadLine();
process.WaitForExit();
public void recieve(object e, DataReceivedEventArgs outLine)
{
    System.Console.WriteLine(DateTime.Now + " - " + outLine.Data);
}

或者

process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.Start();
string output = p.StandardOutput.ReadToEnd();
process.WaitForExit();

这里,“process”是我预先创建的进程

好的,我知道为什么它不能正常工作:7z.exe有漏洞:它在控制台中显示百分比加载,并且仅在当前文件完成时发送信息。例如,在提取中,它工作得很好 :)。我将寻找另一种使用7z函数而不使用7z.exe(可能使用7za.exe或某些DLL)的方法。感谢所有人。 回答问题,OutputDataRecieved事件运行良好!


你为什么不使用可从7zip下载的DLL/SDK呢?它比任何基于控制台的技术都能提供更大的控制力。 - Yahia
看一下你使用 Process 的代码会很有帮助,例如在哪里创建 Process。 - MethodMan
因为7z.exe涵盖了我想要的所有功能。 - Extaze
需要帮助。我将尝试使用cmd。我认为问题来自7z.exe。 - Extaze
7个回答

30

看看这个页面,它似乎是你的解决方案:http://msdn.microsoft.com/en-us/library/system.diagnostics.process.beginoutputreadline.aspxhttp://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx

[编辑]这是一个可行的例子:

        Process p = new Process();
        p.StartInfo.RedirectStandardError = true;
        p.StartInfo.RedirectStandardOutput = true;
        p.StartInfo.UseShellExecute = false;
        p.StartInfo.CreateNoWindow = true;
        p.StartInfo.FileName = @"C:\Program Files (x86)\gnuwin32\bin\ls.exe";
        p.StartInfo.Arguments = "-R C:\\";

        p.OutputDataReceived += new DataReceivedEventHandler((s, e) => 
        { 
            Console.WriteLine(e.Data); 
        });
        p.ErrorDataReceived += new DataReceivedEventHandler((s, e) =>
        {
            Console.WriteLine(e.Data);
        });

        p.Start();
        p.BeginOutputReadLine();
        p.BeginErrorReadLine();

顺便说一下,ls -R C:\会递归地列出C盘根目录下的所有文件。这是很多文件,当第一批结果出现在屏幕上时,我确定它还没有全部显示完。

7zip有可能在显示之前就保存了输出结果。我不确定你向进程传递了什么参数。


我所能找到的是,你应该使用事件OutputDataReceived,目前我没有时间测试它,也许以后再说。(http://harbertc.wordpress.com/2006/05/16/reading-text-from-a-process-executed-programmatically-in-c/) - Michiel van Vaardegem
我尝试了所有的方法,但问题出在7z.exe上。我决定使用C#的7z库(SevenZipSharp),问题得到了解决。谢谢 ;) - Extaze
1
我和7-zip以及输出流重定向有完全相同的问题。如果您查看7-zip的输出,很明显它没有执行标准的控制台输出;相反,它似乎在调用控制台函数以重新编写在显示器上的内容以显示进度百分比。之所以直到最后才能得到输出的原因是,它直到最后才将日志作为一种最终日志写入标准控制台输出。 - Triynko
在测试存档时,BeginOutputReadLine()对于7z.exe完美运作。 - SepehrM
2
如果不调用 p.BeginErrorReadLine();,则错误处理程序将无法被调用。 - Red Riding Hood

9
为了正确处理输出和/或错误重定向,您还必须重定向输入。这似乎是您启动的外部应用程序运行时中的一个功能/错误,并且从我到目前为止所看到的情况来看,它没有在其他任何地方提到。
示例用法:
        Process p = new Process(...);

        p.StartInfo.UseShellExecute = false;
        p.StartInfo.RedirectStandardOutput = true;
        p.StartInfo.RedirectStandardError = true;
        p.StartInfo.RedirectStandardInput = true; // Is a MUST!
        p.EnableRaisingEvents = true;

        p.OutputDataReceived += OutputDataReceived;
        p.ErrorDataReceived += ErrorDataReceived;

        Process.Start();

        p.BeginOutputReadLine();
        p.BeginErrorReadLine();

        p.WaitForExit();

        p.OutputDataReceived -= OutputDataReceived;
        p.ErrorDataReceived -= ErrorDataReceived;

...

    void OutputDataReceived(object sender, DataReceivedEventArgs e)
    {
        // Process line provided in e.Data
    }

    void ErrorDataReceived(object sender, DataReceivedEventArgs e)
    {
        // Process line provided in e.Data
    }

3
重定向标准输入对我解决了这个问题。 - Daniel Abbatt
这太不可思议了;我将一些Python脚本转换为exe文件,非常简单的打印操作直到程序退出后才被捕获,直到我指定了RedirectStandardInput。我不明白为什么没有其他人(包括最佳答案的投票者)似乎遇到过这个问题,但我们却遇到了这个问题。 - pete
附言,它仍然非常不稳定。它经常在很长一段时间之后才触发回调或出现错误。而如果我们在控制台中打开,我们总是可以立即看到输出。我想做的就是让它与在控制台中打开时一样。有人知道解决方案吗? - pete

7
我不知道是否还有人在寻找这个问题的解决方案,但出现了几次,因为我正在使用Unity编写一些游戏工具,并且由于某些系统与mono的互操作性受到限制(例如PIA用于从Word中读取文本),我经常不得不编写特定于操作系统的(有时为Windows,有时为MacOS)可执行文件,并从Process.Start()启动它们。

问题是,当您像这样启动可执行文件时,它将在另一个线程中启动,该线程会阻止您的主应用程序,导致挂起。如果您想在此期间向用户提供有用的反馈,超出了各自操作系统生成的旋转图标,那么您就有点进退两难了。使用流行也无效,因为线程仍会被阻塞,直到执行完成。

我找到的解决方案对某些人来说可能看起来很极端,但我发现对我而言非常有效,就是使用套接字和多线程设置可靠的同步通信。当然,这只适用于您编写两个应用程序的情况。如果不是这样,我认为您就没有办法了。...我想测试是否可以使用传统流方法仅使用多线程实现,如果有人愿意尝试并在此处发布结果,那就太好了。

无论如何,这是目前适用于我的解决方案:

在主应用程序或调用应用程序中,我会执行类似于以下操作:

/// <summary>
/// Handles the OK button click.
/// </summary>
private void HandleOKButtonClick() {
string executableFolder = "";

#if UNITY_EDITOR
executableFolder = Path.Combine(Application.dataPath, "../../../../build/Include/Executables");
#else
executableFolder = Path.Combine(Application.dataPath, "Include/Executables");
#endif

EstablishSocketServer();

var proc = new Process {
    StartInfo = new ProcessStartInfo {
        FileName = Path.Combine(executableFolder, "WordConverter.exe"),
        Arguments = locationField.value + " " + _ipAddress.ToString() + " " + SOCKET_PORT.ToString(), 
        UseShellExecute = false,
        RedirectStandardOutput = true,
        CreateNoWindow = true
    }
};

proc.Start();

这里是我建立套接字服务器的地方:

/// <summary>
/// Establishes a socket server for communication with each chapter build script so we can get progress updates.
/// </summary>
private void EstablishSocketServer() {
    //_dialog.SetMessage("Establishing socket connection for updates. \n");
    TearDownSocketServer();

    Thread currentThread;

    _ipAddress = Dns.GetHostEntry(Dns.GetHostName()).AddressList[0];
    _listener = new TcpListener(_ipAddress, SOCKET_PORT);
    _listener.Start();

    UnityEngine.Debug.Log("Server mounted, listening to port " + SOCKET_PORT);

    _builderCommThreads = new List<Thread>();

    for (int i = 0; i < 1; i++) {
        currentThread = new Thread(new ThreadStart(HandleIncomingSocketMessage));
        _builderCommThreads.Add(currentThread);
        currentThread.Start();
    }
}

/// <summary>
/// Tears down socket server.
/// </summary>
private void TearDownSocketServer() {
    _builderCommThreads = null;

    _ipAddress = null;
    _listener = null;
}

以下是线程的socket处理程序...请注意,您有时需要创建多个线程;这就是为什么我在那里有_builderCommThreads列表的原因(我从其他代码中移植过来,其中我正在做类似的事情,但是连续调用多个实例):

/// <summary>
/// Handles the incoming socket message.
/// </summary>
private void HandleIncomingSocketMessage() {
    if (_listener == null) return;

    while (true) {
        Socket soc = _listener.AcceptSocket();
        //soc.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 10000);
        NetworkStream s = null;
        StreamReader sr = null;
        StreamWriter sw = null;
        bool reading = true;

        if (soc == null) break;

        UnityEngine.Debug.Log("Connected: " + soc.RemoteEndPoint);

        try {
            s = new NetworkStream(soc);
            sr = new StreamReader(s, Encoding.Unicode);
            sw = new StreamWriter(s, Encoding.Unicode);
            sw.AutoFlush = true; // enable automatic flushing

            while (reading == true) {
                string line = sr.ReadLine();

                if (line != null) {
                    //UnityEngine.Debug.Log("SOCKET MESSAGE: " + line);
                    UnityEngine.Debug.Log(line);

                    lock (_threadLock) {
                        // Do stuff with your messages here
                    }
                }
            }

            //
        } catch (Exception e) {
            if (s != null) s.Close();
            if (soc != null) soc.Close();
            UnityEngine.Debug.Log(e.Message);
            //return;
        } finally {

        //
        if (s != null) s.Close();
        if (soc != null) soc.Close();

        UnityEngine.Debug.Log("Disconnected: " + soc.RemoteEndPoint);
        }
    }

    return;
}

当然,你需要在顶部声明一些内容:
private TcpListener _listener = null;
private IPAddress _ipAddress = null;
private List<Thread> _builderCommThreads = null;
private System.Object _threadLock = new System.Object();

然后在被调用的可执行文件中,设置另一端(在这种情况下我使用了静态变量,您可以使用任何您想要的):

private static TcpClient _client = null;
private static Stream _s = null;
private static StreamReader _sr = null;
private static StreamWriter _sw = null;
private static string _ipAddress = "";
private static int _port = 0;
private static System.Object _threadLock = new System.Object();

/// <summary>
/// Main method.
/// </summary>
/// <param name="args"></param>
static void Main(string[] args) {
    try {
        if (args.Length == 3) {
            _ipAddress = args[1];
            _port = Convert.ToInt32(args[2]);

            EstablishSocketClient();
        }

        // Do stuff here

        if (args.Length == 3) Cleanup();
    } catch (Exception exception) {
        // Handle stuff here
        if (args.Length == 3) Cleanup();
    }
}

/// <summary>
/// Establishes the socket client.
/// </summary>
private static void EstablishSocketClient() {
    _client = new TcpClient(_ipAddress, _port);

    try {
        _s = _client.GetStream();
        _sr = new StreamReader(_s, Encoding.Unicode);
        _sw = new StreamWriter(_s, Encoding.Unicode);
        _sw.AutoFlush = true;
    } catch (Exception e) {
        Cleanup();
    }
}

/// <summary>
/// Clean up this instance.
/// </summary>
private static void Cleanup() {
    _s.Close();
    _client.Close();

    _client = null;
    _s = null;
    _sr = null;
    _sw = null;
}

/// <summary>
/// Logs a message for output.
/// </summary>
/// <param name="message"></param>
private static void Log(string message) {
    if (_sw != null) {
        _sw.WriteLine(message);
    } else {
        Console.Out.WriteLine(message);
    }
}

我正在使用这个方法在Windows上启动一个命令行工具,该工具使用PIA来从Word文档中提取文本。我尝试在Unity中使用PIA的.dll文件,但遇到了与mono的互操作问题。我还在MacOS上使用它来调用启动批处理模式下的其他Unity实例和在这些实例中运行编辑器脚本的shell脚本,并通过此套接字连接向该工具反馈。这很棒,因为现在我可以发送反馈给用户,进行调试,监视并响应过程中的特定步骤等等。
希望对你有所帮助。

1
虽然从技术上讲不是问题的答案,但它与问题密切相关且非常有帮助。 - PRMan

6
问题是由调用 Process.WaitForExit 方法 引起的。根据文档,这样做的作用是:

设置等待关联进程退出的时间段,并阻塞当前线程的执行,直到时间已经过去或进程已退出。为避免阻塞当前线程,请使用 Exited 事件

因此,为了防止线程阻塞直到进程退出,可以将 Process 对象的 Process.Exited 事件 处理程序连接起来,如下所示。只有 EnableRaisingEvents 属性的值为 true 时,Exited 事件才能发生。
    process.EnableRaisingEvents = true;
    process.Exited += Proc_Exited;


    private void Proc_Exited(object sender, EventArgs e)
    {
        // Code to handle process exit
    }

以这种方式操作,您将能够通过Process.OutputDataReceived事件像当前一样在进程运行时获取输出。 (PS-该事件页面上的代码示例也犯了使用Process.WaitForExit的错误。)
另一个注意点是,在Exited方法触发之前,您需要确保Process对象没有被清除。 如果您的Process在using语句中初始化,则可能会出现问题。

2

我曾经在多个项目中使用了CmdProcessor类,这个类的介绍可以在这里找到。一开始看上去有些令人望而生畏,但实际上非常容易使用。


1

Windows对管道和控制台的处理方式不同。 管道是有缓冲区的,而控制台则没有。 RedirectStandardOutput连接管道。只有两种解决方案。

  1. 更改控制台应用程序,在每次写入后刷新其缓冲区
  2. 编写一个Shim来模拟https://www.codeproject.com/Articles/16163/Real-Time-Console-Output-Redirection中所述的控制台

请注意,RTConsole无法处理STDERR,它也存在相同的问题。

感谢https://stackoverflow.com/users/4139809/jeremy-lakeman与我分享这些信息,这与另一个问题有关。


这是唯一正确的解决方案;解决了长达数月的谜团,我简直不敢相信它还没有得到赞同。在我将我的Python程序中的每个print()调用写成"flush=True"之后,所有问题都得到了解决。 - pete

0

试试这个。

        Process notePad = new Process();

        notePad.StartInfo.FileName = "7z.exe";
        notePad.StartInfo.RedirectStandardOutput = true;
        notePad.StartInfo.UseShellExecute = false;

        notePad.Start();
        StreamReader s = notePad.StandardOutput;



        String output= s.ReadToEnd();


        notePad.WaitForExit();

将上述内容放在一个线程中。

现在,为了更新UI的输出,您可以使用一个带有两行的计时器

  Console.Clear();
  Console.WriteLine(output);

这可能对你有帮助


1
据我所见,这段代码也会等待直到完成。正如问题中所述,他不想等到完成,而是希望实时输出。 - Michiel van Vaardegem
正如Michiel所说,这是为了获得“最终”输出。 - Extaze

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