[Für Details siehe meinen Artikel “Perfekter Service” im dotnetpro Magazin 1/2016.]
Implementation patterns used for Windows Services can seriously influence performance, reactivity, computing resource consumption, stability and energy consumption of our systems. This post shares alternative patterns for creating Windows Services. Download complete code.
Design Aspects
When designing Windows Services consider the following aspects:
- How and when should the service be started?
- If OnStart() takes > 30 sec the SCM will abort the service.
- Choose a suitable pattern to implement processing. Ex: Event-driven, polling loops, poll-timers.
- Maintain thread hygiene.
- If OnStop() takes >~90 sec the SCM will abort the service. Exact time limit is undocumented and Windows version specific.
- Implement graceful shutdown.
- Windows Services must not have a direct UI. You can however create a UI and control the service via its OnCustomCommand method or implement a custom WCF-Interface, see Simple WCF-Services.
- Robust error handling.
- Sufficient tracing and logging.
- Windows Services must be installed.
- Windows Services cannot be started directly in the IDE. See test console app below.
Startup and Recovery
Windows Service start & stop options:
- Start via SCM startup type:
- Automatic
- Automatic (delayed start)
- Manual
- Start via Task Scheduler Event Filter.
- Start/Stop via Service Trigger Events:
- via code in service TriggerStartServiceInstaller.AfterInstall()
- via sc.exe Ex: sc triggerinfo myService start/device/0850302a-b344-4fda-9be9-90576b8d46f0
Windows service SCM recovery options:
- No Action
- Restart the Service
- Start a Program
- Restart the Computer
Processing Patterns
We can generally choose between event-driven processing, polling loops and timers. With loops it is important to implement a delay to prevent consuming a complete CPU when idle. For optimal reactivity such delays should be cancellable, ex by candellationToken.WaitHandle.WaitOne(someTimeSpan).
- Event-driven processing. Generally preferred.
- Polling loop in a dedicated thread. OK for a single worker loop.
- Polling loop in tasks. With many worker loops use a task for each.
- Poll using a timer. No need for a dedicated thread or task to prevent blocking the main thread.
Event driven processing
Public Class WinService Sub OnStart(ByVal args() As String) _eventWorker = New EventWorker _eventWorker.StartWorking(_cts.Token) … Sub OnStop() _cts.Cancel() 'EventWorker automatically waits on Cancel() … Public Class EventWorker Private WithEvents _fileWatcher As BufferingFileSystemWatcher Private _CT As CancellationToken Private _workingEvent As New ManualResetEvent(False) Public Sub StartWorking(CT As CancellationToken) _CT = CT _fileWatcher = New BufferingFileSystemWatcher("d:\temp\in", "*.*") _fileWatcher.EnableRaisingEvents = True _CT.Register( Sub() _fileWatcher.Dispose() _workingEvent.WaitOne() End Sub) End Sub Private Sub _fileWatcher_All(sender As Object, e As FileSystemEventArgs) Handles _fileWatcher.All Try If _CT.IsCancellationRequested Then Return Work(e) Catch ex As ApplicationException … End Sub Sub Work(e As FileSystemEventArgs) Try _workingEvent.Reset() _trace.InfoFormat("{0}: {1}", e.ChangeType, e.Name) Finally _workingEvent.Set() End Try End Sub …
LoopWorker in dedicated Thread
Public Class WinService Sub OnStart(ByVal args() As String) _workerThread = New Thread(Sub() _loopWorker.DoWork(_cts.Token)) _workerThread.IsBackground = True _workerThread.Start() … Sub OnStop() _cts.Cancel() _workerThread.Join() … Public Class LoopWorker Sub DoWork(ct As CancellationToken) Do ct.ThrowIfCancellationRequested() Work() ct.WaitHandle.WaitOne(TimeSpan.FromSeconds(3)) Loop …
LoopWorker in Tasks
Public Class WinService Sub OnStart(ByVal args() As String) _workerTask = Task.Run(Sub() _loopWorker.DoWork(_cts.Token), _cts.Token) … Sub OnStop() _cts.Cancel() _workerTask.Wait() … Public Class LoopWorker Sub DoWork(ct As CancellationToken) Do ct.ThrowIfCancellationRequested() Work() ct.WaitHandle.WaitOne(TimeSpan.FromSeconds(3)) Loop …
LoopWorker using a Timer
Public Class WinService Sub OnStart(ByVal args() As String) _timerWorker = New TimerWorker _timerWorker.StartWorking(_cts.Token) … Sub OnStop() _cancellationTokenSource.Cancel() ‘TimerWorker automatically waits on Cancel() … Public Class TimerWorker Public Sub StartWorking(cancellationToken As CancellationToken) _cancellationToken = cancellationToken _pollTimer = New System.Threading.Timer(AddressOf _pollTimer_Elapsed, Nothing, 0, Timeout.Infinite) _cancellationToken.Register( Sub() _pollTimer.Dispose(_timerDisposedEvent) 'Cancel outstanding tick _ timerDisposedEvent.WaitOne() End Sub)) … Sub _pollTimer_Elapsed(__ As Object) If _cancellationToken.IsCancellationRequested Then Exit Sub Work() Try _pollTimer.Change(_pollInterval, Timeout.InfiniteTimeSpan) Catch ex As ObjectDisposedException 'ignore End Try …
Self-installing Windows Service with Test Console App
Shared Sub Main(args() As String) If Environment.UserInteractive Then Dim cmd = String.Concat(args).ToLower() Select Case cmd Case "-i", "/i", "/install" ManagedInstallerClass.InstallHelper(New String() {Assembly.GetExecutingAssembly().Location}) Case "-u", "/u", "/uninstall" ManagedInstallerClass.InstallHelper(New String() {"/u", Assembly.GetExecutingAssembly().Location}) Case Is <> "" Console.WriteLine("Usage: {0} [-i | -u]", My.Application.Info.AssemblyNam Console.ReadKey() Case Else RunAsConsoleApp() End Select Else RunAsService() End If End Sub Private Shared Sub RunAsService() Dim ServicesToRun() As System.ServiceProcess.ServiceBase ServicesToRun = New System.ServiceProcess.ServiceBase() {New WinService} System.ServiceProcess.ServiceBase.Run(ServicesToRun) End Sub Private Shared Sub RunAsConsoleApp() Dim testService As New WinService Console.Title = My.Application.Info.AssemblyName Console.WindowWidth = 120 testService.OnStart(Nothing) Console.WriteLine("Win service processing... Press to stop.") Console.ReadLine() testService.OnStop() Console.WriteLine("Win service stopped. Press any key to confirm.") Console.ReadKey() End Sub
Pingback: Thoughts and Experiments about Cloud Encryption | Peter Meinl: Software Development Tips