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:
- show the messagebox,
- finish opening the test image,
- 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:
- start Notepad,
- log Done,
- 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.