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.

image

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!

11 thoughts on “Creating a Windows Service using VB.Net

  1. There is an example on MSDN that provides an example with NotifyFilters. Not sure if it’s .NET 3.5 compatible, but in any case it doesn’t work 🙂

    Unfortunately I was unable to make your example work either – seems like I am missing something that would make events fire on file change/creation/deletion or rename.

  2. Great post. Some of this detail is missing from other similar articles that mean they don’t work! Many thanks.

  3. I have a question. ok well, this is a method of installation. But if my product will get to someone else … that person will need to install the service again using InstallUtil? but if that person have not Visual Studio, or even any idea how to do this? My question is how to create a setup to install the service wherever executed? sorry for my bad English.

  4. You can distribute InstallUtil, but you most likely want to package your solution into a setup.exe file instead. You can use Windows Installer for that.

  5. I used the same lines of codes to call a SQL stored procedure, if calling from window form, it works. But it failed when I call from Window Service. I added a setup project to my window service project and it install the .exe file successfully, but it failed to call the stored procedure.

    Can you help please.

    Thanks!!!!!

  6. I’m geting some errors writting to Event log

    Service cannot be started. System.Security.SecurityException: The source was not found, but some or all event logs could not be searched. Inaccessible logs: Security.

Leave a Reply to Paul Cancel reply

Your email address will not be published. Required fields are marked *