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.