Sunday, July 18, 2010

How to make a service in .NET

Here's a complete, self-contained way to build a system service using .NET. I was looking for a way to do it on the internets, but most of the examples rely on .NET template (which relies on designer, which is ugly) and don't have a way to install the service programmatically.

Without much ado, here's the code. All of it. Just replace ServiceMainThread with your code, and you're done. It even supports installing multiple instances of itself.

//-----------------------------------------------------------------------
// <copyright>
// Copyright (C) Sergey Solyanik.
//
// This file is subject to the terms and conditions of the Microsoft Public License (MS-PL).
// See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL for more details.
// </copyright>
//----------------------------------------------------------------------- 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.ServiceProcess;
using System.Text;
using System.Threading;

namespace CSServiceHost
{
    /// <summary>
    /// Main service class.
    /// </summary>
    public class MyService : ServiceBase
    {
        /// <summary>
        /// The thread that contains the execution path for the service.
        /// </summary>
        Thread runner;

        /// <summary>
        /// Event which gets signalled when the service stops.
        /// </summary>
        EventWaitHandle stop;

        /// <summary>
        /// Processes the start event for service.
        /// </summary>
        /// <param name="args"></param>
        protected override void OnStart(string[] args)
        {
            stop = new EventWaitHandle(false, EventResetMode.ManualReset);
            runner = new Thread(ServiceMainThread);
            runner.Start();
        }

        /// <summary>
        /// Processes the stop event for service.
        /// </summary>
        protected override void OnStop()
        {
            stop.Set();
            runner.Join();
        }

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        /// <param name="args"> Program arguments. See help.</param>
        static void Main(string[] args)
        {
            if (args.Length > 0)
            {
                ProcessServiceCommand(args);
                return;
            }

            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[] 
            { 
                new MyService() 
            };

            ServiceBase.Run(ServicesToRun);
        }

        /// <summary>
        /// Executes service installation/uinstallation, or runs it as a process.
        /// </summary>
        /// <param name="args"> Program arguments.</param>
        private static void ProcessServiceCommand(string[] args)
        {
            string exe = Assembly.GetExecutingAssembly().Location;

            if ("/install".Equals(args[0], StringComparison.OrdinalIgnoreCase))
            {
                object[] assemblyAttributes =
                    Assembly.GetExecutingAssembly().GetCustomAttributes(false);

                string instance =
                    (from a in assemblyAttributes
                     where a is AssemblyTitleAttribute
                     select ((AssemblyTitleAttribute)a).Title).SingleOrDefault();
                string name =
                    (from a in assemblyAttributes
                     where a is AssemblyDescriptionAttribute
                     select ((AssemblyDescriptionAttribute)a).Description).SingleOrDefault();
                string account = null;
                string password = String.Empty;
                for (int i = 1; i < args.Length; ++i)
                {
                    if (args[i].StartsWith(
                        "/instance:", StringComparison.OrdinalIgnoreCase))
                    {
                        instance = args[i].Substring(10);
                    }
                    else if (args[i].StartsWith(
                        "/name:", StringComparison.OrdinalIgnoreCase))
                    {
                        name = args[i].Substring(6);
                    }
                    else if (args[i].StartsWith(
                        "/account:", StringComparison.OrdinalIgnoreCase))
                    {
                        account = args[i].Substring(9);
                    }
                    else if (args[i].StartsWith(
                        "/password:", StringComparison.OrdinalIgnoreCase))
                    {
                        password = args[i].Substring(10);
                    }
                    else
                    {
                        Console.Error.WriteLine("Could not parse: {0}", args[i]);
                    }
                }

                InstallService(exe, instance, name, account, password);
            }
            else if ("/uninstall".Equals(
                args[0], StringComparison.OrdinalIgnoreCase))
            {
                object[] assemblyAttributes =
                    Assembly.GetExecutingAssembly().GetCustomAttributes(false);
                string instance =
                    (from a in assemblyAttributes
                     where a is AssemblyTitleAttribute
                     select ((AssemblyTitleAttribute)a).Title).SingleOrDefault();

                for (int i = 1; i < args.Length; ++i)
                {
                    if (args[i].StartsWith(
                        "/instance:", StringComparison.OrdinalIgnoreCase))
                    {
                        instance = args[i].Substring(10);
                    }
                    else
                    {
                        Console.Error.WriteLine("Could not parse: {0}", args[i]);
                    }
                }

                UninstallService(instance);
            }
            else if ("/run".Equals(args[0], StringComparison.OrdinalIgnoreCase))
            {
                MyService service = new MyService();
                service.OnStart(new string[0]);
                Console.WriteLine("Service is running as a process.");
                Console.WriteLine("Press <ENTER> to stop and exit.");
                Console.ReadLine();
                service.OnStop();
            }
            else
            {
                Console.WriteLine("To install service:");
                Console.WriteLine("    {0} /install", exe);
                Console.WriteLine("        [/instance:instance_name [/name:display_name]]");
                Console.WriteLine("        [/account:account [/password:password]]");
                Console.WriteLine("To uninstall service:");
                Console.WriteLine("    {0} /uninstall [/instance:instance_name]", exe);
                Console.WriteLine("To run as a regular process:");
                Console.WriteLine("    {0} /run", exe);
            }
        }

