原文地址: http://visualstudiomagazine.com/Articles/2005/10/01/Write-a-Better-Windows-Service.aspx?Page=1
Writing a Windows service is significantly more involved than many
authors would have you believe. Here are the tools you need to create a
Windows service robust enough for the real world.
- 源码:
Most programmers know that you typically implement Windows services as executable programs. What many developers don‘t realize is that a single executable can contain more than one service. For example, the standard Windows intetinfo.exe process hosts several services, including IIS Admin, World Wide Web Publishing, and Simple Mail Transfer Protocol (SMTP).
All services hosted by one executable and configured to use the same Log On account run in the same process. The first service to start launches the executable, and the last service to stop causes the process to exit. If you configure the services using different Log On properties—say, service X runs as John, service Y runs as Mary, and both services are implemented in the same executable—you end up starting a separate process for each user identity of the launched services.
A single process can contain more than one Windows service, and each service can perform several tasks (see Figure 1). For example, an online shop can use a service to send notifications about shipped products, payment collections, and weekly promotions. You can implement these operations as different services, or you can run them as different threads of the same service. A common approach is to implement logically related tasks as multiple threads of a single service and non-related tasks as different services.
If you read a typical article explaining how to write a Windows service in C# or Visual Basic .NET, you get the impression that building Windows services is easy. Simply pick the Windows Service project template, follow the instructions provided in the MSDN documentation or online tutorials, add code to implement the application logic, and voilà, the service is ready. Unfortunately, many developers discover only after the fact that the Windows services they create using this approach are hard to manage.
For one, you can‘t debug your service by pressing the F5 key from the Visual Studio .NET IDE. After you figure out how to launch the service executable from command line, debug messages won‘t appear if you try to display them by calling Console.Write or MessageBox.Show. You also get an error if you install the service using a Windows installer (MSI) package and then attempt to deploy a hot fix by executing an updated MSI file in repair mode. Finally, if your service performs multiple tasks running at scheduled times or timed intervals, you need to design all aspects of your solution, including the timing and multithreading. Faced with these issues, many developers turn to the Web and newsgroups to slog through the issues one-by-one. That brute-force method can work, after a fashion, but a far better solution is to avoid all these problems in the first place.
This article comes with two code samples written in C#. The first sample contains a project to build a library (My.Utilities.dll), which provides classes for implementing easy-to-use Windows services. The SampleService project illustrates how you can incorporate this library in a project that implements several Windows services, each performing multiple tasks. There is also a sample setup project (SampleSetup), which shows how to implement the Windows service installer.
The My.Utilities library code sample that accompanies this article includes several classes for building Windows service hosts, services, and threads (see Table 1). All classes related to Windows services belong to the My.Utilities.Services namespace.
Implement Service Processes
You can use the WindowsService class, which extends ServiceBase, to implement service processes and define properties of Windows services. When you derive a service process from WindowsService, you can debug it directly from the Visual Studio .NET IDE. It lets you execute the process from command line, display debug messages, and perform self-installation. This class has several helpful members, but you need to be aware of only a few of them.
The static IsInteractive property is a simple wrapper for the obscure UserInteractive member of the Framework‘s System.Environment class. The service process can use it to determine whether it was launched by Service Control Manager (SCM) or an interactive user (for example, during debugging).
The overloaded Run method uses different mechanisms to execute code depending on the value of the IsInteractive property. If the value is false, it simply calls the Run method of the ServiceBase class; otherwise, it calls the Start method of each hosted service (see Listing 1).
When called from ServiceBase-derived classes, the .NET Framework‘s Console.Write and MessageBox.Show do not display any output. You can display debug messages during interactive execution by using the static ShowMessageBox method, which I implemented by reverse-engineering the private LateBoundMessageBoxShow method of the ServiceBase class.
Be sure to add your code to the Start and Stop methods if you implement the main operations in a class derived from WindowsService rather than using the OnStart and OnStop event handlers. It‘s even better to isolate the business logic in the dedicated worker threads.
A WindowsService object keeps track of the worker threads assigned to it through the WorkerThreads property, which contains an array of objects derived from the WorkerThread class. In addition to several helper members, WorkerThread declares two abstract methods: Start and Stop. When you launch a Windows service, it iterates through the worker threads and invokes the Start method on each of them. Similarly, it calls the Stop method after receiving a signal to stop (see Listing 2).
You derive your own worker thread class from WorkerThread by adding the initialization and business logic to the Start routine and using the Stop method to implement the clean-up procedure. WorkerThread doesn‘t offer much functionality, but several classes derived from it do. These classes simplify the implementation of the timer-based operations.
If your Windows service performs operations at scheduled times or timed intervals, you can base them on TimerThread, DailyThread, or WeeklyThread (see Figure 2). The TimerThread class executes the Run method at timed intervals defined through the Interval property. If the execution time of the Run method exceeds the specified interval, the thread keeps skipping the scheduled execution and waiting for the next interval until the Run method completes.
If your service contains several threads executing at the same or close intervals, you might not want to fire all of them at the same time. You can make sure that the threads start at different times by setting their initial Delay properties to different values.
Execute Daily Tasks
You execute DailyThread and WeeklyThread once per day. DailyThread and WeeklyThread use the execution time of the day defined in the DailyExecutionTime property. The DailyExecutionTime property accepts a string value in the 24-hour format, such as "18:30." By default, the worker thread classes use GMT (UTC) for time-related operations, but you can change it by setting the value of the UseLocalTime property.
DailyThread assumes that you must perform the given task every day, while WeeklyThread can run on the specified days of the week. For example, your service might need to send notifications to company employees on Wednesdays or workdays only. Use the DaysOfExecution property to specify the days you want to perform an operation. This property takes a bitmask value defined in the DaysOfWeek enumerated type.
You don‘t typically need to follow a rigid execution schedule for classes derived from TimerThread, but it‘s critical for daily and weekly operations to be performed once and only once per day. If a service that sends daily notifications at 6 a.m. was down from 5:55 a.m. to 6:05 a.m., you would probably want to send the notification as soon as the service starts at 6:06 a.m.. On the other hand, if the service was down from 5:55 a.m. to 5:50 a.m. of the next day, it might make sense to wait for the next execution at 6 a.m. instead of sending two notifications within five minutes.
When a daily (or weekly) thread starts, it checks the current time and compares it with the scheduled execution time using the 24-hour clock. If the execution time is in the past (for example, execution time is 6 a.m. and current time is 7 a.m.), the execution is scheduled for the next day. In the meantime, the thread keeps waking up after the interval defined in the WakeUpInterval property to check whether it must call the Run method. The thread executes the Run method after it reaches the scheduled execution time or if it detects that the last operation was performed more than a day ago and the next execution is not scheduled within the next 12 hours. Note that the WeeklyThread class also takes into account days of the week the task should execute. The thread resets the LastExecutionTime property after executing the Run method successfully.
There is one problem with this approach. If the service stops (say the server crashes), you lose the information about the last execution time. You can preserve the last execution time in a persistent location (such as a database table or text file) by overriding the SetLastExecutionTime and GetLastExecutionTime methods.
Build a Windows Service
Building a Windows service requires several steps. Begin by creating a new project using the Windows Service template, then add a reference to the utilities library (My.Utilities.dll). Next, include a reference to the My.Utilities.Services namespace in your source code files (where needed), and change the base class for your Windows service process from ServiceBase to WindowsService. These steps are all straightforward.
The next few steps get a little trickier, so I‘ll illustrate them using a sample Windows service. You implement the operation performed by your services in the worker thread classes derived from WorkerThread. If you need to perform an operation at scheduled times or at timed intervals, simply derive the worker thread from TimerThread, DailyThread, or WeeklyThread, and override the Run method.
The SampleService project performs three tasks executed by two Windows services. The first service sends an e-mail notification to the users whose passwords are about to expire once per day, except during the weekend. The second service queries an external data source every two minutes and copies any new users it finds to the local database. It also applies updates to the existing users stored in the local database every five minutes.
You can view these tasks as three distinct operations, so the solution splits them into separate worker thread classes: PasswordCheckThread (derived from WeeklyThread), NewUserThread, and UserUpdateThread. You derive the latter two worker threads from TimerThread.
The worker thread classes implement the business logic of the designated operations by overriding the Run methods. Note that I kept the code simple by making the Run methods in the sample classes write only informational messages to the application log file using a static helper method of the service host. The sample illustrates a case when an operation takes longer to complete than expected by forcing NewUserThread and UserUpdateThread to sleep at random intervals. Note that the worker threads don‘t contain any initialization logic or data, such as execution frequency or initial delay. The server process performs these tasks.
The Main function of the service process handles initialization and invocation of the hosted services and worker threads. Implementing the service process takes only a few steps. Create a Windows Service project, then rename the wizard-generated Service1 class (and file) to something more meaningful, such as ServiceHost. Next, change the Main function definition to include the command-line parameters (you can also delete the function contents):
1 using My.Utilities.Services; 2 ... 3 // Derive service host from WindowsService 4 // instead of ServiceBase. 5 public class ServiceHost: 6 WindowsService 7 { 8 ... 9 private static void Main 10 ( 11 string[] args 12 ) 13 { 14 } 15 }
In Main, create objects for each worker thread that you have implemented already and initialize their runtime properties:
1 NewUserThread newUserThread = 2 new NewUserThread(); 3 4 newUserThread.Name = "New User Check"; 5 newUserThread.Delay= 0; 6 newUserThread.Interval = 2 * 60 * 1000;
You can also pass initialization parameters through overloaded constructors, but you need to implement those as well. You might want to store these values in a configuration file in a real application.
Create the Service Objects
After you initialize the worker threads, create the Windows service objects and define their properties. At a minimum, you need to specify the service name and assign the worker threads to it:
1 WindowsService userCheckService = 2 new WindowsService(); 3 userCheckService.ServiceName = 4 "Test Service: User Check"; 5 6 userCheckService.WorkerThreads = 7 new WorkerThread[] 8 { 9 newUserThread, 10 userUpdateThread 11 };
Finally, create an array of the ServiceBase objects holding the
initialized services and execute the overloaded Run method passing the
array and optional command-line arguments to it:
1 ServiceBase[] services = 2 new ServiceBase[] 3 { 4 userCheckService, 5 emailService 6 }; 7 Run(services, args);
The Run method implemented by WindowsService lets you execute the service manually. Note that this method is different from the one implemented in the ServiceBase class, which takes one parameter instead of two.
At this point, you must be able to debug your services or execute them from the command prompt. For testing purposes, set breakpoints or add code to display a debug message using WindowsService.ShowMessageBox in the Run methods of the worker threads:
1 override protected void Run 2 ( 3 object source 4 ) 5 { 6 WindowsService.ShowMessageBox( 7 "Executing {0}", 8 Name); 9 ... 10 }
Add the installer class to the service project to install and uninstall your service. Rename the class and file to something meaningful such as WindowsServiceInstaller, and make sure that you derive it from System.Configuration.Install.Installer. The class also needs the RunInstaller attribute:
1 using System.Configuration.Install; 2 using System.ServiceProcess; 3 ... 4 [RunInstaller(true)] 5 public class WindowsServiceInstaller : 6 Installer 7 { 8 ... 9 }
Use the class constructor to implement the initialization logic (see Listing 3). I don‘t like the fact that this code hard-codes the name and display name of the services because you also use the names of the services in the Main function of the service host. Remember to change the string literals in several places if you ever decide to rename a service; otherwise, the service will fail. You can avoid this problem by defining and referencing the names of the services using static properties:
1 public class ServiceHost: WindowsService 2 { 3 internal static string 4 UserCheckServiceName = 5 "Sample Service: User Check"; 6 7 internal static string 8 EmailServiceName = 9 "Sample Service: Email Notifier"; 10 ... 11 }
The sample code uses identical values for both the service name and its display name, but they don‘t have to be the same. You must also be aware that the Account property of a ServiceInstaller object that is assigned the value of ServiceAccount.User (instead of ServiceAccount.LocalSystem) will cause the setup process to fail if the user running the installer mistypes the account name or password.
Create the Setup File
You can install the services after you implement the installer class. A typical approach is to use the .NET Framework‘s Installer Tool (InstallUtil.exe). I‘ll skip this option because it‘s been covered extensively elsewhere. Instead, I‘ll describe two alternatives: incorporating the installer in an MSI package and creating a self-installer.
You can enable the MSI package to install your services by defining a custom action and associating it with the service executable. If you use the Setup Project template in Visual Studio .NET, simply switch to the Custom Actions view, and add a new custom action to the Install, Commit, Rollback, and Uninstall events, associating them with the service executable (see Figure 3). The Services Control Panel displays your services after you build the MSI file and run the setup.
You still need to address one important problem. If you fix a bug in the service code and want to deploy it by executing the modified setup package in the repair mode, the setup fails. You need to reinstall the application to deploy the fix. You can use one of several alternatives if this becomes a hassle (and it will).
One option is to add code to the service installer constructor to detect whether the relevant services are installed already (you can use helper methods exposed by the WindowsService class for this). If the services are installed, the logic won‘t create the corresponding installer objects, effectively making it a no-op. You can achieve the same effect by setting the Condition field of the custom action associated with the Install event to NOT REINSTALL (case-sensitive) (see Figure 4).
You can enable self-installation in a Windows service host class derived from WindowsService by adding this block of code at the beginning of the Main method (the first parameter of the InstallOrUninstall method expects the command-line switches; the second parameter must contain an instance of your service installer class):
1 public class ServiceHost: 2 WinodwsService 3 { 4 ... 5 private static void Main 6 ( 7 string[] args 8 ) 9 { 10 if (InstallOrUninstall(args, 11 new WindowsServiceInstaller())) 12 { 13 return; 14 } 15 ... 16 } 17 }
Inserting this code and executing the program from command prompt with the /i switch installs the services, while the /u switch uninstalls them.
I‘ve documented the source code extensively, so take a look at the code comments if you want to learn how the functionality described in this article is implemented in the utilities library. You can also check the provided help file located under the library project folder; it describes several features not mentioned in this article.