Two weeks ago I began the series with a discussion on how to make a new C# Windows Service template that is “debuggable” (you can find the article here). I had then intended the next week to follow up with a discussion on how to modify that template to make the service “self-installing”.
Unfortunately, with my work schedule I wasn’t able to complete the series last week due to a product deployment schedule that involved a lot of nights, so I did a brief piece on features I would like to see in C# 5.0 and beyond instead (here).
But now my evenings have quieted down again and I’m ready to discuss part two of my C# Windows Services template which will cover how to make your service “self-installing”.
Taking the debuggable service template we built from the last discussion, we will now build on it and add the ability to let the service install itself.
The Other Problem With The Default Template
The second problem with the existing C# Windows Service template (the first being that its hard to debug) is that the code generated can’t be run under the debugger, and you have to write your own installer for it.
Unfortunately, this is a manual process that must be done each time, unless, of course, we write a helper class and incorporate it into our template.
You’ll notice we have a lot of helper code going into our template now. Unfortunately, with generating templates there is not an easy way to extract references that aren’t part of the .NET Framework (or aren’t in the GAC).
So for now we will have our helper classes in a solution folder called Framework, although we could easily extract it into its own class library, sign it, and put it in the GAC instead if we chose to make our template more clean.
Building an Installable Service
The first thing I’m going to do is reorganize a few of my “helper” classes to put them in a folder called Framework and modify the namespaces accordingly. This will make it easy to extract it into a class library later if we so choose:
Now that we’ve isolated a lot of our extra helper code into its own folder which (when closed) makes our solution look much cleaner. This is the same code we’d pull into a class library (perhaps called WindowsServiceFramework) later if we so choose.
Now that we’ve cleaned up a bit, let’s think about how we can tell this service how to install itself. Basically we need to be able to provide the details on what the service is called and how it behaves.
We could just call a method call, but that feels clunky. On things like this where you are specifying static configuration information (such as installer data), I prefer to use attributes. I’d like to be able to decorate my ServiceImplementation class with an attribute that will tell it how to install itself. In my mind this makes the most sense and is the most obvious.
This is how I envision it being used by someone using this template:
1 : [WindowsService(
"SystemMonitorProcessingService",
2: DisplayName = "System Monitor Processing Service",
3: Description =
"The message processing service for the system monitor suite.",
4: StartMode = ServiceStartMode.Automatic)] 5
: public class ServiceImplementation : IWindowsService 6 : {
7 : // ...
8:
}
So let’s define this attribute, in the Framework folder:
1 : [AttributeUsage(AttributeTargets.Class, AllowMultiple = false,
Inherited = true)] 2 : public class WindowsServiceAttribute
: Attribute 3 : {
4 : // The name of the service.
5 : public string Name {
get; set;
}
6 : 7 : // The displayable name that shows in service manager (defaults to
// Name).
8 : public string DisplayName {
get; set;
}
9 : 10 : // A textural description of the service name (defaults to Name).
11 : public string Description {
get; set;
}
12 : 13 : // The user to run the service under (defaults to null). A null or
// empty
14 : // UserName field causes the service to run as
// ServiceAccount.LocalService.
15 : public string UserName {
get; set;
}
16 : 17
: // The password to run the service under (defaults to null). Ignored
18 : // if the UserName is empty or null, this property is ignored.
19 : public string Password {
get; set;
}
20 : 21 : // Specifies the event log source to set the service's EventLog to.
// If this is
22 : // empty or null (the default) no event log source is set. If
// set, will auto-log
23 : // start and stop events.
24 : public string EventLogSource {
get; set;
}
25 : 26 : // The method to start the service when the machine reboots
// (defaults to Manual).
27 : public ServiceStartMode StartMode {
get; set;
}
28 : 29 : // True if service supports pause and continue (defaults to true).
30 : public bool CanPauseAndContinue {
get; set;
}
31 : 32 : // True if service supports shutdown event (defaults to true).
33 : public bool CanShutdown {
get; set;
}
34 : 35 : // True if service supports stop event (defaults to true).
36 : public bool CanStop {
get; set;
}
37 : 38 : // Marks an IWindowsService with configuration and installation
// attributes.
39 : public WindowsServiceAttribute(string name) 40 : {
41 : // set name and default description and display name to name.
42 : Name = name;
43 : Description = name;
44 : DisplayName = name;
45 : 46 : // default all other attributes.
47 : CanStop = true;
48 : CanShutdown = true;
49 : CanPauseAndContinue = true;
50 : StartMode = ServiceStartMode.Manual;
51 : EventLogSource = null;
52 : Password = null;
53 : UserName = null;
54:
}
55:
}
Now that we have that attribute defined, we can give our ServiceImplementation class a boiler-plate version of this attribute that the user will fill in later after creating the template.
1 : // Remember, $safeprojectname$ and $projectname$ are template parameters
2 : // that get filled in when the code is generated.
3
: [WindowsService(
"$safeprojectname$", 4: DisplayName = "$projectname$",
5: Description = "The description of the $projectname$ service.",
6: EventLogSource = "$projectname$",
7: StartMode = ServiceStartMode.Automatic)] 8
: public class ServiceImplementation : IWindowsService 9 : {
10 : // ...
11:
}
Okay, so now we have an attribute! Unfortunately, attributes are not magic. You have to query them and then use them. I have a helper extension method I like to use that makes it easier to query class attributes (I have another one for assembly attributes as well), so we’ll drop that in our Framework too but feel free to use it elsewhere.
1 : // some of my favorite extension methods off of the Type class
2 : public static class TypeExtensions
3 : {
4 : // Queries a type for a list of all attributes of a specific type.
5 : public static IEnumerable<T> GetAttributes<T>(
this Type typeWithAttributes) 6 :
where T : Attribute 7 : {
8 : // Try to find the configuration attribute for the default logger if it
// exists
9 : object[] configAttributes = Attribute.GetCustomAttributes(
typeWithAttributes, 10: typeof(T), false);
11 : 12 : // get just the first one
13
: if (configAttributes != null && configAttributes.Length > 0) 14 : {
15 : foreach (T attribute in configAttributes) 16 : {
17 : yield return attribute;
18:
}
19:
}
20:
}
21 : 22 : // Queries a type for the first attribute of a specific type.
23
: public static T GetAttribute<T>(this Type typeWithAttributes) 24 :
where T : Attribute 25 : {
26 : return GetAttributes<T>(typeWithAttributes).FirstOrDefault();
27:
}
28:
}
Now that we have that, we can query the attribute easily off of our IWindowsService implementation. But once we have that data what do we do with it? Well, we have to write an actual installer class that uses the values to install or uninstall the windows service.
The first thing we need to do is add a reference to System.Configuration.Install. This gives us access to the Install class which we will subclass to make our windows service installer. Once we add this reference, right-click on the Framework folder and add a Installer Class item. This will give you an Install class component which we can then fill in with the details of how to install our service.
Go into the code behind of the service and change the code to:
1 : // A generic windows service installer
2 : [RunInstaller(true)] 3 : public partial class WindowsServiceInstaller
: Installer 4 : {
5 : // Gets or sets the type of the windows service to install.
6 : public WindowsServiceAttribute Configuration {
get; set;
}
7 : 8 : // Creates a blank windows service installer with configuration in
// ServiceImplementation
9 : public WindowsServiceInstaller()
: this(typeof(ServiceImplementation)) 10 : {
11:
}
12 : 13 : // Creates a windows service installer using the type specified.
14 : public WindowsServiceInstaller(Type windowsServiceType) 15 : {
16 : if (!windowsServiceType.GetInterfaces().Contains(
typeof(IWindowsService))) 17 : {
18 : throw new ArgumentException(
"Type to install must implement IWindowsService.",
19: "windowsServiceType");
20:
}
21 : 22 : var attribute =
windowsServiceType.GetAttribute<WindowsServiceAttribute>();
23 : 24 : if (attribute == null) 25 : {
26 : throw new ArgumentException(
"Type to install must be marked with a WindowsServiceAttribute.",
27: "windowsServiceType");
28:
}
29 : 30 : Configuration = attribute;
31:
}
32 : 33 : // Performs a transacted installation at run-time of the
// AutoCounterInstaller and any other listed installers.
34 : public static void RuntimeInstall<T>() 35:
where T : IWindowsService 36 : {
37 : string path = "/assemblypath=" + Assembly.GetEntryAssembly().Location;
38 : 39 : using (var ti = new TransactedInstaller()) 40 : {
41 : ti.Installers.Add(new WindowsServiceInstaller(typeof(T)));
42 : ti.Context = new InstallContext(null, new[] { path });
43 : ti.Install(new Hashtable());
44:
}
45:
}
46 : 47 : // Performs a transacted un-installation at run-time of the
// AutoCounterInstaller and any other listed installers.
48 : public static void RuntimeUnInstall<T>(
params Installer[] otherInstallers) 49:
where T : IWindowsService 50 : {
51 : string path = "/assemblypath=" + Assembly.GetEntryAssembly().Location;
52 : 53 : using (var ti = new TransactedInstaller()) 54 : {
55 : ti.Installers.Add(new WindowsServiceInstaller(typeof(T)));
56 : ti.Context = new InstallContext(null, new[] { path });
57 : ti.Uninstall(null);
58:
}
59:
}
60 : 61 : // Installer class, to use run InstallUtil against this .exe
62 : public override void Install(
System.Collections.IDictionary savedState) 63 : {
64 : ConsoleHarness.WriteToConsole(
ConsoleColor.White, "Installing service {0}.", Configuration.Name);
65 : 66 : // install the service
67 : ConfigureInstallers();
68 : base.Install(savedState);
69:
}
70 : 71 : // Removes the counters, then calls the base uninstall.
72 : public override void Uninstall(
System.Collections.IDictionary savedState) 73 : {
74 : ConsoleHarness.WriteToConsole(ConsoleColor.White,
"Un-Installing service {0}.",
Configuration.Name);
75 : 76 : // load the assembly file name and the config
77 : ConfigureInstallers();
78 : base.Uninstall(savedState);
79:
}
80 : 81 : // Method to configure the installers
82 : private void ConfigureInstallers() 83 : {
84 : // load the assembly file name and the config
85 : Installers.Add(ConfigureProcessInstaller());
86 : Installers.Add(ConfigureServiceInstaller());
87:
}
88 : 89 : // Helper method to configure a process installer for this windows
// service
90 : private ServiceProcessInstaller ConfigureProcessInstaller() 91
: {
92 : var result = new ServiceProcessInstaller();
93 : 94
: // if a user name is not provided, will run under local service acct
95 : if (string.IsNullOrEmpty(Configuration.UserName)) 96 : {
97 : result.Account = ServiceAccount.LocalService;
98 : result.Username = null;
99 : result.Password = null;
100:
}
101 : else 102 : {
103 : // otherwise, runs under the specified user authority
104 : result.Account = ServiceAccount.User;
105 : result.Username = Configuration.UserName;
106 : result.Password = Configuration.Password;
107:
}
108 : 109 : return result;
110:
}
111 : 112 : // Helper method to configure a service installer for this
// windows service
113 : private ServiceInstaller ConfigureServiceInstaller() 114
: {
115 : // create and config a service installer
116 : var result = new ServiceInstaller 117 : {
118 : ServiceName = Configuration.Name,
119 : DisplayName = Configuration.DisplayName,
120 : Description = Configuration.Description,
121 : StartType = Configuration.StartMode,
122 :
};
123 : 124 : return result;
125:
}
126:
}
Yes that’s a mass of code, but it’s really not that bad, here’s the highlights:
- Construtor – queries the IWindowsService implementing type for the WindowsServiceAttribute and uses that as our configuration.
- Install() – this virtual method is overridden from the Install class and is invoked by installutil for installation.
- Uninstall() – this virtual method is overridden from the Install class and is invoked by installutil for uninstallation.
- RuntimeInstall() – this additional method is used to correctly invoke the Install() method at runtime (instead of installutil).
- RuntimeUnInstall() – this additional method is used to correctly invoke the Uninstall() method at runtime (instead of installutil).
- ConfigureXxx() – Windows Services need both a process and service installed, these configuration methods examine our WindowsServiceAttribute and create the correct sub-installers.
So, once we have this installer class, we can modify our Program class to check for an –install or –uninstall command line option and call the installer directly (note, you must have administrator access on a machine to install a service on it):
1 : static class Program
2 : {
3 : // The main entry point for the windows service application.
4 : static void Main(string[] args) 5 : {
6 : // if install was a command line flag, then run the installer at
// runtime.
7 : if (args.Contains("-install",
StringComparer.InvariantCultureIgnoreCase)) 8 : {
9 : WindowsServiceInstaller.RuntimeInstall<ServiceImplementation>();
10:
}
11 : 12
: // if uninstall was a command line flag, run uninstaller at runtime.
13
: else if (args.Contains("-uninstall",
StringComparer.InvariantCultureIgnoreCase)) 14
: {
15 : WindowsServiceInstaller.RuntimeUnInstall<ServiceImplementation>();
16:
}
17 : 18 : // otherwise, fire up the service as either console or windows
// service based on UserInteractive property.
19 : else 20 : {
21 : var implementation = new ServiceImplementation();
22 : 23
: // if started from console, file explorer, etc, run as console app.
24 : if (Environment.UserInteractive) 25 : {
26 : ConsoleHarness.Run(args, implementation);
27:
}
28 : 29 : // otherwise run as a windows service
30 : else 31 : {
32 : ServiceBase.Run(new WindowsServiceHarness(implementation));
33:
}
34:
}
35:
}
36:
}
The Contains() method is a nice LINQ extension method that allows you to check for the existence of an item in a collection, it is much better than the Contains() method in arrays because it allows you to specify a comparer, in this case we can use StringComparer.InvariantCultureIgnoreCase so that we do a string-insensitive compare so that variations like –UNINSTALL, –UnInstall, etc all work.
Now, we’ve got all our code, so we should make sure that everything compiles well and then change our namespace to use the $safeprojectname$ template parameter.
Helpful Hint: because template parameters do not compile, I like to use a simple namespace in my templates as I’m working on them (for instance, DebuggableInstallableService) and then use menu option Edit –> Find and Replace –> Replace in Files… to replace all instances of that temporary namespace to $safeprojectname$ before exporting the template.
So, before we export, take a final look at your project, it should look something like this when all expanded:
So, if I’ve explained all my steps adequately and you’ve followed them correctly we should be good to go, let’s export into a template. Choose the menu option File –> Export Template…
Keep the default options, and hit [Next >] …
Make sure you fill in the details that you would like to see for the service name, description, and appropriate pictures (for the pretty factor). Make sure the Automatically import the template into Visual Studio checkbox is selected (so you can use it right away).
You can also select Display an explorer window on the output files folder checkbox if you wish to copy or email the template file to friends and co-workers so they can import it (by double-clicking on it) as well.
Once you fill in everything, press [Finish] to generate and import the template.
Going for a Test Drive…
So, your template is now in Visual Studio and ready to use! Let’s create a simple test application to verify everything works as planned! So close whatever you are currently working on in Visual Studio and create a new project, from the project template list, select your new debuggable installable windows service template:
Enter an appropriate test project name, and select [OK] to generate the project. If all goes well, you should be presented with a project that contains all our template code including the Framework folder and its classes. Let’s add some simple console writes to the ServiceImplementation class just for kicks and grins:
1 : [WindowsService(
"TestInstallableService", 2: DisplayName = "TestInstallableService",
3: Description =
"The description of the TestInstallableService service.",
4: EventLogSource = "TestInstallableService",
5: StartMode = ServiceStartMode.Automatic)] 6
: public class ServiceImplementation : IWindowsService 7 : {
8 : // This method is called when the service gets a request to start.
9 : public void OnStart(string[] args) 10 : {
11 : ConsoleHarness.WriteToConsole(ConsoleColor.Green, "Hi mom!");
12:
}
13 : 14 : // This method is called when the service gets a request to stop.
15 : public void OnStop() 16 : {
17 : ConsoleHarness.WriteToConsole(ConsoleColor.Red, "Bye mom!");
18:
}
19 : 20 : // ...
21:
}
Now try to run your service from the debugger, you should see:
So our debuggable service aspect still works, now let’s try the installer aspects. Let’s try using installutil to verify we can install the service using the .NET installutil tool. Start a Visual Studio command prompt and then navigate to your project output folder and type:
installutil –i TestInstallableService.exe
Substitute whatever you called the test project name of course for the executable above, you should see some output like:
If everything was correct and you have admin authority on your box, you should see a success message. Now, if you open your administrative services console, you should see your new service installed as a service:
It’s there and ready to run as a service! But I promised you the service was self-installing as well and I wasn’t lying. Why do we care? Because installutil is often not in the path of your production machines and so it’s just a lot easier to have the option to have the service install itself.
So uninstall your service real quick using installutil again:
installutil –u TestInstallableService.exe
You should see a success message, now try using the –install command line parameter from your executable directly, from the command prompt type:
TestInstallableService –install
And once again you should see it go through the install steps and you should verify the service exists in the services menu.
Summary
Windows Services can be great tools for long-running processes that handle asynchronous requests from queues or sockets, unfortunately the default Windows Service template does not make it easy to debug or install the services.
Hopefully, by following these code examples, I’ve shown you some hints that can make your own Windows Services more robust and create a new Windows Service template of your own so you don’t have to repeat the steps every time.
Hope you enjoyed it!
Technorati Tags: C#,.NET,Windows Service,Service,Template,Visual Studio