【问题标题】:Why won't my solution work to P/Invoke NotifyServiceStatusChange in C#?为什么我的解决方案不适用于 C# 中的 P/Invoke NotifyServiceStatusChange?
【发布时间】:2013-11-19 01:32:07
【问题描述】:

我正在尝试在 C# 中 P/Invoke NotifyServiceStatusChange event 以检查服务何时停止。我设法让它编译和运行没有任何错误,但是现在,当我停止服务时,它似乎不想通知它已经死了。任何想法为什么会这样?您可以通过将此代码复制到空白控制台应用程序中来测试它;只需确保将“我的服务名称”替换为您的服务名称(下面有此字符串的两个实例)。

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.");
        }
    }
}

【问题讨论】:

  • 你所说的“杀死”服务是什么意思,你的意思是杀死进程,如果是这样我认为服务控制器不会触发状态更改事件,你可以查看Windows事件日志确认
  • 我的意思是停止服务。将其编辑到问题中。
  • @Alexandru,我更新了我的答案,现在可以使用了。

标签: c# .net winapi pinvoke


【解决方案1】:

如果您想保留当前正在尝试的功能,则需要多线程。

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.");
    }
}

【讨论】:

  • 之前没试过,也没看实际的Marshalling代码,是不正确的。
  • 好吧,伙计!做得很好。我知道有办法。我希望我能给你三倍/四倍的投票。对于很多人来说,这是一个巨大的突破!
  • 我把它简化了一点,并贴在下面!顺便说一句,它可以通过一些调整来完成,你仍然不需要那个阅读器类! :P 非常感谢!
  • 请将您的解决方案发布到stackoverflow.com/questions/20018525/…,以便我接受它作为正确答案。
  • @Alexandru 是的,肯定有成千上万的人迫切希望这样做。尽管如此,我相信我在你之前的问题中也对你说过同样的话。为什么你无视罗曼给你的建议?特别是托管代码中的 APC 不起作用。
【解决方案2】:

在此处阅读相关问题:http://social.msdn.microsoft.com/Forums/vstudio/en-US/f68fb826-036a-4b9c-81e6-4cbd87931feb/notifyservicestatuschange-not-working-in-c-for-windows-service-notification

重要引用:“系统调用指定的回调函数作为异步过程调用(APC)排队到调用线程。调用线程必须进入警报等待”

我不记得当你进入 Thread.Sleep 时 .NET 框架 4 是使用可警报等待还是在等待句柄上使用某种形式的等待,即使它对异步 I/O、内部计时器线程等使用可警报等待。

但是,只需在某个等待句柄上尝试 Thread.Sleep 或某种形式的 Wait,而不是 Console.ReadLine,请确保您的线程在您终止服务时被这些 API 阻塞。这可能会产生神奇的效果——但据我所知,这是一种危险的方式,因为 .NET 运行时不希望用户代码在 APC 上执行。至少,尽量不要直接从您的回调中使用 NET 框架资源或绝对任何 API(尤其是与同步相关的或内存分配) - 只需设置一些原始变量并退出。

使用 APC,对您来说最安全的解决方案是在某种本机模块中实现回调,并从一些非 .NET 线程调度,通过共享变量与托管代码互操作、管道或 COM 接口。

或者,正如 Hans Passant 在您问题的另一份副本中所建议的那样,只需从托管代码中进行轮询。绝对安全,易于实施,保证有效。

相关信息的优秀来源是 Joe Duffy 的书(他涵盖了很多主题,尤其是警报等待和 .NET):http://www.amazon.com/Concurrent-Programming-Windows-Joe-Duffy/dp/032143482X

更新:刚刚查阅了 Joe Duffy 的书,确实如此,在 APC 上调度 .NET 代码可能会导致死锁、访问冲突和通常不可预测的行为。所以答案很简单:不要从托管线程执行 APC

【讨论】:

  • 我仍然想为这个 Win32 函数找到一个 C# 解决方案,但我认为你提出了一些很好的观点。你是对的,Thread.Sleep 不起作用。我尝试使用指向句柄的指针调用 WaitForSingleObjectEx,但它似乎总是得到响应,我猜那是因为我的线程不是 Win32 线程,而是 .NET 线程对象......并且没有相同在警报状态下等待的能力。
  • 似乎工作得很好; Motes 实际上在上面发布了一个可行的解决方案!
  • 好吧,很高兴它对你有用 - 只要你保持回调最小并使用仅静态变量,它可能大部分时间都可以工作。但是你已经被警告了:你的 .NET 代码不应该在 APC 上运行是有原因的,一个是你可能会遇到 GC 与你的回调同时运行,在你访问它们时重新定位变量。
  • 对于它可能关心的每个人,我已经在 Dave 的 cmets 中回答了这些问题。
  • 我无法在该子线程中发表评论,因此将在此处回复:Alexandru,这对您的情况非常有用。您似乎在使用 .NET 尽可能安全地使用它,但是要拥有一个强大的解决方案,您需要生成一个非托管线程并处理来自非托管线程的 APC。除非您这样做,否则您的代码会受到许多未记录的行为的影响。就是这样。
【解决方案3】:

从@Motes 的回答中简化了很多...(编辑:我把它放到一个人们可以用来轻松等待服务停止的类中;它会阻塞!

再次编辑:如果您在函数中的任何位置使用 GC.Collect() 强制进行垃圾收集,请确保这有效...结果,您确实需要 SERVICE_STATUS_PROCESS。

另一个编辑:如果你中止你的线程,请确保它可以工作(注意:不能中止睡眠线程,所以如果你打算中止这个线程......那么确保你至少给它一个超时因此终结器可以在超时命中后运行),还添加了超时。还确保将 OS 线程一对一映射到当前的 .NET 线程。

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)
        {

        }
    }
}

是的!我们做到了。这是一段多么美好的旅程。

【讨论】:

  • 你不需要多线程,或者使用reader 类,除非你想在等待 APC 时读取行​​。我赞成您的另一个问题,因为您似乎纠正了评论者的投诉,但我不想回答这个问题。你已经有了,Passant 或 Heffernan 可能会因为它是“不受管理的”方法或其他东西而否决它。真的,我只是想说你可能想在NotifyServiceStatusChange 之后保留GC.KeepAlive(changeDelegate),因为它们不是对changeDelegate 的其他托管引用,因此它可能会被GC'd。事实上,没有它,我的例子对我不起作用。
  • 是的,@Alexandru,你为什么要删除 KeepAlive?在我看来,您认为如果程序成功运行一次,则表明它是正确的。这从来都不是真的。当你有 GC 时,这是一个特别冒险的假设。您调用的 API 函数在其文档中提出了一些非常具体的要求,您确实需要满足这些要求。为了“简化”而删除满足这些要求的代码显然是错误的。
  • @DavidHeffernan 因为结构已经固定。委托被指向为结构的一部分。因此,它的引用将留在非托管世界中。它还将停留在线程范围内,在托管世界中,以便重新编组。
  • @DavidHeffernan 我认为我们已经通过自己为结构处理 GC 来处理 GC 并发,但是您担心回调,它再次在结构中被引用。在我们释放结构之前,它将一直保留回调。
  • 不,该指令实际上是默认假设。我只是在调试时开始明确。尝试在调用SleepEx 之前添加GC.Collect(),这应该会显示您的代码是否可能会失败。我认为从调用 SleepEx 开始的无限等待状态是委托避免垃圾收集的唯一原因。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-04-27
  • 2017-08-14
  • 2010-11-14
  • 1970-01-01
  • 1970-01-01
  • 2017-01-30
  • 1970-01-01
相关资源
最近更新 更多