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.

Errorhandling in VBA. Teil 1.

Grundlagen 1/2

VBA bringt mit „On Error“ ein eigenes Konstrukt zur Fehlerbehandlung mit. Grundsätzlich gibt es zwei Arten wie man in VBA auf einen Fehler reagieren kann.
1. Ignorieren
2. Reagieren

Während im ersten Teil das Thema „ignorieren“ behandelt wird, wird sich der zweite Teil mit dem Thema „reagieren“ auseinandersetzen.

Der Standardfall.
Nehmen wir als Anschauungsobjekt eine einfache Prozedur. Nennen wir sie kreativerweise mal „RaisingError“. Diese Funktion kann erstmal recht wenig und sieht wie folgt aus:

Public Sub RaisingError
	debug.print 1 / 0
End Sub

Führen wir diese Funktion aus, erhalten wir eine Fehlermeldung in Form einer Dialogbox in wir die Ausführung beenden können, oder in die Debug-Umgebung springen können.
Für einen Entwickler ist das sicherlich noch kein Beinbruch, aber für einen Anwender ist das schnell die totale Überforderung. Erstens weiß dieser im Zweifel nichts mit „Debuggen“ anzufangen, und erst recht wird er im VBA-Editor nicht seine zweite Heimat haben, am wichtigsten aber ist, dass er keine Ahnung von dem Code haben wird, den ihr geschrieben habt. Also, was soll der arme Mensch dann in der Debug-Umgebung…

Fehler ignorieren. On Error Resume Next
Setzen wir bei der obigen Prozedur mal ein “On Error Resume Next” vor die 1. Zeile und starten die Prozedur erneut. Die Dialogbox, die uns darauf aufmerksam macht, dass eine Division durch Null ein Fehler ist, taucht nun nicht auf. Der Fehler wird schlicht ignoriert. Das ist auch gut so, jedenfalls in dem hier vorliegenden Fall.

Public Sub RaisingError
	On Error Resume Next

	debug.print 1 / 0
End Sub

Der ausgelöste Fehler wird zwar ignoriert, dennoch lässt er sich anzeigen. Hierfür können wir das Err-Objekt abfragen. Das Err-Objekt hat, neben ein paar anderen, zwei zentrale Eigenschaften: Number und Description. In Number wird der als letztes aufgetretene Fehlercode festgehalten, in Description eine manchmal mehr, manchmal weniger aussagekräftige Fehloerbeschreibung. Ein Fehler mit dem Fehlercode 0 ist dabei ein „Kein Fehler aufgetreten“-Antifehler.
Für unser obiges Beispiel heißt das, dass wir nach der 2.Zeile prüfen könnten, ob bei der geplanten Debug-Ausgabe ein Fehler aufgetreten ist. Wenn das der Fall ist, dann müsste der Fehlercode im Err-Objekt ungleich 0 sein.

Public Sub RaisingError
	On Error Resume Next

	debug.print 1 / 0

	If Err.Number <> 0 Then
		Debug.Print “Hier ist ein Fehler aufgetreten.”
		Debug.Print „Genauer: Fehlercode „ & Err.Number & „ Beschreibung: „ & Err.Description
	End If
End Sub

Nun muss man hier aber noch eine Anmerkung unterbringen. Sehen wir uns die folgende Prozedur an:

Public Sub RaisingError
	On Error Resume Next

	debug.print 1 / 0

	debug.print 1 + 2

	If Err.Number <> 0 Then
		Debug.Print “Hier ist ein Fehler aufgetreten.”
		Debug.Print „Genauer: Fehlercode „ & Err.Number & „ Beschreibung: „ & Err.Description
	End If
End Sub

Hier ist in der uns wohl bekannten Zeile der „übliche“ Fehler aufgetreten, in der folgenden Zeile wird eine einfache Addition ausgeführt bei der kein Fehler auftreten sollte. Dennoch finden wir in der folgenden Zeile bei der Fehlercode-Abfrage einen Fehler der ungleich 0 ist. Sprich: der letzte vorkommende Fehler muss zurückgesetzt werden um ein wirklich verlässliches Ergebnis zu erhalten. Hierfür kommt die Methode „Clear“ des Err-Objekts zum Einsatz.

Public Sub RaisingError
	On Error Resume Next

	debug.print 1 / 0
	Err.Clear

	debug.print 1 + 2

	If Err.Number <> 0 Then
		Debug.Print “Hier ist ein Fehler aufgetreten.”
		Debug.Print „Genauer: Fehlercode „ & Err.Number & „ Beschreibung: „ & Err.Description
	End If
