How to debug your Windows Service ?

Debugging a windows service might be a little bit hard. Because each change in your code implies to uninstall/reinstall the service. A way to avoid this issue is to launch the service from the command line. Then you will be able to add breakpoints in your source, even in the OnStart method…

Launching the service from the command line

There is a few things to change in order to allow launching a service from the command line.

Changing application properties

In the Application tab of the service property window, set the output type to “Console Application”

Update the Main method

Now we have to detect when service is launched from the command line (or from the IDE) and when it is launched from the mmc or using the net start command.
For this, we will update the Main method in Program.cs file.
before:

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

ServiceBase.Run(ServicesToRun);

after:

ServiceBase[] servicesToRun = new ServiceBase[] { new Service1() };

if (Environment.UserInteractive)
{
  // executable has been started from command line (or from the Visual Studio IDE)
  Type type = typeof(ServiceBase);
  BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;
  MethodInfo method = type.GetMethod("OnStart", flags);

  foreach (ServiceBase service in servicesToRun)
  {
    try
    {
      method.Invoke(service, new object[] {args});
      Console.ReadKey(true);
    }
    catch (Exception e)
    {
      Console.WriteLine("Unable to start the service '" + service.ServiceName + "'. "
        + "System returns: " + e.Message
        + (e.InnerException != null ? " " + e.InnerException.Message : ""));
    }
  }
}
else
{
  // Normal service behaviour (when started as a service)
  ServiceBase.Run(servicesToRun);
}

Please note, that you have to update the Main prototype in order to allow passing arguments.

public static void Main()

becomes

public static void Main(string[] args)

Then, we can add some “extra stuff” to this service:

  • Allow the user to stop the service from the command line (using CTL+C)
  • Add a TraceSwitch and the abiliy to change the trace level from the console (using CTL+D)
  • Display this new features when user press the ENTER key.
public static class Program
{
  public static TraceSwitch tTrace = new TraceSwitch("TraceLevelSwitch", "Maximum level of displayed trace"); // Default value is set in the .config file
  ///

  /// The main entry point for the application.
  /// Arguments passed through the command line
  ///
  public static void Main(string[] args)
  {

    // More than one user Service may run within the same process. To add
    // another service to this process, change the following line to
    // create a second service object. For example,
    //
    // ServicesToRun = new ServiceBase[] {new Service1(), new MySecondUserService()};
    ServiceBase[] servicesToRun = new ServiceBase[] { new Service1() };

    if (Environment.UserInteractive)
    {
      // executable has been started from command line (or from the Visual Studio IDE)
      Type type = typeof(ServiceBase);
      BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;
      MethodInfo method = type.GetMethod("OnStart", flags);

      PrintUsage();
      foreach (ServiceBase service in servicesToRun)
      {
        try
        {
          method.Invoke(service, new object[] {args});
        }
        catch (Exception e)
        {
          Console.WriteLine("Unable to start the service '" + service.ServiceName + "'. "
                           + "System returns: " + e.Message
                           + (e.InnerException != null ? " " + e.InnerException.Message : ""));
        }
     }

     ConsoleKeyInfo cki;
     Console.TreatControlCAsInput = true;
     do
     {
       StringBuilder sb = new StringBuilder();
       sb.Length = 0;

       // Wait for user input
       cki = Console.ReadKey(true);
       if (cki.Modifiers != 0)
       {
         if ((cki.Modifiers & ConsoleModifiers.Alt) != 0)
         {
           sb.Append("ALT+");
         }

         if ((cki.Modifiers & ConsoleModifiers.Shift) != 0)
           sb.Append("SHIFT+");
        if ((cki.Modifiers & ConsoleModifiers.Control) != 0)
           sb.Append("CTL+");
      }

      sb.Append(cki.Key.ToString());
     // Process user input
     if ((cki.Key == ConsoleKey.Enter)
          && ((cki.Modifiers & ConsoleModifiers.Control) == 0)
          && ((cki.Modifiers & ConsoleModifiers.Shift) == 0)
          && ((cki.Modifiers & ConsoleModifiers.Alt) == 0))
     {
       PrintUsage();
     }
     else if ((cki.Key == ConsoleKey.D)
          && ((cki.Modifiers & ConsoleModifiers.Control) != 0)
          && ((cki.Modifiers & ConsoleModifiers.Shift) == 0)
          && ((cki.Modifiers & ConsoleModifiers.Alt) == 0))
     {
       // Decrease the trace level
       // (set it to Verbose if the current value is Off)
       if (tTrace.Level == TraceLevel.Off)
         tTrace.Level = TraceLevel.Verbose;
       else
         tTrace.Level--;

       Console.WriteLine("New trace level: " + tTrace.Level.ToString());
     }
     else if (!((cki.Key == ConsoleKey.C
             && (cki.Modifiers & ConsoleModifiers.Control) != 0)
             && ((cki.Modifiers & ConsoleModifiers.Shift) == 0)
             && ((cki.Modifiers & ConsoleModifiers.Alt) == 0)))
                 Console.WriteLine(sb + " - ignored.");
   } while (!((cki.Key == ConsoleKey.C
              && (cki.Modifiers & ConsoleModifiers.Control) != 0)
              && ((cki.Modifiers & ConsoleModifiers.Shift) == 0)
              && ((cki.Modifiers & ConsoleModifiers.Alt) == 0)));

   // User press CTL+C keys -> Exit

   // Close all services
   foreach (ServiceBase service in servicesToRun)
     try
     {
       service.Stop();
     }
     catch (Exception ex)
     {
       // Just log it
       Console.WriteLine("Error occured when trying to stop service(s). System returns: "
                           + ex.Message
                           + (ex.InnerException != null ? " " + ex.InnerException.Message : ""));
     }
   }
   else
   {
     // Normal service behaviour (when started as a service)
     ServiceBase.Run(servicesToRun);
   }
  }

