This guide will cover certain ways to detect restart or shutdown of the PC, as well as other similar events, such as log off or sleep. We will be handling the OS shutdown event using WinAPI for different types of applications - console, GUI applications, and services. If you’re interested in the general information on Windows process monitoring, you can also check out this article.

Written by:
Tatyana Zakatova,
Software Developer of Apriorit

Why we need to detect computer shutdown?

Computer shutdown detection is required to complete short, but very important actions, such as:

  • Saving all unsaved data
  • Notify distributed system components via network that the current component will be shut off
  • Add record about the shutdown into the log in order to better analyze the situation

What Windows API tools allow to detect shutdown events?

General process goes as follows: we create a callback function and register it in the system. When certain Windows event occurs, system calls the callback, transferring information about this event via input parameters. Our function analyzes input parameter data, decides which event has occurred and executes certain code as a response to this event.

Since syntax and possible functionality differs for different types of applications, we created a separate basic tutorial for each of them.

Console applications

In order to catch shutdown event in a console application, we should start by creating our own handler function:

BOOL WINAPI HandlerRoutine(_In_ DWORD dwCtrlType);

Next, register it as a handler for this console application by using the following API function:

BOOL WINAPI SetConsoleCtrlHandler(_In_opt_ PHANDLER_ROUTINE HandlerRoutine, _In_ BOOL Add);

We can do it in this way, for example:

BOOL WINAPI HandlerRoutine(DWORD dwCtrlType)
{
    switch (dwCtrlType)
    {
    case CTRL_SHUTDOWN_EVENT:
			//Computer is shutting down
            return TRUE;
    case CTRL_LOGOFF_EVENT:
			//Current user logs off
            return TRUE;
    default:
            //We don't care about this event
			//Default handler is used
    }
    return FALSE;
}

int _tmain(int argc, _TCHAR* argv[])
{
    SetConsoleCtrlHandler(HandlerRoutine, TRUE); //adding HandlerRoutine to the list of handlers
    getchar(); //simulates work
    return 0;
}

The HandlerRoutine function should return TRUE if it has finished processing the event. If this function returns FALSE, then the next handler from the list for this console application will be used.

Take note that the system launches HandlerRoutine in a separate thread, therefore, additional actions may be needed in order to ensure synchronization. 

Small yet interesting detail – if a console application is loading the gdi32.dll or user32.dll libraries (or calls functions, used in these dlls without working with them directly), then the system starts to consider it as a GUI application. In this situation, MSDN recommends to use a detector via windows messages. For this purposes, it is recommended to create a fake window with zero size.

MSDN also warns that when processing any of these signals, C run-time functions that call console functions, as well as console functions themselves may not work correctly. This happens when internal console cleanup routines are called before the execution of the process signal handler.

More detailed information on SetConsoleCtrlHandler and handling of WinAPI events for console applications can be viewed here.

GUI applications

GUI application receives information about target events via window messages. We’re interested in the following window messages: WM_QUERYENDSESSION and WM_POWERBROADCAST. The first message (as seen from the title) appears when the process of closing user session is initiated. Shutting computer off as well as restarting it also causes user session to end, thus messages about it are delivered via the same window message. From the second message we can get information about the suspend event. We can get different types of events from wParam and lParam, transferred together with the message. Here is the part of our handler of window messages:

//...
    case WM_POWERBROADCAST:
        {
            if (wParam == PBT_APMSUSPEND)
                //Computer is suspending
            break;
        }
    case WM_QUERYENDSESSION:
        {
            if (lParam == 0)
                //Computer is shutting down
            if ((lParam & ENDSESSION_LOGOFF) == ENDSESSION_LOGOFF)
                //User is logging off
            break;
        }
//...

For WM_QUERYENDSESSION, the IParam parameter indicates a specific event. If it equals zero, then this is restart/shutdown. In all other cases this parameter can contain several values, with the ENDSESSION_LOGOFF being the one we are interested in.

What can we do after we get WM_QUERYENDSESSION?

If we don’t do anything, then in several seconds the system itself will tell the user that “these applications are preventing shutdown…” and user can either cancel shutdown or forcibly continue it, regardless of the waiting applications. In this situation, in terms of interaction with user, we can on the one hand close the application voluntarily in order to let the system shut down immediately (when no other applications are blocking it, of course). On the other hand, we can transfer additional information to the user about why they shouldn’t reboot right now (works starting with Windows Vista).

    case WM_QUERYENDSESSION:
        {
            if (lParam == 0)
			{ 
				//Computer is shutting down
		        ShutdownBlockReasonCreate(hwnd, _T("Please, don't kill me"));
			}			
			break;
        }

It will look like this:

Windows Restart message

At the same time, if we don’t want our application to be shown in the list of apps that block the shutdown, we can use the SetProcessShutdownParameters function, specifically its second parameter. We should change its value to SHUTDOWN_NORETRY, and our application will not block the shutdown.

    DWORD dwLevel = 0;
    DWORD dwFlags = 0;
    if (GetProcessShutdownParameters(&dwLevel, &dwFlags))
        SetProcessShutdownParameters(dwLevel, SHUTDOWN_NORETRY);

Take note that this does not work if it is not the default handler that is called for WM_QUERYENDSESSION. Therefore, in order for make this code work, we need to modify our window message handler in the following way:

     case WM_QUERYENDSESSION:
        {
            if (lParam == 0)
            {
                //Computer is shutting down
            }
            if ((lParam & ENDSESSION_LOGOFF) == ENDSESSION_LOGOFF)
            {
                //User is logging off
            }
            return DefWindowProc(hwnd, msg, wParam, lParam);
        }

You can read more about WM_QUERYENDSESSION here.

