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