1

I recently found that the cause of an obscure bug was due to Process.Start. The method triggered a pump in the event queue while the process was starting, which in turn made code run that was dependent on the process being started.

The bug seemed to be timing related since it did not happen consistently, and was quite difficult to reproduce. Since we have more places in the code where we use Process.Start, and we might very well use it more in the future, I thought it would be good to get to the bottom of how to trigger it more consistently.

We created a standalone winforms project with a single button, here is the click eventhandler:

private void button1_Click(object sender, EventArgs e) {
    this.BeginInvoke(new MethodInvoker(() => { MessageBox.Show("Hello World"); }));
    var process = new System.Diagnostics.Process() { EnableRaisingEvents = true };
    process.Exited += CurrentEditorProcess_Exited;
    // Does pump
    process.StartInfo = new System.Diagnostics.ProcessStartInfo(@"C:\Users\micnil\Pictures\test.png");
    // Does not pump
    //process.StartInfo = new System.Diagnostics.ProcessStartInfo("Notepad.exe");
    process.Start();
    Thread.Sleep(3000);
    Console.WriteLine("Done");
}

Running this code will:

  1. show the messagebox,
  2. finish opening the test image,
  3. After exiting the messagebox, the program will sleep 3 seconds and then log "Done".

If I replace the ProcessStartInfo argument with "Notepad.exe" instead, the code will:

  1. start Notepad,
  2. log Done,
  3. Show the message box.

This can be reproduced consistently. The problem is, the program that we are starting is more similar to notepad. It is our own custom text editor built in WPF. So why does starting our executable sometimes trigger a pump, but I cannot make Notepad do the same?

We found a few others that has been hit by the same problem:

  • Which blocking operations cause an STA thread to pump COM messages?
    • No answer to why
  • Process.Start causes processing of Windows Messages: Qouting Dave Andersson

    ShellExecuteEx may pump window messages when opening a document in its associated application, specifically in the case where a DDE conversation is specified in the file association.

    Additionally, ShellExecuteEx may create a separate thread to do the heavy lifting. Depending on the flags specified in the SHELLEXECUTEINFO structure, the ShellExecuteEx call may not return until the separate thread completes its work. In this case, ShellExecuteEx will pump window messages to prevent windows owned by the calling thread from appearing hung.

    You can either ensure that the variables in question are initialized prior to calling Process.Start, move the Process.Start call to a separate thread, or call ShellExecuteEx directly with the SEE_MASK_ASYNCOK flag set.

"May create a thread" - How do I know when?, "You can either ensure that the variables in question are initialized prior to calling Process.Start" - How?

We solved the problem by setting process.StartInfo.UseShellExecute = false before starting the process. But the question remains, does anyone know how to write a minimal program that would trigger a pump from starting a executable like Notepad?


Update:

Done some more investigation and have two new findings:

private void button1_Click(object sender, EventArgs e) {
    this.BeginInvoke(new MethodInvoker(() => {
            Log("BeginInvoke");
    }));
    string path = Environment.GetEnvironmentVariable("PROCESS_START_EXE");
    var process = new System.Diagnostics.Process() { EnableRaisingEvents = true };
    process.Exited += CurrentEditorProcess_Exited;
    process.StartInfo = new System.Diagnostics.ProcessStartInfo(path, "params");
    process.Start();
    Thread.Sleep(3000);
    Log("Finished Sleeping");
}

1:

When setting the environment variable PROCESS_START_EXE to "Notepad" the logging becomes:

BeginInvoke
Finished Sleeping

When setting the environment variable PROCESS_START_EXE to "Notepad.exe" (note the ".exe") the logging becomes:

Finished Sleeping
BeginInvoke

Which I find strange, but I don't think is related to the problem we were having. We always specify the exact path and filename to the executable, including ".exe"

2:

The scenario in which I found the bug, was that i launched the application through a Windows shortcut with a target similar to this:

C:\Windows\System32\cmd.exe /c "SET PROCESS_START_EXE=<path to custom editor> && START /D ^"<path to application directory>^" App.exe"

It first sets an environment variable with the path to the executable that is supposed to be started, and then launches the winforms application. If I use this shortcut to launch the application, and use the environment variable to start the process, then the logging always becomes:

BeginInvoke
Finished Sleeping

This does seem like the problem we were having. Only that I couldn't reproduce it consistently every time. Why does it matter that I am setting the environment variable right before launching the application?

Note that if I use the same shortcut, but do not use the environment variable to start the process, the message loop will not pump.

micnil
  • 4,705
  • 2
  • 28
  • 39
  • What happens if you declare the click handler `async`, and use `await Task.Delay(3000);` instead of `Thread.Sleep` – Flydog57 Jan 18 '19 at 22:55
  • @Flydog57, I did that and attach the debugger on line `process.Start();`. When I click "Step over", I come to the `await Task.Delay(3000);`. Then after that the MessageBox is shown, and finally the Done logging. So I don't think the Start method did any pumping. – micnil Jan 18 '19 at 23:04
  • Could be worth noting that our code is always run on the STA thread, and the process starting code is very similar to the example in the question. One difference is that we are passing arguments to the process as the second param. – micnil Jan 18 '19 at 23:09
  • https://stackoverflow.com/a/43211503/17034 – Hans Passant Jan 19 '19 at 00:16
  • 1
    That's not going to happen with Notepad. The OS you use is important, probably Win10 when this bytes, it opens PNG files with TWinUI. Nothing you can do about it but take appropriate countermeasures to make the re-entrancy problem less painful, as shown in the linked post. – Hans Passant Jan 19 '19 at 00:24
  • @HansPassant Thanks for the link, that does seem like the exact issue we were having, only it was not related to the Explorer process. It seems like when opening anything other than a executable (with .exe extension), the message loop will pump. The problem for us was that it seemed like it also pumped for executables *sometimes*. Made an update to the question with further investigation, and a reproducable exaxmple to the weird behavior (see #2) – micnil Jan 19 '19 at 16:10

0 Answers0