4. Writing Code for the TriggerStartService Example
The example service must perform a number of tasks
to ensure reliable operation. For example, because this is a
trigger-start service, the service must ensure that it gets installed
only on an operating system that supports trigger-start services. Of
course, the example must perform some useful task. In this case, the
service will detect the opening and closing of a particular port and
log it in the System event log. The following sections describe the
various code elements of this example.
4.1. Creating the Trigger
The trigger code appears as part of the TriggerStartServiceInstaller.CS file. To add this code, right-click the TriggerStartServiceInstaller.CS file in Solution Explorer and choose View Code from the Context menu. You'll see the code editor for the TriggerStartServiceInstaller.CS file. To begin this process, add the following using statements to this file:
using System.ServiceProcess;
using System.Runtime.InteropServices;
Now that the file is configured, add the method and variables shown in Listing 1. This method creates a trigger object that the system uses to start and stop the service at the right time.
Example 1. Defining the service trigger
// Define the GUIDs used for the trigger subtype. You can obtain the GUIDs from // http://msdn.microsoft.com/library/dd405512.aspx. Guid FIREWALL_PORT_OPEN_GUID = new Guid("b7569e07-8421-4ee0-ad10-86915afdad09"); Guid FIREWALL_PORT_CLOSE_GUID = new Guid("a144ed38-8e12-4de4-9d96-e64740b1a524");
private Boolean ConfigurePortTrigger(String ServiceName) { // Obtain access to the service controller for this service. using (ServiceController SC = new ServiceController(ServiceName)) { try { // Create a string to hold the port information. String PortNumber = "23\0TCP\0\0";
// Define a pointer to the port information. IntPtr PortNumberPtr = Marshal.StringToHGlobalUni(PortNumber);
// Define the port data. SERVICE_TRIGGER_SPECIFIC_DATA_ITEM PortData = new SERVICE_TRIGGER_SPECIFIC_DATA_ITEM(); PortData.dwDataType = ServiceTriggerDataType.SERVICE_TRIGGER_DATA_TYPE_STRING; PortData.pData = PortNumberPtr; PortData.cbData = (uint)(PortNumber.Length * 2);
// Create a pointer to the port data. // Begin by allocating the required memory from the global heap. IntPtr PortDataPtr = Marshal.AllocHGlobal( Marshal.SizeOf(typeof(SERVICE_TRIGGER_SPECIFIC_DATA_ITEM)));
// Next, place the pointer to the GUID in FirewallPortOpen. Marshal.StructureToPtr(PortData, PortDataPtr, false);
// Create the port open trigger.
// Create a pointer to the FIREWALL_PORT_OPEN_GUID GUID. IntPtr FirewallPortOpen = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Guid))); Marshal.StructureToPtr( FIREWALL_PORT_OPEN_GUID, FirewallPortOpen, false);
// Create the start service trigger. SERVICE_TRIGGER StartTrigger = new SERVICE_TRIGGER();
// Place data in the various start trigger elements. StartTrigger.dwTriggerType = ServiceTriggerType.SERVICE_TRIGGER_TYPE_FIREWALL_PORT_EVENT; StartTrigger.dwAction = ServiceTriggerAction.SERVICE_TRIGGER_ACTION_SERVICE_START; StartTrigger.pTriggerSubtype = FirewallPortOpen; StartTrigger.pDataItems = PortDataPtr; StartTrigger.cDataItems = 1;
// Create the port close trigger.
// Create a pointer to the FIREWALL_PORT_OPEN_GUID GUID. IntPtr FirewallPortClose = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Guid))); Marshal.StructureToPtr( FIREWALL_PORT_CLOSE_GUID, FirewallPortClose, false);
// Create the stop service trigger. SERVICE_TRIGGER StopTrigger = new SERVICE_TRIGGER();
// Place data in the various stop trigger elements. StopTrigger.dwTriggerType = ServiceTriggerType.SERVICE_TRIGGER_TYPE_FIREWALL_PORT_EVENT; StopTrigger.dwAction = ServiceTriggerAction.SERVICE_TRIGGER_ACTION_SERVICE_STOP; StopTrigger.pTriggerSubtype = FirewallPortClose; StopTrigger.pDataItems = PortDataPtr; StopTrigger.cDataItems = 1;
// Create an array of service triggers. IntPtr ServiceTriggersPtr = Marshal.AllocHGlobal( Marshal.SizeOf(typeof(SERVICE_TRIGGER)) * 2); // Add the start service trigger. Marshal.StructureToPtr(StartTrigger, ServiceTriggersPtr, false); // Add the stop service trigger. Marshal.StructureToPtr(StopTrigger, new IntPtr((long)ServiceTriggersPtr + Marshal.SizeOf(typeof(SERVICE_TRIGGER))), false);
// Create a pointer to the service's trigger information // structure. IntPtr ServiceTriggerInfoPtr = Marshal.AllocHGlobal( Marshal.SizeOf(typeof(SERVICE_TRIGGER_INFO)));
// Define the service trigger information structure. SERVICE_TRIGGER_INFO ServiceTriggerInfo = new SERVICE_TRIGGER_INFO();
// Fill the structure with information. ServiceTriggerInfo.cTriggers = 2; ServiceTriggerInfo.pTriggers = ServiceTriggersPtr;
// Place a pointer to the structure in ServiceTriggerInfoPtr. Marshal.StructureToPtr( ServiceTriggerInfo, ServiceTriggerInfoPtr, false);
// Change the service's configuration to use triggers. Boolean Result = ServiceNative.ChangeServiceConfig2( SC.ServiceHandle.DangerousGetHandle(), ServiceConfig2InfoLevel.SERVICE_CONFIG_TRIGGER_INFO, ServiceTriggerInfoPtr);
// Get any errors. int ErrorCode = Marshal.GetLastWin32Error();
// Clean up from all of the allocations. Marshal.FreeHGlobal(PortNumberPtr); Marshal.FreeHGlobal(PortDataPtr); Marshal.FreeHGlobal(FirewallPortOpen); Marshal.FreeHGlobal(FirewallPortClose); Marshal.FreeHGlobal(ServiceTriggersPtr); Marshal.FreeHGlobal(ServiceTriggerInfoPtr);
// Check for an exception. if (!Result) { Marshal.ThrowExceptionForHR(ErrorCode); return false; } else return true; } catch { return false; } } }
|
The code begins by defining two Guid objects, FIREWALL_PORT_OPEN_GUID and FIREWALL_PORT_CLOSE_GUID. These objects correspond to constants used within C++ to provide values to the Win32 API call, ChangeServiceConfig2().
If you look at any of the trigger-start service examples, you'll see a
confusing list of GUIDs and wonder where the developer obtained them.
These GUIDs appear at http://msdn.microsoft.com/library/dd405512.aspx.
In short, you can't use just any GUID value; you must use the specific
GUIDs that Microsoft has defined for trigger event types. The reason
these objects are defined globally is that you may need to use them in
more than one location (unlike this example, where they're used only
once).
The ConfigurePortTrigger() method begins by creating a ServiceController object, SC, that uses the name of the service, ServiceName, to access the example service. Everything within the using
block applies to the example service. Because P/Invoke code is less
stable than managed code, you want to place it all within a try...catch block (or even several try...catch blocks) to provide more granular error control.
It's important to remember that there's a boundary
between native code and managed code. This example is working in both
worlds. In order to make managed code and native code work together
successfully, you must marshal data between the two environments.
Consequently, you see a number of uses of the Marshal class
within this example. C++, and therefore the Win32 API, also relies on
null-terminated strings. The P/Invoke code for this example begins by
creating a multi-string, a single string that contains multiple substrings. The String, PortNumber, contains two substrings — 23 is the first string and TCP is the second string. Each of these strings is null-terminated using the \0 escape character, and the string as a whole is null-terminated by another \0 escape character.
This string exists in managed memory, so the Win32 API can't access it. To make PortNumber available to the Win32 API, the code calls Marshal.StringToHGlobalUni(), which copies the string to native memory and returns a pointer to the native memory as an IntPtr, PortNumberPtr. The SERVICE_TRIGGER_SPECIFIC_DATA_ITEM documentation at http://msdn.microsoft.com/library/dd405515.aspx specifies that the multi-string you supply must be in Unicode format and not ANSI format, which is why this example uses the Marshal.StringToHGlobalUni() method to perform the marshaling.
The SERVICE_TRIGGER_SPECIFIC_DATA_ITEM structure is used to create that data element, PortData. As with many Win32 API structures, you must specify the kind of data that you're supplying in PortData.dwDataType, which is a SERVICE_TRIGGER_DATA_TYPE_STRING in this case. The PortData.pData
element contains a pointer to the native memory location that holds the
string you created. You must also supply the length of that string as a
uint (not an int). Because the string is in Unicode format, you must multiply the value of PortNumber.Length by 2 in order to obtain the correct data length.
Interestingly enough, the data structure PortData
is also in managed memory, so again, the code must marshal it to native
memory where the Win32 API can access it. Unlike strings, it's not easy
to determine the size of the native memory structure pointed to by PortDataPtr. The code calls Marshal.AllocHGlobal()
to allocate the memory required by the native memory structure from the
global heap, but it has to tell the method how much memory to allocate.
The code calls Marshal.SizeOf() to determine the native memory size of SERVICE_TRIGGER_SPECIFIC_DATA_ITEM. You absolutely must not call the standard sizeof() method to determine the size of the data structure, because sizeof() returns the managed size.
Allocating the memory required by PortData is only the first step. The memory doesn't contain any data yet. The Marshal.StructureToPtr() method moves the data in PortData to native memory and then places a pointer to that memory in PortDataPtr. The third argument is set to false because you don't want to delete any old content.
All these steps have created a data element for the
trigger and marshaled it to native memory. Now it's time to create an
actual trigger. The first step is to marshal the trigger subtype
described by FIREWALL_PORT_OPEN_GUID to native memory. Because a Guid is a structure, the code uses the same technique as it did for PortData — it allocates the memory by calling Marshal.AllocHGlobal() and then moves the data to that memory by calling Marshal.StructureToPtr().
You create a trigger by defining a SERVICE_TRIGGER object. In this case, the code creates StartTrigger
and then begins filling it with data. Remember that a trigger consists
of four elements: trigger type, trigger subtype, action, and data. The
trigger, StartTrigger.dwTriggerType, is a simple enumerated value, SERVICE_TRIGGER_TYPE_FIREWALL_PORT_EVENT. The action, StartTrigger.dwAction, is also an enumerated value, SERVICE_TRIGGER_ACTION_SERVICE_START. The trigger subtype, StartTrigger.pTriggerSubtype, is a pointer to the previously marshaled data pointed to by FirewallPortOpen. Likewise, the data, StartTrigger.pDataItems, is a pointer to the previously marshaled data pointed to by PortDataPtr. The structure also requires that you tell the Win32 API how many data items PortDataPtr contains, using the StartTrigger.cDataItems element.
The process for creating the stop trigger is the
same as the process for the start trigger. At this point, the code has
two managed triggers. The triggers point to native memory, but the data
itself, the enumerated values and pointers, resides in managed memory.
The code must create an array of triggers and place it in ServiceTriggersPtr.
You would normally use a managed code process to create the array, but
creating the array for native code use is completely different.
The code begins by allocating memory on the global heap for the array. Notice that the call to Marshal.AllocHGlobal() allocates enough memory for two SERVICE_TRIGGER data structures. As before, allocating the memory doesn't magically transfer the data. Transferring the first data structure, StartTrigger, is easy. The code simply calls Marshal.StructureToPtr() as usual for any data structure. The second transfer is a little harder because StopTrigger must end up after StartTrigger in the array. The technique the code uses to accomplish this task is to call Marshal.StructureToPtr() again, with a pointer to the native memory version of StartTrigger and a space allocated for StopTrigger. You can create an array of any size using this approach. If you had another trigger to add, you'd still call Marshal.StructureToPtr() with ServiceTriggersPtr as the first item of the second argument.
At this point, the code has created two triggers and
placed them in an array. However, in order for the Win32 API to work
with just about any data, it has to be placed in a package. The
package, in this case, is a SERVICE_TRIGGER_INFO structure, ServiceTriggerInfo. The code fills ServiceTriggerInfo.cTriggers with the number of triggers and then places the pointer to the native memory trigger array, ServiceTriggersPtr, in ServiceTriggerInfo.pTriggers. ServiceTriggerInfo is in managed memory, so the code creates ServiceTriggerInfoPtr, which points to the same data in native memory, by calling Marshal.StructureToPtr().
All the configuration is now completed. The code calls ServiceNative.ChangeServiceConfig2() with the handle to the service (SC.ServiceHandle.DangerousGetHandle()), an enumerated value that tells what kind of change to make (SERVICE_CONFIG_TRIGGER_INFO), and a pointer to the required data (ServiceTriggerInfoPtr). Notice the call to DangerousGetHandle().
A dangerous handle is essentially a native code handle to the service.
It's dangerous because the handle is outside the control of the managed
environment, which means that odd things can happen to it, like getting
de-allocated while still in use. Unfortunately, there isn't any
alternative to providing the handle in this case. You can find a list
of other tasks that you can perform using ChangeServiceConfig2() at http://msdn.microsoft.com/library/ms681988.aspx.
The ServiceNative.ChangeServiceConfig2() call returns a Boolean value, Result,
that indicates success or error, but doesn't tell you what error
occurred. To obtain the actual error information, the code calls Marshal.GetLastWin32Error() and places the value in ErrorCode. The error code is a simple number. You can look up the text version of the number using the ErrLook.EXE utility found in the \Program Files\Microsoft Visual Studio 10.0\Common7\Tools folder of your setup. The code also calls Marshal.ThrowExceptionForHR() to create a managed exception that should include human readable information for the error.
The code has allocated a lot of native
memory. None of this memory is automatically cleaned up. In fact, if
you don't manually clean it up, Windows will continue to think that the
memory is allocated. The memory will remain inaccessible until the next
reboot, creating a memory leak. In order to prevent a memory leak, your
code must call Marshal.FreeHGlobal() for each memory allocation as shown in the example code.