Creating easy to debug Windows Services in .NET

When you want a process to run continuously, even when no user is logged in to a machine and when you want that process to start running as soon as Windows does, you may want to create a Windows Service.

Using .NET’s System.ServiceProcess namespace you can easily write code that allows your program to act as a Windows Service. The namespace also includes classes that let you manage services, such as starting, stopping and installing them.

Creating a basic Windows Service

When using the Visual Studio template for a “Windows Service Application”, a project will be created that shows the basic skeleton. This is easily recreated:

  1. Create a new Console Application.
  2. Add a reference to System.ServiceProcess.
  3. Write the following code:
public static class Program
{
  public static void Main(string[] args)
  {
    ServiceBase.Run(new SimpleService());
  }
}

public class SimpleService : ServiceBase
{
  public SimpleService()
  {
    ServiceName = "SimpleService";
  }
    
  protected override void OnStart(string[] args)
  {
    // Start your service!
  }
}

However, if you now hit F5, you’ll get an error saying “Cannot start service from the command line or a debugger. A Windows Service must first be installed […].”:

Service Start Failure

Attaching the debugger

The MSDN way to debug a service is pretty convoluted: you’ll have to install the service, start it and attach Visual Studio’s debugger to the service process. And then when you spot an issue and fix it, you’ll have to perform the mentioned steps again.

Environment.UserInteractive

You can easily detect whether the current process is running “user-interactive” as defined on MSDN testing Environment.UserInteractive, adapted from Stack Overflow:

public static class Program
{
  public static void Main(string[] args)
  {
    // Instantiate the service.
    var myService = new MyService();

    // User Interactive? Debug!
    if (Environment.UserInteractive)
    {
      myService.Start();
    }
    else
    {
      // Otherwise, run as Windows Service.
      ServiceBase.Run(myService);
    }
}

public class MyService : ServiceBase
{
  public void Start()
  {
    // Do the actual starting.
  }

  /// <summary>
  /// Called when this service is started 
  /// through the Service Control Manager.
  /// </summary>
  /// <param name="args"></param>
  protected override void OnStart(string[] args)
  {
    this.Start();
  }        
}

Now when you hit F5 (which automatically attaches the debugger to your process), you’re debugging your service logic! This is the simplest and least invasive way making it possible to debug a Windows Service without installing it and attaching the debugger.

However, you preferably want extract the service logic and do your debugging on that class. This way you can reuse the base class among your projects and you improve testability.

In order to support more options than just debugging, we also support command line arguments:

public static class Program
{
  public static void Main(string[] args)
  {
    // Instantiate the business logic of the service.
    IServiceLogic myServiceLogic = new MyServiceLogic();

    // No arguments? Run the Service and exit when service exits.
    if (!args.Any())
    {
      // CustomServiceBase operates on an IServiceLogic instance,
      // which is injected here using myServiceLogic.
      var myService = new CustomServiceBase(myServiceLogic);
      ServiceBase.Run(myService);
      return;
    }

    // Parse arguments.
    switch (args.First().ToUpperInvariant())
    {
      case "/D":
      case "/DEBUG":
        myServiceLogic.Start(args.Skip(1));
        break;
      default:
        PrintUsage();
        break;
    }
  }
  
  private static void PrintUsage()
  {
    Console.WriteLine("Usage:");
    Console.WriteLine();
    Console.Write(" {0} ", Path.GetFileName(Assembly.GetEntryAssembly().Location));
    Console.WriteLine("/D[ebug]\tStart the service logic");
  }
}

An interface IServiceLogic has been introduced, which defines the methods that can be called on the logic. This interface is implemented by MyServiceLogic:

public interface IServiceLogic
{
  void Start(IEnumerable<string> args);

  void Stop();
}

public class MyServiceLogic : IServiceLogic
{
  public void Start(IEnumerable<string> args)
  {
    Debug.WriteLine("Start(): {0}", string.Join(", ", args));
  }

  public void Stop()
  {
    // Do nothing.
  }
}

The CustomServiceBase looks like this:

[System.ComponentModel.DesignerCategory("Code")] // Prevent double-clicking the file to open designer mode.
public class CustomServiceBase : ServiceBase
{
  private readonly IServiceLogic _serviceLogic;

  public CustomServiceBase(IServiceLogic serviceLogic)
  {
    ServiceName = serviceLogic.ServiceName;
    _serviceLogic = serviceLogic;
  }

  protected override void OnStart(string[] args)
  {
    _serviceLogic.Start(args);
  }

