I have seen a lot of questions on the Forums about Windows Services. The questions run anywhere from “How can I debug my Service?” to “How can I see the MessageBox my Service is showing?” (hint – you can’t do that last one, it’s not possible to use any UI features in a Windows Service).
So, what’s the secret to making Windows Services easy? Well, first of all, begin by hosting your Service(s) in a Console Application. The transition from tinkering with it while you’re developing, to deploying it as a Windows Service, is made a lot easier this way.
Your Console Application project will contain 4 files: app.config, Program.cs, Services.cs and ProjectInstaller.cs. You could, in a real application, have separate files for the Services and for the ServiceHost. I’ve simplified by putting it all in the same file.
The Program.cs starts everything running (as is normally the case with any Console app):
namespace ConsoleApplication1 { class Program { static MyServiceHost oService; // Be sure to add a System.ServiceProcess reference and using static void Main(string[] args) { if (Environment.UserInteractive == false) { ServiceBase[] ServicesToRun; ServicesToRun = new ServiceBase[] { new MyServiceHost() // others may be added as follows, we only have one // , new MyOtherHost() }; ServiceBase.Run(ServicesToRun); } else { if (ConfigurationManager.AppSettings.Count > 0) { oService = new MyServiceHost(); oService.Start(); Console.ReadLine(); oService.Stop(); } else { Console.WriteLine("Config file is missing ..."); Console.ReadLine(); } } } } }
Notice in the above code we check to see whether or not we’re running interactively. A Console application would be interactive, a Windows Service would not. You should be able to see how that works from looking at the above code.
Next, the Services and ServiceHost classes:
namespace ConsoleApplication1 { // Service classes are just regular classes, that have only one requirement: // They must implement a Start and Stop method. public class MyServiceOne { public void Start() { // startup code here } public void Stop() { // stop code here } } public class MyServiceTwo { public void Start() { // startup code here } public void Stop() { // stop code here } } // Make sure that you add a reference to System.ServiceProcess in your project references // I've got threading in this class, be sure to also add a reference to System.Threading public partial class MyServiceHost : System.ServiceProcess.ServiceBase { // This Windows Service can host as many Service classes as you want static MyServiceOne ServiceOne; static MyServiceTwo ServiceTwo; private Thread SqlThread; private bool EndLoop = false; private string ConnectionString; public MyServiceHost() { this.CanStop = true; this.CanShutdown = true; this.CanPauseAndContinue = false; } protected override void OnStart(string[] args) { // In the case of my Services, I use SqlServer. I need to be sure it's up and running // before my Services start. How could this not be the case? Well, machines could have been // rebooted because of Windows Updates or for other kinds of maintenance. Whether or not // SqlServer is on the same machine as your Services really doesn't matter. Your Services // could start before SqlServer starts. I handle this possiblity by waiting for SqlServer // to start first. This is done in the DoWork() method. ThreadStart threadStart = new ThreadStart(DoWork); this.SqlThread = new Thread(threadStart); this.SqlThread.Start(); } protected override void OnStop() { if (this.EndLoop == false) this.EndLoop = true; // Notice this interesting tidbit (it applies to my app, but depending on what // your services do, it may also apply to yours ... so, consider this just an FYI): // When I start my services (in the StartService() method), I start ServiceOne and then ServiceTwo // When I stop them, I stop them in the opposite order, first stop ServiceTwo and then ServiceOne. if (ServiceTwo != null) ServiceTwo.Stop(); // Sleep for a second if you need a bit of time between service's stopping System.Threading.Thread.Sleep(1000); if (ServiceOne != null) ServiceOne.Stop(); } protected override void OnShutdown() { // There may be a bug in the .NET Framework in that this method // is not being called on system shutdown for SYSTEM account services. // Not sure when/if it will be fixed, but I'm putting code here to stop // stuff on the off-chance that it will be called eventually. if (ServiceTwo != null) ServiceTwo.Stop(); System.Threading.Thread.Sleep(1000); if (ServiceOne != null) ServiceOne.Stop(); } protected void DoWork() { this.ConnectionString = ConfigurationManager.ConnectionStrings["MyConnectionString"].ConnectionString; ; if (string.IsNullOrEmpty(this.ConnectionString)) { Console.WriteLine("No MyConnectionString or No Config File."); return; } while (this.EndLoop == false) { if (this.SqlServerIsRunning()) { this.StartService(); EndLoop = true; } else { if (Environment.UserInteractive) Console.WriteLine("Waiting for SQL Server ..."); Thread.Sleep(30000); // 30 seconds } } } protected bool SqlServerIsRunning() { bool IsConnected = false; try { using (SqlConnection conn = new SqlConnection(this.ConnectionString)) { conn.Open(); IsConnected = true; } } catch { } return IsConnected; } protected void StartService() { ServiceOne = new MyServiceOne(); ServiceTwo = new MyServiceTwo(); ServiceOne.Start(); ServiceTwo.Start(); } // These 2 methods are only used when not runnning as a Service, for testing from a Console window. public void Start() { this.OnStart(null); Console.WriteLine("Services Started"); } public void Stop() { Console.WriteLine("Services Stopping ..."); this.OnStop(); } } }
OK, well, that was pretty painless, right? Go ahead and run this Console application. If you’d like to expand on ServiceOne and ServiceTwo to actually do something, go ahead and spin off some new threads or something … whatever your services might have to do. You can set breakpoints in Visual Studio and debug to your heart’s content. You’ll notice that as soon as you hit the Enter key in the Console window, the service will stop.
Now, that’s all there is to hosting a Service in a Console Application. Once you’ve got it all debugged and you’re ready to go, you’re going to want to install your service as a Windows Service. This is also not too difficult. You’ll have to add an Installer class to your project. You can right-click your project, “Add New Item” and choose “Installer Class” … however, that adds some stuff that you’ll just have to take out anyway (it adds a Designer.cs class and you really don’t need that). Instead, just right-click and add a new class and copy my Installer class as a starting point and it’s much easier. Here it is:
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Configuration.Install; using System.Linq; namespace ConsoleApplication1 { [RunInstaller(true)] public partial class MyServiceInstaller : System.Configuration.Install.Installer { #region Declarations private System.ServiceProcess.ServiceProcessInstaller serviceProcessInstaller1; private System.ServiceProcess.ServiceInstaller serviceInstaller1; private System.ServiceProcess.ServiceInstaller serviceInstaller2; public string ServiceName { get { return this.serviceInstaller1.ServiceName; } set { this.serviceInstaller1.ServiceName = value; } } public string ServiceAccount { set { switch (value.ToLower()) { case "user" : this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.User; break; case "localservice" : this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalService; break; case "networkservice" : this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.NetworkService; break; } } } public override string HelpText { get { return base.HelpText + "\r\n\r\nSpecific to My Messaging: \r\n\r\n" + "/ServiceName=[servicename] \r\n" + " Use if installing multiple My Messaging Services. \r\n" + " Defaults to My.MessagingService if switch not used.\r\n\r\n" + "/ServiceAccount [User] | [LocalService] | [NetworkService] \r\n" + " Use if not installing under the System account. \r\n\r\n"; } } private bool DebugIt = false; #endregion #region Constructor public MyServiceInstaller() { //InitializeComponent(); // do our own this.serviceProcessInstaller1 = new System.ServiceProcess.ServiceProcessInstaller(); this.serviceInstaller1 = new System.ServiceProcess.ServiceInstaller(); this.serviceInstaller1.StartType = System.ServiceProcess.ServiceStartMode.Manual; this.serviceInstaller1.ServicesDependedOn = new string[] { "Message Queuing", "Distributed Transaction Coordinator" }; // // serviceProcessInstaller1 // this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem; this.serviceProcessInstaller1.Password = null; this.serviceProcessInstaller1.Username = null; // // ProjectInstaller // this.Installers.AddRange(new System.Configuration.Install.Installer[] { this.serviceProcessInstaller1, this.serviceInstaller1}); } #endregion #region Methods protected override void OnBeforeInstall(IDictionary savedState) { base.OnBeforeInstall(savedState); this.SetPropertiesFromCommandLineSwitches(); this.SetDebugService(); } protected override void OnBeforeUninstall(IDictionary savedState) { base.OnBeforeUninstall(savedState); this.SetPropertiesFromCommandLineSwitches(); this.SetDebugService(); } private void SetPropertiesFromCommandLineSwitches() { // command line switches: /ServiceName= /ServiceAccount= (and /debug= but not really using debug right now) // See HelpText property in the Declarations and the ServiceName and Service account properties string name = this.Context.Parameters["ServiceName"]; if (string.IsNullOrEmpty(name)) this.ServiceName = "My.MessagingService"; else this.ServiceName = name; string account = this.Context.Parameters["ServiceAccount"]; if (string.IsNullOrEmpty(account) == false) this.ServiceAccount = account; if (this.Context.Parameters.ContainsKey("debug")) this.DebugIt = true; } private void SetDebugService() { if (this.DebugIt) { this.serviceInstaller2 = new System.ServiceProcess.ServiceInstaller(); this.serviceInstaller2.ServiceName = "My.Debug.Service"; this.Installers.Add(this.serviceInstaller2); } } #endregion } }
Next, we’ll need just a few lines of code to execute via a Command line. It’s easiest to create two batch files, one for installing and one for uninstalling. The .NET Framework has an Install Utility called “InstallUtil.exe”, located in the c:\Windows\Microsoft.NET\Framework folder. If you have multiple versions of the .NET Framework installed on a machine, you must make sure you use the correct InstallUtil, depending on the .NET version you’ve targeted in your project properties. It’s easy to do with batch files. Here’s a batch file to install the above Service:
@echo off REM change if yours is a different path set INSTALL_UTIL_HOME=C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319 set PATH=%PATH%;%INSTALL_UTIL_HOME% echo . echo . echo Installing Service My.MessagingService echo . echo . installutil My.MessagingService.exe echo . echo Done. echo . pause
To uninstall, your batch file will be essentially the same except you’d echo Uninstalling Service, and you’d use the /u parameter:
@echo off REM change if yours is a different path set INSTALL_UTIL_HOME=C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319 set PATH=%PATH%;%INSTALL_UTIL_HOME% echo . echo . echo Uninstalling Service My.MessagingService echo . echo . installutil /u My.MessagingService.exe echo . echo Done. echo . pause
That’s all there is to it. Have fun and happy coding! =0)