locked
Redirecting command window messages to rich text box RRS feed

  • 問題

  • Hello All,

    I am trying to run a batch script from my C# desktop application.

    I want to display all errors and messages from command prompt to be redirected to a rich text box. Now, I want these messages to be displayed the same time as it is output - not to display all the messages after the batch script has finished running. This is what I did:

    private void button1_Click(object sender, EventArgs e) { // Start the child process. Process p = new Process(); // Redirect the output stream of the child process. p.StartInfo.CreateNoWindow = true; p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardInput = true; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.RedirectStandardError = true;

    p.StartInfo.FileName = "batchScript.bat";
        p.StartInfo.Arguments = string.Format("\"{0}\" \"{1}\"", arg1, arg2);

    p.Start(); string text = p.StandardOutput.ReadToEnd(); richTextBox1.Text = text; p.WaitForExit(); }

    But my code only outputs all the messages to the text box when the batch script has finished executing. How do I make it printout the messages instantly?

    I rarely uses C# and .Net and do not know about the topic at hand in depth.

    Regards,

    Sudhi


    • 已編輯 Pillasaar 2013年7月2日 上午 10:46 Added two lines of code that was I missed last time while adding to this page
    2013年7月1日 下午 04:56

解答

  • 1) WaitForExit is a blocking call.  If you call it on the UI thread (which you are) then your UI will freeze until the other process ends.  During that time you won't see any messages.  If you want to see the messages while the process is running then you're going to have to move the process call to a separate thread.  If you're running .NET 4.5 then you can probably use async/await in combination with the newer IProgress interface easily accomplish this.  You'd await on the process WaitForExit (using Task.Run most likely).  The issue though is that your event handler for messages will be called on the wrong thread so you'd need to Invoke the message to the UI thread using richTextBox1.Invoke.

    If you're using .NET 4 (or even if you're not) then I'd recommend using BackgroundWorker instead.  Because you want to do some work without blocking the UI thread but want to receive messages on the UI thread then BWC is the easiest approach in my opinion.  In the DoWork handler you'll setup and call the process along with WaitForExit.  In your data event handler for the process you'll use BWC.ReportProgress to notify the UI of a new message.  The UI will hook up the progress event and add the received message to the text box.

    2) Handle ErrorDataReceived just like you are for OutputDataReceived.  Note that order is not deterministic between the 2 streams so you cannot easily mingle the 2 streams together in the order they were generated.

    3) I wouldn't use a batch script in code.  A batch script requires that you start the command processor.  By default the cmd processor won't wait for the script to finish before returning control to you.  Therefore you generally have to pass the appropriate option (/C I believe) to cause the processor to terminate when it is done so you can monitor for process termination.  I'm speculating that this is the problem you're seeing.

    Michael Taylor
    http://msmvps.com/blogs/p3net

    • 已標示為解答 Pillasaar 2013年7月5日 下午 03:32
    2013年7月3日 下午 01:53

所有回覆

  • I have added two lines of code that I somehow missed last time.

    Is nobody replaying because what I have done is utter rubbish? :-|

    Anyone? Please?

    • 已編輯 Pillasaar 2013年7月2日 上午 10:48 Added 2 lines of code that I missed last time.
    2013年7月2日 上午 09:07
  • You cannot use ReadToEnd if you want to capture all the data and display it "as it appears".  The ReadToEnd is going to block until the stream is closed.  Besides this approach is going to cause problems if the called process generates lots of output as the buffers may fill up.  You'll need to async read the output and error streams.  The documentation for the methods has a complete example of how to handle it. 

    Michael Taylor
    http://msmvps.com/blogs/p3net

    2013年7月2日 下午 02:11
  • Thank you very much for you help Michael! I modified the code as follows:

    private void button1_Click(object sender, EventArgs e)
    {
        // Start the child process.
        Process p = new Process();
    
        p.OutputDataReceived += new DataReceivedEventHandler  (OutputHandler);
    
        // Redirect the output stream of the child process.
        p.StartInfo.CreateNoWindow = true;
        p.StartInfo.UseShellExecute = false;
        p.StartInfo.RedirectStandardInput = true;
        p.StartInfo.RedirectStandardOutput = true;
        p.StartInfo.RedirectStandardError = true;
        p.StartInfo.FileName = "batchScript.bat";
        p.StartInfo.Arguments = string.Format("\"{0}\" \"{1}\"", arg1, arg2);
    
        p.Start();
    
        p.BeginOutputReadLine();
    
        //p.WaitForExit();
    }
    
    private void OutputHandler(Object source,   DataReceivedEventArgs outLine)
    {
        // Collect the sort command output. 
        if (!String.IsNullOrEmpty(outLine.Data))
        {
            richTextBox1.AppendText(outLine.Data + "\r\n");
        }
    }

    However, there are still a few issues.

    (i) As you would notice, the line 

    p.WaitForExit();

    is commented out. When I un-comment it, no output appears in the rich textbox. 

    (ii) How do I redirect error messages?

    (iii) My batchscript is as follows:

    for /l %%x in (1, 1, 10) do (@echo %%x 
    TIMEOUT 2)
    
    pause

    When I double click and run the batch file, it waits for two seconds for every iteration of the loop. But when I run it from my application, it just runs to finish without any delay. Probably, this may be a question to the batch script experts?

    Thanks!

    2013年7月3日 上午 10:31
  • 1) WaitForExit is a blocking call.  If you call it on the UI thread (which you are) then your UI will freeze until the other process ends.  During that time you won't see any messages.  If you want to see the messages while the process is running then you're going to have to move the process call to a separate thread.  If you're running .NET 4.5 then you can probably use async/await in combination with the newer IProgress interface easily accomplish this.  You'd await on the process WaitForExit (using Task.Run most likely).  The issue though is that your event handler for messages will be called on the wrong thread so you'd need to Invoke the message to the UI thread using richTextBox1.Invoke.

    If you're using .NET 4 (or even if you're not) then I'd recommend using BackgroundWorker instead.  Because you want to do some work without blocking the UI thread but want to receive messages on the UI thread then BWC is the easiest approach in my opinion.  In the DoWork handler you'll setup and call the process along with WaitForExit.  In your data event handler for the process you'll use BWC.ReportProgress to notify the UI of a new message.  The UI will hook up the progress event and add the received message to the text box.

    2) Handle ErrorDataReceived just like you are for OutputDataReceived.  Note that order is not deterministic between the 2 streams so you cannot easily mingle the 2 streams together in the order they were generated.

    3) I wouldn't use a batch script in code.  A batch script requires that you start the command processor.  By default the cmd processor won't wait for the script to finish before returning control to you.  Therefore you generally have to pass the appropriate option (/C I believe) to cause the processor to terminate when it is done so you can monitor for process termination.  I'm speculating that this is the problem you're seeing.

    Michael Taylor
    http://msmvps.com/blogs/p3net

    • 已標示為解答 Pillasaar 2013年7月5日 下午 03:32
    2013年7月3日 下午 01:53
  • Hello again!

    I used the background worker and it has solved a lot of my issues. This is how my code looks like now:

    Button click event:

    private void button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.RunWorkerAsync();
    }

    Back ground worker:

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        ProcessStartInfo pStartInfo = new ProcessStartInfo("cmd.exe", "/c " +"batchscript.bat");
    
        pStartInfo.CreateNoWindow = true;
        pStartInfo.UseShellExecute = false;
        pStartInfo.RedirectStandardInput = true;
        pStartInfo.RedirectStandardOutput = true;
        pStartInfo.RedirectStandardError = true;
    
        pStartInfo.Arguments = string.Format("\"{0}\" \"{1}\"" , arg1, arg2);
        Process process1 = new Process();
    
        process1.OutputDataReceived += new DataReceivedEventHandler(OutputHandler);
        process1.ErrorDataReceived += new DataReceivedEventHandler(ErrorHandler);
    
        process1.StartInfo = pStartInfo;
        process1.SynchronizingObject = richTextBox1;
    
        process1.Start();
        process1.BeginOutputReadLine();
    
        process1.WaitForExit();
    }

    Message output handlers:

    private void OutputHandler(Object source, DataReceivedEventArgs outLine)
    {
        // Collect the sort command output. 
        if (!String.IsNullOrEmpty(outLine.Data))
        {
            richTextBox1.AppendText(outLine.Data + "\r\n");
        }
    }
    
    private void ErrorHandler(Object source, DataReceivedEventArgs outLine)
    {
        // Collect the sort command output. 
        if (!String.IsNullOrEmpty(outLine.Data))
        {
            richTextBox1.AppendText(outLine.Data + "\r\n");
        }
    }

    Before starting to use the /c option, I got all the output displayed on to the text box eventhough with the issue that I mentioned in the previous post.

    When I started using /c using the code above, it opens the command window, but it doesn't do anything else. It is as if it doesn't see the arguments after cmd.exe. I also noticed that pStartInfo.filename is now cmd.exe and not the file name that I am providing after the /c argument.

    Thanks again for your help. Much appreciated.

    2013年7月3日 下午 04:51
  • You cannot access the UI from the event handlers.  You need to handle the BWC's ProgressChanged event.  Within that event you can append text to your control.  Your output/error handlers should call BWC's ReportProgress method to cause the event to be raised.

    As for the problem with /c, you are overwriting your arguments. The ctor that you're calling initially accepts the binary name as the first parameter and the argument to pass to it as the second parameter.  Later on you assign a new value to Arguments which wipes out the second parameter you specified initially.  You need to use one approach or the other.  Personally I tend to use the Arguments property as I find it cleaner.  As such you need to include the /c and script name as part of the arguments when you set it.

    Michael Taylor
    http://msmvps.com/blogs/p3net

    2013年7月3日 下午 06:00
  • Hello Michael,

    I made the following modifications as you suggested -

    Output handler:

    private void OutputHandler(Object source, DataReceivedEventArgs outLine)
    {
        message = outLine.Data + "\r\n";
       // Raise BWC event and give a progress of 50 as a dummy value.
        backgroundWorker1.ReportProgress(50); 
    }

    Background worker:

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {           
        ProcessStartInfo pStartInfo = new ProcessStartInfo();
    
        pStartInfo.CreateNoWindow = true;
        pStartInfo.UseShellExecute = false;
        pStartInfo.RedirectStandardInput = true;
        pStartInfo.RedirectStandardOutput = true;
        pStartInfo.RedirectStandardError = true;
    
        pStartInfo.FileName = "cmd.exe";
        pStartInfo.Arguments = string.Format("\"/c \" \"C:\\batchscript.bat\" \"{0}\" \"{1}\"", arg1, arg2);
                
        Process process1 = new Process();
    
        process1.OutputDataReceived += new DataReceivedEventHandler(OutputHandler);
        process1.ErrorDataReceived += new DataReceivedEventHandler(ErrorHandler);
        process1.StartInfo = pStartInfo;
    
        process1.Start();
    
        process1.BeginOutputReadLine();
    
        process1.WaitForExit();
    }

    To give you some back ground information on what I am trying to do:

    I am trying to create a GUI for my C compiler to make my life a bit easier in my day job. So in my batch script I have commands for rebuilding my C project. arg1 and arg 2 are my build options.

    Now, by doing the modifications above, the comments that I have in the batch script get printed in rich text box immediately; then it sends the build command to my C project. Then the form just waits for a long time as if nothing is happening(it doesn't get hang or anything) and then displays a huge number of compiled file names together. Then it waits for a long time again and displays the rest of the files compiled. If I try the build command in command window, it will start displaying the currently compiled c files one by one from the beginning itself without any delay, till the whole build gets over.

    If I put a break point in the output handler, while the c compiler is compiling, the break point is not hit till it starts printing out the first chunk of messages. This means the process is indeed not sending out any message during this period?

    Also, using /k or /c as cmd argument resulted in the same behavior. Using no argument caused cmd.exe to run, but nothing happened after this (i.e the batch file was not run).

    Thank you,

    Sudhi



    • 已編輯 Pillasaar 2013年7月5日 下午 12:45 Added details about cmd option.
    2013年7月5日 上午 09:28
  • FYI since you want to capture errors you also need to include a call to BeginErrorReadLine.

    As for the buffering behavior I suspect it is because of the fact that you're using a script file so you have a couple of levels of indirection going on.  Your program sends its output to the processor which then displays it which triggers your GUI to react.  As a test can you temporarily eliminate the batch script and just call your compiler directly and see if the messages start appearing correctly.  If so then it is likely a processor issue.  Note that you can sort of see a similar behavior when you compile code in VS with diagnostics turned on.  Compiler output generally appears in chunks whereas messages from MSBuild are pretty quick.

    Now some follow up questions.  Did you write your own compiler and are trying to wrap a GUI around it or are you trying to wrap an existing compiler?  Is the compiler part of Visual Studio or something else?  Can you implement the same functionality by just using PowerShell rather than building a full GUI?

    2013年7月5日 下午 01:50
  • Yes, the errors need to be redirected too, I just did not include that in my sample code.

    When I just used the command instead of the batch script, it still did the same - waiting for a long time and then printing out a chunk of messages. When I try the same command from a command window, the messages appear immediately. So the delay is in my application recognizing that a message has appeared in the message stream?

    And now the answer to your questions -

    I didn't write my own compiler. I write embedded software for electronic devices and command line compilers are still common in embedded software development. I was just trying to give the compiler a GUI - just as a hobby project.

    It is not part of visual studio, it is a gnu compiler for C language.

    I do not know anything about powershell, I will have to ask Mr. google! If you think this is something worth looking at for this kind of application, do you have any starting point tutorial to suggest? 

    My knowledge of .Net and desktop applications are limited and is self taught, so I wouldn't be surprised if there is an easy and straight forward alternative to do it.

    Once again thank you very much for your help with this!

    Thanks and regards,

    Sudhi

    2013年7月5日 下午 03:01
  • It sounds like you might have to live with the delay then.  Unless it is really bad it might not be worth trying to figure out.  The delay could be because of the invoke that has to happen between the worker thread and the UI thread.  You could try to speed it up by calling DoEvents after adding the message to the control but I don't know if that is worth the effort.

    I would forgo the GUI then and just use Powershell.  It is a lot like a command script but full typed and a lot nicer to use.  Unfortunately it has a pretty steep learning curve even if you know batch scripting.  Nevertheless to run something like the gnu compiler you can do something similar to this:

    $outputFile = "MyProgram.exe"
    
    $filesToCompile = get-childitem . -Include *.c -Recurse
    [System.String]::Join(" ", $filesToCompile)
    .\gcc.exe -o $outputFile [System.String]::Join(" ", $filesToCompile)

    The above isn't fully tested but what it does is recursively get all .c files in the current and child directories.  Then it passes them all to the GCC compiler for compilation.  If you have Windows 7 installed the Powershell has a helpful IDE included that makes this a lot nicer to use.  With PS you can fancy things up like prompting for parameters, calling any .NET method, etc.

    2013年7月5日 下午 03:18
  • It is a significant delay unfortunately - around 15 mins before any message is printed out since the command is issued. I am probably going to give up on this :(

    Yes, I have windows 7, so I will try powershell too.

    You have indeed been very helpful, Michael; much appreciated.

    2013年7月5日 下午 03:31
  • 15 mins is way too long unless your CPU is pegged because of the compilation.  I agree that something else must be going on. 
    2013年7月5日 下午 03:49
  • Try the CSConsole on my SkyDrive.
    2013年7月5日 下午 06:28
  • Hi Sudhi 

    can you share the whole source code , similar thing i am working in cpp and runing into exception.

    Thanks in Advance!

    2020年2月5日 下午 03:43