Ein Quick-And-Dirty Ansatz gegen Windows Search Indexer im Akkubetrieb

Es gibt Tage da freut es einen mehr als sonst, dass man ein Softwareentwickler ist. Neulich beispielsweise war so ein Tag an denen man eben mal schnell ein Problem lösen kann und sich damit das Leben – zumindest ein wenig – erleichtern kann.
Mein heutiges Problem war folgendes. Ich bin häufig mit meinem Notebook unterwegs und dann auch auf lange Akkulaufzeiten angewiesen. Nun habe ich aber festgestellt, dass es manche Programme rein gar nicht stört wenn man im Akkubetrieb ist und diese fröhlich weiterlaufen ohne eine Chance zu haben, das per Settings zu ändern.
Mein heutiger Kandidat war Microsoft Windows Search. An sich ein gutes Programm was viel (Such-)Arbeit ersparen kann. Aber eben auch ein Programm welches ziemlich regelmäßig die Festplatte indiziert und damit Strom verbraucht. Wenn man unterwegs ist, ist das meines Erachtens völlig unnötig, eine Indizierung kann durchaus auch nur dann stattfinden, wenn man an der Steckdose hängt.
Nun gibt es zugegebenermaßen die Option die Indizierung anzuhalten, was aber den Nachteil hat, dass man das immer wieder machen muss, wenn man die Steckdose verlässt. Vergisst man das mal, läuft die Indizierung wieder. Also musste eine andere Lösung her und mein Quick and Dirty-Attempt führte mich zu folgendem Algorithmus:

1. Prüfe alle x Sekunden ob Notebook im Akkubetrieb ist oder nicht
2. Wenn in Akkubetrieb, dann Stoppe den Windows Search Indexing Dienst
3. Wenn in Netzbetrieb, dann Starte den Windows Search Indexing Dienst, sofern er nicht schon läuft

Da das ganze im Hintergrund laufen soll, sollte das Programm seinerseits ein Dienst werden. Obendrauf sollte noch eine kleine Anwendung für die Tray um die Einstellungen für den Dienst zu verändern.
Der Service selber steuert sich über einen Timer in Form eines Delegates. Das Abfrageintervall (Polling) sowie alle anderen Einstellungen werden in der Registry abgelegt. das geht flott und einfach mit Bordmitteln des .net-Frameworks und bedarf hier nur einem einzelnen Auftruf.

    Protected Overrides Sub OnStart(ByVal args() As String)
        ' Code zum Starten des Dienstes hier einfügen. Diese Methode sollte Vorgänge
        ' ausführen, damit der Dienst gestartet werden kann.
        MainStart()
    End Sub

    Private Sub MainStart()
        SetPollInterval()
        InitTimer()
    End Sub

    Private Sub SetPollInterval()
        'Setze einen Standardwert (Klassen-Konstante)
        _iPollInterval = POLL_INTERVAL_DEFAULT

        Try
            ' hier wird auf die Registry zugegriffen und aus einem 
            ' speziellen Bereich die Einstellungen geholt.
            ' In dem Fall alles durch Klassen-Konstanten definiert.
            _iPollInterval = GetSetting(_SETTINGS_APPLICATION, _SETTINGS_SECTION, "PollInterval", POLL_INTERVAL_DEFAULT.ToString)
        Catch ex As Exception
            ' Im fall von einem Zugriffsfehler, schreibe in EventLog
            WriteEventLog("[WiSIM.SetPollInterval] Could not access Registry Settings. Using default values. " & ex.Message, 3)
            _iPollInterval = POLL_INTERVAL_DEFAULT
        End Try
    End Sub

#Region "Timer"
    Private Sub InitTimer()
        Try
            ' Timer in Form eines Delegates erstellen.
            KeepAliveDelegate = New TimerCallback(AddressOf ControlTimer)
            KeepAliveTimer = New Timer(KeepAliveDelegate, Nothing, New TimeSpan(0), New TimeSpan(0, 0, _iPollInterval))
        Catch ex As Exception
            'couldn't init Delegates
            WriteEventLog("[WiSIM.InitTimer] " & ex.Message, 3)
        End Try
    End Sub

    ' Hier die Methode, die bei Ablauf eines Timerintervalls aufgerufen 
    ' wird. Hier spielt sich also alles ab und ist sozusagen die 
    ' Main()-Methode des Dienstes
    Private Sub ControlTimer(ByVal State As Object)
        Try
            SetPollInterval()

            ' Wir prüfen noch ein wenig ob nicht noch ein früherer
            ' Durchlauf aktiv ist. Eine Karenz von 2 Sekunden sollte
            ' in dem Fall dafür reichen
            If LastPolling.AddSeconds(_iPollInterval - 2) < Now Then
                LastPolling = Now

                _bIsRunning = True

                Try
                    ' Wieder Zugriff auf die Registry. 
                    _bIsRunning = CType(GetSetting(_SETTINGS_APPLICATION, _SETTINGS_SECTION, "IsRunning", True), Boolean)
                Catch ex As Exception
                    WriteEventLog("[WiSIM.ControlTimer] Could not access Registry Settings. Using default values. " & ex.Message, 3)
                    _bIsRunning = True
                End Try

                If _bIsRunning Then
                    WriteEventLog("WiSIM is enabled.", 1)
                    ' Hier der Aufruf um die eigentliche Funktionalität
                    ' zu starten. Also die Prüfung des Energiestatus etc.
                    Process()
                Else
                    WriteEventLog("WiSIM was disabled.", 1)
                End If
            Else
                WriteEventLog("[WiSIM.ControlTimer] Thread tries to run wild. Attempt caught.", 2)
            End If
        Catch ex As Exception
            WriteEventLog("[WiSIM.ControlTimer] " & ex.Message, 3)
        End Try
    End Sub
#End Region

So, nun müssen wir also den Power Status von unserem Computer rausfinden. Dafür bedienen wir uns einem Kernel32.dll-Aufruf

    Private Declare Auto Function GetSystemPowerStatus Lib "kernel32.dll" (ByRef lpSystemPowerStatus As SYSTEM_POWER_STATUS) As Integer

darüber hinaus nehmen wir noch ein paar Definitionen mit dazu, um das ganze ein wenig strukturierter werden zu lassen. Die Sektion erklärt sich wohl von selber:

#Region "Structures and Enumerations"
    Public Structure SYSTEM_POWER_STATUS
        Public ACLineStatus As ACLineStatus
        Public BatteryFlag As BatteryFlag
        Public BatteryLifePercent As Byte
        Public Reserved1 As Byte
        Public BatteryLifeTime As Integer
        Public BatteryFullLifeTime As Integer
    End Structure

    Public Enum BatteryFlag As Byte
        High = 1
        Low = 2
        Critical = 4
        Charging = 8
        NoSystemBattery = 128
        Unknown = 255
    End Enum

    Public Enum ACLineStatus As Byte
        Offline = 0
        Online = 1
        Unknown = 255
    End Enum

    Public Enum ServiceRunCommand As Byte
        StartCommand = 1
        StopCommand = 2
    End Enum
#End Region

Nun haben wir im Prinzip alles was wir brauchen. Mit GetPowerStatus() können wir den Status unserer Energieversorgung abfragen und später komfortabel abfragen.

    Private Sub Process()
        Dim oPowerState As SYSTEM_POWER_STATUS
        oPowerState = New SYSTEM_POWER_STATUS
        oPowerState = GetPowerStatus()

        If IsNothing(oPowerState) Then
            WriteEventLog("[WiSIM.Process] Could not decide wether to Stop or Start Windows Search Service due to unknown AC Status. Step was skipped.", 2)
            Exit Sub
        End If

        sEventLogEntry = ""
        bWriteSuccessEntry = False

        If oPowerState.ACLineStatus = ACLineStatus.Online Then
            'start
            WriteEventLog("[WiSIM.Process] ACLineStatus is ONLINE", 1)
            ControlIndexingService(ServiceRunCommand.StartCommand)
        Else
            'stop
            WriteEventLog("[WiSIM.Process] ACLineStatus is OFFLINE", 1)
            ControlIndexingService(ServiceRunCommand.StopCommand)
        End If

        If bWriteSuccessEntry And Len(sEventLogEntry) > 0 Then
            WriteEventLog(sEventLogEntry, 1)
        End If
    End Sub

Die Funktion “GetPowerStatus” besteht seinerseits nur aus einem Aufruf der oben definierten Kernel-Funktion:

    Public Function GetPowerStatus() As SYSTEM_POWER_STATUS
        Dim oSystemPowerStatus As SYSTEM_POWER_STATUS
        oSystemPowerStatus = New SYSTEM_POWER_STATUS

        Try
            GetSystemPowerStatus(oSystemPowerStatus)
        Catch ex As Exception
            oSystemPowerStatus = Nothing
            WriteEventLog("[WiSIM.GetPowerStatus] " & ex.Message, 3)
        End Try

        Return oSystemPowerStatus
    End Function

Nun müssen wir nur noch abfragen ob der Windows Search Indexer aktiv ist oder nicht und darauf reagieren. Hierzu stellt .net das ServiceController-Objekt (Namespace System.ServiceProcess) zur Verfügung, das alle Funktionen mitbringt, die wir brauchen. Hier fragen wir einfach über den Dienstnamen (der heißt bei Windows Indexing Service „WSearch“) den Index-Service ab. Das Argument „RunCommand“ gibt an, ob der Service den Start, Stop oder den Pause-Befehl erhalten soll. Entsprechend wird darauf reagiert und der Windows-Dienst gesteuert.

    Public Function ControlIndexingService(ByVal RunCommand As ServiceRunCommand) As Boolean
        Dim oServiceController As ServiceController
        Dim bReturn As Boolean
        bReturn = True

        oServiceController = New ServiceController(_INDEXING_SERVICE_NAME, System.Environment.MachineName)
        oServiceController.Refresh()

        bWriteSuccessEntry = False

        Try
            Select Case RunCommand
                Case ServiceRunCommand.StartCommand
                    'is already running ?
                    bWriteSuccessEntry = True
                    sEventLogEntry = "Windows Search is already running."

                    If oServiceController.Status <> ServiceControllerStatus.Running Then
                        oServiceController.Start()
                        sEventLogEntry = "Windows Search is starting."
                    End If
                Case ServiceRunCommand.StopCommand
                    If oServiceController.CanStop Then
                        oServiceController.Stop()
                        bWriteSuccessEntry = True
                        sEventLogEntry = "Windows Search is stopping."
                    End If
            End Select
        Catch ex As Exception
            WriteEventLog("[WiSIM.ControlIndexingService] " & ex.Message, 3)
            bReturn = False
        End Try

        Return bReturn
    End Function

Das ist auch schon alles. Wie gesagt, es ist ein Quick-and-Dirty-Ansatz der ein wenig im Powercoding-verfahren (zu deutsch: alles schnell, schnell) entstanden ist, der aber sein Dienst klagenfrei verrichtet und mich wieder ein wenig fauler gemacht hat...
Wie oben schon erwähnt, habe ich es mir nicht nehmen lassen, diesen Service um ein Settings-Progrämmchen zu erweitern. Das stelle ich dann in den kommenden Tagen vor.