C# ProcessStartInfo read Output "live" (Concurrent) - c#

I'm trying to write a Console wrapper WPF gui that simply runs a selection of .bat files, I'd like to be able to view any output from the .bat files "live" (as if it were running in cmd).
I've looked into OutputDataReceived and event handlers which append text and then sent this to the screen, however it still waits until the Process has finished before anything appears on the screen.
How do I get the output from the .bat to appear in "real time"?
Snippits of my code so far (this is in a form):
The form has one button (go) and one multi-line text field (textArea).
private void go_Click(object sender, EventArgs e)
{
ExecuteCommand();
}
public void ExecuteCommand()
{
int ExitCode;
ProcessStartInfo ProcessInfo;
Process Process;
//ProcessInfo = new ProcessStartInfo("cmd.exe", "/C z:\foo.bat");
ProcessInfo = new ProcessStartInfo(#"z:\foo.bat"); ;
ProcessInfo.CreateNoWindow = true;
ProcessInfo.UseShellExecute = false;
ProcessInfo.RedirectStandardError = true;
ProcessInfo.RedirectStandardOutput = true;
Process = new Process();
Process.StartInfo = ProcessInfo;
Process.OutputDataReceived += new DataReceivedEventHandler(OutputToTextArea);
Process.Start();
// Start the asynchronous read of the sort output stream.
Process.BeginOutputReadLine();
Process.WaitForExit();
ExitCode = Process.ExitCode;
Process.Close();
}
private int numOutputLines = 0;
private void OutputToTextArea(object sendingProcess, DataReceivedEventArgs outLine)
{
// Collect the sort command output.
if (!String.IsNullOrEmpty(outLine.Data))
{
numOutputLines++;
this.AppendToTextArea("[" + numOutputLines.ToString() + "] - " + outLine.Data + Environment.NewLine);
}
}
private void AppendToTextArea(string s)
{
if (this.textArea.InvokeRequired)
{
// It's on a different thread, so use Invoke.
this.BeginInvoke (new MethodInvoker(() => textArea.AppendText(s)));
} else {
textArea.AppendText(s);
}
}
Where my foo.bat is just a for loop:
ECHO OFF
FOR /L %%i IN (1,1,10) DO (
echo %%i
ping -n 2 127.0.0.1 >nul
)

Well yeah, you're currently blocking the main thread (which is the UI thread) as you wait for process exit in ExecuteCommand, which is directly called from the UI thread (in go_Click).
Just start a new thread (or use a ThreadPool) (Winforms example):
private void button1_Click(object sender, EventArgs e)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(this.ExecuteCommand));
}
public void ExecuteCommand(object state)
{
...
}
If you're using WPF, you probably want to use a BackgroundWorker.

If you wish to keep it simple you can just start a command prompt with the /K argument and pass in the batch file.
string arguments = #"z:\foo.bat";
Process.Start("cmd.exe", "/K " + arguments);
The cmd.exe /K opens and command prompt and runs your foo.bat but remains on the screen.

Related

Executing batch file from C# winforms ignores timeout

