P/Invoke NotifyServiceStatusChange from C#

This article actually touches up on some advanced topics of C#, and some things that you may not have ever come across. MSDN has this to say about threads:

An operating-system ThreadId has no fixed relationship to a managed thread, because an unmanaged host can control the relationship between managed and unmanaged threads. Specifically, a sophisticated host can use the CLR Hosting API to schedule many managed threads against the same operating system thread, or to move a managed thread between different operating system threads.

What this is really trying to explain is thread affinity, and that you are not guaranteed to have a native thread map 1-to-1 to a managed thread depending on the CLR (Common Language Runtime) that is hosting your code. This is important to know when you P/Invoke into native functions that require calling back into your C# code after a period of time (such as NotifyServiceStatusChange). We want to maintain that 1-to-1 relationship using Thread.BeginThreadAffinity() because the marshaling layer needs to have a valid callback reference at all times.

Here is the code that you can use to P/Invoke NotifyServiceStatusChange in C# in order to wait for a service to stop:

using System;
using System.Runtime.InteropServices;
using System.Threading;

class ServiceAssistant
{
    [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
    public class SERVICE_NOTIFY
    {
        public uint dwVersion;
        public IntPtr pfnNotifyCallback;
        public IntPtr pContext;
        public uint dwNotificationStatus;
        public SERVICE_STATUS_PROCESS ServiceStatus;
        public uint dwNotificationTriggered;
        public IntPtr pszServiceNames;
    };

    [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
    public struct SERVICE_STATUS_PROCESS
    {
        public uint dwServiceType;
        public uint dwCurrentState;
        public uint dwControlsAccepted;
        public uint dwWin32ExitCode;
        public uint dwServiceSpecificExitCode;
        public uint dwCheckPoint;
        public uint dwWaitHint;
        public uint dwProcessId;
        public uint dwServiceFlags;
    };

    [DllImport("advapi32.dll")]
    static extern IntPtr OpenService(IntPtr hSCManager, string lpServiceName, uint dwDesiredAccess);

    [DllImport("advapi32.dll")]
    static extern IntPtr OpenSCManager(string machineName, string databaseName, uint dwAccess);

    [DllImport("advapi32.dll")]
    static extern uint NotifyServiceStatusChange(IntPtr hService, uint dwNotifyMask, IntPtr pNotifyBuffer);

    [DllImport("kernel32.dll")]
    static extern uint SleepEx(uint dwMilliseconds, bool bAlertable);

    [DllImport("advapi32.dll")]
    static extern bool CloseServiceHandle(IntPtr hSCObject);

    delegate void StatusChangedCallbackDelegate(IntPtr parameter);

    /// <summary> 
    /// Block until a service stops, is killed, or is found to be already dead.
    /// </summary> 
    /// <param name="serviceName">The name of the service you would like to wait for.</param>
    /// <param name="timeout">An amount of time you would like to wait for. uint.MaxValue is the default, and it will force this thread to wait indefinitely.</param>
    public static void WaitForServiceToStop(string serviceName, uint timeout = uint.MaxValue)
    {
        // Ensure that this thread's identity is mapped, 1-to-1, with a native OS thread.
        Thread.BeginThreadAffinity();
        GCHandle notifyHandle = default(GCHandle);
        StatusChangedCallbackDelegate changeDelegate = ReceivedStatusChangedEvent;
        IntPtr hSCM = IntPtr.Zero;
        IntPtr hService = IntPtr.Zero;
        try
        {
            hSCM = OpenSCManager(null, null, (uint)0xF003F);
            if (hSCM != IntPtr.Zero)
            {
                hService = OpenService(hSCM, serviceName, (uint)0xF003F);
                if (hService != IntPtr.Zero)
                {
                    SERVICE_NOTIFY notify = new SERVICE_NOTIFY();
                    notify.dwVersion = 2;
                    notify.pfnNotifyCallback = Marshal.GetFunctionPointerForDelegate(changeDelegate);
                    notify.ServiceStatus = new SERVICE_STATUS_PROCESS();
                    notifyHandle = GCHandle.Alloc(notify, GCHandleType.Pinned);
                    IntPtr pinnedNotifyStructure = notifyHandle.AddrOfPinnedObject();
                    NotifyServiceStatusChange(hService, (uint)0x00000001, pinnedNotifyStructure);
                    SleepEx(timeout, true);
                }
            }
        }
        finally
        {
            // Clean up at the end of our operation, or if this thread is aborted.
            if (hService != IntPtr.Zero)
            {
                CloseServiceHandle(hService);
            }
            if (hSCM != IntPtr.Zero)
            {
                CloseServiceHandle(hSCM);
            }
            // Keep our callback method around until it is called (until this line of code).
            GC.KeepAlive(changeDelegate);
            if (notifyHandle != default(GCHandle))
            {
                notifyHandle.Free();
            }
            Thread.EndThreadAffinity();
        }
    }

    static void ReceivedStatusChangedEvent(IntPtr parameter)
    {
        // Do nothing.
    }
}

Its so simple, that it can just be called as follows:

ServiceAssistant.WaitForServiceToStop("YourWindowsServiceName");

Note that this is significantly different from the WaitForStatus method that is available to you out of the box in C#, because the WaitForStatus method polls every 250ms between status checks according to the remarks, whereas NotifyServiceStatusChange is event-driven and subscribes to that particular event (so its less overhead in terms of CPU usage).

Alexandru

"To avoid criticism, say nothing, do nothing, be nothing." - Aristotle

"It is wise to direct your anger towards problems - not people; to focus your energies on answers - not excuses." - William Arthur Ward

"Science does not know its debt to imagination." - Ralph Waldo Emerson

"Money was never a big motivation for me, except as a way to keep score. The real excitement is playing the game." - Donald Trump

"All our dreams can come true, if we have the courage to pursue them." - Walt Disney

"Mitch flashes back to a basketball game held in the Brandeis University gymnasium in 1979. The team is doing well and chants, 'We're number one!' Morrie stands and shouts, 'What's wrong with being number two?' The students fall silent." - Tuesdays with Morrie

I'm not entirely sure what makes me successful in general programming or development, but to any newcomers to this blood-sport, my best guess would be that success in programming comes from some strange combination of interest, persistence, patience, instincts (for example, someone might tell you that something can't be done, or that it can't be done a certain way, but you just know that can't be true, or you look at a piece of code and know something doesn't seem right with it at first glance, but you can't quite put your finger on it until you think it through some more), fearlessness of tinkering, and an ability to take advice because you should be humble. Its okay to be wrong or to have a bad approach, realize it, and try to find a better one, and even better to be wrong and find a better approach to solve something than to have had a bad approach to begin with. I hope that whatever fragments of information I sprinkle across here help those who hit the same roadblocks.

Leave a Reply

Your email address will not be published. Required fields are marked *