fredag den 4. marts 2011

Creating a windows service that runs as a console too

 

Someone asked me to create a step by step guide on how to make a windows service that is easy to debug too. So here it is. You can do this in two ways. Either start by choosing the Windows Service template, chance the Application Type under MY Project –> Application to “Console Application” then select “Sub Main” in Startup Object. Add a Module, and add a function main to that. After that you can add a project installer under Service Class. Or if you want a bit more control you can start by creating a Windows Forms Application or Console Application and then add the Service things.

Create a Windows Application.

image

Add a reference to System.ServiceProcess and System.Configuration.Install

image

 

Add a class called ServiceManager with the following code

Public Class ServiceManager

    Private _serviceName As String
    Public Const PROCESSBASICINFORMATION As UInteger = 0

    <System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, Pack:=1)> _
    Public Structure Process_Basic_Information
        Public ExitStatus As IntPtr
        Public PepBaseAddress As IntPtr
        Public AffinityMask As IntPtr
        Public BasePriority As IntPtr
        Public UniqueProcessID As IntPtr
        Public InheritedFromUniqueProcessId As IntPtr
    End Structure

    <System.Runtime.InteropServices.DllImport("ntdll.dll", EntryPoint:="NtQueryInformationProcess")> _
    Public Shared Function NtQueryInformationProcess(ByVal handle As IntPtr, ByVal processinformationclass As UInteger, ByRef ProcessInformation As Process_Basic_Information, ByVal ProcessInformationLength As Integer, ByRef ReturnLength As UInteger) As Integer
    End Function

    Sub New(ByVal serviceName As String)
        _serviceName = serviceName
    End Sub

    Public ReadOnly Property status() As System.ServiceProcess.ServiceControllerStatus
        Get
            Using serviceController As New System.ServiceProcess.ServiceController(_serviceName)
                If Not serviceController Is Nothing Then
                    Try
                        Return serviceController.Status
                    Catch ex As Exception
                        Return ServiceProcess.ServiceControllerStatus.Stopped
                    End Try
                Else
                    Return ServiceProcess.ServiceControllerStatus.Stopped
                End If
            End Using
        End Get
    End Property

    Function IsServiceInstalled() As Boolean
        Using serviceController As New System.ServiceProcess.ServiceController(_serviceName)
            Try
                Dim status As System.ServiceProcess.ServiceControllerStatus = serviceController.Status
            Catch generatedExceptionName As InvalidOperationException
                Return False
            Catch ex As Exception
                EventLog.WriteEntry("ServiceManager", ex.ToString(), EventLogEntryType.[Error])
                Return False
            End Try
            Return True
        End Using
    End Function

    Private Function GetAssemblyInstaller(ByVal commandLine As String()) As System.Configuration.Install.AssemblyInstaller
        Dim installer As New System.Configuration.Install.AssemblyInstaller()
        installer.Path = System.Reflection.Assembly.GetExecutingAssembly().Location
        installer.CommandLine = commandLine
        installer.UseNewContext = True
        Return installer
    End Function

    Sub UninstallService()
        If Not IsServiceInstalled() Then
            Exit Sub
        End If
        Dim commandLine As String() = New String(0) {}
        commandLine(0) = System.Reflection.Assembly.GetExecutingAssembly().Location
        Dim mySavedState As IDictionary = New Hashtable()
        mySavedState.Clear()
        Dim installer As System.Configuration.Install.AssemblyInstaller = GetAssemblyInstaller(commandLine)
        Try
            installer.Uninstall(mySavedState)
        Catch ex As Exception
            EventLog.WriteEntry("ServiceManager", ex.ToString(), EventLogEntryType.[Error])
        End Try
    End Sub

    Sub InstallService()
        If IsServiceInstalled() Then
            Exit Sub
        End If

        Try
            Dim commandLine As String() = New String(0) {}
            commandLine(0) = System.Reflection.Assembly.GetExecutingAssembly().Location
            Dim mySavedState As IDictionary = New Hashtable()
            Dim installer As System.Configuration.Install.AssemblyInstaller = GetAssemblyInstaller(commandLine)
            Try
                installer.Install(mySavedState)
                installer.Commit(mySavedState)
            Catch ex As Exception
                installer.Rollback(mySavedState)
                EventLog.WriteEntry("ServiceManager", ex.ToString(), EventLogEntryType.[Error])
            End Try
        Catch ex As Exception
            EventLog.WriteEntry("ServiceManager", ex.ToString())
        End Try
    End Sub

    Sub StartService()
        If Not IsServiceInstalled() Then Exit Sub
        Using serviceController As New System.ServiceProcess.ServiceController(_serviceName)
            If serviceController.Status = ServiceProcess.ServiceControllerStatus.Stopped Then
                Try
                    serviceController.Start()
                    WaitForStatusChange(serviceController, ServiceProcess.ServiceControllerStatus.Running)
                Catch ex As InvalidOperationException
                    EventLog.WriteEntry("ServiceManager", ex.ToString(), EventLogEntryType.[Error])
                End Try
            End If
        End Using
    End Sub

    Sub StopService()
        If Not IsServiceInstalled() Then Exit Sub
        Using serviceController As New System.ServiceProcess.ServiceController(_serviceName)
            If serviceController.Status <> ServiceProcess.ServiceControllerStatus.Running Then
                Exit Sub
            End If

            serviceController.Stop()
            WaitForStatusChange(serviceController, ServiceProcess.ServiceControllerStatus.Stopped)
        End Using
    End Sub

    Private Sub WaitForStatusChange(ByVal serviceController As System.ServiceProcess.ServiceController, ByVal newStatus As System.ServiceProcess.ServiceControllerStatus)
        If Not IsServiceInstalled() Then Exit Sub
        Dim count As Integer = 0
        While serviceController.Status <> newStatus AndAlso count < 30
            System.Threading.Thread.Sleep(1000)
            serviceController.Refresh()
            count += 1
        End While

        If serviceController.Status <> newStatus Then
            Throw New Exception("Failed to change status of service. New status: " & newStatus)
        End If
    End Sub

    Public Shared Function getParentProcess(ByVal Process As Process) As Process
        Dim pi As New Process_Basic_Information
        Dim RetLength As UInteger
        NtQueryInformationProcess(Process.Handle, PROCESSBASICINFORMATION, pi, Runtime.InteropServices.Marshal.SizeOf(pi), RetLength)
        Return Process.GetProcessById(pi.InheritedFromUniqueProcessId)
    End Function
