How to shutdown explorer.exe gracefully from C++
2009-08-01
This blog entry is just about a number of hacks I use in a setup to restart explorer.exe
.
I don't want to start a discussion whether this is good practice or not.
Rebooting was not an option.
The following code has been tested on Vista and Windows 7 with elevated User Access Control, UAC.
Collection of ideas
The web does contain some fragments, and I collected some ideas. There are articles on stackoverflow.com which get rather philosophical and others that provide source code that simply did not work but got me starting.
Operation modes of explorer.exe
Basically, explorer works in two different configurations:
- One
explorer.exe
process, all windows, task bar, start menu, folders - Two
explorer.exe
processes, task bar and start menu, and the other one manages the folders ("folder options" Launch folder windows in a separate process)
Both configurations need to be considered.
Shut Down Mechanisms
This section will discuss some of the mechnisms to shut down applications.
Restart Manager
There is a meachnism to restart applications built in the Microsoft Installer.
When uninstalling a shell extension, for example, MSI displays a dialog and asks the user to restart explorer.exe
.
The mechanism is provided by the Restart Manager API.
Sadly, it seems not always to work. explorer.exe
might be shut down but not restarted.
Some people have reported similar problems. Additionally, the API looks cryptic and requires at least Windows Vista,
but my setup was targeting XP also.
TerminateProcess()
Some people suggested that TerminateProcess()
would just be an option. Life could be simple, after termination, moreover,
explorer.exe
automatically restarts itsself.
But TerminateProcess()
would instantly terminate any pending IO, potentially bringing your system and data in an unstable state.
It is never a good practice to call TerminateProcess()
except when there is no other option.
WM_QUIT
Posting WM_QUIT
would shut down the application gracefully. The main message loop of any Windows application should react on WM_QUIT
.
There are two methods to send a message to another process:
a) Find all windows of that process and post a WM_QUIT
to them.
b) Use the Toolhelp API to find out about the first thread of the process and post WM_QUIT
directly using PostThreadMessage()
.
I felt that a) is more complex but more stable. An application should behave properly when messages are sent to windows, but a message that is sent to a thread queue felt too intrusive.
Step by step
Step 1, Find all explorer.exe windows
To find all explorer.exe
windows, we snap all processes using the toolhelp API
:
HANDLE snapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
then get all process ids:
vector<DWORD> queryProcessIds(HANDLE snapshot)
{
vector<DWORD> r;
PROCESSENTRY32 entry;
zero(entry);
entry.dwSize = sizeof(entry);
if (!::Process32First(snapshot, &entry))
return r;
r.push_back(entry.th32ProcessID);
while (true)
{
PROCESSENTRY32 entry;
zero(entry);
entry.dwSize = sizeof(entry);
if (!::Process32Next(snapshot, &entry))
return r;
r.push_back(entry.th32ProcessID);
}
}
To filter out explorer.exe
windows, I assumed that explorer.exe
is always started from the Windows
directory. So we go for the first module in each process and ask where it was loaded from.
vector<DWORD> filterExplorerProcessIds(const vector<DWORD>& ids)
{
vector<DWORD> r;
TCHAR winDir[MAX_PATH];
if (!::GetWindowsDirectory(winDir, MAX_PATH))
return r;
wstring winD(winDir);
winD = winD + L"\\explorer.exe";
for(vector<DWORD>::const_iterator it = ids.begin(); it != ids.end(); ++it)
{
if (isExplorerProcess(winD, *it))
r.push_back(*it);
}
return r;
}
bool isExplorerProcess(const wstring& explorerPath, DWORD processId)
{
HANDLE snapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, processId);
if (snapshot == INVALID_HANDLE_VALUE)
return false;
bool r = isExplorerProcess(explorerPath, snapshot);
::CloseHandle(snapshot);
return r;
}
bool isExplorerProcess(const wstring& explorerPath, HANDLE moduleSnapshot)
{
MODULEENTRY32 entry;
zero(entry);
entry.dwSize = sizeof(entry);
if (!::Module32First(moduleSnapshot, &entry))
return false;
return 0 == _wcsicmp(entry.szExePath, explorerPath.c_str());
}
Step 2: Send messages to the windows
Having the process ids of all the explorer.exe
instances at hand, we can try to send them messages.
After some experimentation, I learned that the two instances behave differently:
- The task bar
explorer.exe
is shutting down in response to aWM_QUIT
. - The process, that manages the folder windows, ignores
WM_QUIT
as long there are open folder windows.
A WM_ENDSESSION
closes the folder windows. The following code does both: it initiates the taskbar shutdown and the closes all folder windows:
void tryClose(DWORD dwPID)
{
// TerminateAppEnum() posts WM_QUIT to all windows whose PID
// matches your process's.
::EnumWindows((WNDENUMPROC)TerminateAppEnum, (LPARAM) dwPID);
}
BOOL CALLBACK TerminateAppEnum( HWND hwnd, LPARAM lParam )
{
DWORD dwID;
GetWindowThreadProcessId(hwnd, &dwID);
if(dwID == (DWORD)lParam)
{
// Win7: Separate Explorer Windows do close their windows on
// WM_ENDSESSION (independent of lparam), but the process
// does not shutdown :(
::PostMessage(hwnd, WM_ENDSESSION, NULL, ENDSESSION_CLOSEAPP);
// Win7: Explorer itsself ends gracefully on a WM_QUIT
::PostMessage(hwnd, WM_QUIT, 0, 0);
}
return true;
}
To summarize: We now have shut down the explorer task bar process and closed all folder windows. But the explorer.exe
process that previously managed the folder windows is not gone yet.
Step 3: end folder windows' explorer.exe
I learned by experimentation that the remaining explorer.exe
process finally terminates on a WM_QUIT
after the windows are closed. Sending a WM_QUIT
, as long there are open folder windows, simply gets ignored.
To send out a WM_QUIT
directly to a process without an associated window handle, we have to send the message to the first thread.
bool postWMQuitToProcess(DWORD processId)
{
HANDLE snapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, processId);
if (snapshot == INVALID_HANDLE_VALUE)
return false;
bool r = postWMQuitToProcess(processId, snapshot);
::CloseHandle(snapshot);
return r;
}
bool postWMQuitToProcess(DWORD processId, HANDLE snapshot)
{
bool r = false;
THREADENTRY32 entry;
zero(entry);
entry.dwSize = sizeof(entry);
if (!::Thread32First(snapshot, &entry))
return false;
if (entry.th32OwnerProcessID == processId && ::PostThreadMessage(entry.th32ThreadID, WM_QUIT, 0, 0))
return true;
zero(entry);
entry.dwSize = sizeof(entry);
while (::Thread32Next(snapshot, &entry))
{
if (entry.th32OwnerProcessID != processId)
continue;
if (::PostThreadMessage(entry.th32ThreadID, WM_QUIT, 0, 0))
return true;
}
return false;
}
Step 4: final termination
And finally, if something does not work out, we may terminate remaining explorer.exe
processes.
At first we need to find out when the processes are terminated:
To wait for a termination of a process, we need to get a HANDLE
by using OpenProcess()
. Then there are two different methods to test if the process is alive:
WaitForSingleObject()
waits for the process handle to be signalled. This looked like the best method to use, but sometimes, I have no idea why, it just misses and never returns.GetExitCodeProcess()
returns !=STILL_ACTIVE
when the process is terminated, this worked reliable:
.
bool waitForTermination( DWORD dwPID, DWORD dwTimeout )
{
// If we can't open the process with the proper rights,
// then we give up immediately.
HANDLE hProc = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwPID);
if(!hProc)
return true; // we assume, it is already terminated.
int pollDelay = 100;
for(int rounds = (dwTimeout / pollDelay); true; --rounds)
{
DWORD exitCode;
if (!::GetExitCodeProcess(hProc, &exitCode))
break;
if (exitCode != STILL_ACTIVE)
{
::CloseHandle(hProc);
return true;
}
if (!rounds)
break;
::Sleep(pollDelay);
};
::CloseHandle(hProc);
return false;
}
When a process has not been terminated by other means after a while, we hope that it is safe to finally kill it the ungentle way:
bool terminateProcess(DWORD dwPID)
{
HANDLE hProc = ::OpenProcess(PROCESS_TERMINATE, FALSE, dwPID);
if(!hProc)
return true; // we assume, it is already termianted.
bool ret = ::TerminateProcess(hProc,0) ? true: false;
::CloseHandle(hProc);
return ret;
}
Gluing it all together
The final shutdownExplorer()
function first sends messages to the explorer.exe
windows, then waits 10 seconds for their termination while sending WM_QUIT
to their main processes. Processes that are alive another 10 seconds later, get terminated.
bool shutdownExplorer()
{
HANDLE snapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE)
return false;
vector<DWORD> pids = queryProcessIds(snapshot);
pids = filterExplorerProcessIds(pids);
::CloseHandle(snapshot);
for (vector<DWORD>::const_iterator it = pids.begin(); it != pids.end(); ++it)
tryClose(*it);
// win7: explorer windows (when started in separate process) do
// not terminate after their windows are closed, but they will accept a WM_QUIT after a while
// first round (10 seconds)
for (int i = 0; i != 20; ++i)
for (vector<DWORD>::const_iterator it = pids.begin(); it != pids.end(); ++it)
if (!waitForTermination(*it, 500))
{
// Log::W("Process active after closing and sending WM_QUIT to its windows (500 milliseconds), sending WM_QUIT to first thread");
postWMQuitToProcess(*it);
}
// second round (10 seconds) for waiting.
for (vector<DWORD>::const_iterator it = pids.begin(); it != pids.end(); ++it)
if (!waitForTermination(*it, 10000))
{
// Log::W("Process active after sending WM_QUIT to first thread (15 seconds), terminating");
terminateProcess(*it);
}
return true;
}
Conclusion and outlook
Admittedly, this a lot of code to shut down explorer.exe
, but when it comes to usability, saving one restart at each installation is worth the effort.
The code above is not thoroughly tested yet and probably needs to be reviewed and modified for the final setup. I will post updates here if there are any significant changes.
Feel free to use the code above, and when you find out some simpler or safer ways to restart the Windows Explorer, I am happy to update this blog entry.
armin
explorer.exe
, I own you