.NET Windows Services - Timer, Debugging, and Installation

We frequently get forum posts on eggheadcafe.com that revolve around people writing Windows Services in .NET and having issues. Since I have some pretty good "base code" I thought it would be a good time to resurrect it and point out some features and techniques. One of the most common issues developers run into is that they attempt to start a timer or some other recurring business logic in the OnStart method of their Service class.

You cannot do this, because it will make your service blow up. The OnStart method gives you exactly 30 seconds to set up your business logic and start it. OnStart is really for the Service Control Manager - it has to return before 30 seconds are up or you'll get a "Cannot Start Service XYZ" OS error.  

What you do want to do in OnStart is create an instance of your business logic class, calling it on a background thread, and call it's custom Start method. I'll show you how to do that, and I'll also show how you can use an installer with custom actions built in, as well as being able to install your service without any reliance on the InstallUtil.exe utility at all. Finally, I will show you how you can debug your service automatically without having to attach the debugger manually to your running process.  All this is included in the demo service that you can download at the end of this article.

What this demo service does is watch your filesystem for a specific file and when that file has been changed, it refreshes a Cache. In order to handle the updating of the file, we'll use the trusty Northwind SQL Server database with a trigger that runs xp_CmdShell to "touch" the file, altering it's LastModified date whenever there is an INSERT, UPDATE or DELETE on the Northwind database Employees Table. So if you went in with SQL Management Studio and edited Nancy Davolio's last name to read "Davolioooo' and saved your work, the service's Cache would be automatically refreshed. But you don't need to use this technique to do what I do in the demo; you can use the setup technique to perform any kind of recurring business logic you want inside your Service, such as running a timer, and so on.

First, let's have a look at the main Service class, "TestService.cs":

namespace TestService
{
using System;
using System.Collections;
using System.ComponentModel;
using System.Configuration.Install;
using System.Reflection;
using System.ServiceProcess;
using System.Threading;
using TestLibrary;

public class TestService : ServiceBase
{
/// <summary>
/// Required designer variable.
/// </summary>
private Container components = null;

private TestLib test;

public TestService()
{
// This call is required by the Windows.Forms Component Designer.
InitializeComponent();

// TODO: Add any initialization after the InitComponent call
}

// The main entry point for the process
private static void Main(string[] args)
{
string opt = null;
// check for arguments
if (args.Length > 0)
{
opt = args[0];
if (opt != null && opt.ToLower() == "/install")
{
TransactedInstaller ti = new TransactedInstaller();
ProjectInstaller pi = new ProjectInstaller();
ti.Installers.Add(pi);
String path = String.Format("/assemblypath={0}",
                            Assembly.GetExecutingAssembly().Location);
String[] cmdline = {path};
InstallContext ctx = new InstallContext("", cmdline);
ti.Context = ctx;
ti.Install(new Hashtable());
}
else if (opt != null && opt.ToLower() == "/uninstall")
{
TransactedInstaller ti = new TransactedInstaller();
ProjectInstaller mi = new ProjectInstaller();
ti.Installers.Add(mi);
String path = String.Format("/assemblypath={0}",
                            Assembly.GetExecutingAssembly().Location);
String[] cmdline = {path};
InstallContext ctx = new InstallContext("", cmdline);
ti.Context = ctx;
ti.Uninstall(null);

}
}
if (opt == null) // e.g. ,nothing on the command line
{
#if ( ! DEBUG )
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[] {new TestService()};
ServiceBase.Run(ServicesToRun);


#else
// debug code: allows the process to run as a non-service
// will kick off the service start point, but never kill it
// shut down the debugger to exit
TestService service = new TestService();
service.OnStart(null);
Thread.Sleep(Timeout.Infinite);
#endif
}
}

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
components = new Container();
this.ServiceName = "TestService";
}

/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose(disposing);
}

/// <summary>
/// Set things in motion so your service can do its work.
/// </summary>
protected override void OnStart(string[] args)
{
test = new TestLib();
test.Start();
}

/// <summary>
/// Stop this service.
/// </summary>
protected override void OnStop()
{
test.Stop() ;
}
}
}


The first feature I draw your attention to is the block of code starting with "if (opt != null && opt.ToLower() == "/install")". This technique allows you to run your executable with command line arguments of /install or /uninstall. The service will use the TransactedInstaller class and the ProjectInstaller class to install itself, with no requirement to use that ugly InstallUtil.exe utility.  If you just run the Service in the Visual Studio debugger, of course there is nothing on the command line and therefore these blocks of code do not get executed.

The next feature is a cool trick that let's you run your service as a "plain old executable" in the debugger, and not as a Service.  if the project is being run in Debug mode, this is what gets called:

// debug code: allows the process to run as a non-service
// will kick off the service start point, but never kill it
// shut down the debugger to exit
TestService service = new TestService();
service.OnStart(null);
Thread.Sleep(Timeout.Infinite);


Next, let's look at the ProjectInstaller class and review some additional features.

using System;
using System.Collections;
using System.ComponentModel;
using System.Configuration.Install;