        /// <summary>
        /// Installs service.
        /// </summary>
        /// <param name="exe"> Path to the executable. </param>
        /// <param name="instance"> Name of the service instance. </param>
        /// <param name="name"> Display name of the service. </param>
        /// <param name="account"> Account name or NULL for LocalSystem. </param>
        /// <param name="password"> Password or empty string if any of
        /// the machine accounts. </param>
        private static void InstallService(
            string exe,
            string instance,
            string name,
            string account,
            string password)
        {
            IntPtr scm = Win32.OpenSCManager(
                null, null, Win32.SC_MANAGER_CREATE_SERVICE);
            if (scm.ToInt32() == 0)
            {
                Console.Error.WriteLine(
                    "Failed to open SCM (error {0}).", Win32.GetLastError());
                return;
            }

            try
            {
                IntPtr service = Win32.CreateService(
                    scm,
                    instance,
                    name,
                    Win32.SERVICE_ALL_ACCESS,
                    Win32.SERVICE_WIN32_OWN_PROCESS,
                    Win32.SERVICE_AUTO_START,
                    Win32.SERVICE_ERROR_NORMAL,
                    exe,
                    null,
                    0,
                    null,
                    account,
                    password);

                if (service.ToInt32() == 0)
                {
                    Console.Error.WriteLine(
                        "Failed to create service (error {0}).",
                        Win32.GetLastError());
                    return;
                }

                try
                {
                    if (Win32.StartService(service, 0, null) == 0)
                    {
                        Console.Error.WriteLine(
                            "Failed to start service (error {0}).",
                            Win32.GetLastError());
                    }
                    else
                    {
                        Console.WriteLine(
                            "Service installed successfully.");
                    }
                }
                finally
                {
                    Win32.CloseServiceHandle(service);
                }
            }
            finally
            {
                Win32.CloseServiceHandle(scm);
            }
        }

        /// <summary>
        /// Uninstalls service.
        /// </summary>
        /// <param name="instance"> Service instance. </param>
        private static void UninstallService(string instance)
        {
            IntPtr scm = Win32.OpenSCManager(
                null, null, Win32.SC_MANAGER_ALL_ACCESS);
            if(scm.ToInt32() == 0)
            {
                Console.Error.WriteLine(
                    "Failed to open SCM (error {0}).",
                    Win32.GetLastError());
                return;
            }

            try
            {
                IntPtr service = Win32.OpenService(
                    scm, instance, Win32.DELETE | Win32.SERVICE_STOP);
                if (service.ToInt32() == 0)
                {
                    Console.Error.WriteLine(
                        "Failed to open service (error {0}).",
                        Win32.GetLastError());
                    return;
                }

                try
                {
                    Win32.SERVICE_STATUS stat;
                    if (0 == Win32.ControlService(
                        service, Win32.SERVICE_CONTROL_STOP, out stat))
                    {
                        Console.Error.WriteLine(
                            "Could not stop the service (error {0}).",
                            Win32.GetLastError());
                    }

                    while (Win32.QueryServiceStatus(service, out stat) != 0
                        && stat.dwCurrentState != Win32.SERVICE_STOPPED)
                    {
                        Thread.Sleep(1000);
                    }

                    if (Win32.DeleteService(service) == 0)
                    {
                        Console.Error.WriteLine(
                            "Failed to delete service (error {0}).",
                            Win32.GetLastError());
                    }
                    else
                    {
                        Console.WriteLine(
                            "Service successfully uninstalled.");
                    }
                }
                finally
                {
                    Win32.CloseServiceHandle(service);
                }
            }
            finally
            {
                Win32.CloseServiceHandle(scm);
            }
        }

