Creating a Windows Service


The service that you create will host 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 22-4 gives 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 Windows Forms 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:

image from book
Figure 22-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.

A Class Library Using Sockets

You could build any functionality in the service, such as scanning for files to do a backup or a virus check or starting a WCF server, for example. 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 Vista, 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 <windir>\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 good old qotd server that returns an ASCII string.

First, create a Class Library called QuoteServer and implement the code for the server. The following steps through the source code of your QuoteServer class in the file QuoteServer.cs:

  using System; using System.IO; using System.Threading; using System.Net; using System.Net.Sockets; using System.Text; using System.Collections.Generic; namespace Wrox.ProCSharp.WinServices {    public class QuoteServer    {       private TcpListener listener;       private int port;        private string filename;       private List<string> quotes;       private Random random;       private Thread listenerThread; 

The constructor QuoteServer() is overloaded, so that a file name and a port can be passed to the call. The constructor where just the file name is passed uses the default port 7890 for the server. The default constructor defines the default file name for the quotes as quotes.txt:

  public QuoteServer() : this ("quotes.txt") { } public QuoteServer(string filename) : this(filename, 7890) { } public QuoteServer(string filename, int port) {    this.filename = filename;    this.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 StringCollection quotes. In addition, you are creating an instance of the Random class that will be used to return random quotes:

  protected void ReadQuotes() {    quotes = new List<string>();    Stream stream = File.OpenRead(filename);    StreamReader streamReader = new StreamReader(stream);    string quote;    while ((quote = streamReader.ReadLine()) != null)    {       quotes.Add(quote);    }    streamReader.Close();    stream.Close();    random = new Random(); } 

Another helper method is GetRandomQuoteOfTheDay(). This method returns a random quote from the StringCollection quotes:

  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 StringCollection 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 35, “Accessing the Internet.” Here a thread 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 thread is set as a background thread so that the application can exit without stopping this thread. The Name property of the thread is set because this helps with debugging, as the name will show up in the debugger:

  public void Start() {    ReadQuotes();    listenerThread = new Thread(ListenerThread);    listenerThread.IsBackground = true;    listenerThread.Name = "Listener";    listenerThread.Start(); } 

The thread function ListenerThread() creates a TcpListener instance. The AcceptSocket() method waits for a client to connect. As soon as a client connects, AcceptSocket() returns with a socket associated with the client. Next, GetRandomQuoteOfTheDay() is called to send the returned random quote to the client using socket.Send():

  protected void ListenerThread() {    try    {       IPAddress ipAddress = IPAddress.Parse("127.0.0.1");       listener = new TcpListener(ipAddress, port);       listener.Start();       while (true)       {          Socket clientSocket = listener.AcceptSocket();          string message = GetRandomQuoteOfTheDay();          UnicodeEncoding encoder = new UnicodeEncoding();          byte[] buffer = encoder.GetBytes(message);          clientSocket.Send(buffer, buffer.Length, 0);          clientSocket.Close();       }    }    catch (SocketException ex)    {       Console.WriteLine(ex.Message);    } } 

In addition to the Start() method, the following methods are needed to control the service: Stop(), Suspend(), and Resume():

  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 re-read with this method:

        public void RefreshQuotes()       {          ReadQuotes();       }    } } 

Before building a service around the server, it is useful to build a test program that just creates an instance of the QuoteServer and calls Start(). This way, you can test the functionality without the need to handle service-specific issues. This test server must be started manually, and you can easily walk through the code with a debugger.

The test program is a C# console application, TestQuoteServer. You have to reference the assembly of the QuoteServer class. The file containing the quotes must be copied to the directory c:\ProCSharp\Services (or you have to change the argument in the constructor to specify where you have copied the file). After calling the constructor, the Start() method of the QuoteServer instance is called. Start() returns immediately after having created a thread, so the console application keeps running until Return is pressed:

  static void Main() {    QuoteServer qs = new QuoteServer(          @"c:\ProCSharp\WindowsServices\quotes.txt", 4567);    qs.Start();    Console.WriteLine("Hit return to exit");    Console.ReadLine();    qs.Stop(); } 

Note that QuoteServer will be running on port 4567 on localhost using this program - you will have to use these settings in the client later.

TcpClient Example

The client is a simple WPF Windows application where 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 (see Figure 22-5).

image from book
Figure 22-5

Server and port information to connect to the server is configured with settings of the application. You can add settings with the Settings tab inside the properties of the project (see Figure 22-6). Here, you can define the ServerName and PortNumber settings, and define some default values. From here the settings go into the configuration file. With the Scope set to User, the settings go into a user-specific configuration file, and 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.

image from book
Figure 22-6

You have to add the following using directives to your code:

  using System; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Net.Sockets; 

Within the constructor of the class QuoteOfTheDayWindow, you can define a handler method to the Click event of the button buttonGetQuote:

  public QuoteOfTheDayWindow() {    InitializeComponent();    this.buttonGetQuote.Click += new RoutedEventHandler(OnGetQuote); } 

The major functionality of the client lies in the handler for the click event of the Get Quote button:

  protected void OnGetQuote(object sender, RoutedEventArgs e) {    System.Windows.Input.Cursor oldCursor = this.Cursor;    this.Cursor = Cursors.Wait;        string serverName = Properties.Settings.Default.ServerName;    int port = Properties.Settings.Default.PortNumber;        TcpClient client = new TcpClient();    NetworkStream stream = null;    try    {       client.Connect(serverName, port);       stream = client.GetStream();       byte[] buffer = new Byte[1024];       int received = stream.Read(buffer, 0, 1024);       if (received <= 0)       {          return;       }      textQuote.Content = Encoding.Unicode.GetString(buffer).Trim('\0');    }    catch (SocketException ex)    {       MessageBox.Show(ex.Message, "Error Quote of the day",             MessageBoxButton.OK, MessageBoxImage.Error);    }    finally    {       if (stream != null)       {          stream.Close();       }              if (client.Connected)             {          client.Close();       }    }    this.Cursor = oldCursor; } 

After starting the test server and this Windows application client, you can test the functionality. Figure 22-7 shows a successful run of this application.

image from book
Figure 22-7

Next, you implement the service functionality in the server. The program is already running, so what else do you need? Well, the server program should be automatically started at boot time without anyone logged on to the system. You want to control this by using service control programs.

Windows Service Project

Using the new project wizard for C# Windows Services, you can now start to create a Windows Service. For the new service, use the name QuoteService (see Figure 22-8).

image from book
Figure 22-8

After you click the OK button to create the Windows Service application, you will see the Designer surface (just as with Windows Forms applications). However, you can’t insert any Windows Forms components, because the application cannot directly display anything on the screen. The Designer surface is used later in this chapter to add other components, such as performance counters and event logging.

Selecting the properties of this service opens up the Properties editor window (see Figure 22-9).

image from book
Figure 22-9

With the service properties, 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 is the name of the service written to the registry and is used to control the service.

  • CanHandleSessionChangeEvent defines if the service can handle change events from a terminal server session.

  • CanHandlePowerEvent 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.

Important 

The default service name is WinService1, regardless of what the project is called. You can install only one WinService1 service. If you get installation errors during your testing process, you might already have installed one WinService1 service. Therefore, make sure that you change the name of the service with the Properties editor to a more suitable name at the beginning of the service development.

Changing these properties with the Properties editor sets the values of your ServiceBase-derived class in the InitalizeComponent() method. You already know this method from Windows Forms applications. With services it’s used in a similar way.

A wizard generates the code, but change the file name 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 Win32 API calls. The class is private, so it cannot be used in your code.

The sequence diagram in Figure 22-10 shows the interaction of the SCM, the class QuoteService, and the classes from the System.ServiceProcess namespace. In the sequence diagram, you can see the lifelines of objects vertically and the communication going on in the horizontal direction. The communication is time-ordered from top to bottom.

image from book
Figure 22-10

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 have 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 wizard–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’s the automatically generated code:

  // The main entry point for the process static void Main() {    ServiceBase[] ServicesToRun;        // More than one user Service may run within the same process. To    // add another service to this process, change the following line    // to create a second service object. For example,    //    //   ServicesToRun = New System.ServiceProcess.ServiceBase[]    //   {    //      new Service1(), new MySecondUserService()    //   };    //        ServicesToRun = new ServiceBase[]    {       new QuoteService()    };    ServiceBase.Run(ServicesToRun); } 

If there’s 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:

  System.ServiceProcess.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 where 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 following instructions would not be 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 service control manager would assume that the service startup failed. You have 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():

 protected override void OnStart(string[] args) {    quoteServer = new QuoteServer(          @"c:\ProCSharp\WindowsServices\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:

 protected override void OnStop() {    quoteServer.Stop(); }

In addition to OnStart() and OnStop(), you can override the following handlers in the service class:

  • OnPause() is called when the service should be paused.

  • OnContinue() is 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, the CanPauseAndContinue property must be set to true.

  • OnShutdown() is 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 additional time. Similarly to OnPause() and OnContinue(), a property must be set to enable this behavior: CanShutdown must be set to true.

  • OnPowerEvent() is called when the power status of the system changes. The information about the change of the power status is in the argument of type PowerBroadcastStatus. PowerBroadcastStatus is an enumeration with values such as BatteryLow and PowerStatusChange. Here, you will also get information if the system would like to suspend (QuerySuspend), where you can approve or deny the suspend.

    Tip 

    Later in this chapter you can read more about power events.

  • OnCustomCommand() is a handler that can serve custom commands that are sent by a service control program. The method signature of OnCustomCommand() has an int argument where you get 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 re-reading the quotes file with the custom command 128:

  protected override void OnPause() {    quoteServer.Suspend(); } protected override void OnContinue() {    quoteServer.Resume(); } protected override void OnShutdown() {    OnStop(); } public const int commandRefresh = 128; protected override void OnCustomCommand(int command) {    switch (command)    {       case commandRefresh:          quoteServer.RefreshQuotes();          break;       default:          break;    } } 

Threading and Services

With services, you have to deal with threads. As stated earlier, the SCM will assume that the service failed if the initialization takes too long. To deal with this, you have to create a thread.

The OnStart() method in your service class must return in time. If you call a blocking method like AcceptSocket() from the TcpListener class, you have to start a thread for doing this. 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

A service must be configured in the registry. All services can be found in HKEY_LOCAL_MACHINE\System \CurrentControlSet\Services. You can view the registry entries by using regedit. The type of the service, display name, path to the executable, startup configuration, and so on are all found here. Figure 22-11 shows the registry configuration of the W3SVC service.

image from book
Figure 22-11

This configuration can be done 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, and a ServiceInstaller and a ServiceProcessInstaller instance are created.

Figure 22-12 shows the class diagram of the installer classes for services.

image from book
Figure 22-12

With this diagram in mind, let’s 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’s possible to build transaction-based installations. With a transaction-based installation, it’s possible to 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 you can see in Figure 22-12, the Installer class has Install(), Commit(), Rollback(), and Uninstall() methods, and they are called from installation programs.

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 will be used later) check for this attribute.

Similarly to Windows Forms applications, InitializeComponent() is called inside the constructor of the ProjectInstaller class:

  using System; using System.Collections; using System.ComponentModel; using System.Configuration.Install; namespace Wrox.ProCSharp.WinServices {    [RunInstaller(true)]    public partial class ProjectInstaller : Installer    {       public ProjectInstaller()       {          InitializeComponent();       }    } 

The ServiceProcessInstaller and ServiceInstaller Classes

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 have to add ServiceInstaller objects - three ServiceInstaller instances are needed in that case.

  partial class ProjectInstaller {    /// <summary>    ///    Required designer variable.    /// </summary>    private System.ComponentModel.Container components = null;    /// <summary>    ///    Required method for Designer support - do not modify    ///    the contents of this method with the code editor.    /// </summary>    private void InitializeComponent()    {       this.serviceProcessInstaller1 =                   new System.ServiceProcess.ServiceProcessInstaller();       this.serviceInstaller1 =                   new System.ServiceProcess.ServiceInstaller();       //       // serviceProcessInstaller1       //       this.serviceProcessInstaller1.Password = null;       this.serviceProcessInstaller1.Username = null;       //       // serviceInstaller1       //       this.serviceInstaller1.ServiceName = "QuoteService";       //       // ProjectInstaller       //       this.Installers.AddRange(          new System.Configuration.Install.Installer[]             {this.serviceProcessInstaller1,              this.serviceInstaller1});    }    private System.ServiceProcess.ServiceProcessInstaller                   serviceProcessInstaller1;    private System.ServiceProcess.ServiceInstaller serviceInstaller1; } 

ServiceProcessInstaller installs an executable that implements the class ServiceBase. ServiceProcessInstaller has properties for the complete process. The following table explains the properties shared by all the services inside the process.

Open table as spreadsheet

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

HelpText is 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 explains the different values of the Account property.

Open table as spreadsheet

Value

Meaning

LocalSystem

Setting this value specifies that the service uses a highly privileged user account on the local system, but this account presents an anonymous user to the network. Thus, it doesn’t have rights on the network.

LocalService

This account type presents the computer’s credentials to any remote server.

NetworkService

Similarly to LocalService, this value specifies that the computer’s credentials are passed to remote servers, but unlike LocalService 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.

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 ServicesDependedOn, as described in the following table.

Open table as spreadsheet

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 ServiceStartMode.Disabled 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.

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.

Important 

If you change the name of the service in the ServiceBase-derived class, also change the ServiceName property in the ServiceInstaller object!

Tip 

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 at a later time when you’re 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 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 will see the Set Service Login dialog box at installation time (see Figure 22-13). You can also cancel the installation at this point.

image from book
Figure 22-13

installutil

After adding the installer classes to the project, you can use the installutil.exe utility to install and uninstall the service. This utility can be used 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 service are:

  installutil quoteservice.exe installutil /u quoteservice.exe 

Important 

If 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.”

Client

After the service has been successfully installed, you can start the service manually from the Services MMC (see next section for further details), and then you can start the client application. Figure 22-14 shows the client accessing the service.

image from book
Figure 22-14




Professional C# 2005 with .NET 3.0
Professional C# 2005 with .NET 3.0
ISBN: 470124725
EAN: N/A
Year: 2007
Pages: 427

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net