Bagaimana cara agar keluaran dari perintah muncul dalam kontrol di Formulir secara real-time?

Aug 03 2018

Dari berbagai sumber di web, saya telah mengumpulkan kode berikut untuk menjalankan perintah melalui CMD.exedan menangkap keluaran dari STDOUTdan STDERR.

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

Saya dapat menggunakan ini di aplikasi konsol dalam tiga cara berbeda sebagai berikut:

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

Proses pertama tidak mengumpulkan output apa pun dan hanya menampilkan kode keluar.
Proses kedua tidak mengumpulkan output apa pun tetapi menampilkan jendela.
Efek ini bahwa output muncul di jendela konsol secara real-time.
Proses ketiga menggunakan GetOutput untuk mengumpulkan output.
Efeknya adalah bahwa output tidak muncul sampai proses selesai.
Proses terakhir menggunakan penangan untuk menerima dan menampilkan output secara real-time.
Secara penampilan ini terlihat seperti putaran kedua tetapi sangat berbeda.
Untuk setiap baris keluaran yang diterima ShowString dipanggil.
Show string hanya menampilkan string tersebut.
Namun, ia dapat melakukan apa pun yang diperlukan dengan data tersebut.

Saya mencoba mengadaptasi proses terakhir sehingga saya dapat memperbarui kotak teks dengan output perintah secara real time. Masalah yang saya hadapi adalah bagaimana mendapatkannya dalam konteks yang tepat (karena tidak ada istilah yang lebih baik). Karena OutputHandler dipanggil secara asinkron, ia harus menggunakan InvokeRequired/BeginInvoke/EndInvokemekanisme tersebut untuk menyinkronkan dengan UI thread. Saya mengalami sedikit masalah dengan cara melakukan ini dengan parameter. Dalam kode saya, textBox bisa menjadi salah satu dari beberapa kontrol tab karena beberapa latar belakang "Run" dapat berlangsung.

Sejauh ini saya memiliki ini:

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

Tapi ini sepertinya tidak berhasil. Saya tidak melihat output apa pun ke txtBox.
Sebenarnya program pada dasarnya hang di pawang.

Jika saya mengubah kode untuk menggunakan GetOutput dan kemudian menulis output yang dihasilkan ke kotak teks semuanya berfungsi. Jadi saya tahu bahwa saya telah menyiapkan perintah dengan benar. Dengan menggunakan debugger, saya dapat menetapkan titik putus pada baris "if ( txt.InvokeRequired)" dan saya melihat baris pertama keluaran datang dengan benar. Pada titik ini kode mengambil jalur sebenarnya dari pernyataan if, tetapi jika saya menetapkan breakpoint pada txt.Text += line;baris itu tidak pernah sampai di sana.

Adakah yang bisa membantu saya? Saya yakin saya melewatkan sesuatu.

Jawaban

7 Jimi Aug 04 2018 at 05:00

Penjelasan singkat tentang apa yang dilakukan kode dalam contoh ini:

Perintah shell ( cmd.exe) dijalankan pertama kali, menggunakan start /WAITsebagai parameter. Fungsinya kurang lebih sama seperti /k: konsol dimulai tanpa tugas tertentu, menunggu untuk memproses perintah saat dikirim.

StandardOutput, StandardErrordan StandardInputsemuanya dialihkan, menyetel properti RedirectStandardOutput , RedirectStandardError dan RedirectStandardInput dari ProcessStartInfo ke true.

Aliran Output konsol, ketika ditulis, akan memunculkan acara OutputDataReceived ; isinya bisa dibaca dari e.Dataanggota DataReceivedEventArgs .
StandardErrorakan menggunakan acara ErrorDataReceived untuk tujuan yang sama.
(Anda dapat menggunakan satu event handler untuk kedua event, tetapi, setelah beberapa pengujian, Anda mungkin menyadari bahwa itu mungkin bukan ide yang baik. Memisahkan mereka menghindari beberapa tumpang tindih yang aneh dan memungkinkan untuk dengan mudah membedakan kesalahan dari output normal).

StandardInputdapat dialihkan dengan menetapkannya ke aliran StreamWriter .
Setiap kali string ditulis ke aliran, konsol akan menafsirkan input itu sebagai perintah yang akan dijalankan.

Selain itu, Proses diinstruksikan untuk menaikkan peristiwa Keluarnya setelah penghentian, menyetel properti EnableRaisingEvents ke true.
The Exitedevent dimunculkan ketika Proses ditutup karena sebuah Exitperintah diproses, memanggil .Close () metode atau .Kill () metode.
The .Kill()Metode seharusnya hanya digunakan ketika proses yang tidak merespon lagi, untuk beberapa alasan.

Karena kita perlu meneruskan Output konsol ke beberapa kontrol UI ( RichTextBoxesdalam contoh ini), kita harus menyinkronkan konteks kejadian dengan UI.
Ini dapat dilakukan dengan menggunakan properti Process SynchronizingObject , menyetelnya ke kontrol UI (Jendela kontainer mungkin menjadi pilihan pertama, karena ada lebih dari satu kontrol untuk disinkronkan) atau menggunakan metode Control.BeginInvoke , yang akan menjalankan fungsi delegasi pada utas tempat pegangan kontrol berada.
Di sini, MethodInvoker yang mewakili delegasi digunakan untuk tujuan ini.


Fungsi inti yang digunakan untuk membuat instance Proses dan menyetel propertinya serta penangan kejadiannya:

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

StreamWriter stdin = null;

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

Karena StandardInput telah dialihkan ke StreamWriter:

stdin = cmdProcess.StandardInput;

kami hanya menulis ke Stream untuk menjalankan perintah:

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

Contoh Formulir dapat diunduh dari PasteBin .