End Class

Add a new Class and call it something meaningful. This will be where you onStart onStop functions will be. I called it TestServiceControl

image

Add the following code to the Class

Public Class TestServiceControl
    Inherits System.ServiceProcess.ServiceBase

    Sub New()
        Me.ServiceName = "TestService"
    End Sub

    Protected Overrides Sub OnStart(ByVal args() As String)
        Try
            isStopping = False
            Dim ts As System.Threading.ThreadStart
            ts = AddressOf mainModule.doWork
            workerThread = New System.Threading.Thread(ts)
            workerThread.Start()
            'Me.CanStop = True
        Catch ex As Exception
            Log("TestService.OnStart: " & ex.ToString, EventLogEntryType.Error)
        End Try
    End Sub

    Protected Overrides Sub OnStop()
        Try
            isStopping = True
            ' stall for 10 seconds to let work finish
            For i As Integer = 0 To 10
                Threading.Thread.Sleep(1000)
                If Not workerThread.IsAlive Then Exit For
            Next
            ' Kill it if not done yet
            If workerThread.IsAlive Then workerThread.Abort()
        Catch ex As Exception
            Log("TestService.OnStop: " & ex.ToString, EventLogEntryType.Error)
        End Try
    End Sub

End Class

<System.ComponentModel.RunInstallerAttribute(True)> _
Public Class TestServiceControlInstaller
    Inherits System.Configuration.Install.Installer

    Friend WithEvents myServiceProcessInstaller As New System.ServiceProcess.ServiceProcessInstaller
    Friend WithEvents myServiceInstaller As New System.ServiceProcess.ServiceInstaller

    Sub New()
        myServiceProcessInstaller.Account = System.ServiceProcess.ServiceAccount.LocalSystem
        myServiceProcessInstaller.Password = Nothing
        myServiceProcessInstaller.Username = Nothing
        myServiceInstaller.Description = "Test Service"
        myServiceInstaller.DisplayName = "Test Service"
        myServiceInstaller.ServiceName = "TestService"
        myServiceInstaller.StartType = System.ServiceProcess.ServiceStartMode.Automatic

        Me.Installers.AddRange(New System.Configuration.Install.Installer() {myServiceProcessInstaller, myServiceInstaller})
    End Sub

    Private Sub ServiceProcessInstaller1_BeforeInstall(ByVal sender As Object, ByVal e As System.Configuration.Install.InstallEventArgs) Handles myServiceProcessInstaller.BeforeInstall
        Try
            ' examle of how to dyamicly set what service should run as.
            ' you can find the code to some of these functions else where on my blog
            'If _MachineMemberOfDomain() Then
            'LsaUtility.SetRight(InstallerAccount, "SeServiceLogonRight")
            'AddUserToLocalAdministrators(InstallerDomain & "\" & InstallerAccount)
            'ServiceProcessInstaller1.Account = ServiceProcess.ServiceAccount.User
            'ServiceProcessInstaller1.Username = InstallerDomain & "\" & InstallerAccount
            'ServiceProcessInstaller1.Password = InstallerPassword
            'Else
            'ServiceProcessInstaller1.Account = ServiceProcess.ServiceAccount.LocalSystem
            'End If
        Catch ex As Exception
            Log("ProjectInstaller: " & ex.ToString, EventLogEntryType.Error)
            Throw New Exception(ex.Message, ex)
        End Try
    End Sub