using PAB.ServiceUtils;

namespace TestService
{
/// <summary>
/// Summary description for ProjectInstaller.
/// </summary>
[RunInstaller(true)]
public class ProjectInstaller : System.Configuration.Install.Installer
{
private System.ServiceProcess.ServiceProcessInstaller serviceProcessInstaller1;

// Change this to use the PAB.ServiceUtils.ServiceInstallerEx class
// private System.ServiceProcess.ServiceInstaller serviceInstaller1;

private PAB.ServiceUtils.ServiceInstallerEx serviceInstaller1;

/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;

private string failCommand = (string)System.Configuration.ConfigurationSettings.AppSettings["failCommand"];

public ProjectInstaller()
{
// This call is required by the Designer.
InitializeComponent();

// TODO: Add any initialization after the InitializeComponent call

// NOTE: Setting these properties here does not make them immediately effective.   
// The ServiceInstallerEx configures the service AFTER the base installer is done
// doing its job. That is when the Committed event is fired from the base installer

// Set a description
this.serviceInstaller1.Description = "Test service with autostart!";

// The fail run command is used to spawn another process when this service fails
// it should include the entire command line as would be passed to Win32::CreateProcess()
this.serviceInstaller1.FailRunCommand = this.failCommand ;

// The fail count reset time resets the failure count after N seconds of no failures
// on the service.  This value is set in seconds, though note that the SCM GUI only
// displays it in increments of days.
this.serviceInstaller1.FailCountResetTime = 60*60*24*4;

// The fail reboot message is used when a reboot action is specified and works in
// conjunction with the RecoverAction.Reboot type.

this.serviceInstaller1.FailRebootMsg = "Error- failed reboot restart" ;

// Set some failure actions : Isn't this easy??
// Do note that if you specify less than three actions, the remaining actions will take on
// the value of the last action.  For example, if you only set one action to RunCommand,
// failure 2 and failure 3 will also take on the default action of RunCommand. This is
// a "feature" of the ChangeServiceConfig2() method; Use RecoverAction.None to disable
// unwanted actions.

this.serviceInstaller1.FailureActions.Add( new FailureAction( RecoverAction.Restart, 60000)  );
this.serviceInstaller1.FailureActions.Add( new FailureAction( RecoverAction.RunCommand, 2000 ) );
this.serviceInstaller1.FailureActions.Add( new FailureAction( RecoverAction.Reboot, 3000 ) );

// Configure the service to start right after it is installed.  We do not want the user to
// have to reboot their machine or to have some other process start it.  Do be careful because
// if this service is dependent upon other services, they must be installed PRIOR to this one
// for the service to be started properly

this.serviceInstaller1.StartOnInstall = true;

}

/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}

#region Component Designer generated code
/// <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 PAB.ServiceUtils.ServiceInstallerEx();
//
// serviceProcessInstaller1
//
this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem;
this.serviceProcessInstaller1.Password = null;
this.serviceProcessInstaller1.Username = null;
//
// serviceInstaller1
//
this.serviceInstaller1.ServiceName = ".TestService";
this.serviceInstaller1.AfterInstall += new System.Configuration.Install.InstallEventHandler(this.serviceInstaller1_AfterInstall);
//
// ProjectInstaller
//
this.Installers.AddRange(new System.Configuration.Install.Installer[] {  this.serviceProcessInstaller1, this.serviceInstaller1});

}
#endregion

private void serviceInstaller1_AfterInstall(object sender, System.Configuration.Install.InstallEventArgs e) {

}

}
}

The above code enables us to set a  Fail Command (which you would normally have to do manually in the Service Control Manager UI), and  set the Account. But it also runs the ServiceInstallerEx class (which you can view in the solution) that lets us set a whole bunch of additional Service features: reboot message, Fail Count reset time, Fail run command, and others, all programmatically when the service is installed. We can also make the service autostart as soon as it is installed.

Finally, let's look at how the business logic is implemented:

Note in the OnStart method:

protected override void OnStart(string[] args)
{
test = new TestLib();
test.Start();
}


So we create an instance of our business logic class, and call its Start method:

namespace TestLibrary
{
using System;
using System.Configuration;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Threading;
using PAB.Data.Utils;

/// <summary>
/// Summary description for Class1.
/// </summary>
public class TestLib
{
public Thread runThread = null;
public bool running = false;
public DataSet cacheDs = null;
private static String fileToWatch = Environment.CurrentDirectory + @"\" + ConfigurationSettings.AppSettings["fileToWatch"].ToString();
private FileSystemWatcher watcher;
private string connString = ConfigurationSettings.AppSettings["connectionString"];