All,
I am attempting to execute a series of batch files via a C# winforms app. In this early stage, with a test batch file, I am unable to get the process execution to respect the timeout in my batch file unless i set UseShellExecute = true, which is something i am trying to avoid. My goal is to execute the script file and redirect the output to the GUI as shown in the code here:
Process process;
public void ExecuteScript(string workingDirectory, string batchFileName)
{
if (process != null)
process.Dispose();
process = new Process();
process.StartInfo.WorkingDirectory = workingDirectory;
process.StartInfo.FileName = workingDirectory + batchFileName;
process.StartInfo.Arguments = "";
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardInput = true;
process.EnableRaisingEvents = true;
process.OutputDataReceived += proc_OutputDataReceived;
process.Start();
process.BeginOutputReadLine();
process.Exited += OnProcessExit;
}
private void OnProcessExit(object sender, EventArgs e)
{
Console.WriteLine("the script has ended");
}
private void proc_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
this.Invoke((Action)(() =>
{
textBox1.AppendText(Environment.NewLine + e.Data);
}));
(sender as Process)?.StandardInput.WriteLine();
}
my batch file looks like this:
#echo off
echo This is a running script
timeout /t 10
echo Done sleeping. Will Exit
exit
Is there an appropriate combination of settings i can call to prevent the command window from appearing, while still redirecting the output, and executing the script appropriately?
The problem with your code is that the timeout command is not supported when stdin is redirected. This is a good example of why one should always redirect both stdout and stderr. An error message is actually emitted from the batch file, but because you weren't capturing the stderr stream, you didn't see the error message. All too many questions here on Stack Overflow involving Process scenarios that "don't work" could be easily solved had the person looked at the stderr output.
A work-around to this limitation of the timeout command is to use the waitfor command instead, using a known-nonexistent signal name with a timeout value, e.g. waitfor /t 10 placeholder.
Here is a console program that is entirely self-contained and which demonstrates both the failure of the timeout command when stdin is redirected, as well as the work-around of waitfor:
const string batchFileText =
#"#echo off
echo Starting batch file
timeout /t 5
waitfor /t 5 placeholder
echo Timeout completed
exit";
static void Main(string[] args)
{
string batchFileName = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".bat");
try
{
File.WriteAllText(batchFileName, batchFileText);
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = batchFileName,
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardInput = true,
RedirectStandardError = true,
};
Process process = new Process
{
EnableRaisingEvents = true,
};
process.OutputDataReceived += Process_OutputDataReceived;
process.ErrorDataReceived += Process_ErrorDataReceived;
process.Exited += Process_Exited;
process.StartInfo = psi;
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
}
finally
{
File.Delete(batchFileName);
}
}
private static void Process_Exited(object sender, EventArgs e)
{
WriteLine("Process exited");
}
private static void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data != null)
{
WriteLine($"stdout: {DateTime.Now:HH:mm:ss.sss}: {e.Data}");
}
}
private static void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data != null)
{
WriteLine($"stderr: {DateTime.Now:HH:mm:ss.sss}: {e.Data}");
}
}
Note that the waitfor command writes a message to stderr if the timeout occurs (which it always will in this case). You may or may not want that to show up in the captured stderr stream. If not, you can redirect the stderr of that command specifically by using 2>nul. E.g. waitfor /t 10 placeholder 2>nul.

Why does redirected standard output appear after process has finished?

I am working with Visual Studio 2015 and .NET framework 4.7.2. I have set up a simple test program that executes an external program in C#. The program is a Python script that simply prints some string to stdout every 0.5 seconds. I want to read the stdout of this sub process in my C# application.
The program basically works, but I get the output of the Python script only shortly before the sub process exits. What do I need to change in order to get a more responsive behavior, i.e. getting the output every 0.5 second as soon as the Python script writes it to stdout?
Here's my C# code:
public class Program {
private Process process;
public static void Main(string[] args) {
new Program().init();
}
private void init() {
startPythonProcess();
process.WaitForExit();
Console.ReadLine();
}
private void startPythonProcess() {
if(process==null) {
try {
Console.WriteLine("Starting Python process ...");
string filepath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase).Substring(6);
process = new Process();
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = false;
startInfo.UseShellExecute = false;
startInfo.WorkingDirectory = filepath;
startInfo.FileName = "python.exe";
//startInfo.WindowStyle = ProcessWindowStyle.Normal;
startInfo.RedirectStandardInput = false;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
startInfo.Arguments = string.Format("{0}", Path.Combine(filepath, "test.py"));
process.StartInfo = startInfo;
process.OutputDataReceived += OutputDataReceivedEventHandler;
process.ErrorDataReceived += ErrorDataReceivedEventHandler;
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
} catch(Exception ex) {
Console.WriteLine("Could not start Python process: " + ex.Message);
}
}
}
public void OutputDataReceivedEventHandler(object sender, DataReceivedEventArgs args) {
Console.WriteLine("[PYTHON] INFO: {0}", args.Data);
}
public void ErrorDataReceivedEventHandler(object sender, DataReceivedEventArgs args) {
Console.WriteLine("[PYTHON] ERROR: {0}", args.Data);
}
}
Here's my Python script:
import time
import sys
import logging
logging.basicConfig(level=logging.ERROR)
if __name__ == '__main__':
count = 0
while True:
print('PYTHON: {}'.format(count))
time.sleep(0.5)
count+=1
if count>=25:
break
UPDATE: I uploaded the mini project here.
The print function takes a flush argument which controls whether buffered output is flushed.
The default value of flush is False, meaning flushing is controlled by whatever file print is writing to (for example, sys.stdout).
Set flush to True to force immediate printing.
print('PYTHON: {}'.format(count), flush=True)

