.NET Geek

"It is upon the Trunk that a gentleman works" - Confucius

Self Installing Windows Service

Recently I had to initiate some background worker in a Windows Service. Writing a Windows Service with the built in .Net support is a  no-brainer. The thing that caught me by surprise was that all the documentation states that I needed to add an installer for the service to run. After a little looking  around, it turned out that it is not so difficult to create your own custom installer. I ended up writing a small class that can handle any service and thought I’d share it here.

The Windows Service part functions only as a bootstrapper for the real code that needs to run. I wanted to be able to run the code both in service mode and as a standard console application. So here is a dummy service with the installer. The dummy service just writes a single entry to the event log.

A few shortcuts:
I have hardcoded the logging for traceability, but in a real scenario you would probably want to inject a logger.
The library code is in the same assembly and namespace as the service. You would want to extract that out.

The library code that needs to be run either in service or console mode looks like the following.

using System;

using System.Diagnostics;

using System.Threading;

namespace WindowsService1

{

    public class Scheduler

    {

        private Timer _timer;

 

        public void Start()

        {

            // fire timer once

            _timer = new Timer(OnTimer, null, 0, Timeout.Infinite);

        }

 

        private void OnTimer(object state)

        {

            _timer = null;

            EventLog.WriteEntry("TestScheduler", DateTime.Now + " From scheduler...");

        }

    }

}

I don’t think there’s any need to elaborate on the code above, so we’ll jump to the console application.

Let’s walk through the console application code. The class that handles the operations of the service is the ServiceManager class. In the code below there’s a _commands dictionary that maps between command line arguments and the actions that you can request the ServiceManager to do. Next is the Main() method. Main() serves as a common entry point for several paths of execution. In order to run the code inside Visual Studio, you need to supply a command line argument. You can enter one in the Debug tab in the project settings. Main is entered both when run by you either from a console and when Windows is starting the service. When Windows starts the service it calls Main with no parameters.
The first line in Main(), is probably the most important one. Since you can’t debug a Windows Service by pressing F5 in Visual Studio, you need an alternate way of breaking into the debugger when the application is running in service mode. The Debugger.Break() method will let you break into the service code. A small helper method retrieves the ServiceManagerCommand from the command line argument that you passed to Main. Based on the mapping the code will either start in console mode or execute a command on the service.

using System;

using System.Collections.Generic;

using System.Diagnostics;

using System.ServiceProcess;

using System.Reflection;

 

namespace WindowsService1

{

    static class Program

    {

        /// <summary>

        /// Name of the service as defined in the Service component.

        /// </summary>

        private const string ServiceName = "Service1";

        /// <summary>

        /// Mapping of console command line args to ServiceManagerCommands.

        /// </summary>

        private static Dictionary<string, ServiceManagerCommand> _commands = new Dictionary<string, ServiceManagerCommand>

        {

            {"-console", ServiceManagerCommand.Application},

            {"-install", ServiceManagerCommand.Install},

            {"-uninstall", ServiceManagerCommand.UnInstall},

            {"-start", ServiceManagerCommand.Start},

            {"-stop", ServiceManagerCommand.Stop}

        };

 

        static void Main(string[] args)

        {

            try

            {

                //Debugger.Break(); // Uncomment to debug startup problems

                if (args.Length == 0)

                {

                    ServiceMain();

                    return;

                }

                ServiceManagerCommand command;

                if (!TryParseCommandLine(args, out command))

                {

                    PrintUsage();

                    return;

                }

                ServiceManager serviceManager = new ServiceManager(ServiceName);

                if (command == ServiceManagerCommand.Application)

                {

                    Console.WriteLine("Running in console mode.");

                    // writes an event to the event log

                    Scheduler scheduler = new Scheduler();

                    scheduler.Start();

                    Console.Read();

                }

                else

                {

                    serviceManager.RunCommand(command);

                }

            }

            catch (Exception ex)

            {

                EventLog.WriteEntry("WindowsService1",
                                    ex.ToString(), EventLogEntryType.Error);

            }

        }

 

        private static void ServiceMain()

        {

            ServiceBase[] ServicesToRun;

            ServicesToRun = new ServiceBase[]

                            {

                                new Service1()

                            };

            ServiceBase.Run(ServicesToRun);

        }

 

        private static bool TryParseCommandLine(string[] args,
                                                out ServiceManagerCommand command)

        {

            command = ServiceManagerCommand.Unknown;

            if (args.Length > 1)

                return false;

            string commandLineArg = args[0];

            if (_commands.ContainsKey(commandLineArg))

            {

                command = _commands[commandLineArg];

                return true;

            }

            return false;

        }

 

