Creating a Windows Service using VB.Net
Introduction
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:
InstallUtil c:\thePath\theNameOfYourAssembly.exe
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
Conclusion
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.
Have fun!