Application termination when user logs off


Do you know how windows terminates all the applications when user logs off? I did not think too much about this, and assumed that it is a normal process – after all the WM_QUERYENDSESSION and WM_ENDSESSION processing, the application main window closes, posts WM_QUIT and the application quits in a regular way.


But a recent bug reported for one of my GUI applications caused me to look deeper. The application settings, normally saved at application’s exit, were not saves if user logged off, and this made me look closer at what happens at logoff.


This was a managed application with related code in Main() function that looked like this:

static void Main()
{
    LoadPreferences();
    Application.Run(new MainForm());
    SavePreferences();
}

This works great when the application is closed normally. But if user logs off, the application closes, but the preferences are not saved – the code following Application.Run is never called at all.


What happens? Does Application.Run throw any exception that causes the following code to be bypassed? This was ruled out rather quickly by debugger. I then assumed something in Windows Forms calls ExitProcess in response to WM_ENDSESSION, or maybe default message handler does this – but that was proved wrong too. A repro with unmanaged ATL code showed it is not related to managed libraries at all.


Finally my colleguae debugged this issue a little deeper, and found that CSRSS is blatantly terminating the process after it processed the WM_ENDSESSION event. Here are the outlines of the whole sequence (for non-console applications).


The user selects Start/Log off and then selects “OK” in the confirmation dialog. This calls ExitWindowsEx(EWX_LOGOFF); if during the OK button click, the CTRL key was down, it also adds the EWX_FORCE flag which makes it ignore the result of the WM_QUERYENDSESSION message (as if the applications always returned TRUE). ExitWindowsEx(EWX_LOGOFF) causes roughly the following activity to occur in the logon session’s instance of CSRSS:


For each process in the session
{
    For each UI thread in the process
    {
        For each top-level window in the thread
        {
            Send WM_QUERYENDSESSION and get back the result
            Wait with a short timeout and show an “End program” dialog if it takes too long; if the user says “kill”, call TerminateProcess on the process and continue the “for process” loop
            If EWX_FORCE was not specified and WM_QUERYENDSESSION returned FALSE, break (and continue “UI thread loop”);
        }
        // Note that even if one window in one thread returned FALSE to WM_QUERYENDSESSION, windows in the other threads in the same process are still sent WM_QUERYENDSESSION
        bool bDoShutdown = (all threads agreed (i.e., all windows returned TRUE to WM_QUERYENDSESSION)) or EWX_FORCE
        For each UI thread in the process
        {
            For each top-level window in the thread
            {
                Send WM_ENDSESSION(bDoShutdown);
                If (bDoShutdown) wait for return (wait with a short timeout and show an “End program” dialog if it takes too long; whatever the user says, consider that the return from WM_QUERYENDSESSION) 
            }
        }
        If (bDoShutdown) TerminateProcess on the process
}
I have omitted some details, in particular related to no-UI and console processes, but the general picture should be clear. Probably the Windows guys decided that graceful cleanup is not needed when user logs off, and all the application are closed.


Since the application is forecefully terminated, the application should not rely on being able to execute any code after the message loop. Even classes like SafeHandle are not finalized. All really important cleanup and termination code should be executed when the main form closes, or by providing a handler for the WM_ENDSESSION message . Another option (although I did not try it) is to catch WM_ENDSESSION message and terminate the message loop, then exit the application gracefully – although this goes contrary to Windows design of killing applications fast to ensure quick log off.


Update: Raymond Chen described the reasons for this behavior:
http://blogs.msdn.com/oldnewthing/archive/2008/04/21/8413175.aspx

Comments (7)

  1. Greg D says:

    Did you fix it by sliding SavePreferences() into your main form’s OnClose() override?

  2. michen says:

    Yes, my code now looks like this:

    ReadPreferences();

    MainForm form = new MainForm(prefs);

    form.FormClosed += delegate(object sender, FormClosedEventArgs e) { SavePreferences(); };

    Application.Run( form );

  3. Greg D says:

    Ah, cool.  🙂

    I’ve been doing something similar in my own OnClose(), but I’d never tested the case where the user logs off while the program’s still running, so I wasn’t sure if that was how you’d fixed it.

  4. David Walker says:

    Why don’t you save preferences whenever the user changes them within the application?

    That sounds FAR simpler.

  5. michen says:

    To David, on saving preferences when the user changes them:

    This is a good idea for many explicit preferences. But some "preferences" might change often and it is not good idea to write to a file each time they change.

    E.g. position of the window on a screen; the server I’m connected to; the selected item I’m working with, etc. I definetely do not want to write to a file after each window move, or after most mouse clicks when selection changes. Saving such state at the end is usually better.

  6. David Walker says:

    Ah.  Those aren’t really "preferences", then, but more like user-state or last-appearance-state or something.  Which you still might want to save, although saving all of that data in a hurry when the user is logging off (or the system is shutting down) becomes a problem.

  7. Shantibhushan says:

    I have similar issue but with little twist. I am running an app with different user credential (Run as) than the logged in user.

    My application is receiving anything… I tried catching WM_QUERYENDSESSION, WM_ENDSESSION both…

    Its works pretty good when the same app run under the logged in user.

    Does  anybody here have idea how the problem could be solved?