End Sub

Damit sind wir mit diesem Teil schon am Ende. Wir haben gesehen dass es ein Möglichkeit gibt, die Standard-Fehlerbehandlung auszuschalten und damit die Interaktion zu unterbinden. Mit dem „On Error Resume Next“-Statement wird ein linearer Errorhandler initiiert, welche grundsätzlich einen auftretenden Fehler ignoriert und die Fehlerbehandlung in die Hände des Entwicklers legt.
Das hat den Vorteil dass sehr punktuell auf Fehler reagiert werden kann, aber auch den Nachteil dass man im Prinzip wissen muss, wann und wo ein Fehler auftreten kann. Zudem ist darauf zu achten, dass sich Fehler nach Auftreten nur mit einem Err.Clear zurücksetzt.
Bis zum nächsten Teil.

Errorhandling in VBA

Ich möchte hier mal ein Fass aufmachen und das Thema Errorhandling in VBA an Hand von Access genauer unter die Lupe nehmen. Das ist sicherlich ein komplexes Thema aber auch ein elementares da ohne ein vernünftiges Errorhandling Probleme nicht nur schwerer zu finden sind sondern vor allem weil es den negativen Seiteneffekt hat, dass Programme bei Problemen einen Runtime-Error produzieren. Das kann zu vielerlei Szenarien führen, die dann nicht mehr kontrolliert abgefangen werden können. Bsw dass Werte auf Grund des ad hoc Programmausstiegs verloren gehen, der User das aber gar nicht mitbekommt und daher in Situationen gerät, in der das Programm im freien Fall ist. Wenn all diese Unwägbarkeiten im Sourcecode kontrolliert werden sollen, kann man sich einfach vorstellen, dass sich das Programm dadurch ungeheuer aufblähen kann. Warum also nicht einfach eine funktionierende Fehlerbehandlung einsetzen und so eine Menge Probleme einfach gar nicht erst entstehen lassen.
Die Beiträge zum Thema Errorhandling werden sich auf mehrere Posts verteilen. Angefangen von den Basics bis letztendlich zu einem komplexen Errorhandler der Momentaufnahmen der Anwendung zur Fehlerzeit aufzeichnet und so auch im Nachhinein nachvollziehbar macht.

Beginnen werden wir also mit dem ersten Posting zu dem Thema, den Grundlagen. mehr dazu im Posting „Errorhandling in VBA. Teil 1: Grundlagen.“

Simple XML (2/2)

Fortsetzung von Teil 1.

5. Speichern. Da wir ohne weiteres Zutun die Daten bereits bearbeiten können (der erste und einzigste vorhandene Datensatz wird bei Start der Applikation bereits angezeigt), fehlt noch eine Methode um die Veränderungen zurück in das XML-File zu speichern. Das geht genauso unkompliziert wie auch die Datenabholung:

_oDataSet.WriteXml(_sPATH_TO_XML_FILE)

Das ist alles. Nun packen wir das Ganze noch in eine eigene private Sub SaveData und platzieren ein Button auf das Formular der die Methode SaveData bei Klick aufruft.

6. Navigieren. Wir haben nun ein gebundenes Formular vorliegen. Was fehlt ist eine Navigation um auch durch die gelesenen Datensätze zu blättern. Hierzu brauchen wir zwei neue Button:
cmdForward, Text: „>“ und cmdBack, Text: „<“. Beide haben ein Klick-Ereignis.
Mit oben schon erwähnten DataBinding können wir nun in einem Kontext arbeiten, der uns die Navigation ebenfalls sehr einfach macht. Im Falle von cmdForward ist das

Me.BindingContext(_oDataTable).Position += 1

um einfach in den vorhandenen Datensätzen eine Position weiter zu springen und analog

Me.BindingContext(_oDataTable).Position -= 1

um eine Position zurückzugehen.

Das Beste ist: Sie brauchen sich keine Sorgen darüber zu machen, ob Sie über das „Ziel hinaus“ hinausschießen, also ob bsw die Position kleiner als 0 oder größer als die Anzahl der Datensätze wird. Das wird alles im BindingContext für Sie erledigt, sodass es tatsächlich bei den beiden Aufrufen bleiben kann.
Um zu dem letzen Datensatz zu springen, können Sie übrigens einfach

Me.BindingContext(_oDataTable).Position = Me.BindingContext(_oDataTable).Count

aufrufen.

7. Neuer Datensatz. Sehen wir uns nun an, wie man einen neuen Datensatz hinzufügen kann. Hierzu benötigen wir wieder einen neuen Button der mit „Add New“ betitelt wird und cmdAddNew benannt wird. Hierfür definieren wir das Klick-Ereignis:

Private Sub cmdAddNew_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdAddNew.Click
    Dim oDataRow As DataRow

    Me.txtInternalNumber.Text = Me.BindingContext(_oDataTable).Count + 1
    Me.txtTitle.Text = ""
    Me.txtInterpret.Text = ""

    oDataRow = _oDataTable.NewRow()
    With oDataRow
        .Item("InternalNumber") = Me.txtInternalNumber.Text
        .Item("Title") = Me.txtTitle.Text
        .Item("Interpret") = Me.txtInterpret.Text
    End With

    _oDataTable.Rows.Add(oDataRow)
    oDataRow = Nothing

    SaveData()

    Me.BindingContext(_oDataTable).Position = Me.BindingContext(_oDataTable).Count
End Sub

Da wir innerhalb unserer Datentabelle eine neue Zeile einfügen wollen, definieren wir uns zunächst eine neue DataRow. Diese Datarow initialisieren wir dann als neue Zeile (NewRow) und binden die einzelnen Controls an die jeweiligen Datenfelder (Item = Control.Text). Im weiteren Verlauf fügen wir die neue Datenzeile der Datentabelle zu (Rows.Add) und speichern die gesamte XML-Datei ab.
Um die Controls für die Aufnahme neuer Daten vorzubereiten, löschen wir vorher noch alle Eingaben in den Controls raus (Control.Text = „“) bzw. setzen die InternalNumber auf einen eindeutigen Wert. Diesen können wir, wie schon gesehen, ganz einfach aus dem BindingContext ermitteln. Da der Count, also die Anzahl der im BindingContext enthaltenen Elemente, gleich der Anzahl der CDs-Elemente in der XML-Datei entspricht, ist mit Sicherheit immer der folgende Ausdruck eindeutig:

Me.BindingContext(_oDataTable).Count + 1

Damit wir im Anschluß an diese „Vorbereitung zur Dateneingabe“ auch den Datensatz direkt bearbeiten können, springen wir noch mit der letzten Zeile der Methode zu dem eben angelegten Datensatz.

Ein weiterer positiver Seiteneffekt ist, dass wir (sofern wir keine Löschoperationen vornehmen oder das Control txtInternalNumber bearbeitbar lassen) dadurch eine durchängige, aufsteigende Nummerierung aller CDs erhalten.

Da wir schon einen Save-Button auf dem Formular realisiert haben, können wir nun die Applikation starten, den Button „Add New“ drücken, neue Daten eingeben und mit „Save“ die Daten speichern. Ein Blick ins XML-File bestätigt das.

8. Filtern. Um unsere eingangs postulierte Funktionspalette zu komplettieren benötigen wir nun noch einen Filter. In unserem einfachen Beispiel für eine kleine Statistik.
Wir wollen uns nun noch anzeigen lassen, wie viele CDs wir besitzen, in denen der Interpret gleich „Counting Crows“ ist.
Hierzu müssen wir nur die Datentabelle nach unseren gewünschten Kriterien selektieren. Also als Beispiel:

_oDataTable.Select(„Interpret = ‚Counting Crows’“)

Als Rückgabewert dieser Funktion erhalten wir ein Array von DataRows, können also mit der Length-Methode direkt auf die Anzahl der gefilterten Zeilen zugreifen.

Ergebnis. XML ist wirklich einfach, jedenfalls in den grundsätzlichen Funktionen kommt man mit wenigen Zeilen aus um ein einfaches Programm mit XML-Unterstützung zu kreieren. Die (manuelle) Datenbindung ist da ebenso einfach und ermöglicht vielerlei schöne Möglichkeiten. Man denke da nur an die Möglichkeit statt Daten an die „Text“-Eigenschaft an andere Eigenschaften zu binden…

Anmerkung. Dieses Beispiel funktioniert übrigens auch im Compact Framework also bsw als Windows Mobile Programm. Hierbei ist lediglich eine kleine Änderung nötig um den Pfad zum Daten-XML-File herauszufinden. Siehe hierzu das Posting “Pfad der aktuellen Anwendung ermitteln”.

Das Beispiel-Projekt für VS2005 könnt ihr hier herunterladen.