        private static void PrintUsage()

        {

            string exeName = Assembly.GetExecutingAssembly().ManifestModule.Name;

            Console.WriteLine("Usage:");

            foreach (var item in _commands)

            {

                Console.WriteLine("  " + exeName + " "  + item.Key);

            }

            Console.Read();

        }

    }

}

The above is only one sample way to instantiate the service. You could use a WinForms approach instead if that suits your scenario better.

The ServiceManager class handles all interactions with the Windows Service infrastructure. Similar to the mapping between command line arguments and commands in Main above, we have a mapping dictionary that maps between commands and the actual actions. The dictionary then contains a collection of commands and the delegate to execute for that command. The nice thing about this (somewhat strange?) technique, is that it saves us from using switch or if/else statements to determine what needs to be done. If we need to add a new command, we only need to add it to the mapping and write the actual code for the command.

In order to interact with Windows Services, we use the ServiceController BCL class. Through this class we can both execute commands and query for status for any service. I added two helper properties IsServiceInstalled and IsServiceRunning to show how this can be achieved.

using System;

using System.Collections;

using System.Collections.Generic;

using System.Configuration.Install;

using System.Diagnostics;

using System.ServiceProcess;

using System.Threading;

 

namespace WindowsService1

{

    /// <summary>

    /// Commands that can be executed for a service.

    /// Note: Application is not really a service command, 
    /// but we let the client use it as a marker

    /// for not being in service mode.

    /// </summary>

    public enum ServiceManagerCommand

    {

        Unknown,

        Application,

        Install,

        UnInstall,

        Start,

        Stop,

    }

 

    /// <summary>

    /// Install, Uninstall, Start and Stop services.

    /// </summary>

    public class ServiceManager

    {

        Dictionary<ServiceManagerCommand, Action> _commands;

        protected string _serviceName { get; set; }

 

        /// <summary>

        /// Initialize a ServiceManager.

        /// </summary>

        /// <param name="serviceName">
        /// The name of the service as defined in the Service component.
        /// </param>

        public ServiceManager(string serviceName)

        {

            _serviceName = serviceName;

            InitializeCommands();

        }

 

        private void InitializeCommands()

        {

            _commands = new Dictionary<ServiceManagerCommand, Action>

            {

                {ServiceManagerCommand.Install, InstallService},

                {ServiceManagerCommand.UnInstall, UninstallService},

                {ServiceManagerCommand.Start, StartService},

                {ServiceManagerCommand.Stop, StopService},

            };

        }

 

        public void RunCommand(ServiceManagerCommand command)

        {

            if (_commands.ContainsKey(command))

            {

                _commands[command]();

            }

        }

 

        public virtual bool IsServiceInstalled()

        {

            using (ServiceController serviceController = new ServiceController(_serviceName))

            {

                try

                {

                    ServiceControllerStatus status = serviceController.Status;

                }

                catch (InvalidOperationException)

                {

                    return false;

                }

                catch (Exception ex)

                {

                    EventLog.WriteEntry("ServiceManager",
                                        ex.ToString(), EventLogEntryType.Error);

                    return false;

                }

                return true;

            }

        }

 

        public bool IsServiceRunning()

        {

            using (ServiceController serviceController = new ServiceController(_serviceName))

            {

                if (!IsServiceInstalled())

                    return false;

                return (serviceController.Status == ServiceControllerStatus.Running);

            }

        }

 

        protected virtual void InstallService()

        {

            if (IsServiceInstalled())

                return;

            try

            {

                string[] commandLine = new string[1];

                commandLine[0] = "Test install";

                IDictionary mySavedState = new Hashtable();

                AssemblyInstaller installer = GetAssemblyInstaller(commandLine);

                try

                {

                    installer.Install(mySavedState);

                    installer.Commit(mySavedState);

                }

                catch (Exception ex)

                {

                    installer.Rollback(mySavedState);

                    EventLog.WriteEntry("ServiceManager",
                                        ex.ToString(), EventLogEntryType.Error);

                }

            }

            catch (Exception ex)

            {

                EventLog.WriteEntry("ServiceManager", ex.ToString());

            }

        }

 

        private AssemblyInstaller GetAssemblyInstaller(string[] commandLine)

        {

            AssemblyInstaller installer = new AssemblyInstaller();

            installer.Path = Environment.GetCommandLineArgs()[0];

            installer.CommandLine = commandLine;

            installer.UseNewContext = true;

            return installer;

        }

 

        protected virtual void UninstallService()

