Windows services have long been a hot topic in Visual Basic programming circles. The preceding version of the language, Visual Basic 6, included no native support, forcing developers to resort to third-party tools or API wizardry. In the .NET platform, the mystique finally lifts; creating a Windows service is now as easy as creating any other type of application. Windows services, of course, are long-running applications that have no visual interface and typically work in the background as soon as your computer is started. Services were first introduced with Windows NT and are mediated by the Windows Service Control Manager (SCM). You can start, stop, and configure services through the Computer Management administrative utility. Services are used to manage everything from core operating system services (such as the distributed transaction coordinator) to long-running engines (such as Microsoft SQL Server and Microsoft Internet Information Services [IIS]). Windows services are ideal for server computers because they enable you to create components that run even while the computer is not logged in. (In fact, it's important to remember that even if a user is logged in, each Windows service runs under a specific fixed account, which is probably not the same as the logged-in user account.) This section shows how you can create a Windows service to use for a long-running task or as a component host. The .NET support for Windows service applications originates from the System.ServiceProcess namespace. The types in this namespace serve three key roles:
Creating a Windows ServiceVisual Studio .NET programmers can start by creating a Windows service project. This creates a single class that inherits from ServiceBase. This class has the structure shown in Listing 7-9. I've revealed a portion of hidden designer code because it's conceptually important. Listing 7-9 A basic Windows serviceImports System.ServiceProcess Public Class Service1 Inherits System.ServiceProcess.ServiceBase Public Sub New() MyBase.New() InitializeComponent() End Sub ' This is the main entry point for the process. <MTAThread()> _ Shared Sub Main() ' You could use the method below with an array ' of ServiceBase instances to start multiple services in the ' same process. ServiceBase.Run(New Service1()) End Sub Private Sub InitializeComponent() ' (If you have configured Service1 properties at design-time, ' the property setting code will be added here.) Me.ServiceName = "TestService" End Sub Protected Overrides Sub OnStart(ByVal args() As String) ' Add code here to start your service. ' This method might start a timer or a separate thread ' before it returns. End Sub Protected Overrides Sub OnStop() ' Add code here to stop your service and release resources. End Sub End Class When this application is started, the Main method runs first. The Main method uses the shared ServiceBase.Run and passes the new instance of your service. If you want to start multiple services in the same process so they can interact with each other but be stopped and started individually, you can use this method with an array of services, as shown here: Dim ServicesToRun() As ServiceBase ServicesToRun = New ServiceBase() {New Service1(), New Service2()} System.ServiceProcess.ServiceBase.Run(ServicesToRun) The ServiceBase.Run method doesn't actually start your service technically, it loads it into memory and provides it to the SCM so that it is ready to be executed. What happens next depends on how the service is configured in the SCM. The service itself might be started automatically when the computer boots up, for example, or manually when a user interacts with the Computer Management utility. When the service is started, the SCM calls the OnStart method of your class. However, this method doesn't actually perform the work; instead, it schedules the work. If OnStart doesn't return after a reasonable amount of time (approximately 30 seconds), the start attempt is abandoned and the service is terminated. Therefore, you shouldn't perform any application-specific processing in the OnStart method. Instead, you need to use your OnStart method to set up a new thread or timer, which will then perform the real work. Similarly, when the service is stopped, the OnStop method is called. This is the point where you will release all in-memory objects and stop the timer or thread processing. You have two simple tasks to complete to make your Windows service operable. First, you should set all the relevant ServiceBase properties. In Visual Studio .NET, you can set these properties using the Properties window in Design view (in which case the property setting code is added to the hidden Windows designer region). Alternatively, you can manually add the property set statements to the constructor. Table 7-1 lists the ServiceBase properties. Note that properties such as CanStop and CanShutdown indicate whether a specific feature will be provided to the SCM. If you code the required method but don't set the corresponding property, your code is ignored. Similarly, if you set a property to indicate that your service can perform something it can't, an exception is generated when the command is attempted.
Next, you need to add the logic to the OnStart and OnStop methods. In Listing 7-10, this service just starts a thread that writes debug information. Note that you don't need to call the base OnStart or OnStop ServiceBase methods in your overridden methods because this happens automatically. Listing 7-10 A test service with threadingImports System.ServiceProcess Imports System.Threading Public Class TestService Inherits System.ServiceProcess.ServiceBase Public Sub New() MyBase.New() InitializeComponent() End Sub <MTAThread()> _ Shared Sub Main() ServiceBase.Run(New TestService()) End Sub Private Sub InitializeComponent() Me.ServiceName = "TestService" End Sub Private ServiceThread As Thread Private StopThread As Boolean = False Protected Overrides Sub OnStart(ByVal args() As String) ServiceThread = New Thread(AddressOf DoWork) ServiceThread.Start() End Sub Protected Overrides Sub OnStop() ' We only have 30 seconds to act before the SCM takes matters ' into its own hands. ' 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() ServiceThread.Join() 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 Alternatively, you can start a timer (using the System.Timers.Timer class), which fires at periodic intervals. A timer is best suited for a short, repeated task, whereas a thread can handle a long-running, continuous task that works through several stages. Listing 7-11 shows an example that uses a custom timer to perform a task. Listing 7-11 A timer-based serviceImports System.ServiceProcess Imports System.Timers Public Class TestService Inherits System.ServiceProcess.ServiceBase Public Sub New() MyBase.New() InitializeComponent() End Sub <MTAThread()> _ Shared Sub Main() ServiceBase.Run(New TestService()) End Sub Private Sub InitializeComponent() Me.ServiceName = "TestService" End Sub Private WithEvents ServiceTimer As New Timer(10000) Private Counter As Integer Protected Overrides Sub OnStart(ByVal args() As String) ServiceTimer.Start() End Sub Protected Overrides Sub OnStop() ServiceTimer.Stop() End Sub Private Sub DoWork(ByVal sender As Object, _ ByVal e As ElapsedEventArgs) Handles ServiceTimer.Elapsed Counter += 1 Debug.WriteLine("Now Starting Iteration #" & _ Counter.ToString()) End Sub End Class A third option is to not use a timer or thread but use some sort of event handler. For example, you can create a System.IO.FileSystemWatcher instance to monitor a directory. The OnStart method will connect the handler, and the OnStop method will disconnect it. Chapter 16 presents a case study that uses a long-running Windows service to perform directory monitoring. Note Most services support stopping and starting. When a service stops, any data held in form-level variables should be completely released and the OnStart method should perform the required initialization from scratch. If your service retains a significant amount of information, however, you might want to implement OnPause and OnContinue in addition to OnStart and OnStop. The pause method will then stop the timer or thread from processing, but it will retain all the state information. The continue method will then resume processing immediately, without requiring any initialization. This pattern isn't used in the example because no significant information is retained while the service is working. Installing a Windows ServiceUnfortunately, Windows service applications cannot be debugged inside Visual Studio .NET because they are controlled by the SCM. To test your service, you first need to create an installer. The easiest approach is to let Visual Studio .NET perform some of the work for you. Just click your service code file, put it in Design view, and select the Add Installer link that displays in the Properties window (as shown in Figure 7-4). Figure 7-4. Adding an installer in Visual Studio .NET
A new ProjectInstaller.vb file is 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 7-5). Figure 7-5. The installer components
Taken together, these classes encapsulate the installation process. When included in a setup project, the Windows installer automatically calls the Install method of both classes. The classes then write the required Registry information. Before continuing further, you might want to make two minor modifications:
You can also change these details later by modifying the configuration settings for the service in the Computer Management utility. The project installer class is very simple and performs most of its work automatically using the functionality it gains from the Installer class in the System.Configuration.Install namespace (as shown in Listing 7-12). Listing 7-12 A sample Windows service installerImports System.Configuration.Install Imports System.ServiceProcess <RunInstaller(True)> 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 You can incorporate this installer into a custom setup project, or you can use the InstallUtil.exe utility included with Visual Studio .NET. To do so, build your project, browse to the Bin directory using a command-line window, and type the following instruction (where WindowsService1 is the name of your application): InstallUtil WindowsService1.exe The output for a successful install operation is shown here: Microsoft (R) .NET Framework Installation utility Version 1.0.3512.0 Copyright (C) Microsoft Corporation 1998-2001. All rights reserved. Running a transacted installation. Beginning the Install phase of the installation. See the contents of the log file for the e:\windowsservice1\bin\windowsservice1.exe assembly's progress. The file is located at e:\windowsservice1\bin\windowsservice1.InstallLog. Installing assembly 'e:\windowsservice1\bin\windowsservice1.exe'. Affected parameters are: assemblypath = e:\windowsservice1\bin\windowsservice1.exe logfile = e:\windowsservice1\bin\windowsservice1.InstallLog Installing service TestService... Service TestService has been successfully installed. Creating EventLog source TestService in log Application... The Install phase completed successfully, and the Commit phase is beginning. See the contents of the log file for the e:\windowsservice1\bin\windowsservice1.exe assembly's progress. The file is located at e:\windowsservice1\bin\windowsservice1.InstallLog. Committing assembly 'e:\windowsservice1\bin\windowsservice1.exe'. Affected parameters are: assemblypath = e:\windowsservice1\bin\windowsservice1.exe logfile = e:\windowsservice1\bin\windowsservice1.InstallLog The Commit phase completed successfully. The transacted install has completed. You can now find and start the service using the Computer Management administrative tool (as shown in Figure 7-6). Figure 7-6. The installed service
If you want to update the service, you need to recompile the executable, uninstall the existing service, and then reinstall the new service. To uninstall a service, just use the /u parameter with InstallUtil: InstallUtil WindowsService1.exe /u Debugging a Windows ServiceEven though you can't run a Windows service in Visual Studio .NET, it is still possible to debug it with the IDE. You just need to attach to the service after it is already started. Visual Studio .NET then enables you to set breakpoints and single-step through the code. To attach to a service, begin by loading the appropriate project into Visual Studio .NET. That way, the source code will already be available. Then make sure that the service is installed and currently started by the SCM. Choose Tools | Debug Processes, and in the Processes window (shown in Figure 7-7) be sure to enable the Show System Processes check box if you are running your service under a system account; otherwise, it won't appear in the list. When you find the matching service, select it by clicking the Attach button. Figure 7-7. Attaching the debugger to a service
In the Attach To Process window, choose to debug the code as a Common Language Runtime application (as shown in Figure 7-8). Then click OK. Figure 7-8. Choosing the application type
You are now free to set breakpoints, pause execution, and create variable watches. If you have created the simple debugging service presented previously, some basic information will begin to appear in the Debug window (as shown in Figure 7-9). Figure 7-9. Debugging the service
Note The SCM provides only 30 seconds for you to set OnStart and OnStop breakpoints. If you set a breakpoint and allow the code to be paused for a longer period of time, the service is terminated. Controlling Windows ServicesYou don't need to use an administrative tool to start and stop your services. In fact, you can interact directly with services using the ServiceController class. This is a useful technique if you want to create your own administrative tool that will monitor your custom services on various computers and that will enable you to manage them from a single location. For example, you can create a ServiceController object bound to a specific service by specifying the service name in the constructor (or the service name and machine name, for a remote service). Dim TestService As New ServiceController("TestService") You are then free to query information from properties such as Status, CanStop, and ServiceType. You can also use methods to programmatically start, stop, pause, or continue the service: TestService.Stop() The ServiceController class also provides a shared GetServices method, which returns an array of ServiceController instances representing all the services on a single computer. You can use this technique to create a simple service manager, as shown in Figure 7-10. Figure 7-10. A custom application for controlling services
This application simply fills a DataGrid with a list of services and enables the user to stop or start a selected service. Listing 7-13 shows the form code. Listing 7-13 Managing active servicesImports System.ServiceProcess Public Class ServiceManager Inherits System.Windows.Forms.Form ' (Windows designer code omitted.) Friend WithEvents cmdGetServices As System.Windows.Forms.Button Friend WithEvents gridServices As System.Windows.Forms.DataGrid Friend WithEvents cmdStart As System.Windows.Forms.Button Friend WithEvents cmdStop As System.Windows.Forms.Button Private Sub cmdGetServices_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdGetServices.Click RefreshServices() End Sub Private Sub cmdStart_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdStart.Click If gridServices.CurrentRowIndex > 0 Then Dim ServiceName = _ gridServices.Item(gridServices.CurrentRowIndex, 1) Dim Service As New ServiceController(ServiceName) Service.Start() ' Refresh the display RefreshServices() End If End Sub Private Sub cmdStop_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdStop.Click If gridServices.CurrentRowIndex > 0 Then Dim ServiceName = _ gridServices.Item(gridServices.CurrentRowIndex, 1) Dim Service As New ServiceController(ServiceName) Service.Stop() ' Refresh the display RefreshServices() End If End Sub Private Sub RefreshServices() gridServices.DataSource = ServiceController.GetServices() End Sub End Class Note Our example duplicates the basic functionality of the Computer Management utility. To make it truly useful, you can change it to work with a coordinator XML Web service that provides methods such as RegisterWindowsService and GetRegisteredWindowsServices. Your Windows service can call the RegisterWindowsService method when it first starts and then add information about the service and the computer name to a central database. The monitoring utility can call GetRegisteredWindowsServices to retrieve the full list of running services. A similar approach is developed in Chapter 11 with remote components. Using a Windows Service for a Component HostNow that you have a firm grasp of Windows service programming and how to interact with the SCM, you're ready to adapt the design to a component host. The code in Listing 7-14 is quite simple: The idea is that you start listening for client connections when OnStart is triggered, and you stop listening when OnStop is executed. Listing 7-14 A Windows service component hostImports System.ServiceProcess Imports System.Runtime.Remoting Imports System.Runtime.Remoting.Channels Public Class DBComponentHost Inherits System.ServiceProcess.ServiceBase Public Sub New() MyBase.New() InitializeComponent() End Sub <MTAThread()> _ Shared Sub Main() ServiceBase.Run(New TestService()) End Sub Private Sub InitializeComponent() Me.ServiceName = "DBComponentHost" End Sub ' Register the service. Protected Overrides Sub OnStart(ByVal args() As String) RemotingConfiguration.Configure("SimpleServer.exe.config") End Sub ' Remove all the listening channels. Protected Overrides Sub OnStop() Dim Channel As IChannel For Each Channel In ChannelServices.RegisteredChannels Try ChannelServices.UnregisterChannel(Channel) Catch ' Ignore any channel errors. End Try Next End Sub End Class |