Mysterious hang when executing batch scripts in a C# process and displaying the output in a textbox

0

I am debugging a class library (it's actually a Visual Studio extension) that is responsible to execute custom batch scripts using Windows' cmd.exe and display the results. The way it is set up is it uses a C# Process that calls cmd.exe and executes commands one by one and displays the results. Here's the interesting part: the outputs are displayed in a Windows Form Textbox! The Textbox is supposed to be on top while the commands are being executed (because of reasons beyond my control) and a close button is enabled when all the commands are executed. Only then the user can close this form. The problem is that when the close button is clicked, the application (in this case, Visual Studio) hangs. I receive the following exception when I debug the program:

 System.ComponentModel.InvalidAsynchronousStateException
 HResult=0x80070057
  Message=An error occurred invoking the method.  The destination thread no longer exists.
  Source=System.Windows.Forms
  StackTrace:
   at System.Windows.Forms.Control.WaitForWaitHandle(WaitHandle waitHandle)
   at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous)
   at System.Windows.Forms.Control.Invoke(Delegate method, Object[] args)
   at System.Windows.Forms.Control.Invoke(Delegate method)
   at VSPlugin.Ft.ProgressForm.AppendOutputLine(String line) in T:\...\ProgressForm.cs:line 99
   at VSPlugin.Ft.SystemShell.<>c__DisplayClass35_0.<Execute>b__0(Object sender, DataReceivedEventArgs e) in T:\..\SystemShell.cs:line 117
   at System.Diagnostics.Process.OutputReadNotifyUser(String data)
   at System.Diagnostics.AsyncStreamReader.FlushMessageQueue()
   at System.Diagnostics.AsyncStreamReader.GetLinesFromStringBuilder()
   at System.Diagnostics.AsyncStreamReader.ReadBuffer(IAsyncResult ar)
   at System.IO.Stream.ReadWriteTask.InvokeAsyncCallback(Object completedTask)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.IO.Stream.ReadWriteTask.System.Threading.Tasks.ITaskCompletionAction.Invoke(Task completingTask)
   at System.Threading.Tasks.Task.FinishContinuations()
   at System.Threading.Tasks.Task.FinishStageThree()
   at System.Threading.Tasks.Task.FinishStageTwo()
   at System.Threading.Tasks.Task.Finish(Boolean bUserDelegateExecuted)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
   at System.Threading.Tasks.Task.ExecuteEntry(Boolean bPreventDoubleExecution)
   at System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

This is the class that is supposed to execute the batch scripts. The scripts are stored as a string in the Script object. ExecuteAsTask() is the entry method of this class that is invoked when a button somewhere in the plugin is clicked.

class SystemShell
{
    protected Ft.System System { get; private set; }
    public List<string> Script { get; protected set; }
    public List<string> Output { get; private set; }

    public CommonParams Param { get; private set; }

    private Process OutputConsole { get; set; }
    private bool IncludeProgress { get; set; }
    private bool IgnorePreview { get; set; }
    public SystemShell(CommonParams fcparam, bool ignorePreview, bool includeProgress)
    {
        System = new Ft.System();
        Param = fcparam;
        Output = new List<string>();
        IncludeProgress = includeProgress;
        IgnorePreview = ignorePreview;
    }



    public Task ExecuteAsTask()
    {
        return Task.Run(() => this.Execute());
    }

