Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I redirect a console program's output to a text box in a thread safe way?

I am having trouble redirecting console output to a windows forms text box. The problem is thread related. I am running a console app in the following way,

private void RunConsoleApp()
{
    Process proc = new Process();
    proc.StartInfo.FileName = "app.exe";
    proc.StartInfo.Arguments = "-a -b -c";
    proc.StartInfo.UseShellExecute = false;

    // set up output redirection
    proc.StartInfo.RedirectStandardOutput = true;
    proc.StartInfo.RedirectStandardError = true;    
    proc.EnableRaisingEvents = true;
    proc.StartInfo.CreateNoWindow = true;

    // Set the data received handlers
    proc.ErrorDataReceived += proc_DataReceived;
    proc.OutputDataReceived += proc_DataReceived;

    proc.Start();
    proc.BeginErrorReadLine();
    proc.BeginOutputReadLine();
    proc.WaitForExit();

    if (proc.ExitCode == 0)
    {
        out_txtbx.AppendText("Success." + Environment.NewLine);
    }
    else
    {
        out_txtbx.AppendText("Failed." + Environment.NewLine);
    }
}

and then capture and process the data with this output handler,

// Handle the date received by the console process
void proc_DataReceived(object sender, DataReceivedEventArgs e)
{
    if (e.Data != null)
    {
        if ((e.Data.EndsWith("DONE.")) || (e.Data.EndsWith("FAILED.")) ||
            (e.Data.StartsWith("RESET")))
        {
            // This crashes the application, but is supposedly the correct method
            this.AppendText(e.Data + Environment.NewLine);

            // This works, but the debugger keeps warning me that the call
            // is not thread safe
            //out_txtbx.AppendText(e.Data + Environment.NewLine);
        }
    }
}

The console text is then appended like this,

delegate void AppendTextDelegate(string text);

// Thread-safe method of appending text to the console box
private void AppendText(string text)
{
    // Use a delegate if called from a different thread,
    // else just append the text directly
    if (this.out_txtbx.InvokeRequired)
    {
        // Application crashes when this line is executed
        out_txtbx.Invoke(new AppendTextDelegate(this.AppendText), new object[] { text });
    }
    else
    {
        this.out_txtbx.AppendText(text);
    }
}

From all the documentation and examples I have seen this appears to be the correct method, except that it is crashing the application when out_txtbx.Invoke is called.

What could be broken and what alternative ways are there to do this?


Solution (as pointed out by Hans Passant)

The problem is that the app is stuck in a "deadly embrace" as a result of the line,

proc.WaitForExit();

That line should be removed and the method should look like this,

private void RunConsoleApp()
{
    Process proc = new Process();
    proc.StartInfo.FileName = "app.exe";
    proc.StartInfo.Arguments = "-a -b -c";
    proc.StartInfo.UseShellExecute = false;

    // set up output redirection
    proc.StartInfo.RedirectStandardOutput = true;
    proc.StartInfo.RedirectStandardError = true;    
    proc.EnableRaisingEvents = true;
    proc.StartInfo.CreateNoWindow = true;

    // Set the data received handlers
    proc.ErrorDataReceived += proc_DataReceived;
    proc.OutputDataReceived += proc_DataReceived;

    // Configure the process exited event
    proc.Exited += new EventHandler(ProcExited);

    proc.Start();
    proc.BeginErrorReadLine();
    proc.BeginOutputReadLine();

    // This blocks the main thread and results in "deadly embrace"
    // The Process.Exited event should be used to avoid this.
    //proc.WaitForExit();
}

and an event handler should be provided,

/// <summary>
/// Actions to take when console process completes
/// </summary>
private void ProcExited(object sender, System.EventArgs e)
{
    Process proc = (Process)sender;

    // Wait a short while to allow all console output to be processed and appended
    // before appending the success/fail message.
    Thread.Sleep(40);

    if (proc.ExitCode == 0)
    {
        this.AppendText("Success." + Environment.NewLine);
        ExitBootloader();
    }
    else
    {
        this.AppendText("Failed." + Environment.NewLine);
    }

    proc.Close();
}
like image 408
Duncan Drennan Avatar asked Dec 15 '10 18:12

Duncan Drennan


Video Answer


1 Answers

proc.WaitForExit();

It is called deadlock. Your main thread is blocked, waiting for the process to exit. That stops it from taking care of essential duties. Like keeping the UI updated. And making sure that Control.Invoke() requests are dispatched. That stops the AppendText() method from completing. Which stops the process for exiting. Which stops your UI thread from ever getting past the WaitForExit() call. "Deadly embrace", aka deadlock.

You cannot block your main thread. Use the Process.Exited event instead.

like image 154
Hans Passant Avatar answered Oct 08 '22 00:10

Hans Passant