Stopping a continuous Event in C# with (f.ex.) a button

This is probably simple, and the question might not be very good, but I'm looking for the best or most efficient way to accomplish this:
A button click starts an Event, which then runs a method that continiously pings an IP address. The ping output is displayed in a text box.
A click on the same button stops the ping task.
Here's the (I think) relevant code:
The method run by the Event connected to the Ping button:
private void pingClicked (object sender, EventArgs e) {
pinger();
}
The pinger() method:
private void pinger() {
string command = "/c ping " + ipadrtextbox.Text;
if (contchkbox.Checked) {
command += " -t";
}
ProcessStartInfo procStartInfo = new ProcessStartInfo("CMD", command);
Process proc = new Process();
proc.StartInfo = procStartInfo;
proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
proc.Start();
procStartInfo.CreateNoWindow = true;
procStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
procStartInfo.RedirectStandardOutput = true;
procStartInfo.UseShellExecute = false;
proc.OutputDataReceived += new
DataReceivedEventHandler(proc_OutputDataReceived);
proc.Start();
proc.BeginOutputReadLine();
procStartInfo.CreateNoWindow = true;
procStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
}
void proc_OutputDataReceived(object sender, DataReceivedEventArgs e) {
if (e.Data != null) {
string newLine = e.Data.Trim() + Environment.NewLine;
MethodInvoker append = () => pingoutput.AppendText(newLine);
pingoutput.BeginInvoke(append);
}
}
A while-loop in the pinger method results in a complaint that "An async read operation has already been started on the stream.", so that's apparently not the way to go.
Also, I haven't found a way for the method to listen for a buttonpress elsewhere in the application, and then stop the task with roc.CancelOutputRead(). But I expect that's the way the task should be stopped?
Keep the running process as a private member of your class then:
private void pingClicked (object sender, EventArgs e) {
if( process != null && !process.HasExited )
{
process.CancelOutputRead()
process.Kill();
process=null;
} else {
pinger();
}
}

Running string as a Batch file in c#

