Yesterday I got a call from my brother, or actually he sent me a message over MSN. He wanted to know if I could help him create a program with a very specific requirement. Where he works they have a system that creates a lock file when you enter a new journal into the system. However because of a bug (?) in that system, this file is not always deleted after the journal have been entered. If this file is not removed, no more journals can be entered and the system runs amok.
So my brother wanted a program that could monitor a folder for this file, if it’s not removed within a specified time, say 1 minute, an e-mail notification is supposed to be sent. Since this program was supposed to run on the server, it needed to be a Windows Service.
Creating a Windows Service
Creating a Windows Service might not be something you do on a daily bases, during my career I’ve only made 3 or 4 of them, and this one was the very first I’ve created using .Net. Developing a Windows Service used to be pretty darn difficult, but not any more. As it turned out it’s fairly easy these days.
You start by selecting the Windows Service project type in the New Project dialog box in Visual Studio.
Doing that will give you a ServiceBase designer on which you can drop your controls, but since a Service normally don’t interact with the desktop, since it’s supposed to be running even if nobody is logged in, you shouldn’t use input controls such as text boxes or buttons. Using a Timer is a normal way of handling the work the service should do, but in my case I only needed a FileSystemWatcher, so I just dropped that on to the designer surface. The designer also have a handful of properties, many of which are named Can… such as CanPauseAndContinue and CanStop. If you set the CanPauseAndContinue property to True you can override the OnPaus and OnContinue methods. In my case I wasn’t interested in that but I did want an administrator to be able to stop the service so I left the CanStop property as True. If you, like me, leave the properties with their default settings you will have to override the OnStart and OnStop methods and I will cover that shortly.
I needed a way to store settings for this service, such as the time it would wait before sending an e-mail, the folder and file it was going to watch, and various SMTP and mail settings, such as the subject and the body text. To keep it as simple as possible I decided to add an App.Config file. To do that just right click on the project in the solution explorer and select Add > New Item, in the context menu. Find the Application Configuration template and click the Add button. Note, leave the name as app.config Visual Studio will automatically rename this file and copy it to the Bin folder when you build the project. To be able to read the config file you need to add a reference to System.Configuration.
I will not go into the details of using an app.config file but in short you need to add an <appSettings> section under the <configuration> node under which you add your own settings.
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="emailSubject" value="Can somebody remove this file please?" /> <!-- more settings go here… -->
You can then read these values using the ConfigurationManager.AppSettings() method (this requires that you have imported the System.Configuration namespace which we set a reference to earlier).
In my service I added a private method which I simply called ReadSettings. I will not show you all of that code since it’s mainly boiler plate, but a part of it looks like this:
Private Function ReadSettings(ByRef logMessage As String) As Boolean Dim emailRecipients As String() _eMailTo = New List(Of String) Try emailRecipients = ConfigurationManager.AppSettings("emailTo").Split(";"c) Catch ex As Exception logMessage = _
"No e-mail recipients with valid e-mails found, " & _
"edit ""emailTo"" in the config file." Return False End Try For Each email In emailRecipients email = email.Trim If Not String.IsNullOrEmpty(email) AndAlso IsValidEmail(email) Then _eMailTo.Add(email) End If Next If _eMailTo.Count = 0 Then logMessage = "No e-mail recipients with valid e-mails found." Return False End If Try _subject = ConfigurationManager.AppSettings("emailSubject") Catch ex As Exception logMessage = "No e-mail subject found" _subject = String.Empty End Try 'read the rest of the settings Return True End Function
This method takes a parameter, logMessage, by reference. In some cases, for example with the e-mail subject, it is allowed to leave that out of the config file and the application will simply send the e-mail without a subject. Other settings, like the address or addresses to send the e-mail to is required. The ReadSettings method will return False if some required setting was not found, or in the wrong format. The actual writing to the log is done by the OnStart method, or rather it’s done by another small helper method that is called by the OnStart method.
Private Sub WriteLogMessage(ByVal message As String, _
ByVal type As EventLogEntryType) If Not EventLog.SourceExists("File Observer") Then EventLog.CreateEventSource("File Observer", "File Observer Log") End If Dim log As New EventLog() log.Source = "File Observer" log.WriteEntry(message, type) End Sub
I put all validation rules in the ReadSettings() method so that when it’s time to send the e-mail, I know that I have e-mail addresses that are valid (or rather, that they have a valid form, not that I can check if the address itself really exists), and that the SMTP IP port is set to a valid integer and so on.
The OnStart() and OnEnd() methods
Even though you can override the constructor of the ServiceBase control, you shouldn’t really put initialization code there. This is because if the service is stopped and then restarted the constructor is not called again, but the OnStart() method is. So initialization should go into that method.
In my very specific case, I used a FileSystemWatcher to monitor the specified folder for the creation of the specified file. If the ReadSettings() method returned True I started monitoring the folder. If not I needed a way to stop the service from starting and write an error message to the event viewer. Unfortunately the OnStart() method does not have a way of signaling an error, so what you need to do is to call the SetServiceStatus() function which is a Win32 API function. Import the System.Runtime.InteropServices namespace and add the following code to your class.
<StructLayout(LayoutKind.Sequential)> _ Public Structure SERVICE_STATUS Public serviceType As Integer Public currentState As Integer Public controlsAccepted As Integer Public win32ExitCode As Integer Public serviceSpecificExitCode As Integer Public checkPoint As Integer Public waitHint As Integer End Structure Public Enum State SERVICE_STOPPED = &H1 SERVICE_START_PENDING = &H2 SERVICE_STOP_PENDING = &H3 SERVICE_RUNNING = &H4 SERVICE_CONTINUE_PENDING = &H5 SERVICE_PAUSE_PENDING = &H6 SERVICE_PAUSED = &H7 End Enum Private Declare Auto Function SetServiceStatus Lib "ADVAPI32.DLL" ( _ ByVal hServiceStatus As IntPtr, _ ByRef lpServiceStatus As SERVICE_STATUS _ ) As Boolean
Private _serviceStatus As SERVICE_STATUS
So the following is the code I used in the OnStart() method.
Protected Overrides Sub OnStart(ByVal args() As String) Dim handle As IntPtr = Me.ServiceHandle _serviceStatus.currentState = Fix(State.SERVICE_START_PENDING) SetServiceStatus(handle, _serviceStatus) Dim logMessage As String = String.Empty If Not ReadSettings(logMessage) Then WriteLogMessage(logMessage, EventLogEntryType.Error) _serviceStatus.currentState = Fix(State.SERVICE_STOPPED) SetServiceStatus(handle, _serviceStatus) Else If Not String.IsNullOrEmpty(logMessage) Then WriteLogMessage(logMessage, EventLogEntryType.Information) End If 'Start the file watching... With FileSystemWatcher1 .BeginInit() .Filter = _fileMask .IncludeSubdirectories = _includeSubFolders .Path = _folderName .EnableRaisingEvents = True .EndInit() _serviceStatus.currentState = Fix(State.SERVICE_RUNNING) SetServiceStatus(handle, _serviceStatus) End With End If End Sub
So if everything works out the way it should I start the FileSystemWatcher by setting its EnableRaisingEvents property to True. In the OnStop() method I simply set this property to False to disable it.
When the FileSystemWatcher finds that the file that is being watched is created it raises the Created() event, in which I start a new thread that will simply sleep for the specified number of seconds and then check if the file still exists. If it does it sends the e-mail.
Private Sub FileSystemWatcher1_Created( _ ByVal sender As Object, _ ByVal e As System.IO.FileSystemEventArgs) Handles FileSystemWatcher1.Created If e.ChangeType = IO.WatcherChangeTypes.Created Then Dim thread As New Threading.Thread(AddressOf WatchFile) thread.Start(e.FullPath) End If End Sub Private Sub WatchFile(ByVal fullPath As Object) Dim fileName As String = CStr(fullPath) Threading.Thread.Sleep(_delayTime * 1000) If IO.File.Exists(fileName) Then SendMail() End If End Sub
Private Sub SendMail() Dim mail As New System.Net.Mail.MailMessage For Each email In _eMailTo mail.To.Add(New Net.Mail.MailAddress(email)) Next mail.From = New Net.Mail.MailAddress(_emailFrom) mail.Subject = _subject mail.Body = _body Dim smtpClient As New Net.Mail.SmtpClient(_smtpHost) With smtpClient .Port = _smtpPort If _requireAuthentication Then .Credentials = New Net.NetworkCredential(_authenticateName, _
_authenticatePassword) End If Try .Send(mail) Catch ex As Exception WriteLogMessage("Unable to send mail: " & ex.Message, _
EventLogEntryType.Error) End Try End With End Sub
Adding an installer to the project
Since this is a Windows Service it has to be installed as such so that it’s listed in the Service control panel applet. So you need to add an installer to the project. Note that this installer is not the same thing as a setup program, it will just add the necessary code that allows this application to be installed as a service using the InstallUtil.exe command line tool that comes with the .Net framework. More about that tool in a second.
To add an installer select your ServiceBase designer and right click on its surface and select Add Installer in the context menu. This will add a new Installer designer to your project that contains two component, a ServiceInstaller and a ServiceProcessInstaller.
Select the ServiceInstaller and change its DisplayName property. This will be the name that is listed in the control panel applet. You can also change the StartType property to Automatic if you want your service to start directly after it has been installed. In my case I left that property as Manual.
Now select the ServiceProcessInstaller and set the Account property to LocalSystem.
That’s it. You can now build the project. To do the installation open up a Visual Studio Command Prompt and type:
And presto! Your service should now be listed among the others in the Services control panel applet. Try to start and stop it from there. If you want to uninstall the service, which you must do if you need to make some changes to the source code, then type the following at the command prompt.
InstallUtil /u c:\thePath\theNameOfYourAssembly.exe
Even though this was a Windows Service with some very specific requirements I hope that this article have answered some questions on how you can create your own services.