Здравствуйте, хабралюди. Если вы никогда не писали службы Windows, но вам вдруг понадобилось, то этот небольшой пост для вас.
Зачем это всё?
Если вы взялись за написание службы, то встаёт вопрос об её отладке. В интернетах в основном пишут про способ, который заключается в подключении отладчика к уже запущенному процессу службы, про альтернативу которому я и хочу рассказать. Т.к. служба — это не совсем обычный процесс Windows, просто запустить её из Visual Studio у вас не получится.
Установка службы
При попытке запуска вам будет показано сообщение, что, мол, служба запуститься не может и вам нужно использовать installutil.exe
для её установки.
Для установки службы вам потребуется консоль Visual Studio, которая находится по пути
Пуск → Программы → Visual Studio 2008 → Visual Studio Tools → Visual Studio 2008 Command Prompt
(в зависимости от версии установленной VS путь может отличаться)
После запуска консоли переходим в директорию, в которую собирается ваша служба в режиме Debug
и устанавливаем её:
installutil.exe /i <имя вашей сборки со службой>
Более вам installutil.exe
не потребуется. Для удобства запускаем консоль управления службами Windows:
services.msc
Теперь консоль Visual Studio можно закрыть.
Переходим к самому интересному
Отладка службы
Способ №1 (подключение отладчика)
- Запустите службу с помощью консоли управления службами Windows.
- В Visual Studio выберите
Tools → Connect To Process
в появившемся диалоговом окне выберите имя процесса службы и нажмите кнопку
Attach
После этого можно расставлять точки останова и отлаживать службу.
У этого способа есть недостаток: без дополнительных ухищрений не получится отладить код, находящийся в обработчике OnStart
.
Способ №2 (подключение отладчика из исходного кода)
В код модуля program.cs
, сразу после начала метода Main()
добавляем следующее:
#if DEBUG
System.Diagnostics.Debugger.Launch();
#endif
Этот код подключает отладчик к процессу и компилируется только в режиме DEBUG
. Теперь для отладки службы её нужно запускать не из Visual Studio, а с помощью консоли управления службами (или команды net start
). Сразу после запуска появится диалоговое окно выбора отладчика:
Выбираете запущенный экземпляр Visual Studio, нажимаете Yes
и наслаждаетесь отладкой!
Способ №3 (запуск с параметрами командной строки)
предложил Gwynbleidd
Есть еще способ — предусматривать возможность запуска службы как консольного приложения. Например, в командной строке передавать /console, и если этот флаг установлен, стартовать приложение как консольное, если нет — как службу.
Преимущества такого подхода:
Очень просто отлаживать, по сути дела, как обычное консольное приложение. В консоль можно выводить отладочную информацию
Спасибо komr за помощь в отладке текста
This post is the second and final one dedicated to debugging .NET Windows services (you may read the first one here). The inwrap tool (presented in the first part) is not very friendly to use and I myself haven’t used it since then 🙂 It’s not the best advertisement of someone’s own work, but it did motivate me to write another tool which maybe will gain your interest. The winsvcdiag is a simple application that allows you to debug a start of a Windows service from Visual Studio (or any other debugger – even the remote one).
Debugging a start of a Windows service
The idea is really simple. I again use the Image File Execution Options to hook upon a service executable. Let’s see how this works for a sample TestService which logic is implemented in a testservice.exe executable. First, we need to create a Debugger value under the key HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\testservice.exe. You may either edit the registry manually or use a shortcut:
winsvcdiag --install testservice.exe
Whichever step you take the result should look as on the image below (the path to the winsvcdiag may differ).
From now, when the service is started by the services.exe process, Windows will first run the winsvcdiag.exe passing to it as a first argument the full path to the service executable. Winsvcdiag starts the process, but in a suspended state (using a special flag CREATE_SUSPENDED – the native part is copied from this CodeProject article):
bool success = NativeMethods.CreateProcess(null, sargs, IntPtr.Zero, IntPtr.Zero, false, ProcessCreationFlags.CREATE_SUSPENDED, IntPtr.Zero, null, ref si, out pi);
and then waits in a loop for a debugger to appear:
while (!isDebuggerPresent) { ... if (!NativeMethods.CheckRemoteDebuggerPresent(pi.hProcess, ref isDebuggerPresent)) { failuresCnt++; continue; } Thread.Sleep(1000); // sleep for 1s before the next check }
As we are not really a debugger we need to disable the hook while we are calling the CreateProcess
function, otherwise winsvcdiag will call itself recursively. Now it’s time for you to set a breakpoint in a service initialization code and attach a debugger to the TestService process (it might ran on a remote machine):
In a moment your breakpoint should be bound and then hit. From now you may debug the service in the usual way. It is very important that you set the breakpoint before attaching to the service process. Otherwise you may miss the method you would like to debug. After you are done with diagnosis uninstall the hook using the –uninstall option – you may always check which hooks are installed with a –list option:
windbgsvc.exe --uninstall testservice.exe
When you debug the start method of a service you don’t have much time – by default the start method should finish within 30s and if it fails to do so it will be killed by the system. As you can imagine 30s usually is not enough to resolve an issue. Fortunately this timeout is configurable in the registry by the ServicesPipeTimeout value under the key HKLM\SYSTEM\CurrentControlSet\Control. It’s a dword which represents time in milliseconds the services.exe will wait for a service to start (it is called a pipe timeout as the services.exe process communicates with its child services using a pipe). Again you may modify the registry manually or use the winsvcdiag.exe – the timeout parameter accepts the time in seconds:
PS > .\winsvcdiag.exe --timeout 120 Timeout changed, but reboot is required for the option to take an effect. A path to the service exe file must be provided.
A reboot is required for this option to take effect.
What about Topshelf services?
Topshelf is quite restrictive when it comes to its command line parameters. It also checks if its parent process is services.exe and if it is not (which is the case when we start the service from winsvcdiag) it will assume that it is running from the command line. To overcome those restrictions I prepared the Topshelf.Diagnostics Nuget package. It contains an extension method for an improved parsing of the service command line as well as a changed check for the way the service is run (I assume that it’s not a command line mode if it’s run from a session zero). To apply those changes to your service you just need to add two lines to the HostConfigurator initialization:
private static void Main() { HostFactory.Run(hc => { ... hc.ApplyCommandLineWithDebuggerSupport(); hc.UseWindowsHostEnvironmentWithDebugSupport(); ... }); }
The code is available in my dotnet-tools github repo and the binaries can be found here.
How to ‘fudge’ Windows Services code so that it can be debugged under Visual Studio .NET.
Introduction
Normally, debugging a Windows service under Visual Studio .NET is painful. Windows services won’t actually run directly within Visual Studio .NET, so the usual technique is to install and start the Windows service and then attach a debugger to it. An alternative approach is to pull the guts out of the service, stick it in a separate library, and then build some other app (e.g., a console app) to sit in front of it. This approach uses neither of those techniques.
When building a C# Windows Service project in Visual Studio, it will leave you with a class containing quite a few methods including a Main()
, such as this:
static void Main() { System.ServiceProcess.ServiceBase[] ServicesToRun; ServicesToRun = new System.ServiceProcess.ServiceBase[] { new Service1() }; System.ServiceProcess.ServiceBase.Run(ServicesToRun); }
Obviously, it’s the Main()
above that ends up executing the service, and it’s the Main()
that this approach manipulates so that the Windows Service can be debugged directly within Visual Studio .NET.
Using the example above (and removing some of the comments), here’s how:
static void Main() { #if (!DEBUG) System.ServiceProcess.ServiceBase[] ServicesToRun; ServicesToRun = new System.ServiceProcess.ServiceBase[] { new Service1() }; System.ServiceProcess.ServiceBase.Run(ServicesToRun); #else Service1 service = new Service1(); service.<Your Service's Primary Method Here>(); System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite); #endif }
It’s crude, but effective (CBE — also known as Commander of the British Empire ;)). Run the service in debug mode to debug it, compile and install it as a release build, and it’s a full and proper Windows service.
You may still wish to pull the guts out of your service into a separate library for unit testing. But this approach allows you to work with almost all of your service code as an actual service.
Introduction
Building Windows services can be challenging, especially for developers whom never written a Window service before. Most documentation including Microsoft warns that a developer should not attach to a process unless you know what the process is and understand the consequences of attaching to and possibly killing that process. This is true when running any project that code not reviewed. This changes slightly when the developer wrote code and wants to debug the service. This article provides details on debugging a Windows service after there has been a code review.
Installing the service
Use Installutil.exe The Installer tool is a command-line utility that allows you to install and uninstall server resources by executing the installer components in specified assemblies.
Alternate method to install is with the following tool which was written in .NET Framework, not .NET Core but if needed would be easy to convert to .NET Core Framework yet see zero reasons for this.
Project setup
Open the service solution in Visual Studio; open the window service to debug, make sure the project is setup for debug under project properties, Build tab. “Define DEBUG constant” must be checked and the Configuration ComboBox must be set to “Active (Debug)” or “Debug”.
Debug point
Place the following into the OnStart event of the service.
#if DEBUG
Debugger.Launch();
#endif
Enter fullscreen mode
Exit fullscreen mode
With the service project as the startup project, click “start” in the IDE toolbar. When the breakpoint is hit on Windows 7 a prompt appears, answer yes to debug then select the open instance of Visual Studio to use to debug. Once this has been done Visual Studio will break on Debugger.Launch. Step through the code and notice there is no difference in debugging any other project type.
Windows 8 and higher have tighten security so that when Debugger.Launch is hit there will be additional prompts to jump through which can become a nuance when there is a need for many debug sessions as this is the best method to figure out why code which looks good is not working as expected.
The solution for Windows 8 and higher is to first open RegEdit to
HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\EnableLUA registry
Enter fullscreen mode
Exit fullscreen mode
Change the value from 1 to 0. This will invoke a prompt from Windows indicating for the change to take affect this computer requires a reboot. Once the system has restarted, open Visual Studio to the Windows Service solution and start the service. At this point the additional security prompts will not appear.
The solution mentioned for Windows 8 and higher is not documented by Microsoft; this information was obtained by the Microsoft team responsible for the Visual Studio debugger and may change in updates to Windows operating system, not Visual Studio.
Warning
Disabling UAC (as done by altering the registry value above) is not advisable as this raises chances of security breach to a computer. This should only be done when the service source is not from an unknown source. Only use the registry setting mentioned above for the time it takes to write code for a service followed by enabling UAC when finished.
Summary
Using techniques presented allow developers to debug window services on Windows 7 and higher machines. Using techniques for Windows 8 and above allow less security prompts by lowering UAC (User Account Control) which means the computer is less safe thus important to enable the setting in the registry back to it’s default.
Windows Services
If you’ve ever worked with windows services, you’ll know that they’re a very powerful tool to have in your background processing arsenal. Unfortunately they can be also quite a pain to work with in developer land. Recently we’ve been spinning up a lot of new windows service projects in work as part of a Business Intelligence Data Processing Project. I thought this would be a good time to brain dump some of the tips & tricks I’ve come across over the past few years for dealing with .Net Windows Services.
I’ll look at the basics for getting a service up and going, using the built project installer & Install Util. Then I’ll take a look at easier ways of running the service inside the IDE, and how to run the service in user interactive mode.
Finally I’ll look at ways to make the service self-installing without having to rely upon the InstallUtil.exe as well as gaining access to configuration settings during the installation process.
[important]The completed solution can be found on GitHub at https://github.com/eoincampbell/demo-windows-service [/important]
Windows Services Basics
I’m going to build a very trivial Windows Service. The service will start a timer, which will then log the current time every 100ms to a file in the same path as the executable. It will also attempt to write to the console if there is one attached. A few notes. Don’t leave this running. It will devour your disk space. You’ll also need to make sure that the account you install the service as has permission to write to that path on the file system.
When I first create a new windows service, there are a number of setup tasks I perform to keep the service organised. YMMV with these.
- If I’m only adding a single Service.cs in this project, then I’ll rename the service class to the same name as the project. This will make the Service Name match with the Executable Name which I’ve found saves confusion. In my example the Project, Service Class, Namespace & Executable are all DemoWindowsService.
- Open the Service.Designer.cs file and ensure that the property this.ServiceName also matches this naming convention
- Add an installer to your service by Right-Clicking the design canvas of the service and choosing «Add Installer» (See Image Below)
- Once you’ve added your ProjectInstaller open that file also and ensure that the property this.serviceInstaller1.ServiceName also matches this naming convention
- We’ll also specify a Description on the serviceInstaller1 object to give our service a meaningful description when it appears in services.msc listing
Add Installer to Windows Service
After building your windows service application, you can then use InstallUtil.exe from the command line to install or uninstall your applicaiton.
[text]InstallUtil.exe DemoWindowsService.exe
net start DemoWindowsService
net stop DemoWindowsService
InstallUtil.exe /u DemoWindowsService.exe[/text]
Debugging Windows Services
Relying on InstallUtil is all well and good but it doesn’t lend itself to an easy developer debugging experience. If you attempt to press F5 in order to start your Windows Service from within the IDE, you’ll be presented with the following rather unhelpful popup.
Windows Service Start Failure
This is because the default code which is added to program.cs relies on launching the Service using the static ServiceBase.Run() method. This is the entry point for «the scum». (The SCM, or Service Control Manager is the external program which controls launching the services). We can still debug our Application from here but it’s a little bit round about.
- Build the Service.
- install it using InstallUtil.exe
- NET START the service.
- Attach to that Process using the Debug menu in Visual Studio.
- Find a bug.
- Detachn & NET STOP the service.
- Uninstall, re-build, wash-rinse-repeat.
Using Debugger.Launch & Debugger.Break
Another option available to trigger debugging is to embed calls to the Debugger directly in your code. These calls could be surrounded by Conditional attributes to prevent Debug Launch statements leaking into release versions of code.
protected override void OnStart(string[] args)
{
DebugLaunch();
MainTimer.Enabled = true;
}
private void MainTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
DebugBreak();
var path = Assembly.GetExecutingAssembly().Location;
var dir = Path.GetDirectoryName(path);
var text = string.Format("Tick: {0:yyyy-MM-dd HH:mm:ss.fff}{1}", DateTime.Now, Environment.NewLine);
File.AppendAllText(dir + "\output.log", text);
Console.Write(text);
}
[Conditional("DEBUG")]
public void DebugLaunch()
{
Debugger.Launch();
}
[Conditional("DEBUG")]
public void DebugBreak()
{
Debugger.Break();
}
[notice]Unfortunately it would appear that this doesn’t work in Windows 8. Microsoft have slowly been phasing out the ability of Windows Services to run in Interactive mode and interact with the desktop. Since the Debugger.Launch statement needs to load a GUI for the user to interact with, the Windows Service would appear to hang on this statement. [/notice]
Launch Windows Service with F5
What would be very helpful is if we could just launch our application from within the Debugger as needed. Well it turns out we can by conditionally launching the service using the ServiceBase.Run() method or by just launching it as a regular instantiated class.
static class Program
{
static void Main(string [] args)
{
var service = new DemoService();
if (Debugger.IsAttached)
{
service.InteractiveStart(args);
Console.WriteLine("Press any key to stop!");
Console.Read();
service.InteractiveStop();
}
else
{
ServiceBase.Run(service);
}
}
}
Now if I press F5, the application entry point will check if the application is in Debug Mode (Debugger Attached) and if so will simply launch the application as a console application. In order to get this to work I’ve had to make a few small changes.
- Go to the Project Properties screen & change the application type from «Windows Application» to «Console Application»
- Add two public wrapper methods to the service for InteractiveStart & InteractiveStop since the OnStart & OnStop methods are protected
- Add a Console.Read() between calling Start and Stop to prevent the Service from immediately shutting down.
Command Line Switch Driven Behaviour
Relying on the Debugger being attached is all well and good but what if I just want to run my application optionally in stand-alone mode. We could extend the runtime condition from the last code snippet to also test whether the application is running in InteractiveMode. But I think I’d prefer a little more power so I’ve added a command line switch to create this behavior. Now I have the option to Install from the command line using InstallUtil, run from the command line interactively, and I can set a startup switch argument in Project Properties -> Debug -> Start Options to run the application in Console mode from the IDE.
static class Program
{
static void Main(string [] args)
{
var service = new DemoService();
if (args.Any() && args[0].ToLowerInvariant() == "--console")
{
RunInteractive(service,args);
}
else
{
ServiceBase.Run(service);
}
}
private static void RunInteractive(DemoService service, string [] args)
{
service.InteractiveStart(args);
Console.WriteLine("Press any key to stop!");
Console.Read();
service.InteractiveStop();
}
}
Self Managed Installation
My service is becoming more self-sufficient and useful but I still have this external dependency on InstallUtil. Wouldn’t it be great if I could just drop my application on a server and have it install itself. Enter the ManagedInstallerClass
static void Main(string [] args)
{
var service = new DemoService();
var arguments = string.Concat(args);
switch(arguments)
{
case "--console":
RunInteractive(service,args);
break;
case "--install":
ManagedInstallerClass.InstallHelper(new [] { Assembly.GetExecutingAssembly().Location });
break;
case "--uninstall":
ManagedInstallerClass.InstallHelper(new [] { "/u", Assembly.GetExecutingAssembly().Location });
break;
default:
ServiceBase.Run(service);
break;
}
}
If you want to simplify matters even further, you can modify your ProjectInstaller.Designer.cs file and specify that the Service should be of type AutomaticStart
//
// serviceInstaller1
//
this.serviceInstaller1.ServiceName = "DemoWindowsService";
this.serviceInstaller1.StartType = ServiceStartMode.Automatic;
InstallUtil Install-Time Configuration
One final thing you might find useful is the ability to query values from your configuration file when using Intsall Util. The problem here is that the AppSettings collection during installation is not the collection in the service.config but the collection in the InstallUtil app.config. Have a read of the following link for getting access to your own config at install time (e.g. in order to install with Default Supplied UserName & Password).
http://trycatch.me/installutil-windows-services-projectinstallers-with-app-config-settings/
Future Proofing
One last piece of advice. If you’re building any sort of sizeable windows service/back-end processor, I would strongly suggest you NOT implement any logic in the service class itself. Typically I will create some sort of «Engine» class that encapsulates all my functionality with 3 external accessible members.
- A public constructor called from Service Constructor
- A public Start or Init method, called from OnStart()
- A public Stop or End method, called from OnStop()
The reason for this is simply to prevent lock-in & tight coupling to the windows service treating it only as a helpful host. This allows me to easily spin up my Engine class in a standalone Console Application, inside a web apps Global.asax or in an Azure Worker role with a minimum of refactoring in order to extract the functionality.
~Eoin Campbell