        /// <summary>
        /// The actual logic.
        /// </summary>
        private void ServiceMainThread()
        {
            for (; ; )
            {
                if (stop.WaitOne(10000))
                {
                    break;
                }

                using (StreamWriter w =
                    new StreamWriter(@"c:\temp\testservice.txt", true))
                    w.WriteLine(
                        "Tick {0} {1}",
                        DateTime.Now,
                        Environment.UserName);
            }
        }

        /// <summary>
        /// Win32 thunks.
        /// </summary>
        private static class Win32
        {
            public const UInt32 SC_MANAGER_ALL_ACCESS = 0xF003F;
            public const UInt32 SC_MANAGER_CREATE_SERVICE = 0x0002;

            public const UInt32 SERVICE_WIN32_OWN_PROCESS = 0x00000010;
            public const UInt32 SERVICE_AUTO_START = 0x00000002;
            public const UInt32 SERVICE_ERROR_NORMAL = 0x00000001;

            public const UInt32 STANDARD_RIGHTS_REQUIRED = 0xF0000;
            public const UInt32 SERVICE_QUERY_CONFIG = 0x0001;
            public const UInt32 SERVICE_CHANGE_CONFIG = 0x0002;
            public const UInt32 SERVICE_QUERY_STATUS = 0x0004;
            public const UInt32 SERVICE_ENUMERATE_DEPENDENTS = 0x0008;
            public const UInt32 SERVICE_START = 0x0010;
            public const UInt32 SERVICE_STOP = 0x0020;
            public const UInt32 SERVICE_PAUSE_CONTINUE = 0x0040;
            public const UInt32 SERVICE_INTERROGATE = 0x0080;
            public const UInt32 SERVICE_USER_DEFINED_CONTROL = 0x0100;
            public const UInt32 SERVICE_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED |
                SERVICE_QUERY_CONFIG | SERVICE_CHANGE_CONFIG |
                SERVICE_QUERY_STATUS | SERVICE_ENUMERATE_DEPENDENTS |
                SERVICE_START | SERVICE_STOP | SERVICE_PAUSE_CONTINUE |
                SERVICE_INTERROGATE | SERVICE_USER_DEFINED_CONTROL;

            public const UInt32 DELETE = 0x10000;

            public const UInt32 SERVICE_CONTROL_STOP = 0x00000001;
            public const UInt32 SERVICE_STOPPED = 0x00000001;

            [StructLayout(LayoutKind.Sequential)]
            public struct SERVICE_STATUS
            {
                public UInt32 dwServiceType;
                public UInt32 dwCurrentState;
                public UInt32 dwControlAccepted;
                public UInt32 dwWin32ExitCode;
                public UInt32 dwServiceSpecificExitCode;
                public UInt32 dwCheckPoint;
                public UInt32 dwWaitHint;
            };

            [DllImport("advapi32.dll")]
            public static extern IntPtr OpenSCManager(
                string lpMachineName,
                string lpSCDB,
                UInt32 scParameter);
            
            [DllImport("advapi32.dll")]
            public static extern IntPtr CreateService(
                IntPtr SC_HANDLE,
                string lpSvcName,
                string lpDisplayName,
                UInt32 dwDesiredAccess,
                UInt32 dwServiceType,
                UInt32 dwStartType,
                UInt32 dwErrorControl,
                string lpPathName,
                string lpLoadOrderGroup,
                int lpdwTagId,
                string lpDependencies,
                string lpServiceStartName,
                string lpPassword);

            [DllImport("advapi32.dll")]
            public static extern void CloseServiceHandle(IntPtr SCHANDLE);

            [DllImport("advapi32.dll")]
            public static extern int StartService(
                IntPtr SVHANDLE,
                UInt32 dwNumServiceArgs,
                string lpServiceArgVectors);

            [DllImport("advapi32.dll", SetLastError = true)]
            public static extern IntPtr OpenService(
                IntPtr SCHANDLE,
                string lpSvcName,
                UInt32 dwNumServiceArgs);

            [DllImport("advapi32.dll", SetLastError = true)]
            public static extern int ControlService(
                IntPtr SCHANDLE,
                UInt32 dwControl,
                [MarshalAs(UnmanagedType.Struct)]
                out SERVICE_STATUS lpServiceStatus);

            [DllImport("advapi32.dll", SetLastError = true)]
            public static extern int QueryServiceStatus(
                IntPtr SCHANDLE,
                [MarshalAs(UnmanagedType.Struct)]
                out SERVICE_STATUS lpServiceStatus);

            [DllImport("advapi32.dll")]
            public static extern int DeleteService(IntPtr SVHANDLE);

            [DllImport("kernel32.dll")]
            public static extern int GetLastError();
        }
    }
}

No comments: