Windows services are long-running applications that have no visual interface and run quietly in the background. Windows services are used to support essential services such as Internet Information Services (IIS), Microsoft SQL Server, COM+, and Message Queuing, and they typically run under a local system account and load when your computer is first started. The Windows Service Control Manager (SCM) mediates Windows services, and you can start, stop, and configure services through the Computer Management administrative console.
Unlike Microsoft Visual Basic 6, which had no intrinsic support for Windows services, .NET includes types in the System.ServiceProcess namespace for creating, controlling, and installing Windows services. Visual Studio .NET even provides a dedicated Windows service project type. If you create a Windows service application, Visual Studio .NET will generate a basic class that extends System.ServiceProcess.ServiceBase, includes the basic initialization logic required to run the service, and provides an OnStart and OnStop method where you will add your code. The OnStart method is triggered when the service is started by the SCM (either automatically at startup or manually), and the OnStop method is triggered when the service is stopped. Remember, all Windows services must perform their work asynchronously. Thus, you must use the OnStart code to configure a new timer, a thread, or an event handler where the actual work will take place. The OnStop method will detach these resources and stop any in-progress work.
Note |
To learn more about the basics of asynchronous programming, refer to the recipes in Chapter 7. |
The first two recipes in this chapter show the basic design patterns for creating a Windows service. Recipe 13.3 shows you how to install a Windows service (a necessity before you can test or use it), while recipes 13.5, 13.6, and 13.7 show how you can interact with installed Windows services, either to retrieve information or control their execution. Finally, recipe 13.8 considers how you can use a system tray to provide status information from a Windows service.
You want to create a Windows service that periodically "wakes up" and performs a task.
Initialize a timer when the service is started, and react to the timer events to perform your task.
When a Windows service is launched, the SCM calls the OnStart method. The OnStart method should perform as little work as possible—in fact, if the OnStart method hasn't completed after 30 seconds, the SCM will abort the service completely. One possible design is to use the OnStart method to create a timer. The timer event-handling code will then perform the actual work.
The following example creates a Windows service that performs a task every 10 seconds. This task consists of writing a debug message and writing a single piece of information in a text file. To use this example as written, you must import the System.ServiceProcess and System.Timers namespaces.
Public Class TimerService Inherits System.ServiceProcess.ServiceBase Public Sub New() MyBase.New() InitializeComponent() End Sub _ Public Shared Sub Main() Dim ServicesToRun() As ServiceBase ServicesToRun = New ServiceBase() {New TimerService} System.ServiceProcess.ServiceBase.Run(ServicesToRun) End Sub Private Sub InitializeComponent() Me.ServiceName = "TimerService" End Sub ' This fires every 10 seconds. Private WithEvents ServiceTimer As New Timer(10000) ' Track the number of timer events. Private Counter As Integer Protected Overrides Sub OnStart(ByVal args() As String) ' Start the timer. ServiceTimer.Start() End Sub Protected Overrides Sub OnStop() ' Stop the timer. ServiceTimer.Stop() End Sub Private Sub DoWork(ByVal sender As Object, _ ByVal e As ElapsedEventArgs) Handles ServiceTimer.Elapsed Counter += 1 Debug.WriteLine("Repetition #" & Counter.ToString()) Try Dim fs As New System.IO.FileStream( _ "c:ServiceTest.txt", IO.FileMode.Create) Dim w As New System.IO.StreamWriter(fs) w.Write("Test #" & Counter.ToString()) w.Flush() fs.Close() Catch Err As Exception Debug.WriteLine(Err.ToString()) End Try End Sub End Class
To enhance this example, you might want to add a Boolean member variable that the OnStop method can set to instruct the Timer event handler to stop processing. This technique is shown in recipe 13.2.
Note |
System.Timers.Timer is known as a server timer, and it differs from the System.Windows.Forms.Timer class you might be familiar with. A server timer, unlike a user interface timer, uses multiple threads. In other words, if you schedule a server timer to fire every 10 seconds and the task takes more than 10 seconds, more than one thread might run at once. This could cause synchronization problems. For example, if two threads run the DoWork method shown earlier, an error could be generated because the file cannot be accessed by two threads at once. |
You want to create a Windows service that performs some type of long-running task continuously.
Create a new thread when the service is started, and perform all your work on that thread.
As explained earlier, the OnStart method is used to set your Windows service processing in motion, but it can't perform the work directly itself. One of the most common design patterns is to use the OnStart method to create and start a new thread. The OnStop method can then terminate this thread.
This thread can process continuously, using a loop if it needs to repeat the same work. In addition, you might want to use a Boolean member variable to allow the OnStop method to signal a polite stop (a technique first described in recipe 7.10).
The following example shows a Windows service that loops continuously, writing debug messages. To test that it is working, you should use recipe 13.4 to attach a debugger so that you can see these messages. To use this example as written, you must import the System.ServiceProcess and System.Threading namespaces.
Public Class ThreadService Inherits System.ServiceProcess.ServiceBase Public Sub New() MyBase.New() InitializeComponent() End Sub _ Public Shared Sub Main() Dim ServicesToRun() As ServiceBase ServicesToRun = New ServiceBase() {New ThreadService} System.ServiceProcess.ServiceBase.Run(ServicesToRun) End Sub Private Sub InitializeComponent() Me.ServiceName = "ThreadService" End Sub ' This is the thread where the actual work takes place. Private ServiceThread As Thread ' This signals the thread to stop processing. Private StopThread As Boolean = False Protected Overrides Sub OnStart(ByVal args() As String) ' Create and start the thread. ServiceThread = New Thread(AddressOf DoWork) ServiceThread.Start() End Sub Protected Overrides Sub OnStop() ' Try to signal the thread to end nicely, ' and wait up to 20 seconds. StopThread = True ServiceThread.Join(TimeSpan.FromSeconds(20)) ' If the thread is still running, abort it. If (ServiceThread.ThreadState And _ ThreadState.Running) = ThreadState.Running Then ServiceThread.Abort() End If End Sub Private Sub DoWork() Dim Counter As Integer Do Until StopThread Counter += 1 Debug.WriteLine("Now Starting Iteration #" & _ Counter.ToString()) Thread.Sleep(TimeSpan.FromSeconds(10)) Loop End Sub End Class
You need to install your Windows service so that you can run or test it.
Create an installer class, and use the InstallUtil command-line utility.
Because Windows services are controlled by the SCM, you must install them before you can run or debug them. To install a Windows service, you must create an installer class. Microsoft Visual Studio .NET can generate this installer class automatically. In the Solution Explorer, simply right-click your service code file, choose View Designer, and select the Add Installer link that displays in the Properties window (as shown in Figure 13-1).
Figure 13-1: The Add Installer link.
A new ProjectInstaller.vb file will be added to your project. This file contains all the code required to install the service. This installer uses two installer components that are automatically added to the design-time view: ServiceProcessInstaller1 and ServiceInstaller1 (as shown in Figure 13-2).
Figure 13-2: The installer components.
You can modify the ServiceProcessInstaller.Account property to configure the user account that will be used to run the service when it is first installed. You can set this to LocalSystem so that the service runs under a system account with broad privileges. You can also modify the ServiceInstaller.StartType property to determine whether the service will be launched automatically at startup. Both of these details can also be configured later using the Computer Management console. The name of the service is set by the ServiceInstaller.ServiceName property.
Following is a sample project installer class. To use this example as written, you must import the System.ServiceProcess, System.Configuration.Install, and System.ComponentModel namespaces.
Public Class ProjectInstaller Inherits System.Configuration.Install.Installer Public Sub New() MyBase.New() InitializeComponent() End Sub Friend ServiceProcessInstaller1 As ServiceProcessInstaller Friend ServiceInstaller1 As ServiceInstaller Private Sub InitializeComponent() Me.ServiceProcessInstaller1 = New ServiceProcessInstaller() Me.ServiceInstaller1 = New ServiceInstaller() Me.ServiceProcessInstaller1.Account = _ ServiceAccount.LocalSystem Me.ServiceInstaller1.ServiceName = "TestService" ' Add the two installers. Me.Installers.AddRange(New Installer() _ {Me.ServiceProcessInstaller1, Me.ServiceInstaller1}) End Sub End Class
There are two ways to install a Windows service using an installer. You can create a dedicated setup project using Visual Studio .NET, or you can use the InstallUtil command-line utility. The second option is more convenient for simple testing or single-machine deployment.
To use InstallUtil, build your project, browse to the Bin directory using a command-line window, and type in the following instruction (where WindowsService1 is the name of your application):
InstallUtil WindowsService1.exe
If there are any embedded spaces in the name of the service file, you'll need to use quotation marks.
InstallUtil "Recipe 13-1.exe"
To uninstall a service, add the /u parameter. If the service is currently running, it will be stopped first and then uninstalled.
InstallUtil WindowsService1.exe /u
You can now find and start the service using the Computer Management administrative console (shown in Figure 13-3).
Figure 13-3: Starting the service with the Computer Management console.
Note |
Both recipe 13.1 and recipe 13.2 include a sample installer that you can use to install the Windows service, as described in this recipe. |
You want to debug a Windows service in Visual Studio .NET (watch debug messages, use variable watches and breakpoints, and so on).
Start the Windows service through the SCM as you would ordinarily, and attach the Visual Studio .NET debugger manually.
You can't run a Windows service from the Visual Studio .NET integrated development environment (IDE) because Windows services can be executed only by the SCM. However, you can start the Windows service through the SCM, and then attach the debugger.
To debug a Windows service, follow these steps:
Figure 13-4: Attaching the debugger to a running process.
Figure 13-5: Choosing the program type to debug.
You can now set breakpoints, pause execution, and create variable watches as you would with any other application in Visual Studio .NET. If you test the application in recipe 13.2, you'll see debug output like that shown in Figure 13-6.
Figure 13-6: The debug output for a Windows service.
You want to retrieve information about all the services on your computer.
Use the shared ServiceController.GetServices method.
.NET allows you to interact with any installed Windows service using the ServiceContoller class. The ServiceController class includes information such as
You can create a ServiceController object bound to a specific service by specifying the service name in the constructor, as shown here:
Dim TestService As New ServiceController("TestService")
You can also use the GetServices method to retrieve all the services on the computer. For example, this code iterates over all the services on the computer and prints the name and status of each on the following page.
Dim Service, Services() As System.ServiceProcess.ServiceController Services = System.ServiceProcess.ServiceController.GetServices() For Each Service In Services Console.WriteLine(Service.ServiceName & " is " & _ Service.Status.ToString()) Next
Note |
To create this application, you need to import a reference to the System.Services.dll assembly, which is included in Windows service projects automatically. |
You can also use data binding to display all the public information exposed in a ServiceController. For example, if you add the following code to a form, you can fill a DataGrid with comprehensive information about all the services on your computer:
Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load gridServices.DataSource = _ System.ServiceProcess.ServiceController.GetServices() End Sub
Some of the information in this form is shown in Figure 13-7.
Figure 13-7: The Windows services on the current computer.
Note |
The ServiceController class also provides a shared GetDevices method that returns an array of ServiceController instances representing all the device driver services on the current computer. |
You want to start or stop a Windows service that is installed on the current computer.
Use the Stop and Start methods of the ServiceController class.
Recipe 13.5 shows the properties of the ServiceController class. The ServiceController class also provides methods such as Start, Stop, Pause, and Continue.
As an example, here's how you might start a service programmatically:
Dim TestService As New ServiceController("TestService") TestService.Start()
The ServiceController class methods allow you to start and stop services and monitor their status, but they won't allow you to interact in an application-specific way. In other words, you can't send a message or call a method in your Windows service code. To do that, you'd need to create a Windows service that hosts a remotable object. Another application that wants to communicate with this service must then call a method of the remotable object. This approach requires .NET Remoting, as described in Chapter 17.
You need to start or stop a service that is running on another computer.
Create a ServiceController instance using the constructor that accepts a machine name. You can then use the Stop and Start methods of the ServiceController class.
You can create a ServiceController instance that represents a service running on another computer, provided that you have the required network rights, by specifying the computer name in the ServiceController constructor. Here's one example:
Dim TestService As New ServiceController("TestService", " ComputerName")
You can also use GetServices or GetDevices to retrieve service information from another computer by supplying a machine name.
Dim Services() As ServiceController Services = ServiceController.GetServices("ComputerName")
You want to create a Windows service that uses some type of user interface, such as a system tray icon.
If possible, do not implement any user interface in a Windows service. As a last resort, enable desktop interaction using the Computer Management console.
Microsoft guidelines discourage user interfaces with any Windows service application. Adding a user interface can lead to security risks and can prevent the service from running at all if the required permission isn't granted. In fact, Windows services should be able to run with absolutely no user intervention—there might not even be a user logged on to the computer.
In cases where you do need a user interface to configure some aspects of the Windows service operation (or to view some data that it has processed), it's recommended that you create a separate application. For example, your Windows service can be configured to write data to a database or an event log, and you can create a separate program that can read this information and present it to the user. A more advanced design is to use .NET Remoting to allow a user interface tool to talk to a Windows service. In this way, you can use a program that can configure a Windows service in real time if required. (This design is similar to the way the SQL Server works with its system tray utility.) For more information on this approach, refer to the .NET Remoting recipes in Chapter 17.
However, if you decide to provide some basic interface through a Windows service, it is possible. For example, you might want to add a system tray icon that indicates the status of the current operation. You can easily add a NotifyIcon control to your Windows service project and configure it in your service code. Following is a rudimentary example that modifies the icon text to show the processing state. To use this example as written, you must import the System.ServiceProcess and System.Threading namespaces.
Public Class IconService Inherits System.ServiceProcess.ServiceBase _ Shared Sub Main() Dim ServicesToRun() As ServiceBase ServicesToRun = New ServiceBase(){New IconService()} System.ServiceProcess.ServiceBase.Run(ServicesToRun) End Sub Private Components As System.ComponentModel.IContainer Friend ServiceIcon As System.Windows.Forms.NotifyIcon Private Sub InitializeComponent () Me.Components = New System.ComponentModel.Container() Dim resources As New System.Resources.ResourceManager( _ GetType(IconService)) ' Configure the system tray icon. ' The bitmap is retrieved from a resource file. ' This code is generated automatically by Visual Studio .NET. Me.ServiceIcon = New System.Windows.Forms.NotifyIcon(Me.Components) Me.ServiceIcon.Icon = CType( _ resources.GetObject("ServiceIcon.Icon"), System.Drawing.Icon) Me.ServiceIcon.Text = "" Me.ServiceIcon.Visible = True Me.ServiceName = "IconService" ' (Some of the automatically generated designer code is omitted.) End Sub Private ServiceThread As Thread Private StopThread As Boolean = False Protected Overrides Sub OnStart(ByVal args() As String) ServiceIcon.Text = "Starting ..." ServiceThread = New Thread(AddressOf DoWork) ServiceThread.Start() End Sub Protected Overrides Sub OnStop() ServiceIcon.Text = "Stopping ..." ' Try to signal the thread to end nicely, ' (and wait up to 20 seconds). StopThread = True ServiceThread.Join(TimeSpan.FromSeconds(20)) ' If the thread is still running, abort it. If (ServiceThread.ThreadState And _ ThreadState.Running) = ThreadState.Running Then ServiceThread.Abort() End If End Sub Private Sub DoWork() Do Until StopThread ServiceIcon.Text = "Processing" Thread.Sleep(TimeSpan.FromSeconds(10)) Loop ServiceIcon.Text = "Not Processing" End Sub End Class
You could extend this design by adding a context menu with event handlers for menu items. Remember, though, that this design isn't recommended because it will limit the scenarios in which you can use this service.
If you install this example Windows service, by default it will not work. When you attempt to start the service, an exception may be thrown or it may start but not display the system tray icon. The solution is to manually enable desktop interaction privileges using the Computer Management console. Find the service in the list, right-click it, and select Properties. Then, in the Log On tab, select the Allow Service To Interact With Desktop check box, as shown in Figure 13-8.
Figure 13-8: Allowing a service to interact with the desktop.
Introduction