Jul 04 2006

Creating a Windows service in C#

Posted by admin under .NET

 

Our goals:

  • Create a service, which will be installed in Service Manager as any service
  • It should however (pretty much like the MySQL service) be able to run it from
    commandline (use a -cl switch). So, in Visual Studio, we should be able to run it in debug mode just like any command line program
  • And, oh, lets put some real usage of it as well - not only this being a technique showcase. The service should be sleeping, waiting for a specific file to be created, then when the file is created(or changed in some way) it should wake up and execute a specific exefile/cmd file. I.e some sort of trigger mechanism where another program could signal (by just creating the file) that a specific process should be run asyncronously.


I will try to take you though the different steps needed - we will be using VS2003 (.NET 1.1) and C# and also show you how to do the installation.

First - we have look at the app.config file - which we will be using to specify the file to wait for and which file to execute.

EEEContent_1

The overall idea is to create a FileSystemWatcher component and by using it we can have the OS to  "wake us up" when there has been a change to the file  f:\temp\triggerfile.txt. When that happen we execute the file f:\temp\test.cmd.

So, first we create the project in VS2003. There is a premade project template for creating a Windows service to use that to get the sceleton code.

 


The specific thing about a service is that it need to interact with the Windows SCM (service control manager).

 

(sorry about swedish words...but you get can probably translate it - it means Start, Stop, Pause, Continue).

As you can see all we can do here is stop. Why we can Start it makes sense - it is already running, but why can't we Pause/Continue? Well, that's up to the developer of the service to decide.

 

Change CanPauseAndContinue to true and you can implement code for pause and continue by overriding  the OnPause and OnContinue virtual functions (see the actual code in the zipfile).

Now, I won't get into the very basics of how the code is done - there are a lof of other articles on creating Windows services on the net, instead I'll try to look at our special requirements, the -cl and debugging options. However some basics first:

Our service class is called FileWatcher and it inherits from System.ServiceProcess.ServiceBase. When Windows starts it will go through the list of all installed services in and load the processes (which means calling the Main function). Now, for our service to be a good citizen in the Windows world we need to pass a list of the services our process implements (one .exe can implement multiple services), so what we do is

EEEContent_2


Now in a sense, Windows has gotten a "handle" to our service , and is therefore able to call the OnStart, OnStop() etc. And that all - now our Main() function is done. We do NOT call OnStart from main - Windows should do it for us, cause it could be that our service has been not been marked as "Autostart".

Now, lets look at OnStart - where the service code actually begins:

First, for our service I have added a nonvisual component System.IO.FileSystemWatcher (variable is called fileSystemWatcher1)  and in OnStart we just activate it:

EEEContent_3

We set the Path to the DirectoryName (i.e f:\temp), the Filter to the filename (i.e triggerfile.txt)
and start the listening by EnableRaisingEvents = true. The filter could be wildcard, we could monitor a directory for changes to, say, all *.txt changes.

Now, our fileSystemWatcher1_Created will be called  whenever the file is created. Please note you  need to write handlers for fileSystemWatcher1_Changed and fileSystemWatcher1_Renamed as well. Lets say you have a file, hello.txt, and renames it to triggerfile.txt (which doesn't exists). One would think that fileSystemWatcher1_Created would be called - but that's not the case, the fileSystemWatcher1_Renamed is instead.  Nevermind, we implement the same code in all handlers, just a call to RunUpdate();

Our RunUpdate first checks to make sure the file does exist, if it does it starts the process.

EEEContent_4


Depending on your requirements when it comes to reentring the function it might be so that you would like to store and next time compare last modify date from the file etc, to ensure you don't run it twice. 

Now, the "-cl" switch - which will enable us to run is as any commandline program instead of as a  service - and also let us debug it from within the VS2003. Lets go back to Main().  We change it to take care of arguments as well. We can then check if "-cl" is specified on the commandline - and if that's the  case we should NOT register with SCM but instead call OnStart:

EEEContent_5

Now in the OnStart, if we do run it from the command line, we can't leave the function - cause it would terminate the while process - instead we put the process to indefinite sleep.


EEEContent_6

This leaves us with the only chose of using ctrl+c to close it - and it will not be that very nice shutdown, but this is only used for debugging purposes - and in rare cases from the real commandline so I'll live with that - the process will die and thereby release it's resources anyway.

By adding -cl to the commandline for debugging we can debug the program directly from within VS2003, no need to attach to the service process or anything:

Installing the service
Now for the installation part. We need to let Windows know about our service - i.e we need to install it. Not that hard in .NET as it was in Win32 (even though it wan't really hard there either, just a matter of knowing how to do it).

We add a installer class to our project

we add a ServiceInstaller for each service the process contains and also we add a ServiceProcessInstaller. Makes sense, doesn't it?

For the ServiceProcessInstaller we can say which account the service should run under - LocalSystem is a good start - you can always changeit later in the Control Manager.

In the ServiceInstaller you just need to enter the correct ServiceName (in our case FileWatcher), and note that is is NOT the same as the classname (it most probably are, but it doesn't have to be). The Service has a property, ServiceName, and that is what need to match with the Installers servicename.

Now, compile and in your bin directory (debug or release) you have FileWatcher.exe and FileWatcher.exe.config. These are files to be installed somewhere on your remote server (or your local). Lets say, you copy the files to c:\myservices\

Now we need to run the installer. For that we use the installutil.exe - which will be in "path" and can be used directly if you use the Visual Studio.NET 2003 Command Prompt, otherwise (on a remote server for example) - locate the file by searching. You might find many, one in v1.1.4322 and one in v2.0.50727 for example. Since we have used .NET 1.1 that's the one we should use.

Start a command prompt, cd to c:\myservices\ and run installutil on filewatcher.exe


Now our service is installed - and should be available in service manager. Now we can try to start it, stop it, pause and continue etc. Remember, if you do a change to the config file - you need to restart the service.

However if you have created a new version of the service, you will NOT need to run installutil, just stop the service, copy the new file iver the old one and then restart service.

>net stop filewatcher
>net start filewatcher

Last note: in the final version I have added a feature. The system just monitors changes to the file.
Consider this:
Service is paused/stopped.
The other program is running and creates the file.
Service is continued/started.
Now will the file RunUpdate be called? No - cause no changes has been made to the file which the service could monitor. Therefore - I simply added a RunUpdate() call in OnContinue() and OnStart(). It will run the cmdfile simply if the file exists - and then delete it.


Things for you to improve(?) if you feel like it:
- support for multiple processes instead of just being able to call a single process
- automatically catch up on changes in config file - without the need of restart