I'm writing an application that creates a batch file and then run :
I know I can create a Batch file and run it
no problem with that .
What I want to do is :
once I have created the string that makes the file , Is there is any way to execute the string as a Batch file ?
something like
string BatchFile = "echo \"bla bla\" \n iperf -c 123 ... ... .. "
Diagnostics.Process.Start(BatchFile);
You can run CMD.EXE with /c as an executable and have the rest as arguments :
Process.Start("cmd.exe", "/c echo \"bla bla\" \n iperf -c 123 ... ... .. ");
for me, I am using this code:
Process process;
private void button1_Click(object sender, EventArgs e)
{
process = new Process();
process.StartInfo.UseShellExecute = false;
process.StartInfo.FileName = "cmd";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardInput = true;
process.Start();
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += new DoWorkEventHandler(worker_DoWork);
worker.RunWorkerAsync();
process.StandardInput.WriteLine("cd d:/tempo" );
process.StandardInput.WriteLine("dir");
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
string line;
while (!process.StandardOutput.EndOfStream)
{
line = process.StandardOutput.ReadLine();
if (!string.IsNullOrEmpty(line))
{
SetText(line);
}
}
}
delegate void SetTextCallback(string text);
private void SetText(string text)
{
if (this.textBox1.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else
{
this.textBox1.Text += text + Environment.NewLine;
}
}
private void FrmMain_FormClosing(object sender, FormClosingEventArgs e)
{
process.StandardInput.WriteLine("exit");
process.Close();
}
You may create your Batch "file" as a long string with lines terminated in \n, exactly as you shown in your example, and then execute that string (I called it a "NotBatch-text") executing cmd.exe and redirecting such string into its Stdin standard handle. This way, your "NotBatch-text" may use a large number of Batch features, like expansion of %variables%, IF and FOR commands nested at any level, and many more. You may also use delayed !variable! expansion if you execute cmd.exe with /V:ON switch. Really, the only things that don't works in the NotBatch-text are: parameters and SHIFT command, and GOTO/CALL :label commands; further details at this post.
If you want to execute a more advanced "NotBatch-text" string, you may even simulate GOTO and CALL :label commands with the aid of a third party program, as described at this post.

Windows Service read cmd lines with eventhandler

I'm trying to run a command in the command prompt and run it as a service. This command starts the queue listener from Laravel. I want to run it as a service so this queue listener always runs in the background. When the listener outputs some lines I want to capture these and send an email. I already tried my code running it as a process and it's working, but when I try to run the code as a service it doesn't start.
Process process();
protected override void OnStart(string[] args)
{
process = new Process();
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.Arguments = "/C php artisan queue:listen --tries=3 --timeout=0 --memory=1024";
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.WorkingDirectory = "C:/xampp/htdocs/phpproject";
process.OutputDataReceived += new DataReceivedEventHandler(p_OutputDataReceived);
process.Start();
process.BeginOutputReadLine();
process.WaitForExit();
}
private void p_OutputDataReceived(object sendingProcess,
DataReceivedEventArgs outLine)
{
// Collect the command output.
if (!String.IsNullOrEmpty(outLine.Data.ToString()))
{
sendMail(process.StandardOutput.ToString());
}
}
Edit:
When I comment the process.WaitForExit() line the service runs and the queue listener does its work!.
But the next problem I have is the service never hits the eventhandler. It did when I was running it only as a process. Any clue why this isn't working as a service?
You can try something like this;
private Process process = null;
private DataReceivedEventHandler TheDataReceievedEventHandler;
private void startProcess()
{
ProcessStartInfo processStartInfo = new ProcessStartInfo(#"cmd.exe", #"/C php artisan queue:listen --tries=3 --timeout=0 --memory=1024")
{
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = #"C:/xampp/htdocs/phpproject",
};
if ((process = Process.Start(processStartInfo)) != null)
{
process.EnableRaisingEvents = true;
process.Exited += new EventHandler(ExitedHandler);
TheDataReceievedEventHandler = new DataReceivedEventHandler(StandardOutputHandler);
process.OutputDataReceived += TheDataReceievedEventHandler;
process.BeginOutputReadLine();
process.ErrorDataReceived += TheDataReceievedEventHandler;
process.BeginErrorReadLine();
}
}
private void ExitedHandler(object sender, EventArgs e)
{
throw new NotImplementedException(); // the service you're trying to run closed it self.
}
private void StandardOutputHandler(object sender, DataReceivedEventArgs e)
{
Console.WriteLine(e.Data);
}
The problem is if the service you are trying to run closes it self, it will stop outputting and the process will be closed. Similar to CMD behavior each time you send a command to CMD it will close it self after you receive the error or output.
So if for example; I want to use CMD to see my task-list, i will have to build a loop were i run a CMD process on a regular interval because after each command it will close it self.
Edit
If you can't stop the program using its own logic you will need to kill the process of the program itself. You can do that using the following code;
try // If you have no administrator privilege, try will fire.
{
foreach (Process proc in Process.GetProcessesByName("name process")) // You can get the name by looking in your task manager.
{
proc.Kill();
}
}
catch(Exception ex)
{
// Add error handling
}

Categories