I've built Form App that I use for some time , Now I want to Catch the StandardError of my process as well as its standartOutput
I've looked at answers in SO and MSDN and yet and cant get it right
My code :
public void RunProcess(string FileName, string Arguments,, bool IsPrintOutput = true)
{
process = new Process();
process.ErrorDataReceived += new DataReceivedEventHandler(OnDataReceivedEvent);
if (IsPrintOutput) process.OutputDataReceived += new DataReceivedEventHandler(OnDataReceivedEvent);
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.FileName = FileName;
process.StartInfo.Arguments = Arguments;
if (EventWhenExit)
{
process.EnableRaisingEvents = true;
process.Exited += new EventHandler(myprocess_Exited);
}
process.Start();
process.BeginOutputReadLine();
//run polling on stored logs to print them to screen
PollingService();
}
I've check it with Iperf and I see that when I run it with correct argument I get correct output
but when I just send it with out any argumnet I see that with cmd I get
C:\>iperf.exe
Usage: iperf [-s|-c host] [options]
Try `iperf --help' for more information.
And my App I get Nothing !
what am I missing here ?
Thanks
You can stop reading here ! If you want to see the details of inner method continue below :
private void OnDataReceivedEvent(object sender, DataReceivedEventArgs e)
{
string ProcessOutput = e.Data;
ProcessLog.Add(e.Data);
}
private void PollingService()
{
var T = new Thread (()=>
{
while (true /* ProcessRunning*/)
{
if (ProcessLogIndex < ProcessLog.Count)
{
lock (this)
{
var tempList = ProcessLog.GetRange(ProcessLogIndex, ProcessLog.Count - ProcessLogIndex);
ProcessLogIndex = ProcessLog.Count;
foreach (var ToSend in tempList)
{
onDataOutputFromProcess(this, ToSend, sProcessNameID.ToString());
}
}
}
Thread.Sleep(400);
}
});
T.IsBackground = true;
T.Start();
}
I don't see a call to BeginErrorReadLine() anywhere in the code you posted. If you don't call that method, then the Process class won't actually redirect the stderr to your event handler.
I believe the above is the issue, but if you are actually calling that somewhere (and just didn't show it), then it is worth considering that there are some strange console programs out there that don't actually used stderr (or stdout) for error output. Instead, they write directly to the console window or some other non-standard mechanism. In those cases, you won't be able to receive the error output by redirecting stderr.
You can identify those programs by redirecting their output at the command like with e.g. iperf.exe 2> foo.txt. The stderr file handle is 2, and so that syntax redirects that file handle to a file named foo.txt. If the file is empty and you see errors on the screen, then the program is one of those strange programs.
But really, I think you probably just forgot to call BeginErrorReadLine(). :)
Related
I need to execute a Process (CMD executing a script) called from within a Windows Forms app, and update some controls when getting Output from the process.
I am using Windows Forms in .NET Core 5.0.
I have been researching a bit and found the example Extension method InvokeIfRequired,
public static void InvokeIfRequired(this Control control, MethodInvoker action)
{
if (control.InvokeRequired) control.Invoke(action);
else action();
}
Though at first I tried just using that same structure but for each control I needed to update.
This is the code of the function I am calling the process from, and trying to update the controls from:
private int StartGeneration()
{
int result = 0;
using (Process cmd = new Process())
{
cmd.StartInfo.FileName = "cmd.exe";
cmd.StartInfo.WorkingDirectory = #"C:\git\pub";
cmd.StartInfo.RedirectStandardInput = true;
cmd.StartInfo.RedirectStandardOutput = true;
cmd.StartInfo.CreateNoWindow = true;
cmd.Start();
cmd.Exited += (sender, e) =>
{
Console.WriteLine("Exiting from CMD process");
};
cmd.OutputDataReceived += (sender, e) =>
{
lbResult.InvokeIfRequired(new MethodInvoker(delegate
{
lbResult.Text = e.Data;
}));
if (e.Data.ToLower().Contains("exiting"))
{
// The script has ended
lbResult.InvokeIfRequired(new MethodInvoker(delegate
{
lbResult.ForeColor = System.Drawing.Color.Aqua;
lbResult.Text = "Done";
}));
rtbResult.InvokeIfRequired(new MethodInvoker(delegate
{
rtbResult.Visible = true;
rtbResult.Text = cmd.StandardOutput.ReadToEnd();
Size = new System.Drawing.Size(Width, Height + 460);
}));
cmd.StandardInput.Flush();
cmd.StandardInput.Close();
cmd.Close();
}
};
cmd.BeginOutputReadLine();
cmd.StandardInput.WriteLine(BuildPublishingScriptCommand());
cmd.WaitForExit();
}
return result;
}
And the BuildPublishingScriptCommand() function is just a simple function that returns a string, containing the script I need to run in the CMD.
The problem comes when the code gets in the OutputDataReceived event of the process and I try to use the InvokeIfRequired method (or any Invoke method in general).
The application just keeps running infinitely but nothing updates. Though the script completes successfully because I can confirm the work is done (creates some DLLs).
If I debug, I get until the call to InvokeIfRequired and then I lose control to debugging.
It does not give any exception.
I have not tried anything else.
I work at Ubisoft and we use a very old program to manipulate some files. Since it's legacy software, it's really bad and it may happen that the software has crashed and keeps on running. We sadly don't have access to the code, so we're unable to fix that. I was wondering, is it possible to use System.Diagnostics.Process with a "no log timeout"? Here's what I'm trying to achieve
var legacySoftwareProcess = new Process
{
StartInfo =
{
UseShellExecute = false,
RedirectStandardOutput = true,
WorkingDirectory = localPackageFolder,
FileName = CiConfig.DataGeneration.RebuildUnstrippedBatName
}
};
legacySoftwareProcess.IdleLogTimeout = 5 * 60; // 5 minutes
legacySoftwareProcess.Start();
var output = proc.StandardOutput.ReadToEnd();
legacySoftwareProcess.WaitForExit();
if (legacySoftwareProcess.ExitCode != 0)
{
Context.LogMessage(output);
Context.LogError("The process exited with non 0 code");
}
Rather than using:
var output = proc.StandardOutput.ReadToEnd();
You can listen for the event when output data is received from the process:
proc.OutputDataReceived += ResetTimer;
proc.Start();
proc.BeginOutputReadLine(); // not sure that you should use that as it may read output synchronously (I will check that soon)
And in the handler method ResetTimer, as the method name implies, reset a 5-minute timer:
static void ResetTimer(object sender, DataReceivedEventArgs e)
{
if (e.Data != null)
{
// reset the timer
}
}
If timer has elapsed, it means nothing has been outputed for 5 minutes, and you can take action accordingly, ie kill the process.
I need to communicate with external executable (ampl.exe) using standard input and standard output. This exe make calculations during some minutes with some display in the console. It has a prompt so I can succesively launch calculations by using its standard input as soon as a calculation is finished.
The external exe is launched as :
var myProcess = new Process();
myProcess.StartInfo = new ProcessStartInfo("ampl.exe");
myProcess.StartInfo.CreateNoWindow = true;
myProcess.StartInfo.UseShellExecute = false;
myProcess.StartInfo.RedirectStandardOutput = true;
myProcess.StartInfo.RedirectStandardError = true;
myProcess.StartInfo.RedirectStandardInput = true;
myProcess.Start();
I communicate with it by using myProcess.StandardInput and myProcess.StandardOutput (synchronous way).
I use standard input to launch the calcul, for example :
myProcess.StandardInput.WriteLine("solve;");
I want to wait the end of the solve statement, get results in files, prepare new calculation input files and then launching a second solve.
My problem is that I do now know when the first calculation is finished, that is when the exe is waiting for new command in its standard input.
The only way I found is to add a specific display command and wait for getting it it its standard output :
myProcess.StandardInput.WriteLine("solve;");
myProcess.StandardInput.WriteLine("print 'calculDone';");
string output = myProcess.StandardOutput.ReadLine();
while (!output.Contains("calculDone"))
{
output = myProcess.StandardOutput.ReadLine();
}
Is there another way avoiding to use this display command to do this ?
Edit : following advices, I tried the asynchronous way. But I still need to print 'CalculDone' to know when the solve statement ended. I do not get the prompt of ampl.exe (which is 'ampl : ') in the standard output of the process.
AutoResetEvent eventEnd = new AutoResetEvent(false);
var myProcess = new Process();
myProcess.StartInfo = new ProcessStartInfo("ampl.exe");
myProcess.StartInfo.CreateNoWindow = true;
myProcess.StartInfo.UseShellExecute = false;
myProcess.StartInfo.RedirectStandardOutput = true;
myProcess.StartInfo.RedirectStandardError = true;
myProcess.StartInfo.RedirectStandardInput = true;
myProcess.EnableRaisingEvents = true;
myProcess.OutputDataReceived += (sender, e) =>
{
if (e.Data == "commandDone")
{
eventEnd.Set();
}
else if (e.Data != null)
{
Console.WriteLine("ampl: {0}", e.Data);
}
};
myProcess.Start();
myProcess.BeginOutputReadLine();
myProcess.StandardInput.WriteLine("solve;");
myProcess.StandardInput.WriteLine("print 'commandDone';");
eventEnd.WaitOne();
The best option would be to use the Processs.OutputDataReceived event instead of a tight while loop. It’s like the event async pattern, you launch an asynchronous task and wait for an event callback telling you it’s done. The continuation of the asynchronous task would go in the event handler. Remember to unsubscribe the event handler the first time it goes off, otherwise it will be firing when you don’t want it to.
Another option I have never tried is Process.WaitForInputIdle() method, but I’m not sure if this will work in your particular case. If it does you wouldn’t need to write anything to the input stream.
A legacy program "LegacyBuilder" runs "batch.cmd" file and then redirects its output to a file, like this.
ProcessStartInfo processStartInfo = new ProcessStartInfo("C:\\batch.cmd");
processStartInfo.RedirectStandardOutput = true;
processStartInfo.RedirectStandardError = true;
processStartInfo.UseShellExecute = false;
using (Process process = Process.Start(processStartInfo))
{
using (StreamWriter logWriter = new StreamWriter("C:\\log.txt"))
{
while ((logLine = process.StandardOutput.ReadLine()) != null)
{
logWriter.WriteLine(logLine);
}
}
}
The batch.cmd contains this line
C:\SomeApp.exe "arg1" "arg2" "arg3"
SomeApp.exe is using the following method from a different assembly.
SomeAssembly.SomeClass.GenerateOutput()
This GenerateOutput method create a process to a 3rd party console program. After troubles with buffer deadlocking, I discovered (from another SO question I think) the following code never cause such deadlock and output of the 3rd party program is captured by the "LegacyBuilder".
Console.Write("I'm creating the process!");
ProcessStartInfo processStartInfo = new ProcessStartInfo("C:\\3rdPartyConsoleExe.exe");
processStartInfo.RedirectStandardOutput = true;
processStartInfo.RedirectStandardError = true;
processStartInfo.UseShellExecute = false;
using (Process process = new Process())
{
process.StartInfo = processStartInfo;
using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
{
process.OutputDataReceived += (sender, e) =>
{
if (e.Data == null) outputWaitHandle.Set();
else output.AppendLine(e.Data);
};
process.ErrorDataReceived += (sender, e) =>
{
if (e.Data == null) errorWaitHandle.Set();
else error.AppendLine(e.Data);
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
if (process.WaitForExit(60000) && outputWaitHandle.WaitOne(60000) && errorWaitHandle.WaitOne(60000))
{
Console.WriteLine(output.ToString());
Console.WriteLine(error.ToString());
if (process.ExitCode > 0)
Console.WriteLine("PROCESS COMPLETED ExitCode=" + process.ExitCode.ToString());
}
else
Console.WriteLine("PROCESS TIMED OUT");
}
}
Please note the Console.Write("I'm creating the process!");
So far so good, everything is working, all output is being captured by the "LegacyBuilder"
Now, batch.cmd was updated with calling a powershell script.
C:\Windows\SysWOW64\WindowsPowerShell\v1.0\PowerShell.exe -File "C:\powershellscript.ps1"
The powershell script basically loads the SomeAssembly, creates SomeObject and then calls the GenerateOutput method. Like this.
Add-Type -Path "C:\SomeAssembly.dll";
$someCls = New-Object SomeAssembly.SomeClass()
$someCls.GenerateOutput();
The problem: The text "I'm creating the process!" is being captured by the "LegacyBuilder" so this is going to stdout.
But the expected output of the GenerateOutput() method is not generating anything and after several attempted calls it gets deadlocked again. Not even "PROCESS TIMED OUT" gets called.
This is weird.
batch.cmd calls "SomeApp.exe" which calls SomeAssembly.SomeClass.GenerateOutput(). The output of the 3rd party program is present in the file and the batch.cmd successfully continues.
batch.cmd calls powershell which loads a script which create SomeAssembly.SomeClass object and then call GenerateOutput() method. The output is not present, only output from direct Console.Write calls is ... after several attemps, it gets deadlocked and batch.cmd never continues.
Any help? I wish something like "StopBufferDeadlocksInPowershell -includeStupidLegacyNestedProcessCalls" existed...
EDIT 1
I would love to avoid making any changes in "LegacyBuilder" since the code in it is really, really complex (the topmost code sample is just simplification) and while I'd love to rewrite it completely, it would be very time consuming.
After days of headaches, somehow, this made it working.
$someCls.GenerateOutput() | Out-Host;
The buffer is no longer deadlocked.
For one of my implementations I am working on a tool that is supposed to send/retrieve commands/results to/from the cmd window. Everything works fine but the Use case below fails to do anything. It seems as if my application is waiting for something (instead of displaying the result)
From my tool I navigate to the python folder . From the python folder I try to launch python.exe but at this point, my editor does not do anything. it simply keeps on waiting.
For your kind consideration I am also linking the video here. It would be easier for you guys to understand what I am trying to say.
View the Video here (on youtube)
I am also attaching the code that I currently have.
ProcessStartInfo info = new ProcessStartInfo("cmd.exe");
string argument = null;
if (!string.IsNullOrEmpty(startingDirectory) && System.IO.Directory.Exists(startingDirectory))
{
argument += #"cd\";
}
else
{
argument += "\"";
}
info.Arguments = argument;
info.CreateNoWindow = true;
info.RedirectStandardError = true;
info.RedirectStandardInput = true;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
this.shellProcess = System.Diagnostics.Process.Start(info);
this.shellProcess.EnableRaisingEvents = true;
//this.InputStream.AutoFlush = true;
this.shellProcess.Exited += new EventHandler(ProcessExited);
this.ErrorBeginRead();
this.OutputBeginRead();
private void OutputBeginRead()
{
this.shellProcess.StandardOutput.BaseStream.BeginRead(outputBuffer, 0, outputBuffer.Length, new AsyncCallback(this.OnOutputInput), this.shellProcess);
}
private void ErrorBeginRead()
{
this.shellProcess.StandardError.BaseStream.BeginRead(errorBuffer, 0, errorBuffer.Length, new AsyncCallback(this.OnErrorInput), this.shellProcess);
}
Thank you !
EDIT:
Launching python is just an example. I need to use the same method for other normal cmd line commands as well.It would be nice, if somebody can point what i am doing wrong with the code that I have or what I must do , in order to achieve the intended functionality.
EDIT 2 : The normal cmd commands are working perfectly. The command line tools like python,perl are not working .
Edit 3 : So I managed to do move a wee bit forward following Jamie's suggestions. The ui is not "hanging" anymore. but when i access the python interpreter , the interpreter's output is still not visible in my tool. Any suggestions why that might be happening ?
You cannot send commands to a shell this way. The string in info.Arguments is the arguments provided to the program on the command line. If you want the cmd.exe shell to execute a series of command and then quit you will have to provide the /c argument. If you have multiple commands that you want it to perform you will either have to put the commands in a batch file and execute that or enclose them in quotes and separate them with &&, i.e. info.Arguments = #"/c ""cd \ && dir""";. Your other issue with never returning is that cmd.exe opens in interactive mode by default when it is executed without any, or proper, arguments. The /c option tells cmd.exe to execute the relevant commands and then quit.
Additionally, interpreters like python and perl sometimes have weird behaviors when launched directly from ProcessStartInfo. If info.Arguments = #"""MyPerlProgram.pl"""; with perl.exe doesn't work, you may find it necessary to launch them inside cmd.exe to get normal behavior out of them, i.e. info.Arguments = #"/c ""perl.exe ""MyPerlProgram.pl""""";.
See Cmd and ProcessStartInfo.Arguments Property.
To answer your Edit 3 problem, you're probably not correctly hooking into the outputs. Instead of trying to hook the StreamReader's BaseStream, hook the OutputDataReceived event with this.shellProcess.OutputDataReceived += ProcessOutputHandler; before you call Start where ProcessOutputHandler has a signature like public static void ProcessOutputHandler(object sendingProcess, DataReceivedEventArgs outLine). Immediately after calling Start, call this.shellProcess.BeginOutputReadLine();. The process is similar for the error ouput as well. See Process.BeginOutputReadLine Method and Process.BeginErrorReadLine Method for more details.
If you still have a problem, what do you get if you just try process.StartInfo.Arguments = #"/c ""python.exe -c ""import sys; print 'Test.';""""";?
Also, the code below demonstrates most of the necessary concepts for shell communication:
public static void Main()
{
using (Process process = new Process())
{
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.WorkingDirectory = #"C:\";
process.StartInfo.FileName = Path.Combine(Environment.SystemDirectory, "cmd.exe");
// Redirects the standard input so that commands can be sent to the shell.
process.StartInfo.RedirectStandardInput = true;
// Runs the specified command and exits the shell immediately.
//process.StartInfo.Arguments = #"/c ""dir""";
process.OutputDataReceived += ProcessOutputDataHandler;
process.ErrorDataReceived += ProcessErrorDataHandler;
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// Send a directory command and an exit command to the shell
process.StandardInput.WriteLine("dir");
process.StandardInput.WriteLine("exit");
process.WaitForExit();
}
}
public static void ProcessOutputDataHandler(object sendingProcess, DataReceivedEventArgs outLine)
{
Console.WriteLine(outLine.Data);
}
public static void ProcessErrorDataHandler(object sendingProcess, DataReceivedEventArgs outLine)
{
Console.WriteLine(outLine.Data);
}
You may have threading issues causing your problems. I've done some further work with this and was able to get a textbox on a form to update with the following code:
using System;
using System.Diagnostics;
using System.IO;
using System.Timers;
namespace DummyFormsApplication
{
class ProcessLauncher : IDisposable
{
private Form1 form;
private Process process;
private bool running;
public bool InteractiveMode
{
get;
private set;
}
public ProcessLauncher(Form1 form)
{
this.form = form;
process = new Process();
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.WorkingDirectory = #"C:\";
process.StartInfo.FileName = Path.Combine(Environment.SystemDirectory, "cmd.exe");
// Redirects the standard input so that commands can be sent to the shell.
process.StartInfo.RedirectStandardInput = true;
process.OutputDataReceived +=new DataReceivedEventHandler(process_OutputDataReceived);
process.ErrorDataReceived += new DataReceivedEventHandler(process_ErrorDataReceived);
process.Exited += new EventHandler(process_Exited);
}
public void Start()
{
if (running == false)
{
running = true;
InteractiveMode = true;
// Runs the specified command and exits the shell immediately upon completion.
process.StartInfo.Arguments = #"/c ""C:\python27\python.exe -i""";
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
}
}
public void Start(string scriptFileName)
{
if (running == false)
{
running = true;
InteractiveMode = false;
// Runs the specified command and exits the shell immediately upon completion.
process.StartInfo.Arguments = string.Format(#"/c ""C:\python27\python.exe ""{0}""""", scriptFileName);
}
}
public void Abort()
{
process.Kill();
}
public void SendInput(string input)
{
process.StandardInput.Write(input);
process.StandardInput.Flush();
}
private void process_OutputDataReceived(object sendingProcess, DataReceivedEventArgs outLine)
{
if (outLine.Data != null)
{
form.Invoke(form.appendConsoleTextDelegate, new object[] { outLine.Data });
}
}
private void process_ErrorDataReceived(object sendingProcess, DataReceivedEventArgs outLine)
{
if (outLine.Data != null)
{
form.Invoke(form.appendConsoleTextDelegate, new object[] { outLine.Data });
}
}
private void process_Exited(object sender, EventArgs e)
{
running = false;
}
public void Dispose()
{
if (process != null)
{
process.Dispose();
}
}
}
}
I created a form and added a textbox and the following code in the form:
public delegate void AppendConsoleText(string text);
public AppendConsoleText appendConsoleTextDelegate;
private void Form1_Load(object sender, EventArgs e)
{
appendConsoleTextDelegate = new AppendConsoleText(textBox1_AppendConsoleText);
using (ProcessLauncher launcher = new ProcessLauncher(this))
{
launcher.Start();
launcher.SendInput("import sys;\n");
launcher.SendInput("print \"Test.\";\n");
launcher.SendInput("exit()\n");
}
}
private void textBox1_AppendConsoleText(string text)
{
textBox1.AppendText(string.Format("{0}\r\n", text));
}
One thing to note is that if the Form1_Load event doesn't complete, Invoke will hang until it does. If you have long-running code in an event you'll either need to invoke asynchronously using BeginInvoke, or periodically call DoEvents in your long-running code.
EDIT
Per your comment, I've modified the code to work with interactive submissions. There is, however, a problem. The python prompt (>>>) is provided on the StandardError output and it does not echo the StandardInput. It also does not terminate the line. This makes detecting a prompt difficult and causes some out of order output of the prompt characters due to the process_ErrorDataReceived not firing until either the process ends or a line end is seen.
There's not enough code in your question to figure out exactly what your application is hanging on. There are some things in your code which look odd. For example, why are you starting your own error and output read loops instead of using the ones built into the Process class? Like this:
var shellProcess = System.Diagnostics.Process.Start(info);
shellProcess.EnableRaisingEvents = true;
shellProcess.Exited += ProcessExited;
shellProcess.OutputDataReceived += ShellProcess_OutputDataReceived;
shellProcess.ErrorDataReceived += ShellProcess_ErrorDataReceived;
shellProcess.BeginOutputReadLine();
shellProcess.BeginErrorReadLine();
void ShellProcess_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
// Do Something
}
void ShellProcess_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
// Do Something
}
Since your error and output async events are not firing, it leads me to believe that there might be a lifetime issue with the shellProcess. If you post more of your code, we can give better guidance.
I can't see all your code, but you can easily use Steam objects to write/send commands to the CMD Window created by you. e.g.:
StreamWriter inputStream = shellProcess.StandardInput;
//send command to cmd prompt and wait for command to execute with thread sleep
inputStream.WriteLine("echo "CMD just received input");
inputStream.Flush();
In the above example for instance, Command prompt will receive the echo command just like it was entered in the window. To show the output you will have to create StreamReader object and assign it to the process's StandardOutput.