Come ottengo l'output di un comando da visualizzare in un controllo su un modulo in tempo reale?
Da varie fonti sul Web, ho messo insieme il seguente codice per eseguire un comando tramite CMD.exee acquisire l'output da STDOUTe 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;
}
Sono in grado di usarlo in un'app console in tre modi diversi come segue:
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);
}
La prima esecuzione non raccoglie alcun output e mostra solo il codice di uscita.
La seconda esecuzione non raccoglie alcun output ma mostra la finestra.
L'effetto di ciò che l'output viene visualizzato nella finestra della console in tempo reale.
La terza esecuzione utilizza GetOutput per raccogliere l'output.
L'effetto di ciò è che l'output non viene visualizzato fino al completamento dell'esecuzione.
L'ultima esecuzione utilizza un gestore per ricevere e visualizzare l'output in tempo reale.
In apparenza questa sembra la seconda manche ma è molto diversa.
Per ogni riga di output ricevuta viene chiamato ShowString.
Mostra stringa visualizza semplicemente la stringa.
Tuttavia, potrebbe fare tutto ciò di cui ha bisogno con i dati.
Sto cercando di adattare l'ultima esecuzione in modo da poter aggiornare una casella di testo con l'output del comando in tempo reale. Il problema che sto avendo è come metterlo nel giusto contesto (per mancanza di un termine migliore). Poiché OutputHandler viene chiamato in modo asincrono, deve usare il InvokeRequired/BeginInvoke/EndInvokemeccanismo per sincronizzarsi con il thread dell'interfaccia utente. Sto avendo un piccolo problema su come farlo con i parametri. Nel mio codice il textBox potrebbe essere uno dei tanti in un controllo struttura a schede poiché potrebbero essere in corso diversi "Run" in background.
Finora ho questo:
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);
}
Ma questo non sembra funzionare. Non vedo alcun output in txtBox.
In effetti il programma fondamentalmente si blocca nel gestore.
Se cambio il codice per usare GetOutput e poi scrivo l'output risultante nella casella di testo, tutto funziona. Quindi so di aver impostato correttamente il comando. Utilizzando il debugger, sono in grado di impostare un punto di interruzione sulla txt.InvokeRequiredriga "if ( )" e vedo la prima riga di output che arriva correttamente. A questo punto il codice prende il vero percorso dell'istruzione if, ma se imposto un punto di interruzione sulla txt.Text += line;riga non ci arriva mai.
Qualcuno mi può aiutare? Sono sicuro che mi sto perdendo qualcosa.
Risposte
Una breve descrizione di ciò che il codice esegue in questo esempio:
Il comando shell ( cmd.exe) viene eseguito per primo, utilizzando start /WAITcome parametro. Più o meno la stessa funzionalità di /k: la console viene avviata senza alcuna attività specifica, in attesa di elaborare un comando quando ne viene inviato uno.
StandardOutput, StandardErrorE StandardInputsono tutti reindirizzate, modificando RedirectStandardOutput , RedirectStandardError e RedirectStandardInput proprietà del ProcessStartInfo a true.
Il flusso di output della console, quando viene scritto, genererà l' evento OutputDataReceived ; il suo contenuto può essere letto dal e.Datamembro del DataReceivedEventArgs .
StandardErrorutilizzerà il suo evento ErrorDataReceived per lo stesso scopo.
(Potresti usare un singolo gestore di eventi per entrambi gli eventi, ma, dopo un po 'di test, potresti renderti conto che probabilmente non è una buona idea. Separarli evita alcune strane sovrapposizioni e permette di distinguere facilmente gli errori dal normale output).
StandardInputpuò essere reindirizzato assegnandolo a un flusso StreamWriter .
Ogni volta che una stringa viene scritta nel flusso, la console interpreterà quell'input come un comando da eseguire.
Inoltre, il processo è istruito a salire di essa l'Exited evento al momento della risoluzione, impostando la sua EnableRaisingEvents proprietà true.
L' Exitedevento viene generato quando il processo è chiusa per un Exitcomando viene elaborato, chiamando il .Close () o il metodo .Kill () metodo.
Il .Kill()metodo dovrebbe essere utilizzato solo quando un processo non risponde più, per qualche motivo.
Poiché è necessario passare l'output della console ad alcuni controlli dell'interfaccia utente ( RichTextBoxesin questo esempio), è necessario sincronizzare il contesto degli eventi con l'interfaccia utente.
Questa operazione può essere eseguita utilizzando la proprietà Process SynchronizingObject , impostandola su un controllo dell'interfaccia utente (la finestra del contenitore è probabilmente la prima scelta, poiché è presente più di un controllo da sincronizzare) o utilizzando il metodo Control.BeginInvoke , che eseguirà una funzione delegata su il thread a cui appartiene l'handle del controllo.
Qui, un MethodInvoker che rappresenta il delegato viene utilizzato per questo scopo.
La funzione principale utilizzata per istanziare il processo e impostarne le proprietà e i gestori di eventi:
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();
};
}
}
Poiché StandardInput è stato reindirizzato a StreamWriter:
stdin = cmdProcess.StandardInput;
scriviamo semplicemente nello Stream per eseguire un comando:
stdin.WriteLine(["Command Text"]);
Il modulo di esempio può essere scaricato da PasteBin .