  protected override void OnStop()
  {
    _serviceLogic.Stop();
  }
}

When you start the executable without arguments, it will try to run as a Windows Service. This is recommended because when installed as a service, by default it will also be started without arguments. Unless specified otherwise in its image path of course, so it is possible to reverse this behavior by installing it for example using a “/RunService” parameter.

Now when providing the “/D” or “/DEBUG” flag, the instantiated MyServiceLogic with have its Start() method called. You can set the command line arguments by right-clicking the console app project in Visual Studio, selecting Properties and clicking the Debug tab:

Command line arguments

Note: the call to myServiceLogic.Start() passes the remainder of the command line arguments (omitting the “/D[EBUG]”), so your service logic can also support other parameters. A starting service does not receive its commandline arguments at OnStart() nor in Main() though, the parameters you receive there are those entered in the “Start parameters” set in the Properties window of the service through the Services management console.

Installing the service

This debuggable service can’t be installed yet, as running installutil.exe on the assembly will show the error “No public installers with the RunInstallerAttribute.Yes attribute could be found”.

It can also be useful to make the service self-installing, so you won’t have to rely on installutil.exe being present on the target machine. The following code enables a service to install itself, after adding a reference to the .NET assembly System.Configuration.Install:

[System.ComponentModel.DesignerCategory("Code")] // Prevent double-clicking the file to open designer mode.
public class CustomServiceInstaller : Installer
{
  public static void Install(IServiceLogic service)
  {
    var installer = GetInstaller(service);
    installer.Install(new Hashtable());
  }

  public static void Uninstall(IServiceLogic service)
  {
    var installer = GetInstaller(service);
    installer.Uninstall(null);
  }

  public CustomServiceInstaller(IServiceLogic service)
  {
    ServiceProcessInstaller serviceProcessInstaller =
               new ServiceProcessInstaller();
    ServiceInstaller serviceInstaller = new ServiceInstaller();

    // Service Account Information
    serviceProcessInstaller.Account = ServiceAccount.LocalSystem;
    serviceProcessInstaller.Username = null;
    serviceProcessInstaller.Password = null;

    // Service Information
    serviceInstaller.StartType = ServiceStartMode.Automatic;

    // Info from ServiceLogic
    serviceInstaller.ServiceName = service.ServiceName;
    serviceInstaller.DisplayName = service.ServiceDisplayName;
    serviceInstaller.Description = service.ServiceDescription;
    
    this.Installers.Add(serviceProcessInstaller);
    this.Installers.Add(serviceInstaller);
  }

  private static TransactedInstaller GetInstaller(IServiceLogic service)
  {
    var serviceAssembly = service.GetType().Assembly;

    string[] commandLine = new[] { String.Format("/assemblypath={0}", serviceAssembly.Location) };
    string logFile = string.Format("{0}.installlog", serviceAssembly.FullName);

    InstallContext installContext = new InstallContext(logFile, commandLine);
    TransactedInstaller transactedInstaller = new TransactedInstaller();

    transactedInstaller.Installers.Add(new CustomServiceInstaller(service));
    transactedInstaller.Context = installContext;

    return transactedInstaller;
  }
}

The calling code now looks like this:

public static class Program
{
  public static void Main(string[] args)
  {
    // Instantiate the service logic.
    var myServiceLogic = new MyServiceLogic();

    // No arguments? Run the Service and exit when service exits.
    if (!args.Any())
    {
      ServiceBase.Run(new CustomServiceBase(myServiceLogic));
      return;
    }

    // Parse arguments.
    switch (args.First().ToUpperInvariant())
    {
      case "/D":
      case "/DEBUG":
        StartService(myServiceLogic, args);
        break;
      case "/I":
      case "/INSTALL":
        InstallService(myServiceLogic);
        break;
      case "/U":
      case "/UNINSTALL":
        UninstallService(myServiceLogic);
        break;
      default:
        PrintUsage();
        break;
    }
  }

  private static void InstallService(IServiceLogic myService)
  {
    CustomServiceInstaller.Install(myService);
  }

  private static void UninstallService(IServiceLogic myService)
  {
    CustomServiceInstaller.Uninstall(myService);
  }

  /// <summary>
  /// Passes the remainder of the commandline arguments to the ServiceLogic and starts it.            
  /// </summary>
  /// <param name="serviceLogic"></param>
  /// <param name="args"></param>
  private static void StartService(IServiceLogic serviceLogic, IEnumerable<string> args)
  {
    serviceLogic.Start(args.Skip(1));
  }

  private static void PrintUsage()
  {
    Console.WriteLine("Usage:");
    Console.WriteLine();
    string exeName = string.Format(" {0} ", Path.GetFileName(Assembly.GetEntryAssembly().Location));
    Console.Write(exeName + " /D[ebug]\tStart the service logic");
    Console.Write(exeName + " /I[nstall]\tInstall the executable as Windows Service");
    Console.Write(exeName + " /U[ninstall]\tUninstall the Windows Service");
  }
}

Third-party

You don’t want to reinvent the wheel. Using above code, I showed why and how you would do this. There are various libraries that encapsulate this behavior so you don’t have to write and maintain it anymore. To name a few:

Each of these and other libraries have their own mindset. Some are config-oriented, some use properties and others rely on attributes you have to place in your code. There are libraries that come with a user interface to start and stop the service being debugged.
Most or all of these libraries are also available as NuGet packages.

This entry was posted in Tech and tagged , , , . Bookmark the permalink.