简体   繁体   English

如何从命令中获取输出以实时显示在窗体上的控件中?

[英]How do I get output from a command to appear in a control on a Form in real-time?

From various sources on the web, I have put together the following code for executing a command via CMD.exe and capturing output from STDOUT and STDERR .从网络上的各种来源,我整理了以下代码,用于通过CMD.exe执行命令并捕获STDOUTSTDERR的输出。

public static class Exec
{
    public delegate void OutputHandler(String line);

    // <summary>
    /// Run a command in a subprocess
    /// </summary>
    /// <param name="path">Directory from which to execute the command</param>
    /// <param name="cmd">Command to execute</param>
    /// <param name="args">Arguments for command</param>
    /// <param name="hndlr">Command output handler (null if none)</param>
    /// <param name="noshow">True if no windows is to be shown</param>
    /// <returns>Exit code from executed command</returns>
    public static int Run(String path, String cmd, String args,
                          OutputHandler hndlr = null, Boolean noshow = true)
    {
        // Assume an error
        int ret = 1;
        // Create a process
        using (var p = new Process())
        {
            // Run command using CMD.EXE
            // (this way we can pipe STDERR to STDOUT so they can get handled together)
            p.StartInfo.FileName = "cmd.exe";
            // Set working directory (if supplied)
            if (!String.IsNullOrWhiteSpace(path)) p.StartInfo.WorkingDirectory = path;
            // Indicate command and arguments
            p.StartInfo.Arguments = "/c \"" + cmd + " " + args + "\" 2>&1";
            // Handle noshow argument
            p.StartInfo.CreateNoWindow = noshow;
            p.StartInfo.UseShellExecute = false;
            // See if handler provided
            if (hndlr != null)
            {
                // Redirect STDOUT and STDERR
                p.StartInfo.RedirectStandardOutput = true;
                p.StartInfo.RedirectStandardError = true;
                // Use custom event handler to capture output
                using (var outputWaitHandle = new AutoResetEvent(false))
                {
                    p.OutputDataReceived += (sender, e) =>
                    {
                        // See if there is any data
                        if (e.Data == null)
                        {
                            // Signal output processing complete
                            outputWaitHandle.Set();
                        }
                        else
                        {
                            // Pass string to string handler
                            hndlr(e.Data);
                        }
                    };
                    // Start process
                    p.Start();
                    // Begin async read
                    p.BeginOutputReadLine();
                    // Wait for process to terminate
                    p.WaitForExit();
                    // Wait on output processing complete signal
                    outputWaitHandle.WaitOne();
                }
            }
            else
            {
                // Start process
                p.Start();
                // Wait for process to terminate
                p.WaitForExit();
            }
            // Get exit code
            ret = p.ExitCode;
        }
        // Return result
        return ret;
    }

    // <summary>
    /// Run a command in a subprocess and return output in a variable
    /// </summary>
    /// <param name="path">Directory from which to execute the command</param>
    /// <param name="cmd">Command to execute</param>
    /// <param name="args">Arguments for command</param>
    /// <param name="outp">Variable to contain the output</param>
    /// <returns>Exit code from executed command</returns>
    public static GetOutputReturn GetOutput(String path, String cmd, String args)
    {
        GetOutputReturn ret = new GetOutputReturn();
        ret.ReturnCode = Run(path, cmd, args, (line) =>
                             {
                               ret.Output.AppendLine(line);
                             });
        return ret;
    }
}

public class GetOutputReturn
{
    public StringBuilder Output = new StringBuilder();
    public int ReturnCode = 1;
}

I am able to use this in a console app in three different manners as follows:我可以通过以下三种不同的方式在控制台应用程序中使用它:

static void Main(string[] args)
{
    int ret;
    Console.WriteLine("Executing dir with no capture and no window");
    ret = Exec.Run(@"C:\", "dir", "");
    Console.WriteLine("Execute returned " + ret);
    Console.WriteLine("Press enter to continue ...");
    Console.ReadLine();
    Console.WriteLine("Executing dir with no capture and window");
    ret = Exec.Run(@"C:\", "dir", "", null, false);
    Console.WriteLine("Execute returned " + ret);
    Console.WriteLine("Press enter to continue ...");
    Console.ReadLine();
    Console.WriteLine("Executing dir with capture and no window");
    var results = Exec.GetOutput(@"C:\", "dir", "");
    Console.WriteLine(results.Output.ToString());
    Console.WriteLine("Execute returned " + results.ReturnCode);
    Console.ReadLine();
    Console.WriteLine("Executing dir with real-time capture and no window");
    ret = Exec.Run(@"C:\", "dir", "", ShowString);
    Console.WriteLine("Execute returned " + ret);
}

public delegate void StringData(String str);

static void ShowString(String str)
{
    Console.WriteLine(str);
}

public delegate void StringData(String str);

static void ShowString(String str)
{
    Console.WriteLine(str);
}

The first run does not gather any output and just shows the exit code.第一次运行不收集任何输出,只显示退出代码。
The second run does not gather any output but shows the window.第二次运行不收集任何输出,但显示窗口。
The effect of this that the output appears in the console window real-time.输出实时显示在控制台窗口中的效果。
The third run uses GetOutput to gather the output.第三次运行使用 GetOutput 来收集输出。
The effect of this is that the output does not appear until the run is completed.这样做的效果是在运行完成之前不会出现输出。
The last run uses a handler to receive and display the output real-time.最后一次运行使用处理程序来实时接收和显示输出。
In appearance this looks like the second run but it is very different.从外观上看,这看起来像第二次运行,但非常不同。
For each line of output that is received ShowString is called.对于接收到的每一行输出,都会调用 ShowString。
Show string simply displays the string.显示字符串只是显示字符串。
However, it could do anything it needs with the data.但是,它可以对数据做任何它需要的事情。

I am trying to adapt the last run such that I can update a text box with the output of the command in real time.我正在尝试调整上次运行,以便可以使用命令的输出实时更新文本框。 The issue that I am having is how to get it in the right context (for lack of a better term).我遇到的问题是如何在正确的上下文中使用它(因为没有更好的术语)。 Because OutputHandler is called asynchronously, it has to use the InvokeRequired/BeginInvoke/EndInvoke mechanism to get in sync with the UI thread.因为 OutputHandler 是异步调用的,所以它必须使用InvokeRequired/BeginInvoke/EndInvoke机制来与 UI 线程同步。 I am having a little problem with how to do this with parameters.我对如何使用参数执行此操作有一点问题。 In my code the textBox could be one of several in a tab control as several background "Run"'s could be taking place.在我的代码中,文本框可能是选项卡控件中的几个之一,因为可能会发生多个背景“运行”。

So far I have this:到目前为止,我有这个:

private void btnExecute_Click(object sender, EventArgs e)
{
    // Get currently selected tab page
    var page = tcExecControl.SelectedTab;
    // Get text box (always 3rd control on the page)
    var txt = (TextBox)page.Controls[2];
    // Create string handler
    var prc = new Exec.OutputHandler((String line) =>
                  {
                      if (txt.InvokeRequired)
                          txt.Invoke(new MethodInvoker(() =>
                                     { txt.Text += line; }));
                          else txt.Text += line;
                   });
    // Command and arguments are always 1st and 2nd controls on the page
    var result = Exec.Run(@"C:\", page.Controls[0].Text, page.Controls[1], prc);                              
}

But this does not seem to be working.但这似乎不起作用。 I am not seeing any output to the txtBox.我没有看到 txtBox 的任何输出。
In fact the program basically hangs in the handler.实际上程序基本上挂在处理程序中。

If I change the code to use GetOutput and then write the resulting output to the text box everything works.如果我将代码更改为使用 GetOutput,然后将结果输出写入文本框,一切正常。 So I know that I have the command set up properly.所以我知道我已经正确设置了命令。 Using the debugger, I am able to set a break point on the "if ( txt.InvokeRequired )" line and I see the first line of output coming correctly.使用调试器,我可以在“if ( txt.InvokeRequired )”行设置断点,并且我看到第一行输出正确。 At this point the code takes the true path of the if statement, but if I set a breakpoint on the txt.Text += line;此时代码采用 if 语句的真实路径,但如果我在txt.Text += line; line it never gets there.线它永远不会到达那里。

Can anyone help me out?谁能帮我吗? I'm sure I'm missing something.我确定我错过了一些东西。

A brief description of what the code performs in this example:本示例中代码执行的简要说明:

The shell command ( cmd.exe ) is run first, using start /WAIT as parameter.首先运行 shell 命令 ( cmd.exe ),使用start /WAIT作为参数。 More or less the same functionality as /k : the console is started without any specific task, waiting to process a command when one is sent.或多或少与/k相同的功能:控制台在没有任何特定任务的情况下启动,等待发送命令时处理命令。

StandardOutput , StandardError and StandardInput are all redirected, setting RedirectStandardOutput , RedirectStandardError and RedirectStandardInput properties of the ProcessStartInfo to true . StandardOutputStandardErrorStandardInput都被重定向,将ProcessStartInfoRedirectStandardOutputRedirectStandardErrorRedirectStandardInput属性设置为true

The console Output stream, when written to, will raise the OutputDataReceived event;控制台输出流在写入时会引发OutputDataReceived事件; it's content can be read from the e.Data member of the DataReceivedEventArgs .它的内容可以从DataReceivedEventArgse.Data成员中读取。
StandardError will use its ErrorDataReceived event for the same purpose. StandardError将使用它的ErrorDataReceived事件来达到同样的目的。
You could use a single event handler for both the events, but, after some testing, you might realize that is probably not a good idea.您可以为这两个事件使用一个事件处理程序,但是,经过一些测试,您可能会意识到这可能不是一个好主意。 Having them separated avoids some weird overlapping and allows to easily tell apart errors from normal output (as a note, you can find programs that write to the error Stream instead of the output Stream).将它们分开可以避免一些奇怪的重叠,并可以轻松地将错误与正常输出区分开来(请注意,您可以找到写入错误流而不是输出流的程序)。

StandardInput can be redirected assigning it to a StreamWriter stream.可以重定向StandardInput ,将其分配给StreamWriter流。
Each time a string is written to the stream, the console will interpret that input as a command to be executed.每次将字符串写入流时,控制台都会将该输入解释为要执行的命令。

Also, the Process is instructed to rise it's Exited event upon termination, setting its EnableRaisingEvents property to true .此外,进程被指示在终止时引发它的Exited事件,将其EnableRaisingEvents属性设置为true
The Exited event is raised when the Process is closed because an Exit command is processed or calling the .Close() method (or, eventually, the .Kill() method, which should only be used when a Process is not responding anymore, for some reason).当进程关闭时引发Exited事件,因为处理了Exit命令或调用了.Close()方法(或最终调用了.Kill()方法,该方法仅应在 Process 不再响应时使用,例如一些原因)。

Since we need to pass the console Output to some UI controls ( RichTextBoxes in this example) and the Process events are raised in ThreadPool Threads, we must synchronize this context with the UI's.由于我们需要将控制台输出传递给一些 UI 控件(本例中为RichTextBoxes ),并且在 ThreadPool 线程中引发 Process 事件,因此我们必须将此上下文与 UI 同步。
This can be done using the Process SynchronizingObject property, setting it to the Parent Form or using the Control.BeginInvoke method, that will execute a delegate function on the thread where the control's handle belongs.这可以使用 Process SynchronizingObject属性、将其设置为父窗体或使用Control.BeginInvoke方法来完成,该方法将在控件句柄所属的线程上执行委托函数。
Here, a MethodInvoker representing the delegate is used for this purpose.在这里,代表委托的MethodInvoker用于此目的。


The core function used to instantiate the Process and set its properties and event handlers:用于实例化 Process 并设置其属性和事件处理程序的核心函数:

using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;

public partial class frmCmdInOut : Form
{
    Process cmdProcess = null;
    StreamWriter stdin = null;

    public frmCmdInOut() => InitializeComponent();

    private void MainForm_Load(object sender, EventArgs e)
    {
        rtbStdIn.Multiline = false;
        rtbStdIn.SelectionIndent = 20;
    }

    private void btnStartProcess_Click(object sender, EventArgs e)
    {
        btnStartProcess.Enabled = false;
        StartCmdProcess();
        btnEndProcess.Enabled = true;
    }

    private void btnEndProcess_Click(object sender, EventArgs e)
    {
        if (stdin.BaseStream.CanWrite) {
            stdin.WriteLine("exit");
        }
        btnEndProcess.Enabled = false;
        btnStartProcess.Enabled = true;
        cmdProcess?.Close();
    }

    private void rtbStdIn_KeyPress(object sender, KeyPressEventArgs e)
    {
        if (e.KeyChar == (char)Keys.Enter) {
            if (stdin == null) {
                rtbStdErr.AppendText("Process not started" + Environment.NewLine);
                return;
            }

            e.Handled = true;
            if (stdin.BaseStream.CanWrite) {
                stdin.Write(rtbStdIn.Text + Environment.NewLine);
                stdin.WriteLine();
                // To write to a Console app, just 
                // stdin.WriteLine(rtbStdIn.Text); 
            }
            rtbStdIn.Clear();
        }
    }

    private void StartCmdProcess()
    {
        var pStartInfo = new ProcessStartInfo {
             FileName = "cmd.exe",
            // Batch File Arguments = "/C START /b /WAIT somebatch.bat",
            // Test: Arguments = "START /WAIT /K ipconfig /all",
            Arguments = "START /WAIT",
            WorkingDirectory = Environment.SystemDirectory,
            // WorkingDirectory = Application.StartupPath,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            RedirectStandardInput = true,
            UseShellExecute = false,
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden,
        };

        cmdProcess = new Process {
            StartInfo = pStartInfo,
            EnableRaisingEvents = true,
            // Test without and with this
            // When SynchronizingObject is set, no need to BeginInvoke()
            //SynchronizingObject = this
        };

        cmdProcess.Start();
        cmdProcess.BeginErrorReadLine();
        cmdProcess.BeginOutputReadLine();
        stdin = cmdProcess.StandardInput;
        // stdin.AutoFlush = true;  <- already true

        cmdProcess.OutputDataReceived += (s, evt) => {
            if (evt.Data != null)
            {
                BeginInvoke(new MethodInvoker(() => {
                    rtbStdOut.AppendText(evt.Data + Environment.NewLine);
                    rtbStdOut.ScrollToCaret();
                }));
            }
        };

        cmdProcess.ErrorDataReceived += (s, evt) => {
            if (evt.Data != null) {
                BeginInvoke(new Action(() => {
                    rtbStdErr.AppendText(evt.Data + Environment.NewLine);
                    rtbStdErr.ScrollToCaret();
                }));
            }
        };

        cmdProcess.Exited += (s, evt) => {
            stdin?.Dispose();
            cmdProcess?.Dispose();
        };
    }
}

Since the StandardInput has been redirected to a StreamWriter:由于 StandardInput 已被重定向到 StreamWriter:

stdin = cmdProcess.StandardInput;

we just write to the Stream to execute a command:我们只需写入 Stream 以执行命令:

stdin.WriteLine(["Command Text"]);

实时控制台重定向

The sample Form can be downloaded from PasteBin .示例表格可以从 PasteBin 下载


VB.Net version VB.Net版

Controls' names:控件名称:
rtbStdOut -> RichTextBox (blue background), receives StdOut rtbStdOut -> RichTextBox(蓝色背景),接收 StdOut
rtbStdErr -> RichTextBox (in the middle), receives StdErr rtbStdErr -> RichTextBox(中间),接收 StdErr
rtbStdIn -> RichTextBox (at the bottom), writes to StdIn rtbStdIn -> RichTextBox(在底部),写入 StdIn
btnStartProcess -> Button (on the left), starts the Process btnStartProcess -> 按钮(在左侧),启动进程
btnEndProcess -> Button (on the right), stops te Process btnEndProcess -> 按钮(右侧),停止进程

Download this Form from Google Drive 从 Google Drive 下载此表格

Imports System.Diagnostics
Imports System.IO

Public Class frmCmdInOut

    Private cmdProcess As Process = Nothing
    Private stdin As StreamWriter = Nothing

    Protected Overrides Sub OnLoad(e As EventArgs)
        MyBase.OnLoad(e)
        rtbStdIn.Multiline = False
        rtbStdIn.SelectionIndent = 20
    End Sub

    Private Sub btnStartProcess_Click(sender As Object, e As EventArgs) Handles btnStartProcess.Click
        btnStartProcess.Enabled = False
        StartCmdProcess(Me)
        btnEndProcess.Enabled = True

    End Sub

    Private Sub btnEndProcess_Click(sender As Object, e As EventArgs) Handles btnEndProcess.Click
        If stdin.BaseStream IsNot Nothing AndAlso stdin.BaseStream.CanWrite Then stdin.WriteLine("exit")
        btnEndProcess.Enabled = False
        btnStartProcess.Enabled = True
        cmdProcess?.Close()
    End Sub

    Private Sub rtbStdIn_KeyPress(sender As Object, e As KeyPressEventArgs) Handles rtbStdIn.KeyPress
        If e.KeyChar = ChrW(Keys.Enter) Then
            If stdin Is Nothing Then
                rtbStdErr.AppendText("Process not started" + Environment.NewLine)
                Return
            End If

            e.Handled = True
            If stdin.BaseStream.CanWrite Then
                stdin.Write(rtbStdIn.Text + Environment.NewLine)
                stdin.WriteLine() ' To write to a Console app, just stdin.WriteLine(rtbStdIn.Text); 
            End If
            rtbStdIn.Clear()
        End If
    End Sub

    Private Sub StartCmdProcess(synchObj As Control)

        ' Arguments = $"start /WAIT cscript.exe script.vbs /xpr",
        ' Batch File Arguments = "/C START /b /WAIT batchfile.bat",
        ' Test: Arguments = "START /WAIT /K ipconfig /all",

        ' start with /U
        ' StandardErrorEncoding = Encoding.Unicode,
        ' StandardOutputEncoding = Encoding.Unicode,

        Dim pStartInfo = New ProcessStartInfo() With {
            .FileName = "cmd.exe",
            .Arguments = "START /WAIT",
            .CreateNoWindow = True,
            .RedirectStandardError = True,
            .RedirectStandardInput = True,
            .RedirectStandardOutput = True,
            .UseShellExecute = False,
            .WindowStyle = ProcessWindowStyle.Hidden,
            .WorkingDirectory = Application.StartupPath
        }

        cmdProcess = New Process() With {
            .EnableRaisingEvents = True,
            .StartInfo = pStartInfo,
            .SynchronizingObject = synchObj
        }

        cmdProcess.Start()
        cmdProcess.BeginErrorReadLine()
        cmdProcess.BeginOutputReadLine()
        stdin = cmdProcess.StandardInput

        AddHandler cmdProcess.OutputDataReceived,
            Sub(s, evt)
                If evt.Data IsNot Nothing Then
                    rtbStdOut.AppendText(evt.Data + Environment.NewLine)
                    rtbStdOut.ScrollToCaret()
                End If
            End Sub
        AddHandler cmdProcess.ErrorDataReceived,
            Sub(s, evt)
                If evt.Data IsNot Nothing Then
                    rtbStdErr.AppendText(evt.Data + Environment.NewLine)
                    rtbStdErr.ScrollToCaret()
                End If
            End Sub

        AddHandler cmdProcess.Exited,
            Sub(s, evt)
                stdin?.Dispose()
                cmdProcess?.Dispose()
            End Sub
    End Sub
End Class

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM