2

I am trying to P/Invoke the NotifyServiceStatusChange event in C# to check when a service has stopped. I managed to get it to compile and run without any errors, but now, when I stop the service, it doesn't seem to want to notify that its dead. Any ideas why that could be? You can test it out by copying this code into a blank console application; just be sure to replace "My Service Name" with your service name (there are two instances of this string below).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {

        public delegate void StatusChanged(IntPtr parameter);

        public class SERVICE_NOTIFY : MarshalByRefObject
        {
            public uint dwVersion;
            public StatusChanged pfnNotifyCallback;
            public IntPtr pContext;
            public uint dwNotificationStatus;
            public SERVICE_STATUS_PROCESS ServiceStatus;
            public uint dwNotificationTriggered;
            public IntPtr pszServiceNames;
        };

        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", SetLastError = true, CharSet = CharSet.Auto)]
        static extern IntPtr OpenService(IntPtr hSCManager, string lpServiceName, uint dwDesiredAccess);

        [DllImport("advapi32.dll", EntryPoint = "OpenSCManagerW", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern IntPtr OpenSCManager(string machineName, string databaseName, uint dwAccess);

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern uint NotifyServiceStatusChange(IntPtr hService, uint dwNotifyMask, ref IntPtr pNotifyBuffer);

        public static SERVICE_NOTIFY notify;
        public static GCHandle notifyHandle;
        public static IntPtr unmanagedNotifyStructure;

        static void Main(string[] args)
        {
            IntPtr hSCM = OpenSCManager(null, null, (uint)0xF003F);
            if (hSCM != IntPtr.Zero)
            {
                IntPtr hService = OpenService(hSCM, "My Service Name", (uint)0xF003F);
                if (hService != IntPtr.Zero)
                {
                    StatusChanged changeDelegate = ReceivedStatusChangedEvent;
                    notify = new SERVICE_NOTIFY();
                    notify.dwVersion = 2;
                    notify.pfnNotifyCallback = changeDelegate;
                    notify.pContext = IntPtr.Zero;
                    notify.dwNotificationStatus = 0;
                    SERVICE_STATUS_PROCESS process;
                    process.dwServiceType = 0;
                    process.dwCurrentState = 0;
                    process.dwControlsAccepted = 0;
                    process.dwWin32ExitCode = 0;
                    process.dwServiceSpecificExitCode = 0;
                    process.dwCheckPoint = 0;
                    process.dwWaitHint = 0;
                    process.dwProcessId = 0;
                    process.dwServiceFlags = 0;
                    notify.ServiceStatus = process;
                    notify.dwNotificationTriggered = 0;
                    notify.pszServiceNames = Marshal.StringToHGlobalUni("My Service Name");
                    notifyHandle = GCHandle.Alloc(notify);
                    unmanagedNotifyStructure = Marshal.AllocHGlobal((IntPtr)(notifyHandle));
                    NotifyServiceStatusChange(hService, (uint)0x00000001, ref unmanagedNotifyStructure);
                    Console.WriteLine("Waiting for the service to stop. Press enter to exit.");
                    Console.ReadLine();
                }
            }
        }

        public static void ReceivedStatusChangedEvent(IntPtr parameter)
        {
            Console.WriteLine("Service stopped.");
        }
    }
}
Alexandru
  • 12,264
  • 17
  • 113
  • 208

3 Answers3

5

If you want to keep the functionality you are attempting at present, you will need to multithread.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;


namespace ConsoleApplication1
{
    class Program
    {
        [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", SetLastError = true, CharSet = CharSet.Auto)]
        static extern IntPtr OpenService(IntPtr hSCManager, string lpServiceName, uint dwDesiredAccess);

        [DllImport("advapi32.dll", EntryPoint = "OpenSCManagerW", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern IntPtr OpenSCManager(string machineName, string databaseName, uint dwAccess);

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

        [DllImportAttribute("kernel32.dll", EntryPoint = "SleepEx")]
        public static extern uint SleepEx(uint dwMilliseconds, [MarshalAsAttribute(UnmanagedType.Bool)] bool bAlertable);

        public static SERVICE_NOTIFY notify;
        public static GCHandle notifyHandle;
        public static IntPtr unmanagedNotifyStructure;

        static void Main(string[] args)
        {
            IntPtr hSCM = OpenSCManager(null, null, (uint)0xF003F);
            if (hSCM != IntPtr.Zero)
            {
                IntPtr hService = OpenService(hSCM, "Apache2.2", (uint)0xF003F);
                if (hService != IntPtr.Zero)
                { 
                    StatusChanged changeDelegate = ReceivedStatusChangedEvent;


                    notify = new SERVICE_NOTIFY();
                    notify.dwVersion = 2;
                    notify.pfnNotifyCallback = Marshal.GetFunctionPointerForDelegate(changeDelegate);
                    notify.pContext = IntPtr.Zero;
                    notify.dwNotificationStatus = 0;
                    SERVICE_STATUS_PROCESS process;
                    process.dwServiceType = 0;
                    process.dwCurrentState = 0;
                    process.dwControlsAccepted = 0;
                    process.dwWin32ExitCode = 0;
                    process.dwServiceSpecificExitCode = 0;
                    process.dwCheckPoint = 0;
                    process.dwWaitHint = 0;
                    process.dwProcessId = 0;
                    process.dwServiceFlags = 0;
                    notify.ServiceStatus = process;
                    notify.dwNotificationTriggered = 0;
                    notify.pszServiceNames = Marshal.StringToHGlobalUni("Apache2.2");
                    notifyHandle = GCHandle.Alloc(notify, GCHandleType.Pinned);
                    unmanagedNotifyStructure = notifyHandle.AddrOfPinnedObject();
                    NotifyServiceStatusChange(hService, (uint)0x00000001, unmanagedNotifyStructure);

                    GC.KeepAlive(changeDelegate);

                    Console.WriteLine("Waiting for the service to stop. Press enter to exit.");
                    while (true)
                    {
                        try
                        {
                            string keyIn = Reader.ReadLine(500);
                            break;
                        }
                        catch (TimeoutException)
                        {
                            SleepEx(100,true);
                        }
                    }
                    notifyHandle.Free();
                }
            }
        }
        [UnmanagedFunctionPointer(CallingConvention.StdCall)]
        public delegate void StatusChanged(IntPtr parameter);
        public static void ReceivedStatusChangedEvent(IntPtr parameter)
        {
            Console.WriteLine("Service stopped.");
        }

    }
}

class Reader
{
    private static Thread inputThread;
    private static AutoResetEvent getInput, gotInput;
    private static string input;

    static Reader()
    {
        inputThread = new Thread(reader);
        inputThread.IsBackground = true;
        inputThread.Start();
        getInput = new AutoResetEvent(false);
        gotInput = new AutoResetEvent(false);
    }

    private static void reader()
    {
        while (true)
        {
            getInput.WaitOne();
            input = Console.ReadLine();
            gotInput.Set();
        }
    }

    public static string ReadLine(int timeOutMillisecs)
    {
        getInput.Set();
        bool success = gotInput.WaitOne(timeOutMillisecs);
        if (success)
            return input;
        else
            throw new TimeoutException("User did not provide input within the timelimit.");
    }
}
Motomotes
  • 4,111
  • 1
  • 25
  • 24
  • 1
    I didn't try this earlier, and didn't look at the actual Marshalling code, which was incorrect. – Motomotes Nov 19 '13 at 04:33
  • Alright, man! Nicely done. I knew there was a way. I wish I could triple/quadruple up-vote you. This is a huge breakthrough for many people out there! – Alexandru Nov 19 '13 at 12:20
  • I've simplified it a bit more, and posted it below! Just as an aside, it can be done with some tweaks and you still don't need that reader class! :P Thanks so much! – Alexandru Nov 19 '13 at 12:31
  • Please post your solution to http://stackoverflow.com/questions/20018525/how-do-you-p-invoke-notifyservicestatuschange-in-c/20073860#20073860 so that I may accept it as the correct answer. – Alexandru Nov 19 '13 at 14:18
  • @Alexandru Yes, there must be literally thousands of people that have been desperate to do this. All the same, I'm sure I said the same to you at your previous question. And why are you ignoring the advice that Roman gave you? Specifically that APCs in managed code do not work. – David Heffernan Nov 19 '13 at 17:18
  • For everyone it may concern, I have answered these questions in Dave's comments to my answer. – Alexandru Nov 19 '13 at 18:36
  • BTW, the call to GC.KeepAlive is in the wrong place. You need to keep alive the delegate until you stop the notification with CloseServiceHandle or until NotifyServiceStatusChange invokes the callback. The same goes for the GCHandle keeping the structure. – Anton Tykhyy Nov 20 '13 at 18:48
4

Read related question here: http://social.msdn.microsoft.com/Forums/vstudio/en-US/f68fb826-036a-4b9c-81e6-4cbd87931feb/notifyservicestatuschange-not-working-in-c-for-windows-service-notification

Important quote: "the system invokes the specified callback function as an asynchronous procedure call (APC) queued to the calling thread. The calling thread must enter an alertable wait"

I don't remember whether .NET framework 4 uses alertable waiting when you enter Thread.Sleep or some form of Wait on waithandles, even though it uses alertable waiting for asynchronous I/O, for internal timer threads etc.

However just try Thread.Sleep or some flavor of Wait on some waithandle, instead of Console.ReadLine, make sure that your thread is blocked by those APIs at the time when you kill the service. This might do the magic - but, to my knowledge, this is a dangerous way, because .NET runtime does not expect user code to be executed on an APC. At least, try not to use NET framework resources or absolutely any APIs (especially synchronization-related or memory allocation) directly from your callback - just set some primitive variable and quit.

With APCs, the safest solution for you would be to have the callback implemented in some kind of native module, and also scheduled from some non-.NET thread, interoperating with managed code through shared variables, a pipe, or COM interface.

Or, as Hans Passant suggested in another copy of your question, just do polling from managed code. Absolutely safe, easy to implement, guaranteed to work.

Excellent source of relevant information is Joe Duffy's book (he covers a lot of topics, and alertable waits and .NET in particular): http://www.amazon.com/Concurrent-Programming-Windows-Joe-Duffy/dp/032143482X

UPDATE: Just consulted with Joe Duffy's book, yes indeed, scheduling .NET code on an APC may result in deadlocks, access violations and generally unpredictable behavior. So the answer is simple: don't do APC from a managed thread.

Roman Polunin
  • 53
  • 3
  • 12
  • I would still like to find a C# solution for this Win32 function, but I think you bring up some great points. You are correct, Thread.Sleep will not work. I tried invoking WaitForSingleObjectEx with a pointer to the handle, but it always seems to get the response, I'm guessing that's because my thread is not a Win32 thread, but a .NET thread object...and doesn't have the same abilities to wait in an alertable state. – Alexandru Nov 19 '13 at 02:53
  • Seems to work just fine; Motes actually posted a working solution above! – Alexandru Nov 19 '13 at 12:20
  • 1
    Well, glad that it works for you - it will probably work most of the time, as long as you keep callback minimal and use static-only variables. But you've been warned: there are reasons why your .NET code should not run on an APC, one being that you may hit GC running concurrently with your callback, relocating variables while you are accessing them. – Roman Polunin Nov 19 '13 at 16:47
  • For everyone it may concern, I have answered these questions in Dave's comments to my answer. – Alexandru Nov 19 '13 at 18:37
  • I can't comment in that sub-thread, so will reply here: Alexandru, this is great that it works in your case. You seem to be playing it as safe as you could with .NET, however to have a robust solution, you would need to spawn an unmanaged thread and deal with APCs from an unmanaged thread. Unless you do so, your code is subject to a lot of undocumented behavior. That's it. – Roman Polunin Nov 20 '13 at 16:38
3

Simplified a lot of this from @Motes' answer...(EDIT: I put it into a class that people can use to easily wait for a service to stop; it will block!

Edit Again: Made sure this worked if you force garbage collection with GC.Collect() anywhere in the function...turns out, you DO need the SERVICE_STATUS_PROCESS.

Another Edit: Made sure it works if you abort your thread (note on that: can't abort sleeping threads so if you plan to abort this thread...then make sure you give it a timeout at least so the finalizer can run after the timeout hits), also added timeouts. Also ensured mapping 1-to-1 of the OS thread to the current .NET thread.

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

namespace ServiceAssistant
{
    class ServiceHelper
    {

        [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);

        [DllImportAttribute("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);
                }
                GC.KeepAlive(changeDelegate);
                if (notifyHandle != default(GCHandle))
                {
                    notifyHandle.Free();
                }
                Thread.EndThreadAffinity();
            }
        }

        public static void ReceivedStatusChangedEvent(IntPtr parameter)
        {

        }
    }
}

Yes! We did it. What a journey it has been.

Alexandru
  • 12,264
  • 17
  • 113
  • 208
  • 1
    You don't need to multithread, or use the `reader` class, unless you want to read lines, while waiting for the APC. I upvoted your other question, as it seems you corrected the commenters' complaints, but I don't want to answer the question. You already have, and Passant or Heffernan may downvote it for being the "unmanaged" approach or something. Really, I just wanted to say you might want to keep the `GC.KeepAlive(changeDelegate)` after `NotifyServiceStatusChange` as their is no other managed reference to `changeDelegate` so it may get GC'd. In fact, my example didn't work for me without it. – Motomotes Nov 19 '13 at 17:13
  • Yes, @Alexandru, why did you remove KeepAlive? It seems to me that you think that if the program ran successfully once, then that implies that it is correct. That's never true. And it's a particularly risky assumption when you have GC. The API function you are calling calls out some very specific requirements in its documentation and you do need to meet them. Removing code that meets those requirements in order to "simplify" is obviously wrong. – David Heffernan Nov 19 '13 at 17:53
  • 1
    @DavidHeffernan Because the struct is already pinned. The delegate is pointed to as part of the struct. Thus, its reference will stick around in the unmanaged world. It will also stick around in the scope of the thread, in the managed world, for it to marshal back on. – Alexandru Nov 19 '13 at 18:02
  • @DavidHeffernan I think we've handled GC concurrency by handling GC ourselves for the struct, but you're worried about the callback, which again, is referenced in the struct. It will keep the callback there until we free the struct. – Alexandru Nov 19 '13 at 18:13
  • Also, just because a book says that, ".NET code on an APC may result in deadlocks, access violations and generally unpredictable behavior," shouldn't deter someone from trying things out. Persistence is key. To me, I read that line of text, and in my mind it translates to saying something like, "Functions might throw exceptions," or, "Try to understand what the method you're calling is doing." Big surprises there...anyways, I'm sure with a little tweaking you can get P/Invoking to work just fine in a lot of cases. – Alexandru Nov 19 '13 at 18:19
  • What you don't seem to understand is that the fact you have not observed failure does not mean that the code is correct – David Heffernan Nov 19 '13 at 18:25
  • @DavidHeffernan That's not an argument. In fact, you're deflecting on my answer to your own question. You have not provided a reason for why it is incorrect as far as I can tell. P.S. I don't mind being wrong...but at least give me a justifiable reason why its wrong. Mistakes are great. They lead to perfection, and I encourage them. Dave, I encourage you to find a mistake here so that we can make this code even better for everyone else. – Alexandru Nov 19 '13 at 18:34
  • I guess you need to prove that APCs are fine in this context – David Heffernan Nov 19 '13 at 18:38
  • @DavidHeffernan APCs are fine in this context because objects don't go out of scope. – Alexandru Nov 19 '13 at 19:12
  • @Alexandru, I think the `changeDelegate` may not be referenced because the struct contains the `IntPtr` return value from `Marshal.GetFunctionPointerForDelegate(changeDelegate);`. I know that my example did not work without it. That is why I added it. The code may run for years without `changeDelegate` ever being GC'd, only to be GC'd all the time once some framework update occurs, the program code is changed only every so slightly, or maybe even just because some other .NET program happen to run concurrently. Idk really, but I do know my example didn't work on my PC without it. – Motomotes Nov 19 '13 at 21:00
  • @Motes But at the same time, you gave your delegate the UnmanagedFunctionPointer directive...that might change things quite a bit in terms of where the delegate sits in memory. If you remove that directive, does it work for you? – Alexandru Nov 19 '13 at 21:08
  • 1
    No, that directive is actually the default assumed. I just start being explicit when debugging. Try adding `GC.Collect()` before your call to `SleepEx`, that should reveal if your code could potentially fail. I think the indefinite wait state from the call to `SleepEx` is the only reason the delegate avoids garbage collection. – Motomotes Nov 19 '13 at 21:28
  • And what if the managed thread shifts to a different native thread? The APC will be queued onto the calling native thread. I guess that's the real problem. – David Heffernan Nov 19 '13 at 21:49
  • @Motes Great idea. If you invoke garbage collection before the thread sleep, it works on a first iteration. However, on a second iteration, GC.Collect() throws the following exception: Managed Debugging Assistant 'FatalExecutionEngineError' has detected a problem. – Alexandru Nov 19 '13 at 22:16
  • Additional information: The runtime has encountered a fatal error. The address of the error was at 0x7107d4fb, on thread 0x2170. The error code is 0xc0000005. This error may be a bug in the CLR or in the unsafe or non-verifiable portions of user code. Common sources of this bug include user marshaling errors for COM-interop or PInvoke, which may corrupt the stack. – Alexandru Nov 19 '13 at 22:17
  • But it still happens even if I call GC.KeepAlive(changeDelegate); – Alexandru Nov 19 '13 at 22:18
  • In fact, it seems to happen when calling GC.Collect() right after calling autoResetEvent.WaitOne()... – Alexandru Nov 19 '13 at 23:04
  • Ah, I figured it out...its because I removed the second struct...otherwise GC.Collect() will work. I'll work it into the answer shortly. – Alexandru Nov 20 '13 at 01:14
  • @Motes Gentlemen, there you have it. :) – Alexandru Nov 20 '13 at 01:21
  • @DavidHeffernan Gentlemen, there you have it. :) – Alexandru Nov 20 '13 at 01:21
  • Also, to whoever uses this method, please note one thing...in some cases, you might not want to simply throw this class in a DLL if you know that your DLL will be unloaded, as MSDN reads, "Important: If the calling thread is in a DLL and the DLL is unloaded before the thread receives the notification or calls CloseServiceHandle, the notification will cause unpredictable results and might cause the process to stop responding." – Alexandru Nov 20 '13 at 03:37
  • @DavidHeffernan What did you mean when you asked if the managed thread shifts to a different native thread? – Alexandru Nov 20 '13 at 03:54
  • I've found some interesting articles on APC and .NET, and it seems .NET threads sometimes inherently use APC anyways...have a look here: http://www.sellsbrothers.com/writing/askthewonk/HowdoestheThreadAbortExce.htm – Alexandru Nov 20 '13 at 04:07
  • Its actually a REALLY interesting read...and leads me to question why you would ever question using APCs? – Alexandru Nov 20 '13 at 04:10
  • @Alex .net is implemented in native unmanaged Win32. Just the ideal environment for using APCs. Completely different from what is going on here. How can you be sure that .net will keep the managed thread on the same native thread. – David Heffernan Nov 20 '13 at 07:38
  • @DavidHeffernan I'm not sure I understand your question. What does this have to do with keeping a managed thread on a native thread? Let's recap what is going on...I have a managed thread, which subscribes to an event notification in the unmanaged world through marshaling. In the unmanaged world, a delegate handles marshaling in the context of my unmanaged APC thread, which gets a queued event, comes back to the marshaling layer, gets the delegate of my managed thread, and returns properly, by signaling my managed thread. What is the issue? I'm just trying to understand your question. – Alexandru Nov 20 '13 at 13:21
  • The APC is queued onto the native thread that called `NotifyServiceStatusChange`. But the .net runtime might kill that thread, or indeed put a different managed thread onto that native thread. There is not a one-to-one mapping between native and managed threads. – David Heffernan Nov 20 '13 at 13:28
  • @DavidHeffernan Why would the .NET runtime kill the native thread or put a different managed thread onto the native thread? – Alexandru Nov 20 '13 at 14:01
  • I don't know. But it happens. It might well not happen in your scenario, I don't know. But you should be aware that managed threads can be switched onto different native threads. I think if that happened, it would kill what you are trying to do. – David Heffernan Nov 20 '13 at 14:02
  • @DavidHeffernan I've never heard of .NET behaving like this except maybe in some thread-pooling cases where threads are reused. Could you please supply some references to this happening, so that I can look into it? Also, I'm not even entirely sure that we even have a native thread that waits for the APC queue, or if it all queues directly to the managed thread. – Alexandru Nov 20 '13 at 14:10
  • In fact, the first line of the MSDN document on APC says: "An asynchronous procedure call (APC) is a function that executes asynchronously in the context of a particular thread." The function is delegated directly to my managed thread...I don't think we even have a native thread. I might have been wrong about that. It says an APC is queued to a thread...in this case, its queued to my managed thread. – Alexandru Nov 20 '13 at 14:13
  • All managed threads run on native threads. After all, .net is nothing more than a fancy Win32 program. APCs are not queued to managed threads. Win32 doesn't understand managed threads. Links to follow. – David Heffernan Nov 20 '13 at 14:14
  • http://msdn.microsoft.com/en-us/library/74169f59.aspx http://stackoverflow.com/questions/5624128/how-do-i-get-the-real-thread-id-in-a-clr-friendly-way http://stackoverflow.com/questions/1679243/getting-the-thread-id-from-a-thread and so on and so on. I'm sure you could have done the websearch yourself. You seem pretty clued up. I do feel that you want to believe that your approach works and should be more critical of it. – David Heffernan Nov 20 '13 at 14:17
  • @DavidHeffernan All managed threads run on native threads, or all managed threads are native thread wrappers? Keep in mind that, there is a marshaling layer between all of this. Win32 doesn't need to understand managed threads. Marshaling takes care of the "understanding." I read your first link. It doesn't say anything to indicate that managed threads can be switched onto different native threads. Going to read the other links, hang on. – Alexandru Nov 20 '13 at 14:30
  • @DavidHeffernan Okay, an interesting note on your 2nd/3rd links. I see what you're getting at now. The second link explains that a managed thread may be backed by multiple native threads...or essentially, I feel like a better way of saying it is...a managed thread can be exchanged between multiple native threads, and executed in a pooled environment of native threads...however, the managed thread is still a wrapper for all of this, or else all threading would break in .NET. It still doesn't mean that my managed thread will expire or go away, and it doesn't mean the delegate goes away either. – Alexandru Nov 20 '13 at 14:37
  • @DavidHeffernan Satisfied? – Alexandru Nov 20 '13 at 14:43
  • You are guilty of selective reading. The APC is queued onto the native thread that calls this NotifyXXX API. Your reading of the MSDN link is inexplicable. It says: *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 Fiber API to schedule many managed threads against the same operating system thread, or to move a managed thread among different operating system threads.* – David Heffernan Nov 20 '13 at 14:48
  • The managed thread does not go away. For sure. But Win32 does not know about managed threads. The APC is queued onto whichever native thread was hosting your managed thread. – David Heffernan Nov 20 '13 at 14:50
  • @DavidHeffernan Not selective. It says, "an unmanaged host can control the relationship between managed and unmanaged threads." A host, like, the marshaling layer, can control that relationship, so is your question really, "Are there bugs in the marshaling layer?" Its also prefixed with "An operating-system ThreadId"...why is it prefixed with operating-system, rather than an application ThreadId? Actually, even the link you gave me deals with O/S threads...are these questions specific to literally, threads inside of the OS layer, not application layers? I read in detail, and very literally. – Alexandru Nov 20 '13 at 15:00
  • What are these layers you are talking about? I'm not concvinced you fully understand the difference between a native thread and a managed thread. Anyway, I cannot force you to take this seriously. You've got the information in front of you. – David Heffernan Nov 20 '13 at 15:04
  • @DavidHeffernan The document says, "an unmanaged host can control the relationship between managed and unmanaged threads." Who do you interpret to be the **host** in my code above? – Alexandru Nov 20 '13 at 15:08
  • The host is the thing that starts up the .net runtime. That would be your C# application, I guess. Of course, I know nothing about your application. – David Heffernan Nov 20 '13 at 15:12
  • Is my question "Are there bugs in the marshaling layer?" No it is not. – David Heffernan Nov 20 '13 at 15:12
  • And actually, whilst discussing this with you, I have realised that some of my own p/invokes are dubious. I've also made the assumption that managed threads always run on the same native thread. I need to do some checking! – David Heffernan Nov 20 '13 at 15:13
  • @DavidHeffernan Don't get your knickers in a twist, just yet. Are you sure its not the marshaling layer that behaves as the host when you P/Invoke? Please see this diagram: http://www.codeproject.com/KB/cs/wmp_pinvoke/wmp_pinvoke.gif – Alexandru Nov 20 '13 at 15:19
  • I've told you what I know. You can do whatever you like with the information. – David Heffernan Nov 20 '13 at 15:25
  • @DavidHeffernan Okay, but these are points worth looking into. Are you also sure that the APC is queued onto whichever native thread was hosting my managed thread? I thought the delegate was there to handle exactly this case, so that we don't need to worry about it. Isn't that the whole point of marshaling? – Alexandru Nov 20 '13 at 15:36
  • @DavidHeffernan I think this would be a great question to ask the Stack community. Do you want to ask it, or shall I? – Alexandru Nov 20 '13 at 15:43
  • @Alexandru The thing you pass to `NotifyServiceStatusChange` is an unmanaged function pointer. That is not tied to a thread. I don't see where marshalling and delegates come into it. – David Heffernan Nov 20 '13 at 15:44
  • Please can you ask it. I agree that it is a good question to ask. – David Heffernan Nov 20 '13 at 15:46
  • @DavidHeffernan I believe that since we call Marshal.GetFunctionPointerForDelegate(), it converts a delegate into a function pointer that is callable from unmanaged code, which when called, will be marshaled to managed code, so marshaling should handle that. Perhaps I shouldn't have called it unmanagedNotifyStructure in my code since it is managed? – Alexandru Nov 20 '13 at 15:55
  • I'm over my limit now. Don't know how threading is handled with GFPFD and callbacks. Time for your Q! – David Heffernan Nov 20 '13 at 16:01
  • @DavidHeffernan I'll think of a way to word it, and I'll post back with a link when I have asked the question. It has been quite a pleasure going over all of this with you. Many thanks! – Alexandru Nov 20 '13 at 16:04
  • @DavidHeffernan http://stackoverflow.com/questions/20103529/how-do-asynchronous-procedure-calls-handle-delegate-methods-when-you-p-invoke – Alexandru Nov 20 '13 at 18:05
  • @DavidHeffernan Check the answer on there...its amazing. This is why they included Thread.BeginThreadAffinity() and Thread.EndThreadAffinity() in .NET, although as Hans pointed out, you might never run into this case. Then, there is also the case of aborting threads, it needs SafeHandles to cleanup. – Alexandru Nov 21 '13 at 18:59
  • @Motes You should also check out the other question I linked above. Anton makes some amazing points. – Alexandru Nov 21 '13 at 19:00