    public void Execute()
    {            

        ProgressForm pf = null;            
        pf = new ProgressForm(Script.Count);
        pf.Start();            

        Process process = new Process()
        {
            StartInfo = new ProcessStartInfo()
            {
                CreateNoWindow = true,
                RedirectStandardError = true,
                RedirectStandardOutput = true,
                RedirectStandardInput = true,
                UseShellExecute = false,
                WorkingDirectory = Param.ControlPath,
                FileName = @"cmd.exe",
                Verb = @"runas", // to self elevate to administrative privilege...
                Arguments = @"/k"                    
            },                
            EnableRaisingEvents = true // enable raising events because Process does not raise events by default
        };

        DataReceivedEventHandler outputHandler = new DataReceivedEventHandler // attach the event handler for OutputDataReceived before starting the process
        (
            delegate (object sender, DataReceivedEventArgs e)
            {
                // append the new data to the data already read-in
                if (IncludeProgress)
                {
                    pf.AppendOutputLine(e.Data);
                }
                Output.Add(e.Data);
            }
        );

        process.OutputDataReceived += outputHandler;
        process.ErrorDataReceived += outputHandler;


        // start the process
        // then begin asynchronously reading the output
        // then wait for the process to exit
        // then cancel asynchronously reading the output
        process.Start();
        process.BeginOutputReadLine();
        process.BeginErrorReadLine();

        foreach (string command in Script)
        {
            process.StandardInput.WriteLine(command);
            process.StandardInput.Flush();
        }

        process.StandardInput.Close();
        process.WaitForExit();

        process.CancelOutputRead();
        process.CancelErrorRead();

        if (IncludeProgress)
        {
            pf.Stop();
        }

    }
}

And this is the class that is supposed to create the output form:

public class ProgressForm : IWin32Window
{
    [DllImport("user32.dll")]
    private static extern IntPtr GetForegroundWindow();
    IntPtr IWin32Window.Handle { get { return GetForegroundWindow(); } }

    private Form m_form;
    private TextBox m_textbox;
    private Button m_closeButton;
    private bool m_processingComplete;

    private Thread m_formThread;
    public ProgressForm(int maxProgress)
    {
        m_processingComplete = false;

        m_form = new Form()
        {
            Text = "Shell Operations",
            FormBorderStyle = FormBorderStyle.FixedSingle,
            MinimizeBox = false,
            MaximizeBox = false,
            ControlBox = true                
        };

        m_textbox = new TextBox()
        {
            Multiline = true,
            ReadOnly = true,
            ScrollBars = ScrollBars.Vertical,
            Font = new Font(@"Lucida Console", 9),
            BackColor = Color.Black,
            ForeColor = Color.GreenYellow,
        };
        m_textbox.SetBounds(0, 0, 800, 330);

        m_closeButton = new Button()
        {
            Text = @"Close",
            Enabled = false,
            TextAlign = ContentAlignment.MiddleCenter                
        };
        m_closeButton.Click += new EventHandler(this.OnCloseButtonClick);
        m_closeButton.SetBounds(0, 335, 800, 25);

        // Set the client area of the form equal to the size of the Text Box
        m_form.ClientSize = new Size(800, 360);
        // Add the Textbox to the form
        m_form.Controls.Add(m_textbox);
        m_form.Controls.Add(m_closeButton);
    }

    private void OnCloseButtonClick(object sender, EventArgs args)
    {
        if (!m_processingComplete)
        {
            return;
        }
        m_form.Close();
        m_form = null;
    }

    public void Start()
    {
        m_formThread = new Thread(this.RunForm);
        m_formThread.Start();
        // yes I hate myself - but just in case of paranoid delusions
        Thread.Sleep(10);
    }

    private void UpdateCloseButtonEnabled(bool isEnabled)
    {
        m_closeButton.Invoke((MethodInvoker)delegate
        {
            m_closeButton.Enabled = isEnabled;
            // m_form.Refresh();
        });
    }

    public void AppendOutput(string line)
    {
        m_textbox.Invoke((MethodInvoker)delegate
        {
            m_textbox.AppendText(line);
            //m_form.Refresh();
        });
    }

    public void AppendOutputLine(string line)
    {            
        m_textbox.Invoke((MethodInvoker)delegate
        {
            m_textbox.AppendText(line + "\n");
            //m_form.Refresh();
        });
    }

    private void RunForm()
    {
        m_form.ShowDialog(this);
    }

    public void Stop()
    {
        m_processingComplete = true;
        UpdateCloseButtonEnabled(true);
    }
}

So when all the scripts are run, the close button is enabled. Clicking on the "close" button causes Visual studio to hang indefinitely. Any idea or thoughts what is causing this?

c#
.net
multithreading
winforms
process
asked on Stack Overflow Oct 29, 2018 by Ben

0 Answers

Nobody has answered this question yet.


User contributions licensed under CC BY-SA 3.0