问题
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.");
}
}
}
回答1:
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.");
}
}
回答2:
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.
回答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.
来源:https://stackoverflow.com/questions/20061459/why-wont-my-solution-work-to-p-invoke-notifyservicestatuschange-in-c