Professional C# 6 and .NET Core 1.0 - Chapter 39 Windows Services
本文内容为转载,供学习研究。如有侵权,请联系作者删除。
转载请注明本文出处:Professional C# 6 and .NET Core 1.0 - Chapter 39 Windows Services
-----------------------------------------------------------------------
What’s In This Chapter?
The architecture of a Windows Service
Creating a Windows Service program
Windows Services installation programs
Windows Services control programs
Troubleshooting Windows Services
Wrox.com Code Downloads for This Chapter
The wrox.com code downloads for this chapter are found at www.wrox.com/go/professionalcsharp6 on the Download Code tab. The code is in the Chapter 39 download and individually named according to the names throughout the chapter.
Quote Server
Quote Client
Quote Service
Service Control
What Is a Windows Service?
Windows Services are programs that can be started automatically at boot time without the need for anyone to log on to the machine. If you need to have programs start up without user interaction or need to run under a different user than the interactive user, which can be a user with more privileges, you can create a Windows Service. Some examples could be a WCF host (if you can’t use Internet Information Services (IIS) for some reason), a program that caches data from a network server, or a program that reorganizes local disk data in the background.
This chapter starts with looking at the architecture of Windows Services, creates a Windows Service that hosts a networking server, and gives you information to start, monitor, control, and troubleshoot your Windows Services.
As previously mentioned, Windows Services are applications that can be automatically started when the operating system boots. These applications can run without having an interactive user logged on to the system and can do some processing in the background.
For example, on a Windows Server, system networking services should be accessible from the client without a user logging on to the server; and on the client system, services enable you to do things such as get a new software version online or perform some file cleanup on the local disk.
You can configure a Windows Service to run from a specially configured user account or from the system user account—a user account that has even more privileges than that of the system administrator.
NOTE Unless otherwise noted, when I refer to a service, I am referring to a Windows Service.
Here are a few examples of services:
- Simple TCP/IP Services is a service program that hosts some small TCP/IP servers: echo, daytime, quote, and others.
- World Wide Web Publishing Service is a service of IIS.
- Event Log is a service to log messages to the event log system.
- Windows Search is a service that creates indexes of data on the disk.
- Superfetch is a service that preloads commonly used applications and libraries into memory, thus improving the startup time of these applications.
You can use the Services administration tool, shown in Figure 39.1, to see all the services on a system. You get to the program by entering Services on the Start screen.
Figure 39.1
NOTE You can’t create a Windows Service with .NET Core; you need the .NET Framework. To control services, you can use .NET Core.
Windows Services Architecture
Three program types are necessary to operate a Windows Service:
- A service program
- A service control program
- A service configuration program
The service program is the implementation of the service. With a service control program, it is possible to send control requests to a service, such as start, stop, pause, and continue. With a service configuration program, a service can be installed; it is copied to the file system, and information about the service needs to be written to the registry. This registry information is used by the service control manager (SCM) to start and stop the service. Although .NET components can be installed simply with an xcopy—because they don’t need to write information to the registry—installation for services requires registry configuration. You can also use a service configuration program to change the configuration of that service at a later point. These three ingredients of a Windows Service are discussed in the following subsections.
Service Program
In order to put the .NET implementation of a service in perspective, this section takes a brief look at the Windows architecture of services in general, and the inner functionality of a service.
The service program implements the functionality of the service. It needs three parts:
- A main function
- A service-main function
- A handler
Before discussing these parts, however, it would be useful to digress for a moment for a short introduction to the SCM, which plays an important role for services—sending requests to your service to start it and stop it.
Service Control Manager
The SCM is the part of the operating system that communicates with the service. Using a sequence diagram, Figure 39.2 illustrates how this communication works.
Figure 39.2
At boot time, each process for which a service is set to start automatically is started, and so the main function of this process is called. The service is responsible for registering the service-main function for each of its services. The main function is the entry point of the service program, and in this function the entry points for the service-main functions must be registered with the SCM.
Main Function, Service-Main, and Handlers
The main function of the service is the normal entry point of a program, the Main method. The main function of the service might register more than one service- main function. The service-main function contains the actual functionality of the service, which must register a service-main function for each service it provides. A service program can provide a lot of services in a single program; for example, <windows>\system32\services.exe is the service program that includes Alerter, Application Management, Computer Browser, and DHCP Client, among other items.
The SCM calls the service-main function for each service that should be started. One important task of the service-main function is registering a handler with the SCM.
The handler function is the third part of a service program. The handler must respond to events from the SCM. Services can be stopped, suspended, and resumed, and the handler must react to these events.
After a handler has been registered with the SCM, the service control program can post requests to the SCM to stop, suspend, and resume the service. The service control program is independent of the SCM and the service itself. The operating system contains many service control programs, such as the Microsoft Management Console (MMC) Services snap-in shown earlier in Figure 39.1. You can also write your own service control program; a good example of this is the SQL Server Configuration Manager shown in Figure 39.3 which runs within MMC.
Figure 39.3
Service Control Program
As the self-explanatory name suggests, with a service control program you can stop, suspend, and resume the service. To do so, you can send control codes to the service, and the handler should react to these events. It is also possible to ask the service about its actual status (if the service is running or suspended, or in some faulted state) and to implement a custom handler that responds to custom control codes.
Service Configuration Program
Because services must be configured in the registry, you can’t use xcopy installation with services. The registry contains the startup type of the service, which can be set to automatic, manual, or disabled. You also need to configure the user of the service program and dependencies of the service—for example, any services that must be started before the current one can start. All these configurations are made within a service configuration program. The installation program can use the service configuration program to configure the service, but this program can also be used later to change service configuration parameters.
Classes for Windows Services
In the .NET Framework, you can find service classes in the System.ServiceProcess namespace that implement the three parts of a service:
- You must inherit from the ServiceBase class to implement a service. The ServiceBase class is used to register the service and to answer start and stop requests.
- The ServiceController class is used to implement a service control program. With this class, you can send requests to services.
- The ServiceProcessInstaller and ServiceInstaller classes are, as their names suggest, classes to install and configure service programs.
Now you are ready to create a new service.
Creating a Windows Service Program
The service that you create in this chapter hosts a quote server. With every request that is made from a client, the quote server returns a random quote from a quote file. The first part of the solution uses three assemblies: one for the client and two for the server. Figure 39.4 provides an overview of the solution. The assembly QuoteServer holds the actual functionality. The service reads the quote file in a memory cache and answers requests for quotes with the help of a socket server. The QuoteClient is a WPF rich–client application. This application creates a client socket to communicate with the QuoteServer. The third assembly is the actual service. The QuoteService starts and stops the QuoteServer; the service controls the server.
Figure 39.4
Before creating the service part of your program, create a simple socket server in an extra C# class library that will be used from your service process. How this can be done is discussed in the following section.
Creating Core Functionality for the Service
You can build any functionality in a Windows Service, such as scanning for files to do a backup or a virus check or starting a WCF server. However, all service programs share some similarities. The program must be able to start (and to return to the caller), stop, and suspend. This section looks at such an implementation using a socket server.
With Windows 10, the Simple TCP/IP Services can be installed as part of the Windows components. Part of the Simple TCP/IP Services is a “quote of the day,” or qotd, TCP/IP server. This simple service listens to port 17 and answers every request with a random message from the file <windows>\system32\drivers\etc\quotes. With the sample service, a similar server will be built. The sample server returns a Unicode string, in contrast to the qotd server, which returns an ASCII string.
First, create a class library called QuoteServer and implement the code for the server. The following walks through the source code of your QuoteServer class in the file QuoteServer.cs: (code file QuoteServer/QuoteServer.cs):
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace Wrox.ProCSharp.WinServices { public class QuoteServer { private TcpListener _listener; private int _port; private string _filename; private List<string> _quotes; private Random _random; private Task _listenerTask;
The constructor QuoteServer is overloaded so that a filename and a port can be passed to the call. The constructor where just the filename is passed uses the default port 7890 for the server. The default constructor defines the default filename for the quotes as quotes.txt:
public QuoteServer() : this ("quotes.txt") { } public QuoteServer(string filename) : this (filename, 7890) { } public QuoteServer(string filename, int port) { if (filename == null) throw new ArgumentNullException(nameof(filename)); if (port < IPEndPoint.MinPort || port > IPEndPoint.MaxPort) throw new ArgumentException("port not valid", nameof(port)); _filename = filename; _port = port; }
ReadQuotes is a helper method that reads all the quotes from a file that was specified in the constructor. All the quotes are added to the List<string> quotes. In addition, you are creating an instance of the Random class that will be used to return random quotes:
protected void ReadQuotes() { try { _quotes = File.ReadAllLines(filename).ToList(); if (_quotes.Count == 0) { throw new QuoteException("quotes file is empty"); } _random = new Random(); } catch (IOException ex) { throw new QuoteException("I/O Error", ex); } }
Another helper method is GetRandomQuoteOfTheDay. This method returns a random quote from the quotes collection:
protected string GetRandomQuoteOfTheDay() { int index = random.Next(0, _quotes.Count); return _quotes[index]; }
In the Start method, the complete file containing the quotes is read in the List<string> quotes by using the helper method ReadQuotes. After this, a new thread is started, which immediately calls the Listener method—similarly to the TcpReceive example in Chapter 25, “Networking.”
Here, a task is used because the Start method cannot block and wait for a client; it must return immediately to the caller (SCM). The SCM would assume that the start failed if the method didn’t return to the caller in a timely fashion (30 seconds). The listener task is a long-running background thread. The application can exit without stopping this thread:
public void Start() { ReadQuotes(); _listenerTask = Task.Factory.StartNew(Listener, TaskCreationOptions.LongRunning); }
The task function Listener creates a TcpListener instance. The AcceptSocketAsync method waits for a client to connect. As soon as a client connects, AcceptSocketAsync returns with a socket associated with the client. Next, GetRandomQuoteOfTheDay is called to send the returned random quote to the client using clientSocket.Send:
protected async Task ListenerAsync() { try { IPAddress ipAddress = IPAddress.Any; _listener = new TcpListener(ipAddress, port); _listener.Start(); while (true) { using (Socket clientSocket = await _listener.AcceptSocketAsync()) { string message = GetRandomQuoteOfTheDay(); var encoder = new UnicodeEncoding(); byte[] buffer = encoder.GetBytes(message); clientSocket.Send(buffer, buffer.Length, 0); } } } catch (SocketException ex) { Trace.TraceError($"QuoteServer {ex.Message}"); throw new QuoteException("socket error", ex); } }
In addition to the Start method, the following methods, Stop, Suspend, and Resume, are needed to control the service:
public void Stop() => _listener.Stop(); public void Suspend() => _listener.Stop(); public void Resume() => Start();
Another method that will be publicly available is RefreshQuotes. If the file containing the quotes changes, the file is reread with this method:
public void RefreshQuotes() => ReadQuotes(); } }
Before you build a service around the server, it is useful to build a test program that creates just an instance of the QuoteServer and calls Start. This way, you can test the functionality without having to handle service-specific issues. You must start this test server manually, and you can easily walk through the code with a debugger.
The test program is a C# console application, TestQuoteServer. You need to reference the assembly of the QuoteServer class. After you create an instance of the QuoteServer, the Start method of the QuoteServer instance is called. Start returns immediately after creating a thread, so the console application keeps running until Return is pressed (code file TestQuoteServer/Program.cs):
static void Main() { var qs = new QuoteServer("quotes.txt", 4567); qs.Start(); WriteLine("Hit return to exit"); ReadLine(); qs.Stop(); }
Note that QuoteServer will be running on port 4567 on localhost using this program—you have to use these settings in the client later.
QuoteClient Example
The client is a simple WPF Windows application in which you can request quotes from the server. This application uses the TcpClient class to connect to the running server and receives the returned message, displaying it in a text box. The user interface contains two controls: a Button and a TextBlock. Clicking the button requests the quote from the server, and the quote is displayed.
With the Button control, the Click event is assigned to the method OnGetQuote, which requests the quote from the server, and the IsEnabled property is bound to the EnableRequest method to disable the button while a request is active. With the TextBlock control, the Text property is bound to the Quote property to display the quote that is set (code file QuoteClientWPF/MainWindow.xaml):
<Button Margin="3" VerticalAlignment="Stretch" Grid.Row="0" IsEnabled="{Binding EnableRequest, Mode=OneWay}" Click="OnGetQuote"> Get Quote</Button> <TextBlock Margin="6" Grid.Row="1" TextWrapping="Wrap" Text="{Binding Quote, Mode=OneWay}" />
The class QuoteInformation defines the properties EnableRequest and Quote. These properties are used with data binding to show the values of these properties in the user interface. This class implements the interface InotifyPropertyChanged to enable WPF to receive changes in the property values (code file QuoteClientWPF/QuoteInformation.cs):
using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; namespace Wrox.ProCSharp.WinServices { public class QuoteInformation: INotifyPropertyChanged { public QuoteInformation() { EnableRequest = true; } private string _quote; public string Quote { get { return _quote; } internal set { SetProperty(ref _quote, value); } } private bool _enableRequest; public bool EnableRequest { get { return _enableRequest; } internal set { SetProperty(ref _enableRequest, value); } } private void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null) { if (!EqualityComparer<T>.Default.Equals(field, value)) { field = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } public event PropertyChangedEventHandler PropertyChanged; } }
NOTE Implementation of the interface INotifyPropertyChanged makes use of the attribute CallerMemberNameAttribute. This attribute is explained in Chapter 14, “Errors and Exceptions.”
An instance of the class QuoteInformation is assigned to the DataContext of the Window class MainWindow to allow direct data binding to it (code file QuoteClientWPF/MainWindow.xaml.cs):
using System; using System.Net.Sockets; using System.Text; using System.Windows; using System.Windows.Input; namespace Wrox.ProCSharp.WinServices { public partial class MainWindow: Window { private QuoteInformation _quoteInfo = new QuoteInformation(); public MainWindow() { InitializeComponent(); this.DataContext = _quoteInfo; }
You can configure server and port information to connect to the server from the Settings tab inside the properties of the project (see Figure 39.5). Here, you can define default values for the ServerName and PortNumber settings. With the Scope set to User, the settings can be placed in user-specific configuration files, so every user of the application can have different settings. This Settings feature of Visual Studio also creates a Settings class so that the settings can be read and written with a strongly typed class.
Figure 39.5
The major functionality of the client lies in the handler for the Click event of the Get Quote button:
protected async void OnGetQuote(object sender, RoutedEventArgs e) { const int bufferSize = 1024; Cursor currentCursor = this.Cursor; this.Cursor = Cursors.Wait; quoteInfo.EnableRequest = false; string serverName = Properties.Settings.Default.ServerName; int port = Properties.Settings.Default.PortNumber; var client = new TcpClient(); NetworkStream stream = null; try { await client.ConnectAsync(serverName, port); stream = client.GetStream(); byte[] buffer = new byte[bufferSize]; int received = await stream.ReadAsync(buffer, 0, bufferSize); if (received <= 0) { return; } quoteInfo.Quote = Encoding.Unicode.GetString(buffer).Trim('\0'); } catch (SocketException ex) { MessageBox.Show(ex.Message,"Error Quote of the day", MessageBoxButton.OK, MessageBoxImage.Error); } finally { stream?.Close(); if (client.Connected) { client.Close(); } } this.Cursor = currentCursor; quoteInfo.EnableRequest = true; }
After starting the test server and this Windows application client, you can test the functionality. Figure 39.6 shows a successful run of this application.
Figure 39.6
At this point, you need to implement the service functionality in the server. The program is already running, so now you want to ensure that the server program starts automatically at boot time without anyone logged on to the system. You can do that by creating a service program, which is discussed next.
Windows Service Program
Using the C# Windows Service template from the Add New Project dialog, you can now create a Windows Service program. For the new service, use the name QuoteService.
After you click the OK button to create the Windows Service program, the designer surface appears but you can’t insert any UI components because the application cannot directly display anything on the screen. The designer surface is used later in this chapter to add components such as installation objects, performance counters, and event logging.
Selecting the properties of this service opens the Properties dialog, where you can configure the following values:
- AutoLog—Specifies that events are automatically written to the event log for starting and stopping the service.
- CanPauseAndContinue, CanShutdown, and CanStop—Specify pause, continue, shut down, and stop requests.
- ServiceName—The name of the service written to the registry and used to control the service.
- CanHandleSessionChangeEvent—Defines whether the service can handle change events from a terminal server session.
- CanHandlePowerEvent—This is a very useful option for services running on a laptop or mobile devices. If this option is enabled, the service can react to low-power events and change the behavior of the service accordingly. Examples of power events include battery low, power status change (because of a switch from or to A/C power), and change to suspend.
NOTE The default service name is Service1, regardless of what the project is called. You can install only one Service1 service. If you get installation errors during your testing process, you might already have installed a Service1 service. Therefore, ensure that you change the name of the service in the Properties dialog to a more suitable name at the beginning of the service’s development.
Changing these properties within the Properties dialog sets the values of your ServiceBase-derived class in the InitializeComponent method. You already know this method from Windows Forms applications. It is used in a similar way with services.
A wizard generates the code but changes the filename to QuoteService.cs, the name of the namespace to Wrox.ProCSharp.WinServices, and the class name to QuoteService. The code of the service is discussed in detail shortly.
The ServiceBase Class
The ServiceBase class is the base class for all Windows Services developed with the .NET Framework. The class QuoteService is derived from ServiceBase; this class communicates with the SCM using an undocumented helper class, System.ServiceProcess.NativeMethods, which is just a wrapper class to the Windows API calls. The NativeMethods class is internal, so it cannot be used in your code.
The sequence diagram in Figure 39.7 shows the interaction of the SCM, the class QuoteService, and the classes from the System.ServiceProcess namespace. You can see the lifelines of objects vertically and the communication going on horizontally. The communication is time-ordered from top to bottom.
Figure 39.7
The SCM starts the process of a service that should be started. At startup, the Main method is called. In the Main method of the sample service, the Run method of the base class ServiceBase is called. Run registers the method ServiceMainCallback using NativeMethods.StartServiceCtrlDispatcher in the SCM and writes an entry to the event log.
Next, the SCM calls the registered method ServiceMainCallback in the service program. ServiceMainCallback itself registers the handler in the SCM using NativeMethods.RegisterServiceCtrlHandler[Ex] and sets the status of the service in the SCM. Then the OnStart method is called. In OnStart, you need to implement the startup code. If OnStart is successful, the string “Service started successfully” is written to the event log.
The handler is implemented in the ServiceCommandCallback method. The SCM calls this method when changes are requested from the service. The ServiceCommandCallback method routes the requests further to OnPause, OnContinue, OnStop, OnCustomCommand, and OnPowerEvent.
Main Function
This section looks into the application template–generated main function of the
service process. In the main function, an array of ServiceBase classes, ServicesToRun, is declared. One instance of the QuoteService class is created and passed as the first element to the ServicesToRun array. If more than one service should run inside this service process, it is necessary to add more instances of the specific service classes to the array. This array is then passed to the static Run method of the ServiceBase class. With the Run method of ServiceBase, you are giving the SCM references to the entry points of your services. The main thread of your service process is now blocked and waits for the service to terminate.
Here is the automatically generated code (code file QuoteService/Program.cs):
static void Main() { ServiceBase[] servicesToRun = new ServiceBase[] { new QuoteService() }; ServiceBase.Run(servicesToRun); }
If there is only a single service in the process, the array can be removed; the Run method accepts a single object derived from the class ServiceBase, so the Main method can be reduced to this:
ServiceBase.Run(new QuoteService());
The service program Services.exe includes multiple services. If you have a similar service, where more than one service is running in a single process in which you must initialize some shared state for multiple services, the shared initialization must be done before the Run method. With the Run method, the main thread is blocked until the service process is stopped, and any subsequent instructions are not reached before the end of the service.
The initialization shouldn’t take longer than 30 seconds. If the initialization code were to take longer than this, the SCM would assume that the service startup failed. You need to take into account the slowest machines where this service should run within the 30-second limit. If the initialization takes longer, you could start the initialization in a different thread so that the main thread calls Run in time. An event object can then be used to signal that the thread has completed its work.
Service Start
At service start, the OnStart method is called. In this method, you can start the previously created socket server. You must reference the QuoteServer assembly for the use of the QuoteService. The thread calling OnStart cannot be blocked; this method must return to the caller, which is the ServiceMainCallback method of the ServiceBase class. The ServiceBase class registers the handler and informs the
SCM that the service started successfully after calling OnStart (code file QuoteService/QuoteService.cs):
protected override void OnStart(string[] args) { _quoteServer = new QuoteServer(Path.Combine( AppDomain.CurrentDomain.BaseDirectory,"quotes.txt"), 5678); _quoteServer.Start(); }
The _quoteServer variable is declared as a private member in the class:
namespace Wrox.ProCSharp.WinServices { public partial class QuoteService: ServiceBase { private QuoteServer _quoteServer;
Handler Methods
When the service is stopped, the OnStop method is called. You should stop the service functionality in this method (code file QuoteService/QuoteService.cs):
protected override void OnStop() => _quoteServer.Stop();
In addition to OnStart and OnStop, you can override the following handlers in the service class:
- OnPause—Called when the service should be paused.
- OnContinue—Called when the service should return to normal operation after being paused. To make it possible for the overridden methods OnPause and OnContinue to be called, you must set the CanPauseAndContinue property to true.
- OnShutdown—Called when Windows is undergoing system shutdown. Normally, the behavior of this method should be similar to the OnStop implementation; if more time is needed for a shutdown, you can request more. Similarly to OnPause and OnContinue, a property must be set to enable this behavior: CanShutdown must be set to true.
- OnPowerEvent—Called when the power status of the system changes. Information about the change of the power status is in the argument of type PowerBroadcastStatus. PowerBroadcastStatus is an enumeration with values such as Battery Low and PowerStatusChange. Here, you will also get information if the system would like to suspend (QuerySuspend), which you can approve or deny. You can read more about power events later in this chapter.
- OnCustomCommand—This is a handler that can serve custom commands sent by a service control program. The method signature of OnCustomCommand has an int argument where you retrieve the custom command number. The value can be in the range from 128 to 256; values below 128 are system-reserved values. In your service, you are rereading the quotes file with the custom command 128:
protected override void OnPause() => _quoteServer.Suspend(); protected override void OnContinue() => _quoteServer.Resume(); public const int CommandRefresh = 128; protected override void OnCustomCommand(int command) { switch (command) { case CommandRefresh: quoteServer.RefreshQuotes(); break; default: break; } }
Threading and Services
As stated earlier in this chapter, the SCM assumes that the service failed if the initialization takes too long. To deal with this, you need to create a thread.
The OnStart method in your service class must return in time. If you call a blocking method such as AcceptSocket from the TcpListener class, you need to start a thread to do so. With a networking server that deals with multiple clients, a thread pool is also very useful. AcceptSocket should receive the call and hand the processing off to another thread from the pool. This way, no one waits for the execution of code and the system seems responsive.
Service Installation
Services must be configured in the registry. All services are found in HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services. You can view the registry entries by using regedit. Found here are the type of the service, the display name, the path to the executable, the startup configuration, and so on. Figure 39.8 shows the registry configuration of the W3SVC service.
Figure 39.8
You can do this configuration by using the installer classes from the System.ServiceProcess namespace, as discussed in the following section.
Installation Program
You can add an installation program to the service by switching to the design view with Visual Studio and then selecting the Add Installer option from the context menu . With this option, a new ProjectInstaller class is created, along with a ServiceInstaller instance and a ServiceProcessInstaller instance.
Figure 39.9 shows the class diagram of the installer classes for services.
Keep this diagram in mind as we go through the source code in the file ProjectInstaller.cs that was created with the Add Installer option.
The Installer Class
The class ProjectInstaller is derived from System.Configuration.Install.Installer. This is the base class for all custom installers. With the Installer class, it is possible to build transaction-based installations. With a transaction-based installation, you can roll back to the previous state if the installation fails, and any changes made by this installation up to that point will be undone. As shown in Figure 39.9, the Installer class has Install, Uninstall, Commit, and Rollback methods, and they are called from installation programs.
Figure 39.9
The attribute [RunInstaller(true)] means that the class ProjectInstaller should be invoked when installing an assembly. Custom action installers, as well as installutil.exe (which is used later in this chapter), check for this attribute.
InitializeComponent is called inside the constructor of the ProjectInstaller class (code file QuoteService/ProjectInstaller.cs):
using System.ComponentModel; using System.Configuration.Install; namespace Wrox.ProCSharp.WinServices { [RunInstaller(true)] public partial class ProjectInstaller: Installer { public ProjectInstaller() { InitializeComponent(); } } }
Now let’s move to the other installers of the installation program that are invoked by the project installer.
Process Installer and Service Installer
Within the implementation of InitializeComponent, instances of the ServiceProcessInstaller class and the ServiceInstaller class are created. Both of these classes derive from the ComponentInstaller class, which itself derives from Installer.
Classes derived from ComponentInstaller can be used with an installation process. Remember that a service process can include more than one service. The ServiceProcessInstaller class is used for the configuration of the process that defines values for all services in this process, and the ServiceInstaller class is for the configuration of the service, so one instance of ServiceInstaller is required for each service. If three services are inside the process, you need to add three ServiceInstaller objects:
partial class ProjectInstaller { private System.ComponentModel.IContainer components = null; private void InitializeComponent() { this.serviceProcessInstaller1 = new System.ServiceProcess.ServiceProcessInstaller(); this.serviceInstaller1 = new System.ServiceProcess.ServiceInstaller(); this.serviceProcessInstaller1.Password = null; this.serviceProcessInstaller1.Username = null; this.serviceInstaller1.ServiceName ="QuoteService"; this.serviceInstaller1.Description ="Sample Service for Professional C#"; this.serviceInstaller1.StartType = System.ServiceProcess.ServiceStartMode.Manual; this.Installers.AddRange( new System.Configuration.Install.Installer[] {this.serviceProcessInstaller1, this.serviceInstaller1}); } private System.ServiceProcess.ServiceProcessInstaller serviceProcessInstaller1; private System.ServiceProcess.ServiceInstaller serviceInstaller1; }
The class ServiceProcessInstaller installs an executable that contains a class that derives from the base class ServiceBase. ServiceProcessInstaller has properties for the complete service process. The following table describes the properties shared by all the services inside the process.
Property |
Description |
Username, Password |
Indicates the user account under which the service runs if the Account property is set to ServiceAccount.User. |
Account |
With this property, you can specify the account type of the service. |
HelpText |
A read-only property that returns the help text for setting the username and password. |
The process that is used to run the service can be specified with the Account property of the ServiceProcessInstaller class using the ServiceAccount enumeration. The following table describes the different values of the Account property.
Value |
Description |
LocalSystem |
Setting this value specifies that the service uses a highly privileged user account on the local system, and acts as the computer on the network. |
NetworkService |
Similarly to LocalSystem, this value specifies that the computer’s credentials are passed to remote servers; but unlike LocalSystem, such a service acts as a nonprivileged user on the local system. As the name implies, this account should be used only for services that need resources from the network. |
LocalService |
This account type presents anonymous credentials to any remote server and has the same privileges locally as NetworkService. |
User |
Setting the Account property to ServiceAccount.User means that you can define the account that should be used from the service. |
ServiceInstaller is the class needed for every service; it has the following properties for each service inside a process: StartType, DisplayName, ServiceName, and ServicesDependentOn, as described in the following table.
Property |
Description |
StartType |
The StartType property indicates whether the service is manually or automatically started. Possible values are ServiceStartMode.Automatic, ServiceStartMode.Manual, and ServiceStartMode.Disabled. With the last one, the service cannot be started. This option is useful for services that shouldn’t be started on a system. You might want to set the option to Disabled if, for example, a required hardware controller is not available. |
DelayedAutoStart |
This property is ignored if the StartType is not set to Automatic. Here, you can specify that the service should not be started immediately when the system boots but afterward. |
DisplayName |
DisplayName is the friendly name of the service that is displayed to the user. This name is also used by management tools that control and monitor the service. |
ServiceName |
ServiceName is the name of the service. This value must be identical to the ServiceName property of the ServiceBase class in the service program. This name associates the configuration of the ServiceInstaller to the required service program. |
ServicesDependentOn |
Specifies an array of services that must be started before this service can be started. When the service is started, all these dependent services are started automatically, and then your service will start. |
NOTE If you change the name of the service in the ServiceBase-derived class, be sure to also change the ServiceName property in the
NOTE In the testing phases, set StartType to Manual. This way, if you can’t stop the service (for example, when it has a bug), you still have the possibility to reboot the system; but if you have StartType set to Automatic, the service would be started automatically with the reboot! You can change this configuration later when you are sure that it works.
The ServiceInstallerDialog Class
Another installer class in the System.ServiceProcess.Design namespace is
ServiceInstallerDialog. This class can be used if you want the system administrator to enter the account that the service should use by assigning the username and password during the installation. If you set the Account property of the class ServiceProcessInstaller to ServiceAccount.User and the Username and Password properties to null, you see the Set Service Login dialog at installation time (see Figure 39.10). You can also cancel the installation at this point.
Figure 39.10
installutil
After adding the installer classes to the project, you can use the installutil.exe utility to install and uninstall the service. You can use this utility to install any assembly that has an Installer class. The installutil.exe utility calls the method Install of the class that derives from the Installer class for installation, and Uninstall for the uninstallation.
The command-line inputs for the installation and uninstallation of our example service are as follows:
installutil quoteservice.exe
installutil /u quoteservice.exe
NOTEIf the installation fails, be sure to check the installation log files, InstallUtil.InstallLog and <servicename>.InstallLog. Often, you can find very useful information, such as “The specified service already exists.”
After the service has been successfully installed, you can start the service manually from the Services MMC (see the next section for details), and then you can start the client application.
Monitoring and Controlling Windows Services
To monitor and control Windows Services, you can use the Services MMC snap-in that is part of the Computer Management administration tool. Every Windows system also has a command-line utility, net.exe, which enables you to control services. Another Windows command-line utility is sc.exe. This utility has much more functionality than net.exe. You can also control services directly from the Visual Studio Server Explorer. In this section, you also create a small Windows application that makes use of the System.ServiceProcess.ServiceController class to monitor and control services.
MMC Snap-in
Using the Services snap-in to the MMC, you can view the status of all services (see Figure 39.11). It is also possible to send control requests to services to stop, enable, or disable them, as well as to change their configuration. The Services snap-in is a service control program as well as a service configuration program.
Figure 39.11
Double-click QuoteService to get the Properties dialog shown in Figure 39.12. From here you can view the service name, the description, the path to the executable, the startup type, and the status. The service is currently started. The account for the service process can be changed by selecting the Log On tab in this dialog.
Figure 39.12
net.exe Utility
The Services snap-in is easy to use, but system administrators cannot automate it because it is not usable within an administrative script. To control services with a tool that can be automated with a script, you can use the command-line utility net.exe. The net start command shows all running services, net start servicename starts a service, and net stop servicename sends a stop request to the service. It is also possible to pause and continue a service with net pause and net continue (if the service allows it, of course).
sc.exe Utility
Another little-known utility delivered as part of the operating system is sc.exe. This is a great tool for working with services. You can do much more with sc.exe than with the net.exe utility. With sc.exe, you can check the actual status of a service, or configure, remove, and add services. This tool also facilitates the uninstallation of the service if it fails to function correctly.
Visual Studio Server Explorer
To monitor services using the Server Explorer within Visual Studio, select Servers from the tree view, and then select your computer, then the Services element. You can see the status of all services as shown in Figure 39.13. By selecting a service, you can see the properties of the service.
Figure 39.13
Writing a Custom Service Controller
In this section, you create a small WPF application that uses the ServiceController class to monitor and control Windows Services.
Create a WPF application with a user interface as shown in Figure 39.14. The main window of this application has a list box to display all services; four text boxes to show the display name, status, type, and name of the service; and six buttons. Four buttons are used to send control events, one button is used for a refresh of the list, and one button is used to exit the application.
Figure 39.14
NOTE You can read more about WPF and XAML in Chapters 29 through 35.
Monitoring the Service
With the ServiceController class, you can get information about each service. The following table shows the properties of the ServiceController class:
Property |
Description |
CanPauseAndContinue |
Returns true if pause and continue requests can be sent to the service. |
CanShutdown |
Returns true if the service has a handler for a system shutdown. |
CanStop |
Returns true if the service is stoppable. |
DependentServices |
Returns a collection of dependent services. If the service is stopped, then all dependent services are stopped beforehand. |
ServicesDependentOn |
Returns a collection of the services on which this service depends. |
DisplayName |
Specifies the name that should be displayed for this service. |
MachineName |
Specifies the name of the machine on which the service runs. |
ServiceName |
Specifies the name of the service. |
ServiceType |
Specifies the type of the service. The service can be run inside a shared process, whereby more than one service uses the same process (Win32ShareProcess), or run in such a way that there is just one service in a process (Win32OwnProcess). If the service can interact with the desktop, the type is InteractiveProcess. |
Status |
Specifies the service’s status, which can be running, stopped, paused, or in some intermediate mode such as start pending, stop pending, and so on. The status values are defined in the enumeration ServiceControllerStatus. |
In the sample application, the properties DisplayName, ServiceName, ServiceType, and Status are used to display the service information. CanPauseAndContinue and CanStop are used to enable or disable the Pause, Continue, and Stop buttons.
To get all the needed information for the user interface, the class ServiceControllerInfo is created. This class can be used for data binding and offers status information, the name of the service, the service type, and information about which buttons to control the service should be enabled or disabled.
NOTE Because the class System.ServiceProcess.ServiceController is used, you must reference the assembly System.ServiceProcess.
ServiceControllerInfo contains an embedded ServiceController that is set with the constructor of the ServiceControllerInfo class. There is also a read-only property Controller to access the embedded ServiceController (code file
ServiceControlWPF/ServiceControllerInfo.cs):
public class ServiceControllerInfo { public ServiceControllerInfo(ServiceController controller) { Controller = controller; } public ServiceController Controller { get; } // etc. }
To display current information about the service, the ServiceControllerInfo class has the read-only properties DisplayName, ServiceName, ServiceTypeName, and ServiceStatusName. The implementation of the properties DisplayName and ServiceName just accesses the properties of those names of the underlying ServiceController class. With the implementation of the properties ServiceTypeName and ServiceStatusName, more work is needed—the status and type of the service cannot be returned that easily because a string should be displayed instead of a number, which is what the ServiceController class returns. The property ServiceTypeName returns a string that represents the type of the service. The ServiceType you get from the property ServiceController.ServiceType represents a set of flags that can be combined by using the bitwise OR operator. The InteractiveProcess bit can be set together with Win32OwnProcess and Win32ShareProcess. Therefore, the first check determines whether the InteractiveProcess bit is set before continuing to check for the other values. With services, the string returned will be “Win32 Service Process” or “Win32 Shared Process” (code file ServiceControlWPF/ServiceControllerInfo.cs):
public class ServiceControllerInfo { // etc. public string ServiceTypeName { get { ServiceType type = controller.ServiceType; string serviceTypeName =""; if ((type & ServiceType.InteractiveProcess) != 0) { serviceTypeName ="Interactive"; type -= ServiceType.InteractiveProcess; } switch (type) { case ServiceType.Adapter: serviceTypeName +="Adapter"; break; case ServiceType.FileSystemDriver: case ServiceType.KernelDriver: case ServiceType.RecognizerDriver: serviceTypeName +="Driver"; break; case ServiceType.Win32OwnProcess: serviceTypeName +="Win32 Service Process"; break; case ServiceType.Win32ShareProcess: serviceTypeName +="Win32 Shared Process"; break; default: serviceTypeName +="unknown type" + type.ToString(); break; } return serviceTypeName; } } public string ServiceStatusName { get { switch (Controller.Status) { case ServiceControllerStatus.ContinuePending: return"Continue Pending"; case ServiceControllerStatus.Paused: return"Paused"; case ServiceControllerStatus.PausePending: return"Pause Pending"; case ServiceControllerStatus.StartPending: return"Start Pending"; case ServiceControllerStatus.Running: return"Running"; case ServiceControllerStatus.Stopped: return"Stopped"; case ServiceControllerStatus.StopPending: return"Stop Pending"; default: return"Unknown status"; } } } public string DisplayName => Controller.DisplayName; public string ServiceName => Controller.ServiceName; // etc. }
The ServiceControllerInfo class has some other properties to enable the Start, Stop, Pause, and Continue buttons: EnableStart, EnableStop, EnablePause, and EnableContinue. These properties return a Boolean value according to the current status of the service (code file ServiceControlWPF/ServiceControllerInfo.cs):
public class ServiceControllerInfo { // etc. public bool EnableStart => Controller.Status == ServiceControllerStatus.Stopped; public bool EnableStop => Controller.Status == ServiceControllerStatus.Running; public bool EnablePause => Controller.Status == ServiceControllerStatus.Running && Controller.CanPauseAndContinue; public bool EnableContinue => Controller.Status == ServiceControllerStatus.Paused; }
In the ServiceControlWindow class, the method RefreshServiceList gets all the services using ServiceController.GetServices for display in the list box. The GetServices method returns an array of ServiceController instances representing all Windows Services installed on the operating system. The ServiceController class also has the static method GetDevices that returns a ServiceController array representing all device drivers. The returned array is sorted with the help of the extension method OrderBy. The sort is done by the DisplayName as defined with the lambda expression that is passed to the OrderBy method. Using Select, the ServiceController instances are converted to the type ServiceControllerInfo. In the following code, a lambda expression is passed that invokes the ServiceControllerInfo constructor for every ServiceController object. Last, the result is assigned to the DataContext property of the window for data binding (code file ServiceControlWPF/MainWindow .xaml.cs):
protected void RefreshServiceList() { this.DataContext = ServiceController.GetServices(). OrderBy(sc => sc.DisplayName). Select(sc => new ServiceControllerInfo(sc)); }
The method RefreshServiceList, to get all the services in the list box, is called within the constructor of the class ServiceControlWindow. The constructor also defines the event handler for the Click event of the buttons:
public ServiceControlWindow() { InitializeComponent(); RefreshServiceList(); }
Now, you can define the XAML code to bind the information to the controls. First, a DataTemplate is defined for the information that is shown inside the ListBox. The ListBox contains a Label in which the Content is bound to the DisplayName property of the data source. As you bind an array of ServiceControllerInfo objects, the property DisplayName is defined with the ServiceControllerInfo class (code file ServiceControlWPF/MainWindow.xaml):
<Window.Resources> <DataTemplate x:Key="listTemplate"> <Label Content="{Binding DisplayName}"/> </DataTemplate> </Window.Resources>
The ListBox that is placed in the left side of the window sets the ItemsSource property to {Binding}. This way, the data that is shown in the list is received from the DataContext property that was set in the RefreshServiceList method. The ItemTemplate property references the resource listTemplate that is defined with the DataTemplate shown earlier. The property IsSynchronizedWithCurrentItem is set to True so that the TextBox and Button controls inside the same window are bound to the current item selected with the ListBox:
<ListBox Grid.Row="0" Grid.Column="0" HorizontalAlignment="Left" Name="listBoxServices" VerticalAlignment="Top" ItemsSource="{Binding}" ItemTemplate="{StaticResource listTemplate}" IsSynchronizedWithCurrentItem="True"> </ListBox>
To differentiate the Button controls to start/stop/pause/continue the service, the following enumeration is defined (code file ServiceControlWPF/ButtonState.cs):
public enum ButtonState { Start, Stop, Pause, Continue }
With the TextBlock controls, the Text property is bound to the corresponding property of the ServiceControllerInfo instance. Whether the Button controls are enabled or disabled is also defined from the data binding by binding the IsEnabled property to the corresponding properties of the ServiceControllerInfo instance
that return a Boolean value. The Tag property of the buttons is assigned to a value of the ButtonState enumeration defined earlier to differentiate the button within the same handler method OnServiceCommand (code file ServiceControlWPF/MainWindow.xaml):
<TextBlock Grid.Row="0" Grid.ColumnSpan="2" Text="{Binding /DisplayName, Mode=OneTime}" /> <TextBlock Grid.Row="1" Grid.ColumnSpan="2" Text="{Binding /ServiceStatusName, Mode=OneTime}" /> <TextBlock Grid.Row="2" Grid.ColumnSpan="2" Text="{Binding /ServiceTypeName, Mode=OneTime}" /> <TextBlock Grid.Row="3" Grid.ColumnSpan="2" Text="{Binding /ServiceName, Mode=OneTime}" /> <Button Grid.Row="4" Grid.Column="0" Content="Start" IsEnabled="{Binding /EnableStart, Mode=OneTime}" Tag="{x:Static local:ButtonState.Start}" Click="OnServiceCommand" /> <Button Grid.Row="4" Grid.Column="1" Name="buttonStop" Content="Stop" IsEnabled="{Binding /EnableStop, Mode=OneTime}" Tag="{x:Static local:ButtonState.Stop}" Click="OnServiceCommand" /> <Button Grid.Row="5" Grid.Column="0" Name="buttonPause" Content="Pause" IsEnabled="{Binding /EnablePause, Mode=OneTime}" Tag="{x:Static local:ButtonState.Pause}" Click="OnServiceCommand" /> <Button Grid.Row="5" Grid.Column="1" Name="buttonContinue" Content="Continue" IsEnabled="{Binding /EnableContinue, Tag="{x:Static local:ButtonState.Continue}" Mode=OneTime}" Click="OnServiceCommand" /> <Button Grid.Row="6" Grid.Column="0" Name="buttonRefresh" Content="Refresh" Click="OnRefresh" /> <Button Grid.Row="6" Grid.Column="1" Name="buttonExit" Content="Exit" Click="OnExit" />
Controlling the Service
With the ServiceController class, you can also send control requests to the service. The following table describes the methods that can be applied.
Method |
Description |
Start |
Tells the SCM that the service should be started. In the example service program, OnStart is called. |
Stop |
Calls OnStop in the example service program with the help of the SCM if the property CanStop is true in the service class. |
Pause |
Calls OnPause if the property CanPauseAndContinue is true. |
Continue |
Calls OnContinue if the property CanPauseAndContinue is true. |
ExecuteCommand |
Enables sending a custom command to the service. |
The following code controls the services. Because the code for starting, stopping, suspending, and pausing is similar, only one handler is used for the four buttons (code file ServiceControlWPF/MainWindow.xaml.cs):
protected void OnServiceCommand(object sender, RoutedEventArgs e) { Cursor oldCursor = this.Cursor; try { this.Cursor = Cursors.Wait; ButtonState currentButtonState = (ButtonState)(sender as Button).Tag; var si = listBoxServices.SelectedItem as ServiceControllerInfo; if (currentButtonState == ButtonState.Start) { si.Controller.Start(); si.Controller.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(10)); } else if (currentButtonState == ButtonState.Stop) { si.Controller.Stop(); si.Controller.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(10)); } else if (currentButtonState == ButtonState.Pause) { si.Controller.Pause(); si.Controller.WaitForStatus(ServiceControllerStatus.Paused, TimeSpan.FromSeconds(10)); } else if (currentButtonState == ButtonState.Continue) { si.Controller.Continue(); si.Controller.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(10)); } int index = listBoxServices.SelectedIndex; RefreshServiceList(); listBoxServices.SelectedIndex = index; } catch (System.ServiceProcess.TimeoutException ex) { MessageBox.Show(ex.Message,"Timout Service Controller", MessageBoxButton.OK, MessageBoxImage.Error); } catch (InvalidOperationException ex) { MessageBox.Show(String.Format("{0} {1}", ex.Message, ex.InnerException != null ? ex.InnerException.Message: String.Empty), MessageBoxButton.OK, MessageBoxImage.Error); } finally { this.Cursor = oldCursor; } } protected void OnExit(object sender, RoutedEventArgs e) => Application.Current.Shutdown(); protected void OnRefresh_Click(object sender, RoutedEventArgs e) => RefreshServiceList();
Because the action of controlling the services can take some time, the cursor is switched to the wait cursor in the first statement. Then a ServiceController method is called depending on the pressed button. With the WaitForStatus method, you are waiting to confirm that the service changes the status to the requested value, but the wait maximum is only 10 seconds. After that, the information in the ListBox is refreshed, and the selected index is set to the same value as it was before. The new status of this service is then displayed.
Because the application requires administrative privileges, just as most services require that for starting and stopping, an application manifest with the requestedExecutionLevel set to requireAdministrator is added to the project (application manifest file ServiceControlWPF/app.manifest):
<?xml version="1.0" encoding="utf-8"?> <asmv1:assembly manifestVersion="1.0" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <security> <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3"> <requestedExecutionLevel level="requireAdministrator" uiAccess="false" /> </requestedPrivileges> </security> </trustInfo> </asmv1:assembly>
Figure 39.15 shows the completed, running application.
Figure 39.15
Troubleshooting and Event Logging
Troubleshooting services is different from troubleshooting other types of applications. This section touches on some service issues, problems specific to interactive services, and event logging.
The best way to start building a service is to create an assembly with the functionality you want and a test client, before the service is actually created. Here, you can do normal debugging and error handling. As soon as the application is running, you can build a service by using this assembly. Of course, there might still be problems with the service:
- Don’t display errors in a message box from the service (except for interactive services that are running on the client system). Instead, use the event logging service to write errors to the event log. Of course, in the client application that uses the service, you can display a message box to inform the user about errors.
- The service cannot be started from within a debugger, but a debugger can be attached to the running service process. Open the solution with the source code of the service and set breakpoints. From the Visual Studio Debug menu, select Processes and attach the running process of the service.
- Performance Monitor can be used to monitor the activity of services, and you can add your own performance objects to the service. This can add some useful information for debugging. For example, with the Quote service, you could set up an object to provide the total number of quotes returned, the time it takes to initialize, and so on.
Services can report errors and other information by adding events to the event log. A service class derived from ServiceBase automatically logs events when the AutoLog property is set to true. The ServiceBase class checks this property and writes a log entry at start, stop, pause, and continue requests.
Figure 39.16 shows an example of a log entry from a service.
Figure 39.16
NOTE You can read more about event logging and how to write custom events in Chapter 20, “Diagnostics and Application Insights.”
Summary
In this chapter, you have seen the architecture of Windows Services and how you can create them with the .NET Framework. Applications can start automatically at boot time with Windows Services, and you can use a privileged system account as the user of the service. Windows Services are built from a main function, a service-main function, and a handler; and you looked at other relevant programs in regard to Windows Services, such as a service control program and a service installation program.
The .NET Framework has great support for Windows Services. All the plumbing code that is necessary for building, controlling, and installing services is built into the .NET Framework classes in the System .ServiceProcess namespace. By deriving a class from ServiceBase, you can override methods that are invoked when the service is paused, resumed, or stopped. For installation of services, the classes ServiceProcessInstaller and ServiceInstaller deal with all registry configurations needed for services. You can also control and monitor services by using ServiceController.
In the next chapter you can read about ASP.NET Core 1.0, a technology that makes use of a web server that itself is typically running within a Windows Service (if the server is used on the Windows operating system).