Windows Service Worker Options

[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
EventTriggerFilter

Task Scheduler Event Filter

Windows service SCM recovery options:

  • No Action
  • Restart the Service
  • Start a Program
  • Restart the Computer
ServiceRecoveryOptions

SCM service recovery options

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

About Peter Meinl

IT Consultant
This entry was posted in Computers and Internet and tagged . Bookmark the permalink.

One Response to Windows Service Worker Options

  1. Pingback: Thoughts and Experiments about Cloud Encryption | Peter Meinl: Software Development Tips

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s