Thursday, October 17, 2013

Easy Windows Services

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)

8 comments:

  1. Hi Bonnie, nice article - one thing I've found useful when testing services is the ability to stop and start your service quickly without going into the control panel applets, this can be achieved by typing this at a command prompt ( I'm sure you know but others might not )

    NET START yourservice
    NET STOP yourservice

    ReplyDelete
    Replies
    1. Hi Pete! Thanks for your comments ... yes, the command prompt start/stop is much quicker.

      Delete
  2. This is actually a very neat approach. What I personally like doing is modifying MSBuild to stop the service if it exists, and to install it if it is not already there, and finally restarting it at the end of a build.

    I do prefer your approach though as it avoids the need to 'attach debugger' :)

    I could suggest taking it yet a step further. If you modify it to accept command line arguments, you can use ManagedInstallerClass.InstallHelper to install the service via the command line.

    ReplyDelete
    Replies
    1. Thanks edd, I'm glad you like my approach.

      I'm curious about your suggestion about ManagedInstallerClass.InstallHelper. I've searched a bit for examples and it seems like this would be used instead of a batch file? Is that correct? In my existing examples that I posted, where would you use the ManagedInstallerClass.InstallHelper?

      If this takes the place of my batch file, does it still call installutil.exe to do the dirty work?

      Delete
  3. hi bonnie. if did all that you showed from the install section for my windows service. if i run the bat file i get the following error:

    An exception occurred during the Install phase.
    System.Security.SecurityException: The source was not found, but some or all event logs could not be searched. Inaccessible logs: Security.


    after some googling i found i need elevated privilages in order to run it. i tried running the bat file "as administrator", then it gives me the following error:

    Exception occurred while initializing the installation:System.IO.FileNotFoundException: Could not load file or assembly 'file:///C:\Windows\system32\NoTwitFace.exe' or one of its dependencies. The system cannot find the file specified..

    i notice that it is now trying to find my exe in windows\system32.
    i went back and tried the bat file with out admin rights it selects the correct source but doesnt have the security requirements to run installutil.

    ReplyDelete
    Replies
    1. What OS are you running on? I've not had a problem installing a service on XP, Win 7 and 2008R2 server. But I am usually logged onto my machines with Administrator rights ... I don't know if that's got anything to do with it or not. I've not tried to run the batch files "as administrator", but I wouldn't think that would cause it to look for your EXE in the system32 folder, that's really strange!

      Delete
    2. When you elevate a batch file, it's working directory changes to something different then it's position. That will naturally disrupts all the relative paths, causing all kinds of havok.

      The most solid solution is to preface all the relative paths with "%~dp0\". This is a batch variable for "batchfiles drive and path".

      You can just put in one for a change directory at the top, but that might not work if your batch resides on a network file (as then drive then reads "//[network share name])"

      Delete
  4. This comment has been removed by the author.

    ReplyDelete