        /**** IMPORTANT!**** TO ENABLE xp_CmdShell:
         EXEC sp_configure 'show advanced options', 1
         GO
         -- To update the currently configured value for advanced options.
         RECONFIGURE
         GO
         -- To enable the feature.
         EXEC sp_configure 'xp_cmdshell', 1
         GO
         -- To update the currently configured value for this feature.
         RECONFIGURE
         GO
*/

public TestLib()
{

string strTrigger = "CREATE TRIGGER trg_WriteCacheDepFile ON [dbo].[Employees] ";
strTrigger += "FOR INSERT, UPDATE, DELETE AS ";
    strTrigger += @"EXEC master..xp_cmdshell  'echo  > ";
    strTrigger +=@"\\" +Environment.MachineName;
strTrigger += @"\C$\";
string tmp = fileToWatch.Replace(@"C:\", "");
            strTrigger += tmp +"'";  

string strRemoveTrigger = "DROP TRIGGER trg_WriteCacheDepFile";
int res = 0;
try
{
res = SqlHelper.ExecuteNonQuery(connString, CommandType.Text, strRemoveTrigger);
Debug.WriteLine("Remove Trigger: " + res.ToString());
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message + ex.StackTrace);
}
try
{
res = SqlHelper.ExecuteNonQuery(connString, CommandType.Text, strTrigger);
Debug.WriteLine("CREATE Trigger: " + res.ToString());

}
catch (Exception ex)
{
Debug.WriteLine(ex.Message + ex.StackTrace);
}

Debug.Write(strTrigger);
if (!File.Exists(fileToWatch))
{
FileStream fs = File.Create(fileToWatch);
fs.Write(new byte[] {1, 2, 3, 4}, 0, 4);
fs.Close();

}
watcher = new FileSystemWatcher(Environment.CurrentDirectory, ".txt");
watcher.EnableRaisingEvents = true;
watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite;
watcher.Filter = "mycache.txt";
}

public void Start()
{
runThread = new Thread(new ThreadStart(MainLoop));
runThread.IsBackground = true;
running = true;
watcher.Changed += new FileSystemEventHandler(watcher_Changed);
ReloadMyCache();
try
{
runThread.Start();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message + ex.StackTrace);
}
}

public void Stop()
{
running = false;
watcher.Dispose();
runThread.Join();

}

public void MainLoop()
{
while (running)
{
// Your business logic can go here here
}


}

private void ReloadMyCache()
{
try
{
this.cacheDs = SqlHelper.ExecuteDataset(connString, CommandType.Text ,"SELECT * FROM EMPLOYEES", null);
}
catch (Exception ex)
{
EventLog.WriteEntry("Stopping Service: Cache", ex.Message + ex.StackTrace);
//Want to see how to STOP a running service dead in its tracks (if necessary)?
//throw new InvalidOperationException("Service stopped due to unrecoverable error:" + ex.Message);
}

EventLog.WriteEntry("Cache", "MyCache Reloaded.");

}

private void ReloadMyOtherCache()
{
try
{
                 this.cacheDs = SqlHelper.ExecuteDataset(connString, CommandType.Text, "SELECT * FROM EMPLOYEES", null);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message + ex.StackTrace);
}
EventLog.WriteEntry("Cache", "MyCache Reloaded.");

}

private DateTime lastChangeTime;
private string lastFilePath;

private void watcher_Changed(object sender, FileSystemEventArgs e)
{
// handle issue with FileSystemWatcher where multiple events are fired.
// P.S. It's not necessarily a BUG, its just that we don't want this behavior.
TimeSpan span = DateTime.Now.Subtract(lastChangeTime);
if (span.TotalSeconds > 2 || lastFilePath != e.FullPath)
{
// wait a second for any locks to be released
// before taking actions
Thread.Sleep(1000);
EventLog.WriteEntry("TestLib", "Cache Invalidated at " + e.FullPath + ": " + e.ChangeType + DateTime.Now.ToString());
// you can have a switch statement here for various different cache items,
// each with its own different monitored file name...
if (e.FullPath.ToLower().IndexOf("mycache.txt") > -1)
{
ReloadMyCache();
// update bugHelpers
lastChangeTime = DateTime.Now;
lastFilePath = e.FullPath;
}

if (e.FullPath.ToLower().IndexOf("myothercache.txt") > -1)
{
ReloadMyOtherCache();
// update bugHelpers
lastChangeTime = DateTime.Now;
lastFilePath = e.FullPath;
}
}
}
}
}


The start method:

public void Start()
{
runThread = new Thread(new ThreadStart(MainLoop));
runThread.IsBackground = true;
running = true;
watcher.Changed += new FileSystemEventHandler(watcher_Changed);
ReloadMyCache();
try
{
runThread.Start();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message + ex.StackTrace);
}
}


This creates a new thread that invokes the MainLoop method, setting it to a background thread. It sets the running boolean to "true". MainLoop therefore continues running forever until "running" is set to false by the OnStop method. This is how you want to set up your business logic in a Windows Service. You want it to run on a background thread. You do not want any business logic in the Service's OnStart method at all.

You can use this solution as a "template" to develop virtually any kind of Windows Service, and have the installation features you want. In order to get the demo to work properly you need to enable xp_CmdShell in SQL Server. The code to do that is in a comment block in the solution.

You can download the Visual Studio 2010 Demo Solution here.

By Peter Bromberg   Popularity  (4116 Views)