  private static void PrintUsage()
  {
     Console.WriteLine("Press CTL+C to exit");
     Console.WriteLine("Press CTL+D to change the trace level. Current level is " + tTrace.Level.ToString());
     Console.WriteLine("------------------------------------------------");
  }
}

Same snippet, but in Visual Basic

Public NotInheritable Class Program
    Private Sub New()
    End Sub
    Public Shared tTrace As New TraceSwitch("TraceLevelSwitch", "Maximum level of displayed trace")
    ' Default value is set in the .config file
    '''
    ''' The main entry point for the application.
    ''' Arguments passed through the command line
    '''
    Public Shared Sub Main(args As String())

        ' More than one user Service may run within the same process. To add
        ' another service to this process, change the following line to
        ' create a second service object. For example,
        '
        ' ServicesToRun = new ServiceBase[] {new Service1(), new MySecondUserService()};
        Dim servicesToRun As ServiceBase() = New ServiceBase() {New Service1()}

        If Environment.UserInteractive Then
            ' executable has been started from command line (or from the Visual Studio IDE)
            Dim type As Type = GetType(ServiceBase)
            Dim flags As BindingFlags = BindingFlags.Instance Or BindingFlags.NonPublic
            Dim method As MethodInfo = type.GetMethod("OnStart", flags)

            PrintUsage()
            For Each service As ServiceBase In servicesToRun
                Try
                    method.Invoke(service, New Object() {args})
                Catch e As Exception
                    Console.WriteLine("Unable to start the service '" + service.ServiceName + "'. " + "System returns: " + e.Message + (If(e.InnerException IsNot Nothing, " " + e.InnerException.Message, "")))
                End Try
            Next

            Dim cki As ConsoleKeyInfo
            Console.TreatControlCAsInput = True
            Do
                Dim sb As New StringBuilder()
                sb.Length = 0

                ' Wait for user input
                cki = Console.ReadKey(True)
                If cki.Modifiers <> 0 Then
                    If (cki.Modifiers And ConsoleModifiers.Alt) <> 0 Then
                        sb.Append("ALT+")
                    End If

                    If (cki.Modifiers And ConsoleModifiers.Shift) <> 0 Then
                        sb.Append("SHIFT+")
                    End If
                    If (cki.Modifiers And ConsoleModifiers.Control) <> 0 Then
                        sb.Append("CTL+")
                    End If
                End If

                sb.Append(cki.Key.ToString())
                ' Process user input
                If (cki.Key = ConsoleKey.Enter) AndAlso ((cki.Modifiers And ConsoleModifiers.Control) = 0) AndAlso ((cki.Modifiers And ConsoleModifiers.Shift) = 0) AndAlso ((cki.Modifiers And ConsoleModifiers.Alt) = 0) Then
                    PrintUsage()
                ElseIf (cki.Key = ConsoleKey.D) AndAlso ((cki.Modifiers And ConsoleModifiers.Control) <> 0) AndAlso ((cki.Modifiers And ConsoleModifiers.Shift) = 0) AndAlso ((cki.Modifiers And ConsoleModifiers.Alt) = 0) Then
                    ' Decrease the trace level
                    ' (set it to Verbose if the current value is Off)
                    If tTrace.Level = TraceLevel.Off Then
                        tTrace.Level = TraceLevel.Verbose
                    Else
                        tTrace.Level -= 1
                    End If

                    Console.WriteLine("New trace level: " + tTrace.Level.ToString())
                ElseIf Not ((cki.Key = ConsoleKey.C AndAlso (cki.Modifiers And ConsoleModifiers.Control) <> 0) AndAlso ((cki.Modifiers And ConsoleModifiers.Shift) = 0) AndAlso ((cki.Modifiers And ConsoleModifiers.Alt) = 0)) Then
                    Console.WriteLine(sb + " - ignored.")
                End If
            Loop While Not ((cki.Key = ConsoleKey.C AndAlso (cki.Modifiers And ConsoleModifiers.Control) <> 0) AndAlso ((cki.Modifiers And ConsoleModifiers.Shift) = 0) AndAlso ((cki.Modifiers And ConsoleModifiers.Alt) = 0))

            ' User press CTL+C keys -> Exit

            ' Close all services
            For Each service As ServiceBase In servicesToRun
                Try
                    service.[Stop]()
                Catch ex As Exception
                    ' Just log it
                    Console.WriteLine("Error occured when trying to stop service(s). System returns: " + ex.Message + (If(ex.InnerException IsNot Nothing, " " + ex.InnerException.Message, "")))
                End Try
            Next
        Else
            ' Normal service behaviour (when started as a service)
            ServiceBase.Run(servicesToRun)
        End If
    End Sub

    Private Shared Sub PrintUsage()
        Console.WriteLine("Press CTL+C to exit")
        Console.WriteLine("Press CTL+D to change the trace level. Current level is " + tTrace.Level.ToString())
        Console.WriteLine("------------------------------------------------")
    End Sub
End Class

Leave a comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.