Executing batch file from C# winforms ignores timeout - c#

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.

Related

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)

Get command line output in real time not working for all cli

The below code is working fine for ping stackoverflow.com and most other cases but when I using 7z.exe it's not real time, it waits until directory is compressed and then shows the output. The argument that I used for compress is a test.7z dirpath. Can I do anything else?
private ProcessStartInfo GetProcessStartInfo(string filename, string arguments)
{
ProcessStartInfo ProcessStartInfo = new ProcessStartInfo();
ProcessStartInfo.CreateNoWindow = true;
ProcessStartInfo.UseShellExecute = false;
ProcessStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
ProcessStartInfo.RedirectStandardOutput = true;
ProcessStartInfo.RedirectStandardError = true;
ProcessStartInfo.RedirectStandardInput = true;
ProcessStartInfo.FileName = filename;
ProcessStartInfo.Arguments = arguments;
}
private void ProcessRun(string filename, string arguments)
{
Process Process = new Process();
Process.StartInfo = GetProcessStartInfo(filename, arguments);
Process.ErrorDataReceived += Process_OutputDataReceived;
Process.OutputDataReceived += Process_OutputDataReceived;
Process.EnableRaisingEvents = true;
Process.Start();
Process.BeginOutputReadLine();
Process.BeginErrorReadLine();
Process.WaitForExit();
}
public ObservableList<string> Output = new ObservableList<string>();
private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
Output.Add(e.Data);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
OutputListBox.ItemsSource = Output;
ProcessRun("cmd.exe", "/c ping stackoverflow.com");
}
7zip Output:
7zip Progress:
If you're interested in the percentage, you're out of luck - that isn't done by standard output. Standard output only handles streams of data, while the percentage output is actually done by manipulating the console directly. It's not part of the output stream - there is no way to replicate the same effect using the standard I/O streams.
So it's not a problem of your code. If you want to see the same problem using just the Command prompt, try running this:
7z.exe yourarguments > log.txt
The > is an output redirect - instead of writing to the console, the standard output is redirected to a file. When you use it with ping, it immediately prints out standard output as it comes. When you use it with 7zip, you get the same result as with your C# application.

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
}

C# ProcessStartInfo read Output "live" (Concurrent)

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.

ProcessInfo and RedirectStandardOutput

I have an app which calls another process in a command window and that process has updating stats that output to the console window. I thought this was a fairly simple operation but I can't seem to get it to work. Am I missing something?
string assemblyLocation = Assembly.GetExecutingAssembly().Location;
Process process = new Process
{
ProcessStart =
{
RedirectStandardOutput = true,
UseShellExecute = false,
WindowStyle = ProcessWindowStyle.Hidden,
Arguments = arg,
FileName = assemblyLocation.Substring(0, assemblyLocation.LastIndexOf("\\")) + "\\ffmpeg.exe",
CreateNoWindow = true
}
};
process.Start();
Console.WriteLine(process.StandardOutput.ReadToEnd());
process.WaitForExit();
Ideally what I would like is as the output changes within that process I hit or data comes into the reader that I get events off it.
Any help would be great, I feel like this is a newbie question but seem to be missing something.
I've experienced this before. Sometimes, the way in which the process you're calling outputs to the console is not compatible with this sort of output redirection. I've been fortunate enough in this case to be able to modify the external process to get around this.
You might try running your code on another process that outputs to the console, and see if it works properly. It reads about right to me right now.
EDIT:
I went and pulled a code block I've used to do this. This is in a WPF app which redirects the process output to the window. Notice the event binding. Since this is WPF I have to invoke my call to write the data out. Since you aren't worried about blocking, ou should be able to simply replace that with:
Console.WriteLine(e.Data);
Hopefully it helps!
private static void LaunchProcess()
{
Process build = new Process();
build.StartInfo.WorkingDirectory = #"dir";
build.StartInfo.Arguments = "";
build.StartInfo.FileName = "my.exe";
build.StartInfo.UseShellExecute = false;
build.StartInfo.RedirectStandardOutput = true;
build.StartInfo.RedirectStandardError = true;
build.StartInfo.CreateNoWindow = true;
build.ErrorDataReceived += build_ErrorDataReceived;
build.OutputDataReceived += build_ErrorDataReceived;
build.EnableRaisingEvents = true;
build.Start();
build.BeginOutputReadLine();
build.BeginErrorReadLine();
build.WaitForExit();
}
// write out info to the display window
static void build_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
string strMessage = e.Data;
if (richTextBox != null && !String.Empty(strMessage))
{
App.Instance.Dispatcher.BeginInvoke(DispatcherPriority.Send, (ThreadStart)delegate()
{
Paragraph para = new Paragraph(new Run(strMessage));
para.Margin = new Thickness(0);
para.Background = brushErrorBrush;
box.Document.Blocks.Add(para);
});
}
}
I'm not sure exactly what problem you're running into, but if you're looking to act on output as soon as it's generated, try hooking into the process's OutputDataReceived event. You can specify handlers to receive output asynchronously from the process. I've used this approach successfully.
Process p = new Process();
ProcessStartInfo info = p.info;
info.UseShellExecute = false;
info.RedirectStandardOutput = true;
info.RedirectStandardError = true;
p.OutputDataReceived += p_OutputDataReceived;
p.ErrorDataReceived += p_ErrorDataReceived;
p.Start();
p.BeginOutputReadLine();
p.BeginErrorReadLine();
p.WaitForExit();
..
void p_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
Console.WriteLine("Received from standard out: " + e.Data);
}
void p_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
Console.WriteLine("Received from standard error: " + e.Data);
}
See the OutputDataReceived event off Process for more information.
Using lambda expressions, etc:
var info = new ProcessStartInfo(path)
{
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false,
Verb = "runas",
};
var process = new Process
{
EnableRaisingEvents = true,
StartInfo = info
};
Action<object, DataReceivedEventArgs> actionWrite = (sender, e) =>
{
Console.WriteLine(e.Data);
};
process.ErrorDataReceived += (sender, e) => actionWrite(sender, e);
process.OutputDataReceived += (sender, e) => actionWrite(sender, e);
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
Interestingly you can't read from standard output and standard error at the same time:
if you redirect both standard output and standard error and then try to read both, for
example using the following C# code.
[C#]
string output = p.StandardOutput.ReadToEnd();
string error = p.StandardError.ReadToEnd();
p.WaitForExit();
In this case, if the child process writes any text to standard error it will block the
process, because the parent process cannot read from standard error until it has finished
reading from standard output. However, the parent process will not read from standard
output until the process ends. A recommended solution to this situation is to create two
threads so that your application can read the output of each stream on a separate thread.
http://msdn.microsoft.com/en-us/library/system.diagnostics.processstartinfo.redirectstandardoutput(v=vs.71).aspx
flowing code worked in VS2010
void OnOutputDataReceived(object sender, DataReceivedEventArgs e)
{
if (String.IsNullOrEmpty(e.Data) == false)
{
new Thread(() =>
{
this.Dispatcher.Invoke(new Action(() =>
{
// Add you code here
}));
}).Start();
}
}
Check that the output you are expecting is not being sent to the StandardError output instead of the StandardOutput output

Categories