Image

Development

How to process the PreShutdown event in a managed Windows service

If you liked the post about Hyper-V guests in different time zones, here is one more article that comes from our development work on the new OpenStack and Hyper-v based setup of Atomia Hosting Platform.

Several of our products ship some windows services which we use to do periodic background tasks. For example, Automation Server uses a service to perform long-running automation tasks. During a system shutdown, it is important that the service has enough time to cleanly exit and not be terminated in the middle of some complex operation.

Whereas Windows will not shutdown until all applications in the user’s session are properly closed, this does not hold true for services, which only have 12 seconds to process a Shutdown notification, which might be too short for your service. The good news is that Microsoft introduced the PreShutdown event for services in Vista, which you can use in your service’s code to get 3 extra minutes (or even more) to shutdown your service.

Now, when developing your service’s shutdown behavior there are couple of things to keep in mind.

The first trap is that the OnStop method in your service class is not called when the service is stopped due to a system shutdown. However the ServiceBase class exposes an OnShutdown method which is called when the service is stopped due to a system shutdown if you also set the CanShutdown property to true. The OnShutdown has a strict time limit, which is set globally in the registry and should not be changed by any application. In case of Windows Server 2008 R2 this is 12 seconds. However, in my experience my services were terminated even faster than that. Keep in mind that services will shut down in parallel not respecting their decencies. It is clear why Microsoft did that: A slow system shutdown will be blamed on them by their users.

As a side note: If you want to test if those functions are really called you should write some messages to a temporary file. Don’t use the event log as the event log service may already be shut down in which case no messages are logged.

Here comes the PreShutdown event into play. The service by default has 3 minutes time to process this event. Also the service may increase this timeout with the native ChangeServiceConfig2 function (it needs to be done once at installation of the service).
In order to use it we need to hack the ServiceBase class a bit.

First of all we need to override the notification function and then we need to notify the service that this type of event can be handled by us. The code below shows you how to do that. Insert it into your service class and call MyInitialize(false) on your service class before you pass it to ServiceBase.Run. For some technical background look at this article.

The MyServiceControlCallbackEx simply waits for the PreShutdown event and then passes it as a shutdown event to ServiceControlCallbackEx. This will cause OnShutdown being called and all other code that should be run to make service shutdown possible. Your cleanup code should now be in OnShutdown. Note that if you hack the ServiceBase class as we described here, you also need to set CanShutdown to false, because now we are handling the PreShutdown event instead of the Shutdown event.

When the user now shuts down the system she will see a screen saying “Stopping services” after logoff. So, make sure your service doesn’t take too long to shutdown.

Also a word of warning: This code relies on a certain internal architecture of the ServiceBase class, which could be changed without notice in future versions of the .net Framework.

internal virtual int MyServiceControlCallbackEx(int control, int eventType, IntPtr eventData, IntPtr eventContext)
{
    var baseCallback = typeof(ServiceBase).GetMethod("ServiceCommandCallbackEx", BindingFlags.Instance | BindingFlags.NonPublic);
    switch (control)
    {
        case 0x0000000F: //pre shutdown
            // now pretend shutdown was called
            return (int)baseCallback.Invoke(this, new object[] { 0x00000005, eventType, eventData, eventContext });
        default:
            // call base
            return (int)baseCallback.Invoke(this, new object[] { control, eventType, eventData, eventContext });
    }
}

internal virtual void MyInitialize(bool multipleServices)
{
    // call base
    var init = typeof(ServiceBase).GetMethod("Initialize", BindingFlags.Instance | BindingFlags.NonPublic);
    init.Invoke(this, new object[] {multipleServices});

    // register our handler
    var targetDelegate =
        typeof(ServiceBase).Assembly.GetType("System.ServiceProcess.NativeMethods+ServiceControlCallbackEx");

    var commandCallbackEx = typeof(ServiceBase).GetField("commandCallbackEx", BindingFlags.Instance | BindingFlags.NonPublic);
    commandCallbackEx.SetValue(this, Delegate.CreateDelegate(targetDelegate, this, "MyServiceControlCallbackEx"));

    // accept preshutdown command
    var acceptedCommands = typeof(ServiceBase).GetField("acceptedCommands", BindingFlags.Instance | BindingFlags.NonPublic);
    int accCom = (int)acceptedCommands.GetValue(this);
    acceptedCommands.SetValue(this, accCom | 0x00000100);
}

Social

Follow us on Twitter

Like us on Facebook

Visit us on LinkedIn

We share the latest news about Atomia, event photos, and more.

Contact

[email protected]

+46 21 490 2620

Hamngränd 6,
721 30 Västerås,
Sweden

Work at Atomia

Would you like to join our quest to provide the ideal hosting platform? Be part of a fun, dedicated team and work with some of the coolest companies in the hosting industry. Check out our job page.