The Zip file containing the source code for this and the other parts of this series can be found HERE.
In the first part of this series we created a rudimentary TCP server that gives out the current date and time. In this article we will convert that console application into a Windows Service and install it. The transition will be mostly painless but there are a few considerations.
First, we cannot rely on a keypress to stop our server anymore. We will have to implement the OnStop() method for the service, which is called implicitly when Windows attempts to stop a service. Second, we need some way to know that the server is working without having to run the test client all the time, so we will add code to the server to log to a custom Windows Event Log.
We will address all these things in this article. In the third and final article, we will create a Windows Form application to run in the system tray that will allow us to stop and start the server. We will also add pause and continue functionality, which is a lot simpler than it sounds! For now, though, we will concern ourselves with getting the service built and installed.
Add a new project to the TimeApp solution called “TimeService” and rename the “Service1” class “TimeServerService.” Be sure to use the find and replace tool to rename “Service1” to TimeServerService in the class file itself, because it appears in several places: the class name, the constructor, and the ServicesToRun array in the Main() method. Next you need to change a few properties of the TimeServerService.cs in design view. Change the ServiceName property to “Time Server Service” and change “CanShutdown” to “True.”
Once you have changed those properties, click the “Add Installer” link. This will add a file named “ProjectInstaller.cs” to your project. In the “ProjectInstaller.cs” design view, you need to set the “StartType” property to “Manual” for the ServiceInstaller and set the “Account” property to “LocalSystem” for the ServiceProcessInstaller. Save and close the project installer files; they are finished.
Now that the project is set up, we need to open up the TimeServer.cs file in the TimeApp project and get ready to move it over to the TimeServerService.cs class.
First we need to add the System.Net, System.Net.Sockets, and System.Text namespaces to the TimeServerService class. Then copy over the listener field and the readonly fields ip and port from TimeServer. Copy the entire Start() method from TimeServer to TimeServerService next. Add a call to listener.Start() in the OnStart() method, which is called implicitly when a Windows Service is started, and in the TimeServerService constructor paste the line that instantiates the TcpListener from TimeServer.Main().
Your service class should look like this when you’re done (note that I have removed the massive comment blocks that come with the Windows Service class):
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace TimeService
{
public class TimeServerService : System.ServiceProcess.ServiceBase
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
//
// TimeServerService
//
this.CanShutdown = true;
this.ServiceName = "Time Server Service";
}
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
protected override void OnStart(string[] args)
{
this.Start();
}
protected override void OnStop()
{
}
public void Start()
{
this.listener.Start();
Socket s;
Byte[] incomingBuffer;
Byte[] time;
int bytesRead;
while (true)
{
s = this.listener.AcceptSocket();
incomingBuffer = new Byte[100];
bytesRead = s.Receive(incomingBuffer);
time = Encoding.ASCII.GetBytes(
System.DateTime.Now.ToString().ToCharArray());
s.Send(time);
}
}
}
}
Look at the Main() method. You will notice that it’s quite different from the Main() method of a console or form application. With a Windows Service, the Main() method creates an array of services to run and runs them. Of course, “running” simply means creating an instance of the class and calling the OnStart() method.
Now we need to do something about our control loop, since we can’t use console input anymore to kill the program. We also need to decide what type of event logging we are going to do.
To control our loop, we need to add a property to the class; we will call it “isStopped.” We will have to add a couple of lines to the OnStart() and OnStop() methods. Add this to the properties section of the class:
private bool isStopped = false;
The new OnStart(), OnStop(), and Start() methods are shown below:
protected override void OnStart(string[] args)
{
this.isStopped = false;
this.Start();
}
protected override void OnStop()
{
this.isStopped = true;
}
public void Start()
{
this.listener.Start();
Socket s;
Byte[] incomingBuffer;
Byte[] time;
int bytesRead;
while (!this.isStopped)
{
s = this.listener.AcceptSocket();
incomingBuffer = new Byte[100];
bytesRead = s.Receive(incomingBuffer);
time = Encoding.ASCII.GetBytes(
System.DateTime.Now.ToString().ToCharArray());
s.Send(time);
}
this.listener.Stop();
}
Now the server will start and stop using the built in Windows Service hooks.
Run the Visual Studio .NET Command Prompt and change directories to your TimeServerService project’s bin\Debug directory (or Release if you happened to build in Release mode). Type the following command:
installutil TimeService.exe
You should see a lot of stuff scroll by on the screen about the installation, and hopefully at the end you will see a message indicating that the service was successfully installed. Once the service is installed, go ahead and start it:
net start TimeServerService
You should see a message that the services are starting… and starting… and still starting. We forgot something! We need to fire off the Start() method in a separate thread so the OnStart() method doesn’t lead us into a nasty blocking loop. Add a reference to the System.Threading namespace at the top of your service class file, like this:
using System.Threading;
Change the OnStart() method to use a Thread rather than directly calling Start(), like so:
protected override void OnStart(string[] args)
{
this.isStopped = false;
Thread t = new Thread(new ThreadStart(this.Start));
t.Start();
}
Build the project. Before we can try to run the service again, we need to uninstall the old version by using this command:
installutil /u TimeService.exe
Once that’s done, install the service as we did before and start it with the net start command. Now set the test project as your startup project and run it. In the console output window you should see this message (hopefully with a different date and time, of course):
Sending request…
Received response: 1/21/2006 3:53:33 PM
Conclusion
We have now converted our little time application into a Windows Service! We created the service project, the service installer, and installed the service. Then we found out we had created an infinite loop in the service startup procedure so we moved the listener into its own thread to keep from blocking completion of the OnStart() method. We reinstalled the service, started it and tested it, with the desired results.
There are a few things left, though. We still need to start logging events and we still need to create a service controller form, and hopefully we will tuck it away in the system tray. We will cover those things in the final article of this series, coming soon!