Frankly, I thought that one of basic things in Windows development, such as “debugability” and “installability” of services, were changed for the last 10 years in development environments. However, I was disappointed to discover that nothing actually changed. You still cannot build easy to debug (in command line) service, which also can be installed without special additional tools.
Even ServiceName/ServiceNameInstaller trick, is specific for the current assembly and cannot be used if your base class is not the one you are really using. This is not the only approach. Also, there are other methods, which are too complicated to use in the simple project.
So, I decided to write quick basic service which can be used as common base for self-installable and debugable service development. Let’s start.
First of all, we need an abstract
service base:
public abstract class ServiceProvider : ServiceBase
Then is the identification for derived classes:
public static string Name;
public static string ProviderKind;
public ServiceProvider(string name) {
ProviderKind = name;
Name = "MagicService." + ProviderKind;
ServiceName = Name;
}
Now method to start it from the hosting application (e.g. command prompt) or run it if installed.
public void Start(string[] args) {
if (Environment.UserInteractive) {
OnStart(args);
} else {
ServiceBase.Run(this);
}
}
But how to install it? Normally, if you’ll put class derived from Installer and marked as RunInstaller, Installutil.exe can initialize it and install or uninstall the service.
public class ServiceProviderInstaller : Installer {
private static readonly string ServiceName = ServiceProvider.Name;
public ServiceProviderInstaller() {
var processInstaller = new ServiceProcessInstaller {
Account = ServiceAccount.LocalSystem
};
var serviceInstaller = new ServiceInstaller {
DisplayName = "Magic Server " + ServiceProvider.ProviderKind + " Provider",
Description = "Process the interface to the Magic service " +
ServiceProvider.ProviderKind + " provider.",
ServiceName = ServiceName,
StartType = ServiceStartMode.Automatic,
};
this.Installers.Add(processInstaller);
this.Installers.Add(serviceInstaller);
}
But it works only if installer is defined in the same assembly as service and the service itself can be run. In our case, this is not true. Service is abstract and we allow to run service from any other assembly which is referenced to the base one. So what to do? Simple! Let’s create our own installer. We will create the private
instance of installer inside the actual service itself and pass it as additional installer to basic TransactedInstaller. Also, we’ll use calling (the actual running) assembly as the service reference.
public void Install() {
if (Environment.UserInteractive) {
var ti = new TransactedInstaller();
var spi = new ServiceProviderInstaller();
ti.Installers.Add(spi);
var path = "/assemblypath=" + Assembly.GetEntryAssembly().Location;
var ctx = new InstallContext("", new string[] { path });
ti.Context = ctx;
ti.Install(new Hashtable());
}
}
The same way we’ll do uninstaller:
public void Uninstall() {
if (Environment.UserInteractive) {
var ti = new TransactedInstaller();
var spi = new ServiceProviderInstaller();
ti.Installers.Add(spi);
var path = "/assemblypath=" + Assembly.GetEntryAssembly().Location;
var ctx = new InstallContext("", new string[] { path });
ti.Context = ctx;
ti.Uninstall(null);
}
}
We're almost done, the only problem is Component Designer which wants to be run when you click on any class derived from ServiceBase
. I know that Visual Studio developers wanted to do our life easier, but this designer (especially one cannot initialize abstract
classes) is very annoying. In order to get rid of this thing, we can override DesignerCategory of the class and tell VS that it is not SeriviceComponent
anymore. To do this, all we need is one small attribute set on classes.
[System.ComponentModel.DesignerCategory("")]
public abstract class ServiceProvider : ServiceBase {
…
[RunInstaller(true), System.ComponentModel.DesignerCategory(""), SerializableAttribute]
public class ServiceProviderInstaller : Installer {
Take into account that it should be full reference pass in order to help Visual Studio with fast resolving of references.
We're done, let’s put everything together and see what we have and how to use it.
[System.ComponentModel.DesignerCategory("")]
public abstract class ServiceProvider : ServiceBase {
public static string Name;
public static string ProviderKind;
public ServiceProvider(string name) {
ProviderKind = name;
Name = "Magic.Provider." + ProviderKind;
ServiceName = Name;
}
public void Start(string[] args) {
if (Environment.UserInteractive) {
OnStart(args);
} else {
ServiceBase.Run(this);
}
}
public void Install() {
if (Environment.UserInteractive) {
var ti = new TransactedInstaller();
var spi = new ServiceProviderInstaller();
ti.Installers.Add(spi);
var path = "/assemblypath=" + Assembly.GetEntryAssembly().Location;
var ctx = new InstallContext("", new string[] { path });
ti.Context = ctx;
ti.Install(new Hashtable());
}
}
public void Uninstall() {
if (Environment.UserInteractive) {
var ti = new TransactedInstaller();
var spi = new ServiceProviderInstaller();
ti.Installers.Add(spi);
var path = "/assemblypath=" + Assembly.GetEntryAssembly().Location;
var ctx = new InstallContext("", new string[] { path });
ti.Context = ctx;
ti.Uninstall(null);
}
}
}
[RunInstaller(true), System.ComponentModel.DesignerCategory(""), SerializableAttribute]
public class ServiceProviderInstaller : Installer {
private static readonly string ServiceName = ServiceProvider.Name;
public ServiceProviderInstaller() {
var processInstaller = new ServiceProcessInstaller {
Account = ServiceAccount.LocalSystem
};
var serviceInstaller = new ServiceInstaller {
DisplayName = "Magic Service " +
ServiceProvider.ProviderKind + " Provider",
Description = "Process the interface to the Magic service "
+ ServiceProvider.ProviderKind + " provider.",
ServiceName = ServiceName,
StartType = ServiceStartMode.Automatic,
};
this.Installers.Add(processInstaller);
this.Installers.Add(serviceInstaller);
}
protected override void OnCommitted(IDictionary savedState) {
base.OnCommitted(savedState);
var c = new ServiceController(ServiceName);
c.Start();
}
}
In order to use it, just reference the hosting assembly and inherit this class:
public abstract class SampleService : ServiceProvider {
public SampleService()
: base("Sample") {
}
}
And run it from command prompt:
class Program {
static void Main(string[] args) {
var p = new SampleService();
if (args.Length > 0) {
if (args[0] == "/i"){
p.Install();
return;
}
if (args[0] == "/u") {
p.Uninstall();
return;
}
}
p.Start(null);
Console.Read();
p.Stop();
}
}
We're done. The only remaining thing is how to prevent component designer appearance on derived (SampleService
) class. As for now, I found no way to do this and asked collective intelligence to help me with it. Once I’ll have an answer, I will update it here.
Be good people and have a good day!
Related Posts
- RSA private key import from PEM format in C#
- Video encoder and metadata reading by using Windows Media Foundation