I'm trying to display a MessageBox with some debugging information right before my application exits in case of a specific error. The reason I need to display it as a message box vs. just logging it in a file is because I need it to draw my attention right when the error happens. (With a silent logging, I might miss the moment when the error first starts happening.)
And, it would be also nice to open a file at the same time if the user chooses to.
So I'm calling the following "vanila" code from the destructor of one of the globally declared structs. In other words, it will be called right before the process exits:
int nRes = MessageBox(NULL,
L"Specific error occurred, do you want to open the log file?",
L"ERROR",
MB_YESNOCANCEL | MB_ICONERROR | MB_SYSTEMMODAL);
//'nRes' is returned as IDYES immediately w/o displaying a dialog
if(nRes == IDYES)
{
int nResOpen = (int)ShellExecute(NULL, L"open", L"path-to\\file.txt", NULL, NULL, SW_SHOWNORMAL);
BOOL bOpenedOK = nResOpen > 32;
//'nResOpen' is returned as 42 but the file doesn't open
}
The code above works fine if I call it from anywhere else while the process UI was still shown. But the behavior described in the code comments happens when I call it from the destructor right before the app closes.
Any idea how to make it work in this situation?
PS. I'm testing it on a 64-bit Windows 10 Pro. The project is built as x64 MFC/C++ process.
PS2. EDIT:
Adjusted the code to follow suggestions in the comments. To repro -- define the struct as such:
struct TEST_STRUCT{
TEST_STRUCT()
{
}
~TEST_STRUCT()
{
//The call below only plays the error sound...
int nRes = MessageBox(NULL,
L"Specific error occurred, do you want to open the log file?",
L"ERROR",
MB_YESNOCANCEL | MB_ICONERROR | MB_SYSTEMMODAL);
//'nRes' is returned as IDYES immediately w/o displaying a dialog
if(nRes == IDYES)
{
SHELLEXECUTEINFO sei = {0};
sei.cbSize = sizeof(sei);
sei.lpFile = L"path-to\\file.txt";
sei.nShow = SW_SHOW;
sei.fMask = SEE_MASK_NOASYNC | SEE_MASK_FLAG_NO_UI | SEE_MASK_NOCLOSEPROCESS;
::SetLastError(0);
BOOL bOpenedOK = ::ShellExecuteEx(&sei);
int nErr = ::GetLastError();
if(bOpenedOK)
{
if(sei.hProcess)
{
DWORD dwR = ::WaitForSingleObject(sei.hProcess, INFINITE);
DWORD dwExitCode = 0;
if(::GetExitCodeProcess(sei.hProcess, &dwExitCode))
{
//Check error code
}
}
else
{
//When called before app's exit it gets here -- no process handle
//'nErr' == 0x8000000A
}
}
if(sei.hProcess)
CloseHandle(sei.hProcess);
}
}
};
I then created an MFC dialog-based GUI project, and added declaration for the TEST_STRUCT
right before CWinAppEx
derived variable as such:
Then debug it with Visual Studio. (In my case VS 2008 SP1
.) Put the breakpoint on the destructor, run the app and close it. The breakpoint should trigger. Then walk through the code above. I was able to reproduce it on Windows 8.1 as well. (Read comments in the code.)
The problem is calling the code after main
has finished. That is a murky time in the C++ standards, and the runtime and MFC system is shutting itself down. It would be better to launch your code before the end of the application.
If I remember correctly, theApp has a function which is called near the end of the lifespan of the application.
Global variables are initialized from the top of the file (compilation-unit
) to the bottom of the file.
Between different files in the same binary (.exe, .dll), the order of how these files are processed is not defined by any standard.
Modern (C++11
and better) make some effort to ensure they are available, by having dynamically constructed things.
ComplexThing & get_some_stl_resource() {
static ComplexThing resource;
return resource;
}
the code above generates the ComplexThing when first needed, and will destroy it at the end of the program with an added atexit
handler.
The calling of the atexit
things is the opposite order to their creation. In your case, there is no code in the constructor, which gives the language no chance to pin any behavior. So when your destructor is called, there is no guarantees of any functionality, as there was no statement for what would be needed from the construction.
OK. I got it solved. Here's whoever else runs into the same issue:
The reason MessageBox
didn't work is because WM_QUIT
was already posted by the GUI application, which will result in the GetMessage function to return zero
according to MSDN. This means that any GUI-based function invoked by my process at this stage will basically fail. That explains MessageBox
failure.
What it doesn't explain is why ShellExecute
/ ShellExecuteEx
also fail. Since Raymond Chen posted a comment above, so maybe he can explain it. My guess is that it internally calls some GUI component that relies on GetMessage
and when that fails it just blindly returns HRESULT 0x8000000A
, or The data necessary to complete this operation is not yet available.
So here's the workaround.
The MessageBox
solution is actually surprisingly simple. For opening a file, we'll need to use a lower-level API:
//Can't use MessageBox() at this stage since WM_QUIT was already posted,
//will have to improvise...
DWORD dwRespMsgBx = -1;
BOOL bRzMsgBox = ::WTSSendMessage(NULL, ::WTSGetActiveConsoleSessionId(),
buffTitle, lstrlen(buffTitle) * sizeof(WCHAR),
buffMsg, lstrlen(buffMsg) * sizeof(WCHAR),
MB_YESNOCANCEL | MB_ICONERROR | MB_SYSTEMMODAL,
0, &dwRespMsgBx, TRUE);
if(dwRespMsgBx == IDYES)
{
//Open report file
//INFO: Can't use ShellExecute, evidently due to WM_QUIT having already been posted
WCHAR buffPath[MAX_PATH * 2];
if(SUCCEEDED(::StringCchPrintf(buffPath, MAX_PATH * 2,
L"notepad.exe /A \"%s\"", REPORT_FILE_PATH)))
{
STARTUPINFO si = {0};
PROCESS_INFORMATION pi = {0};
si.cb = sizeof(si);
::CreateProcess(NULL, buffPath, NULL, NULL, FALSE,
CREATE_UNICODE_ENVIRONMENT,
NULL, NULL, &si, &pi);
if(pi.hThread)
::CloseHandle(pi.hThread);
if(pi.hProcess)
::CloseHandle(pi.hProcess);
}
}
User contributions licensed under CC BY-SA 3.0