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 36-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 36-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 .NET Remoting 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 XP, 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 — similar to the TcpReceive example in Chapter 35.

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( new ThreadStart(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 = Dns.GetHostEntry("localhost").AddressList[0]; 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(string[] args) { 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 Windows application where you can enter the host name and the port number of the server. This application uses the TcpClient class to connect to the running server, and receives the returned message, displaying it in a multiline text box. There's also a status strip at the bottom of the form (see Figure 36-5).

image from book
Figure 36-5

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

 using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; using System.Net; using System.Net.Sockets; using System.Text; 

The remainder of the code is automatically generated by the IDE, so we won't go into detail here. The major functionality of the client lies in the handler for the click event of the Get Quote button:

 protected void OnGetQuote(object sender, System.EventArgs e)  { statusStrip.Text = ""; string server = textHostname.Text; try { int port = Convert.ToInt32(textPortNumber.Text); } catch (FormatException ex) { statusStrip.Text = ex.Message; return; } TcpClient client = new TcpClient(); NetworkStream stream = null; try { client.Connect(textHostname.Text, Convert.ToInt32(textPortNumber.Text)); stream = client.GetStream(); byte[] buffer = new Byte[1024]; int received = stream.Read(buffer, 0, 1024); if (received <= 0) { statusStrip.Text = "Read failed"; return; } textQuote.Text = Encoding.Unicode.GetString(buffer); } catch (SocketException ex) { statusStrip.Text = ex.Message; } finally { if (stream != null) stream.Close(); if (client.IsConnected) client.Close(); } } 

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

image from book
Figure 36-6

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 36-7).

image from book
Figure 36-7

After you click the OK button to create the Windows Service application, you will see the Designer surface (just like 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 36-8).

image from book
Figure 36-8

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, shutdown, and stop requests.

  • ServiceName is the name of the service written to the registry and is used to control the service.

  • CanHandlePowerEvent is a very useful option for services running on a laptop. 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 derives 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 36-9 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 life- lines 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 36-9

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 were needed for a shutdown, you can request additional time. Similar to OnPause() and OnContinue(), a property must be set to enable this behavior: CanShutdown must be set to true.

  • 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 36-10 shows the registry configuration of the Alerter service.

image from book
Figure 36-10

This configuration can be done by using the installer classes from the System.ServiceProcess name- space, 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 36-11 shows the class diagram of the installer classes for services.

image from book
Figure 36-11

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 36-11, 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.

Similar 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.

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 set- ting 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.

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

Similar to LocalService, this value specifies that the computer's credentials are passed to remote servers, but unlike LocalService such a service acts as a non-privileged 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.

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 identi- cal 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 ser- vices are started automatically, and then your service will start.




Professional C# 2005
Pro Visual C++ 2005 for C# Developers
ISBN: 1590596080
EAN: 2147483647
Year: 2005
Pages: 351
Authors: Dean C. Wills

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