MessageBox and ShellExecute succeed, but nothing is shown if called right before the application exits

0

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:

enter image description here

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.)

c++
windows
winapi
messagebox
windows-shell
asked on Stack Overflow Jun 16, 2017 by c00000fd • edited Jun 17, 2017 by c00000fd

2 Answers

1

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.

answered on Stack Overflow Jun 17, 2017 by mksteve
0

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);
    }
}
answered on Stack Overflow Jun 17, 2017 by c00000fd • edited Jun 17, 2017 by c00000fd

User contributions licensed under CC BY-SA 3.0