End Class

 

Rename Module1 to something meaningful (I called it mainModule ), and add the following code.

Module mainModule

    Friend myManager As New ServiceManager("TestService")
    Public isStopping As Boolean = False
    Public workerThread As System.Threading.Thread
    Public isService As Boolean = False

    Sub Main()
        Try
            ' Uncomment the following like if you need to attach as a debuger to the windows service
            Dim ParentProcess As Process = ServiceManager.getParentProcess(Process.GetCurrentProcess)
            isService = (ParentProcess.ProcessName.ToLower = "services")
            If My.Application.CommandLineArgs.Count > 0 Then
                Dim arg1 As String = My.Application.CommandLineArgs(0).ToLower
                If arg1 = "-u" Or arg1 = "-uninstall" Or arg1 = "/u" Or arg1 = "/uninstall" Then
                    If myManager.IsServiceInstalled Then
                        myManager.UninstallService()
                    Else
                        Console.WriteLine("im not installed as a service.")
                    End If
                    Exit Sub
                End If
            End If
            ' Some might prefere using /i instead. You can also start the service after using myManager.StartService
            If Not myManager.IsServiceInstalled Then
                Try
                    myManager.InstallService()
                Catch ex As Exception
                    Throw ex
                End Try
            End If
            If isService Then
                Try
                    System.ServiceProcess.ServiceBase.Run(New TestServiceControl)
                Catch ex As Exception
                    'screw it, guess im running as console then.
                    isService = False
                End Try
            Else
                doWork()
            End If
            If Debugger.IsAttached Then
                Console.WriteLine("Process has ended. Press enter to exit.")
                Console.ReadLine()
            End If
        Catch ex As Exception
            Log("mainModule.Main " & ex.ToString, EventLogEntryType.Error)
        End Try
    End Sub

    Public Sub Log(ByVal msg As String, ByVal msgType As System.Diagnostics.EventLogEntryType)
        Try
            If isService Then
                System.Diagnostics.EventLog.WriteEntry("TestService", msg, msgType)
            Else
                Console.WriteLine("[" & Now.Hour & ":" & Now.Minute & "." & Now.Second & "] " & msg)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Public Sub doWork()
        Try
            ' Add all you initializing code here. Dont use Main. The Service controler dont like waiting on Start Controls
            ' A cute trick here is closing gracefullt using stopMyself() in case that is needed. You can do that pretty much anywhere
            While Not isStopping
                ' Do what ever you need to do as a service



                If Not isService Then
                    isStopping = True
                Else
                    Threading.Thread.Sleep(10000)
                End If
            End While
        Catch ex As System.Threading.ThreadAbortException
            ' this is known, ignore it ( TestServiceControl.OnStop is killing me )
        Catch ex As Exception
            Log("mMain.doWork: MainLoop cought an unhandled exception: " & ex.ToString, EventLogEntryType.Error)
        End Try
    End Sub

    Sub stopMyself()
        Log("mMain.stopMyself: Forcing my self to stop (CloudInstallerService)", EventLogEntryType.Information)
        If isService Then
            Try
                myManager.StopService()
            Catch ex As Exception
                Log("mMain.stopMyself: " & ex.ToString, EventLogEntryType.Error)
                isStopping = True
            End Try
        Else
            isStopping = True
        End If
    End Sub

End Module

And that’s it. You can now more rapidly work with a Windows Service.
You can download this as a Microsoft Visual Studio 2010 solution here.

Ingen kommentarer:

Send en kommentar