In terms of WM_POWERBROADCAST, we are interested in detecting the suspend event (wParam == PBT_APMSUSPEND). More detailed information is available here.

Besides that, we can get different variants of the resume event and power setting change (for example, when user has changed power plan) in a similar way.

As it stands, sleep is not a very important event for GUI application, although it may be needed in order to message the other components that this PC goes to sleep.

Service

The mechanism of detecting the necessary events for a service is identical to the mechanism for console applications, but with slightly different syntax and much more features.

We can take the generic base for our service here, if necessary.

So, as with the console applications, for services we will create a callback function and register it as a service control requests handler.

Two functions are used to register such callbacks: RegisterServiceCtrlHandler and RegisterServiceCtrlHandlerEx. With the first one you can get and process much less controls, then with the second, this is why in our examples we use only RegisterServiceCtrlHandlerEx

SERVICE_STATUS_HANDLE WINAPI RegisterServiceCtrlHandlerEx(
  _In_     LPCTSTR               lpServiceName,
  _In_     LPHANDLER_FUNCTION_EX lpHandlerProc,
  _In_opt_ LPVOID                lpContext
);

Where HandlerProc is:

DWORD WINAPI HandlerEx(
  _In_ DWORD  dwControl,
  _In_ DWORD  dwEventType,
  _In_ LPVOID lpEventData,
  _In_ LPVOID lpContext
);

You can read about all processed controls here.

We also need to remember to specify the controls that we are waiting for and want to process at the start of the service.

g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_POWEREVENT | SERVICE_ACCEPT_SESSIONCHANGE | SERVICE_ACCEPT_SHUTDOWN;
g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
g_ServiceStatus.dwWin32ExitCode = 0;
g_ServiceStatus.dwCheckPoint = 0;
 
SetServiceStatus (g_StatusHandle, &g_ServiceStatus);

If we don’t do this, then our handler will not be called for these controls.

Beside the control code, handler receives another parameter – eventType. For some controls this parameter equals zero and doesn’t mean anything, but, for example, for SERVICE_CONTROL_POWEREVENT or SERVICE_CONTROL_SESSIONCHANGE this parameter can be used to define which powerevent of sessionchange group event had occurred. The eventData parameter for these events points to a structure (POWERBROADCAST_SETTING for powerevent and WTSSESSION_NOTIFICATION for sessionchange) with additional data on this event.

Here is an example of implementation of such handler:

DWORD WINAPI CtrlHandlerEx( DWORD CtrlCode, DWORD eventType,
                         LPVOID eventData, LPVOID context)
{
    switch (CtrlCode) 
    {
     case SERVICE_CONTROL_STOP :
		{ 
			//Handle service stop
        	return NO_ERROR;
		}
	case SERVICE_CONTROL_POWEREVENT:
        {
			if (eventType == PBT_APMQUERYSUSPEND)
			{
                //Computer is suspending
            }
            return NO_ERROR;
		}
    case SERVICE_CONTROL_SESSIONCHANGE:
        {
            switch (eventType)
            {
                case WTS_SESSION_LOGOFF:
                    //User is logging off
                    break;
                case WTS_SESSION_LOCK:
                    //User locks the session
                    break;
            }
            return NO_ERROR;
        }
    case SERVICE_CONTROL_SHUTDOWN:
        {
            //Computer is shutting down
            return NO_ERROR;
        }
    default:
        {
            //Some event which we don't handle 
        }
    }
    return ERROR_CALL_NOT_IMPLEMENTED;
}  

Control handling (execution of CtrlHandlerEx) is happening in the main thread.

There is a time limit of approximately 20 seconds for the service to process the SERVICE_CONTROL_SHUTDOWN control, at the end of which the process will shut down regardless of whether the service has finished its preparations for the shutdown or not. If service has completed its preparations earlier, then it should return NO_ERROR and end, in order for the system not to wait while the full time limit expires.

If the service performs some long and important action, which does not allow to end the process quickly, we can use another way to handle shutdowns. Instead of the SERVICE_CONTROL_SHUTDOWN control we can subscribe to SERVICE_CONTROL_PRESHUTDOWN. If the service gets the preshutdown notification, then the system suspends the shutdown process and does not resume it until either preshutdown time out has passed or preshutdown control handler has completed its task. The time-out is defined in the SERVICE_PRESHUTDOWN_INFO structure and by default is set to 180,000 milliseconds or three minutes. The SERVICE_PRESHUTDOWN_INFO value can be set with the ChangeServiceConfig2 function. You can find more information on this here.

Note that since the service has received SERVICE_CONTROL_PRESHUTDOWN it no longer can get SERVICE_CONTROL_SHUTDOWN, therefore it makes sense to subscribe only on one of the two events.

And another thing – the SERVICE_CONTROL_PRESHUTDOWN control is not supported on Windows XP and Server2003.

You can use this control in the same way as you can use all others. Don’t forget to add it into the list of accepted controls:

    g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_POWEREVENT | SERVICE_ACCEPT_SESSIONCHANGE | SERVICE_ACCEPT_PRESHUTDOWN; 

And if we catch it, then we can perform certain actions in the handler:

    case SERVICE_CONTROL_PRESHUTDOWN:
        {
            g_logger.WriteToLog("Preshutdown is detected");
            Sleep(30000); //simulating long work
            return NO_ERROR;
        }

In this situation, the message that warns about the system shut down will be shown to the user for 30 seconds.

Windows delayed shutdown

Since this decision is influencing user interaction, it is recommended to use it only as the last resort – if there is a possibility of losing data or if the restoration of the service active state an corresponding data takes very long time after system restart. In all other cases it is best to use SERVICE_CONTROL_SHUTDOWN.

 

Download sample source code: Managing Windows restart / shutdown via Win API (ZIP, 21KB)

Subscribe to updates