        {

            if (!IsServiceInstalled())

                return;

            string[] commandLine = new string[1];

            commandLine[0] = "Test Uninstall";

            IDictionary mySavedState = new Hashtable();

            mySavedState.Clear();

            AssemblyInstaller installer = GetAssemblyInstaller(commandLine);

            try

            {

                installer.Uninstall(mySavedState);

            }

            catch (Exception ex)

            {

                EventLog.WriteEntry("ServiceManager",
                                    ex.ToString(), EventLogEntryType.Error);

            }

        }

 

        protected virtual void StartService()

        {

            if (!IsServiceInstalled())

                return;

            using (ServiceController serviceController = new ServiceController(_serviceName))

            {

                if (serviceController.Status == ServiceControllerStatus.Stopped)

                {

                    try

                    {

                        serviceController.Start();
                        WaitForStatusChange(serviceController, ServiceControllerStatus.Running);

                    }

                    catch (InvalidOperationException ex)

                    {

                        EventLog.WriteEntry("ServiceManager",

                                            ex.ToString(), EventLogEntryType.Error);

                    }

                }

            }

        }

 

        protected virtual void StopService()

        {

            if (!IsServiceInstalled())

                return;

            using (ServiceController serviceController = new ServiceController(_serviceName))

            {

                if (serviceController.Status != ServiceControllerStatus.Running)

                    return;

                serviceController.Stop();

                WaitForStatusChange(serviceController, ServiceControllerStatus.Stopped);

            }

        }

 

        /// <summary>

        /// After a service has been installed, uninstalled, started or stopped,

        /// it might take some time

        /// for the action to complete. Wait here until we get the new status or time out.

        /// </summary>

        /// <param name="serviceController"></param>

        /// <param name="newStatus"></param>

        private static void WaitForStatusChange(ServiceController serviceController,
                                                ServiceControllerStatus newStatus)

        {

            int count = 0;

            while (serviceController.Status != newStatus && count < 30)

            {

                Thread.Sleep(1000);

                serviceController.Refresh();

                count++;

            }

            if (serviceController.Status != newStatus)

            {

                throw new Exception("Failed to change status of service. New status: " + newStatus);

            }

        }

    }

}

If you have any comments and/or improvements I’d be happy to see them.

And as always,

Happy Coding!

 

kick it on DotNetKicks.com

Comments

binaryelves said:

thanks for that!

# January 4, 2009 7:50 PM

Avi Pinto said:

thanks

# January 4, 2009 8:50 PM

Shlomo said:

מאוד נחמד, אני כנראה אשתמש בזה.

תודה

# January 4, 2009 9:03 PM

Kim said:

Update:

Fix: GetAssemblyInstaller() now uses Environment.GetCommandLineArgs()[0] to retrieve the name of the executable instead of GetExecutingAssembly() which would return the assembly containing the ServiceManager class. If ServiceManager was put in a DLL for reuse, it would set the wrong path.

# January 6, 2009 12:28 AM

Reflective Perspective - Chris Alcock » The Morning Brew #258 said:

Pingback from  Reflective Perspective - Chris Alcock  &raquo; The Morning Brew #258

# January 6, 2009 10:53 AM

Vasileios Fasoulas said:

This post was very useful (and with a very elegant design as well).

A useful expansion would be to replace the timer approach with a threaded approach. See

en.csharp-online.net/Creating_a_.NET_Windows_Service  for an overview

# January 6, 2009 3:42 PM

Chris Carter said:

FYI - I had problems implementing this technique when I created a Windows Service Application using the Visual Studio Template then trying to use the above code in the Program.cs that it generated. That template generates a project which is built as a Windows Application as opposed to a Console Application.  Switching the build to a Console Application in the project properties fixed the issue.

I'm a bit of a newbie in this area, and I haven't gotten to the point where the service install occurs (dealing with an Event Log issue), so I'm not sure what the ramifications of that change on the rest of the functionality will be.

# January 9, 2009 3:06 AM

.NET Geek said:

In our current project we have a service that handles messages sent to the application over MSMQ. On

# January 11, 2009 12:11 AM

#.think.in said:

#.think.in infoDose #15 (5th Jan - 9th Jan)

# January 12, 2009 12:57 AM

Mr. T said:

Can this code to converted to VB.NET?

# May 2, 2009 5:21 AM

David Hoyt said:

I handled this way back in 2005: www.codeproject.com/.../hoytsoft_servicebase.aspx

You can find the latest version on codeplex as part of the asp.net enterprise suite project: www.codeplex.com/aspnetSuite

# August 13, 2009 9:55 PM
Leave a Comment

(required) 

(required) 

(optional)

(required) 


Enter the numbers above: