Giving your Windows Service lots of time to finish (F#)

Normally, your Windows services should finish within seconds of Windows telling them that it’s time to quit. However, in some (hopefully rare) cases, you might need several minutes to make a clean exit, much to the disgust of your users. Since ServiceBase.RequestAdditionalTime may not cut it, you must follow the example of some Microsoft services, like TrustedInstaller and Windows Update, who use a different approach.

The problem with ServiceBase.RequestAdditionalTime

When you find a method named RequestAdditionalTime(int32), you would naturally assume that said method does exactly what it says on the tin and gives you any amount of time you request, in order to gracefully shut down your service. What it actually does, is give you a stay of at most the time specified in the HKLM\SYSTEM\CurrentControlSet\Control\WaitToKillServiceTimeout registry key. Of course, the more appropriate
RequestAdditionalTimeUpToWaitToKillServiceTimeout method name would have been humongous even by .NET standards, but a mention of the upper time limit in the method’s documentation would have been nice.

Of course, you could just adjust WaitToKillServiceTimeout to your heart’s content, but since this is applied globally to all services, it might not be a good idea. IBM actually recommends it in their DB2 documentation, but the team who wrote Windows Update preferred another solution (SERVICE_CONTROL_PRESHUTDOWN), and so do I.

Warning: Shutting down through the Services application

A word of warning: What is said above, is only true when Windows is being rebooted or shut down. If the service is being shut down through the Services application, the limit is 125 seconds regardless of WaitToKillServiceTimeout, according to MS documentation.

An alternative: Handling SERVICE_CONTROL_PRESHUTDOWN

If no other services are dependent of your service, you can do your clean-up and finish it right before the system shuts down. You can find out about an impending shutdown by handling the custom command SERVICE_CONTROL_PRESHUTDOWN, and choose your own timeout by setting it specifically for your service. This happens to be precisely the way Microsoft does it for services such as Windows Update and TrustedInstaller.

Overview of SERVICE_CONTROL_PRESHUTDOWN usage

Using SERVICE_CONTROL_PRESHUTDOWN requires the following steps:

  • Set the preshutdown timeout.
  • Tell the service to accept the preshutdown custom command.
  • Add a custom command handler to the ServiceBase derived object, and have that handler set the service status to SERVICE_STOP_PENDING, so that Windows knows that we’re actually doing our best to end our service.
  • Finally, set the service status to SERVICE_STOPPED when we’re done.

So, let’s get started and create a .NET Core console application project for the service. Just as in the post about a minimal F# service, we’ll need to add the Microsoft.Windows.Compatibility package:

Install-Package Microsoft.Windows.Compatibility

and open a few handy namespaces:

open System
open System.IO
open System.Reflection
open System.Runtime.InteropServices
open System.ServiceProcess
open System.Threading

About to get dirty

Now, this is where things start to get dirty. As it turns out, catering for SERVICE_CONTROL_PRESHUTDOWN was nothing the designers of the ServiceBase class had in mind. This may be because it’s such a rare case, or it may be because they want it to be a rare case. Whatever be the case, we must add a bunch of PInvoke definitions in order to achieve our goals:

module Win32Service =
    let SERVICE_ACCEPT_PRESHUTDOWN      = 0x00000100
    let SERVICE_CONFIG_PRESHUTDOWN_INFO = 0x00000007
    let SERVICE_CONTROL_PRESHUTDOWN     = 0x0000000f
    let SERVICE_WIN32_OWN_PROCESS       = 0x00000010

    type ServiceState =
        | SERVICE_STOPPED          = 0x00000001
        | SERVICE_START_PENDING    = 0x00000002
        | SERVICE_STOP_PENDING     = 0x00000003
        | SERVICE_RUNNING          = 0x00000004
        | SERVICE_CONTINUE_PENDING = 0x00000005
        | SERVICE_PAUSE_PENDING    = 0x00000006
        | SERVICE_PAUSED           = 0x00000007

    [<StructLayout(LayoutKind.Sequential)>]
    type ServiceStatus =
        val mutable dwServiceType : int
        val mutable dwCurrentState : ServiceState
        val mutable dwControlsAccepted : int
        val mutable dwWin32ExitCode : int
        val mutable dwServiceSpecificExitCode : int
        val mutable dwCheckPoint : int
        val mutable dwWaitHint : int
        new () =
            { dwServiceType = 0
              dwCurrentState = enum 0
              dwControlsAccepted = 0
              dwWin32ExitCode = 0
              dwServiceSpecificExitCode = 0
              dwCheckPoint = 0
              dwWaitHint = 0
            }

    [<StructLayout(LayoutKind.Sequential)>]
    type SERVICE_PRESHUTDOWN_INFO =
        val mutable dwPreshutdownTimeout : int
        new (timeout) = 
            { dwPreshutdownTimeout = timeout }


    [<DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)>]
    extern bool ChangeServiceConfig2(
        nativeint hService,
        int dwInfoLevel,
        SERVICE_PRESHUTDOWN_INFO lpInfo)

    [<DllImport( "advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)>]
    extern bool QueryServiceConfig2(
        nativeint hService, 
        int dwInfoLevel, 
        SERVICE_PRESHUTDOWN_INFO buffer, 
        int cbBufSize, 
        int& pcbBytesNeeded)

    [<DllImport("advapi32.dll", SetLastError = true)>]
    extern bool SetServiceStatus(nativeint handle, ServiceStatus serviceStatus)

The parts of this Service module will be explained below:

module Service =
    open Win32Service

    let private preshutdownTimeout = 1_900_000
    let serviceName = "SlowEndingService"

    // Not the best place for a log, but it will suffice for this demo.
    let logDir = ("c:/" + serviceName)

    let log text =    
        File.AppendAllLines(
            Path.Combine(logDir, serviceName + ".log"), 
            [DateTime.Now.ToString() + "  " + text])


    type SlowEndingService() as this =
        inherit ServiceBase(ServiceName = serviceName)

        do
            use sc = new ServiceController(serviceName)
            let spi = SERVICE_PRESHUTDOWN_INFO(preshutdownTimeout)
            ChangeServiceConfig2(
                sc.ServiceHandle.DangerousGetHandle(), 
                SERVICE_CONFIG_PRESHUTDOWN_INFO, spi) |> ignore
            // Note that the field name "_acceptedCommands" starts with an
            // underscore in .NET Core, but has no underscore in .NET Framework.
            let acceptedCommandsFieldInfo =
                typeof.GetField("_acceptedCommands", BindingFlags.Instance ||| BindingFlags.NonPublic)
            let acceptedCmds = (int) (unbox (acceptedCommandsFieldInfo.GetValue(this)))
            acceptedCommandsFieldInfo.SetValue(this, acceptedCmds ||| SERVICE_ACCEPT_PRESHUTDOWN)


        member this.SetServiceStopPending() =
            let status = 
                ServiceStatus(
                    dwServiceType = SERVICE_WIN32_OWN_PROCESS,
                    dwCurrentState = ServiceState.SERVICE_STOP_PENDING,
                    dwWaitHint = preshutdownTimeout)
            SetServiceStatus(this.ServiceHandle, status) |> ignore


        override this.OnCustomCommand(command) =
            if command = SERVICE_CONTROL_PRESHUTDOWN then
                this.OnPreshutdown()


        member this.OnPreshutdown() =
            log "Entered OnPreshutdown."
            this.SetServiceStopPending()
            // Simulate a *really* slow service termination.
            for i in 1 .. 30 do
                log ("Sleeping minute nr " + i.ToString())
                Thread.Sleep(60_000)
            this.OnStop()


        override _.OnStart(args) =
            log "Entered OnStart."
            base.OnStart(args)
            // The code to initialize the service goes here.


        override this.OnStop() =
            log "Entered OnStop."
            this.SetServiceStopPending()
            // Any final processing can be done here.
            // Once ServiceStatus is set to SERVICE_STOPPED, it's too late to
            // do anything more.
            let status = 
                ServiceStatus(
                    dwServiceType = SERVICE_WIN32_OWN_PROCESS,
                    dwCurrentState = ServiceState.SERVICE_STOPPED)
            SetServiceStatus(this.ServiceHandle, status) |> ignore

Constructor: Set the preshutdown timeout
First of all, in the SlowEndingService constructor, the preshutdown timeout is set. The call to ChangeServiceConfig2 will write the timeout value to the registry key HKLM\SYSTEM\CurrentControlSet\Services\SlowEndingService\PreshutdownTimeout, so it could actually be done in a service installer instead of during service startup, if you’d prefer that. 

Microsoft sets the timeout to a full hour for Windows Update Services. As a service I’m writing involves shutting down virtual Windows machines, that’s unfortunately how long my timeout has to be.

Constructor: Tell the service to accept the preshutdown custom command
The next thing we do in the constructor, is completely unholy: we use reflection to change a private base class member in order to add SERVICE_ACCEPT_PRESHUTDOWN as an accepted command.

Why the hideous use of reflection to set private members of the base ServiceBase class? Well, we can’t set them with SetServiceStatus, because ServiceBase keeps an internal ServiceStatus field and never asks the Win32 API about status. Thus, any changes you would make using SetServiceStatus will not be reflected in ServiceBase‘s internal state, but will instead be overwritten by it.

Things like this makes ServiceBase seem a rather hastily designed class, and it could seem that an alternative would be to write your own, better class based on it. Unfortunately, ServiceBase depends on classes whose accessibilities are internal (in this case, making things internal is making them slightly infernal), e.g. Interop.Advapi32’s ServiceProcessOptions.

This almost makes for a parodic situation, where Microsoft’s own documentation  includes code that duplicates internal definitions. In other words, instead of choosing public accessibility, they’ve copy/pasted the definitions into wherever you need to access them. Add to this the tutorial’s description of the PInvoke calls you have to make due to the lacking functionality of ServiceBase, and it’s clear that the class’ design leaves something to be desired. Sadly, the solution has been to document workarounds rather than to fix the problem.

OnCustomCommand: Custom command handler
The custom command handler catches SERVICE_CONTROL_PRESHUTDOWN and lets OnPreshutdown do the actual work. This, importantly, sets the service status to SERVICE_STOP_PENDING, to keep the service from being terminated by Windows Service Control Manager. Warning: in this sample, the service will drag on for 30 minutes before stopping. You’ll probably want to change this for initial testing, but for final tests, shorter values than 20 minutes won’t tell you whether your service really gets the benefit of absurdly long timeouts, as Windows will grant you the 20 minutes just for setting the SERVICE_STOP_PENDING status.

OnStop: Set the service status to SERVICE_STOPPED
Whenever you have told Windows Service Control Manager that you accept the SERVICE_CONTROL_PRESHUTDOWN command, you must explicitly set your service status to SERVICE_STOPPED when the service is finished. Otherwise, Windows will wait for 40 seconds after the service process has terminated (that’s my empirical experience).

Also, note that the Service Control Manager can terminate the service process immediately after the call where SetServiceStatus sets the SERVICE_STOPPED status, so don’t perform any work after this, not even a single line of code.

Firing up the service

Now, all that remains is to fire up the service:

module Main =
    open Service

    [<EntryPoint>]
    let main args =
Directory.CreateDirectory(logDir) |> ignore log "----------------------------------------------------------" log "Instantiating service object." use service = new SlowEndingService() ServiceBase.Run(service) // It's highly likely (but not certain) that the service is terminated // immediately when ServiceStatus is set to SERVICE_STOPPED, so the program // flow will probably not return here after the ServiceBase.Run call. log "Returned to main." 0

To try the service, install it with this console command (as administrator):

sc create SlowEndingService binPath=<path to your compiled .exe file>

Then, start the service, for example from the console (again, as administrator):

net start SlowEndingService

NB: If you shut down your computer now, you will suffer the delay set in OnPreshutdown, so be warned. I tested the service in a virtual machine, to avoid being denied the use of my computer during the slow shutdown.

The service is stopped with:

net stop SlowEndingService

To remove it, make sure that the Services application and the task manager are closed, and then issue:

sc delete SlowEndingService

And that, ladies and gentlemen, is how we do that!

  1. No comments yet.

  1. No trackbacks yet.