A Windows Service hybrid Console that installs itself

I have sent this previously to a few of my customers when I see them having issues around console applications and services and the whole installer issue. This is nothing new, I just thought it was time I wrote it down to help others. The normal symptoms I see are two projects; one for a service and one for a console application and then another for a wix project to install the service. This can all be done in a single project, in fact in three files.

Step one

First create a new Console project in Visual Studio. Right click in your project and add an Installer. Let’s call this ContosoInstaller.cs. This is just a special type of class that also has a designer associated with it, but we won’t use the designer. Press F7 and it should switch to the code view. Let’s break this down, as by default Visual Studio will create a class that derives from Installer. Delete all the scaffolding code and replace it with the code below.

 using System.ComponentModel;
using System.ServiceProcess;

namespace Contoso
{
    [RunInstaller(true)]
    public sealed class ContosoServiceInstaller : ServiceInstaller
    {
        public ContosoServiceInstaller()
        {
            this.Description = "Contoso Service Description";
            this.DisplayName = "Contoso Service Name";
            this.ServiceName = "ContosoServiceName";
            this.StartType = System.ServiceProcess.ServiceStartMode.Manual;
        }
    }

    [RunInstaller(true)]
    public sealed class ContosoServiceProcessInstaller 
        : ServiceProcessInstaller
    {
        public ContosoServiceProcessInstaller()
        {
            this.Account = ServiceAccount.NetworkService;
        }
    }
}

We will use our own type that derives from ServiceInstaller rather than Installer. The purpose of the ServiceInstaller class is to help the installer utility write this information in the registry under HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services. The constructor will define all of the things that you typically see in the Services GUI.

image

The second class is an installer for the process inside the service itself. This is similar to above, apart from this is where the user that the process will run as is defined. The last thing to notice is that both of these classes are decorated with a RunInstaller attribute; this has to be set to true. When the install method is called on the assembly installer it will find all classes decorated with this attribute and install them.

Step two

Next right click on the solution and add a new item of type “Windows Service”. Notice that you will now have a class that implements ServiceBase and it overrides two methods. I have called mine ProcessToExceute, but I am sure you have a better service name. As an example I have simply implemented two methods, one that creates an event log and writes to it for starting up and another one for shutting it down.

 using System.Diagnostics;
using System.ServiceProcess;

namespace Contoso
{
    public class ProcessToExecute : ServiceBase
    {
        EventLog eventLog;

        public void Startup()
        {
            // You will need to create this EventLog
            eventLog = new EventLog("Application", ".", "ContosoServiceName");
            eventLog.WriteEntry("I've started your bespoke process.",
                EventLogEntryType.Information, 1, 1);
        }

        public void Shutdown()
        {
            eventLog.WriteEntry("I've stopped your bespoke process.",
                EventLogEntryType.Information, 2, 2);
        }

        protected override void OnStart(string[] args)
        {
            this.Startup();
        }

        protected override void OnShutdown()
        {
            this.Shutdown();
        }
    }
}

Notice, that the methods that exist on ServiceBase to be overridden will simply point to public methods that

You may have come across problems with the EventLog class in web applications as you have to be an administrator to create them on a machine, which typically a web process does not run as. The beauty of the installer class is that it will automatically create an event source with the ServiceName – in this case ContosoServiceName. The downside is that you have to be an administrator. So let’s look at the next step and the main point of this blog

Step three – a console app that is a service too

I want to be able to run my service as a console application sometimes so that I can see what it is doing during development. Therefore, it needs to be able to have some modes at start up. I also want to be able to copy this program to any machine and have it install itself without having to use WiX or using installutil. The Main method just allows the user to pass in –install, –uninstall or  -console. Hopefully all quite explanatory. This means if I want to I can just copy this exe to a machine and run “my.exe –install” and the service will be installed.

 using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration.Install;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;

namespace Contoso
{
    static class Program
    {
        static int Main(string[] args)
        {
            bool install = false,
                uninstall = false,
                console = false,
                rethrow = false;

            try
            {
                foreach (string arg in args)
                {
                    switch (arg)
                    {
                        case "-i":
                        case "-install":
                            install = true; break;
                        case "-u":
                        case "-uninstall":
                            uninstall = true; break;
                        case "-c":
                        case "-console":
                            console = true; break;
                        default:
                            Console.Error.WriteLine
                                ("Argument not expected: " + arg);
                            break;
                    }
                }

                if (uninstall)
                {
                    Install(true, args);
                }
                if (install)
                {
                    Install(false, args);
                }
                if (console)
                {
                    using (var process = new ProcessToExecute())
                    {
                        Console.WriteLine("Starting...");
                        process.Startup();
                        Console.WriteLine("System running; press any key to stop");
                        Console.ReadKey(true);
                        process.Shutdown();
                        Console.WriteLine("System stopped");
                    }
                }
                else if (!(install || uninstall))
                {
                    rethrow = true; // so that windows sees error...
                    ServiceBase[] services =
                        { new ProcessToExecute() };
                    ServiceBase.Run(services);
                    rethrow = false;
                }
                return 0;
            }
            catch (Exception ex)
            {
                if (rethrow) throw;
                Console.Error.WriteLine(ex.Message);
                return -1;
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design",
            "CA1031:DoNotCatchGeneralExceptionTypes",
            Justification = "Swallow the rollback exception and let" +
                "the real cause bubble up.")]
        internal static void Install(bool undo, string[] args)
        {
            try
            {
                Console.WriteLine(undo ? "uninstalling" : "installing");

                using (AssemblyInstaller inst = 
                    new AssemblyInstaller(typeof(Program).Assembly, args))
                {
                    IDictionary state = new Hashtable();
                    inst.UseNewContext = true;
                    try
                    {
                        if (undo)
                        {
                            inst.Uninstall(state);
                        }
                        else
                        {
                            inst.Install(state);
                            inst.Commit(state);
                        }
                    }
                    catch
                    {
                        try
                        {
                            inst.Rollback(state);
                        }
                        catch
                        {
                            // Swallow the rollback exception
                            // and let the real cause bubble up.
                        }

                        throw;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex.Message);
            }
        }
    }
}