Sunday, March 2, 2008

Windows services as console apps

During my PADNUG talk, I mentioned that we do self-hosted WCF services, and that our service host can be run either as a console app or a Windows Service. Since then, I've had several questions about how that works.

I've been using this pattern since .NET 1.0- it makes debugging service startup failures in production a snap, since you can just stop the Windows service and run it as a console app. When we're developing, we run in console mode almost exclusively.

I'll write a description of the process- if I get time (ha!) I'll try to post a basic sample.

The most important thing that many people don't understand is that there is no magical voodoo behind Windows services; they're just console apps that use RPC APIs and callbacks to talk/listen to the global service control manager. The ServiceBase class wraps up those API calls and callbacks into a nice little base class where you can wire up event handlers for start/stop/whatever, but there's no special behavior provided by the runtime or the OS for running in a service. This means that your Main method can behave just like any normal console app.

The trick to making this work is to dissociate your startup and shutdown behavior into methods that don't care whether you're running in service or console mode. Then you just add a "/console" command line parser to the beginning of Main. If someone passed /console to you on the command-line, you skip all the ServiceBase.Run stuff, call your startup methods directly, and do the "Press any key to exit" behavior. If you don't see /console, call ServiceBase.Run and pass an instance of your service class. The service class' OnStart event handler should call your startup methods. The service installer behaves exactly the same, so what gets generated is fine. As an aside- you could go the other way (require "/service" to run as a service), but command-line args aren't a first-class entity for the managed Windows service installer, so you'd have to hack the registry afterward to stuff the command-line args into the image path. Much easier to go the other way, IMHO, but whatever.

To debug in VS, just go to the "Debug" tab in your project properties and add "/console" to your command-line args- you should be off to the races.

I've taken this pattern much further on various projects. We currently use a generic WCF ServiceHost "host" (oy, overloaded terms galore!) that implements this pattern and knows how to load multiple WCF ServiceHosts based on the "service" entries in an app.config's system.serviceModel section. All of our different service setups on different machines use this same generic "host" binary- what WCF service impls get loaded depends on the services defined in the WCF config section. If we have one service entry, one ServiceHost- if we have ten, ten ServiceHosts.

A couple of other little tips:

- Long-running service startup/shutdown is generally a Bad Thing- the SCM will give up on you if you take more than 30s to respond to a startup/shutdown request. This isn't fatal, but it makes for a bad administrative experience. In the past, I'd end up doing long-running stuff on another thread to avoid that. That's equally sucky if you have a fatal error during startup, though, since you can't report the error through the Services panel- the service process appears to start successfully, then silently dies. However, .NET 2.0 added the ability to request more time from the SCM (ServiceBase.RequestAdditionalTime)- makes it much nicer on both fronts.

- Tag your service and installer class definitions with [System.ComponentModel.DesignerCategory("Code")] to disable design view on double-click in the solution explorer. I find Design View pretty much useless with Windows services- it's maddening to remember to right-click->View Code when opening. This tip works for any other code with designer support as well (including Service Installers, where designer mode is nearly as useless). You have to leave the attribute fully qualified, or it won't work- that parser's apparently built for speed, not intelligence.

Hope this helps. Don't hold your breath for sample code, but if I get enough requests, I might whip something up.

6 comments:

Anonymous said...

Nice idea - elegant!

bryan said...

Nice tip about the DesignerCategoryAttribute -- I always forget to Right-click and choose View Code. This definitely saves me some grief.

Travis Spencer said...

Thanks, Matt, for the info. I would really like to see some sample code too if you can find the time.

digit said...

Good tips, thanks!

namtab said...

I know I'm three years late on this one, but I would literally kill for the sln of that ServiceHost host...

Thank you in advance...

namtab said...

I hate to insist but here it goes..

... I beg of you, man, share your glorious creation with us mere mortals..

I find myself managing a LOT of Windows services sln's each one with a different project for:
- worker singleton dll with service logic, DAL helper and types
- configsection class
- servicebase class
- installer (Flexera InstallShield 2010)
- console app for debugging

I could really use an unified service solution that dynamically loades dll/exe services (each one with different configs) based on one, unified config file..

Cheers