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?

Ben
  • 538
  • 1
  • 9
  • 24
  • 1
    Your event delegate handler needs to use `.BeginInvoke(new MethodInvoker(() => { ... }))` to update the Form UI (you haven't set the `process.SynchronizingObject`). Also, `EnableRaisingEvents = true;` is used only to raise the `Exited()` event. You should use that, not `WaitForExit()`. But, I haven't run the sample code. There's probably something else to look at. – Jimi Oct 29 '18 at 14:58
  • 1
    Probably the `ShowDialog()` compatibility with this *threading model*. I think any error in the Process execution will cause the whole thing to deadlock. – Jimi Oct 29 '18 at 15:58
  • 1
    It looks like you're receiving output from the stream after closing, which sounds weird since you make sure the process has exited. As a workaround you could unsubscribe from the event (`process.OutputDataReceived += outputHandler;`) but it does not explain why it's happening in first place. Maybe you should put a breakpoint on `pf.AppendOutputLine(e.Data);` to see what is received when that happens – Kevin Gosse Oct 29 '18 at 20:13
  • @Jimi I tried using `BeginInvoke()` method and setting a `SynchronizingObject` but it didn't help – Ben Oct 30 '18 at 13:50
  • @KevinGosse you are right. It doesn't explain. I extracted the code and executed it in isolation as a console application, and it's fine. There's no exception of any sort. But when it's run as a Visual Studio plugin, the weird exception occures. Also it's very difficult to put a breakpoint on `pf.AppendOutputLine` because there's way too many lines to be printed and it happens only at the end. And weirdly enough, when I reduce the size of data, the exception doesn't happen anymore~ – Ben Oct 30 '18 at 13:53
  • 1
    That was just the result of a quick look at the Process creation. The main problem here - consideration derived from what I see - is the `ShowDialog()` call. When you call that, the underlying thread that started it is in a limbo. Its context is lost and this will end up in a deadlock. This is an opinion. I can't test your code properly. My opinion, too, is that you don't need to start that Form in a thread, if you just need to show the console output inside a control. Try just showing the Form, bringing it to front with other means and just `BeginInvoke()` on the Process output. – Jimi Oct 30 '18 at 20:58
  • 1
    A sample code I posted aboout this: [How do I get output from a command to appear in a control on a form in real-time](https://stackoverflow.com/questions/51680382/how-do-i-get-output-from-a-command-to-appear-in-a-control-on-a-form-in-real-time?answertab=active#tab-top) – Jimi Oct 30 '18 at 21:03
  • @Jimi I think that somehow fixed my problem. Maybe you can post it as the solution – Ben Nov 08 '18 at 15:12
  • I would, if I knew what suggestion, when applied, fixed the problem. You could also self-answer your question, though. – Jimi Nov 09 '18 at 06:43

0 Answers0