Abstürzende Dienste einer betriebswirtschaftlichen Software sind lästig. Sind es mehrere, wird es ärgerlich. Kommt noch ein Software-Lieferant hinzu, der das Problem nicht lösen will oder kann, wird es kompliziert. Eine schnelle und pragmatische Lösung musste kurz vor Ostern her. Wenn da nicht noch ein anders Übel im Weg stehen würde: Es handelt sich um Windows-Dienste.
Kein Weiterkommen mit Bordmitteln
Üblicherweise bieten moderne Betriebssysteme einem Admin das notwendige Instrumentarium zur Steuerung von Diensten. Unter Windows ist es das grafische Snap-In services.msc in der seit NT4 Option Pack kaum veränderten Microsoft Management Console1. Von dort aus lassen sich die Startarten und bis zu drei Fehlerbehandlungen einstellen:
- Dienst neu starten
- Ein Programm ausführen
- Computer neu starten
Dummerweise lassen sich diese Aktionen nur für drei Fehlerbehandlungen nutzen. Nach dem vierten Fehler bleibt ein Dienst für mindestens einen Tag tot. Irgendjemand in Redmond meinte in den ausgehenden 90er Jahren des letzten Jahrtausends den Fehlerzähler nur tageweise einstellen zu müssen. Die grafische Dienststeuerung von Windows ist somit ungeeignet. Die seit Jahrzehnten unveränderte Oberfläche ist ein in Code manifestiertes Statement, an dem sich auch künftig nichts ändern wird.
Wie sieht es mit der PowerShell2 aus? Normalerweise finden sich dort Funktionen, die in der GUI fehlen. Auch hier blickt ein Admin in den Abgrund eines seit Jahrzehnten angesammelten technologischen Scherbenhaufens.
Die für Dienste in Windows zuständige Instanz ist der Service Control Manager, kurz SCM3. Seit NT4-Zeiten führt an diesem by Design kein Weg vorbei. Im .NET Framework existieren zwar Methoden und Eigenschaften zum Starten oder Stoppen von Diensten. Der direkte Weg zum unmanaged4 SCM bleibt konzeptionell verwehrt. Die Funktionsarmut entspricht dem der grafischen Oberfläche.
Einen indirekten, ziemlichen “dirty way” beschreibt der MSDN-Artikel “Writing Windows Services in PowerShell”.5 Aus einem PowerShell-Skript heraus generiertes C-Programm, wird von der Runtime als Dienst registriert und ausgeführt. Die Nachteile dieser Bastellösung sprechen für sich: Wo normalerweise eine EXE-Datei ausreicht muss mit mehrere Skripten und einer EXE gearbeitet werden. Die aus guten Gründen ausgeschaltete Skriptausführung muss systemweit aktiviert werden. Der Overhead des .NET Script-Interpreters6 wird mitgeschleift. Die üblichen Dienstkonten mit niedrigen Privilegien funktionieren nicht. Es sind volle Administrationsrechte notwendig. So sieht sicherheits- und programmiertechnischer Wahnsinn aus, den niemand in einer produktiven Umgebung sehen will. Der Autor des MSDN-Artikels ist sich dessen im Klaren:
A service script written in Windows PowerShell will be good for prototyping a concept and for tasks with low performance costs (…) But for any high performance task, a rewrite in C++ or C# is recommended.
Mit Windows-Bordmitteln ist kein Weiterkommen. Ein System ohne Möglichkeit zur Steuerung von Diensten ist qua Definition kein Betriebssystem und hat nichts in einer produktiven Umgebung zu suchen. Doch diese Diskussion ist an anderer Stelle zu führen.
Ein Dienst muss her
Zurück zum Ausgangsproblem: Wie können störrische, erratisch abstürzende Dienste kontrolliert und automatisch neu gestartet werden? Mit einem Dienst, der via SCM den Status abfragt und diese bei Bedarf neu startet. Geräuschlos und unaufgeregt. Welche Dienste genau zu überwachen sind, steht in einer Textdatei, die von einem Admin editierbar sein soll.
Für solche Aufgabenstellungen habe ich eine Geheimwaffe. Maschinennahe (es kann sogar inline in Assembler programmiert werden), “rocksolid” über viele Jahrzehnte bewährt, mit der vollen Bandbreite aller Sprachfeatures moderner Hochsprachen und doch einfach im Handling. C-kompatible Bibliotheken können direkt eingebunden werden und zu ziemlich alles und jedem bestehen Bindings. Selbstverständlich alles in frei und Open Source.
Die Lösung: Servicewatcher
Ich habe den Dienst in etwa zwei Stunden in der Programmiersprache Freebasic7 erstellt und im fbc integrierten gcc8 compiliert. Die 64 Bit Windows EXE des auf den Namen “servicewatcher” getauften Tools ist nur 56 KB groß. Nativ9 ohne Abhängigkeiten werden nur 524 KB an RAM benötigt. Die CPU Belastung ist nicht messbar.
Dabei habe ich das Rad nicht neu erfunden. Ein fertiges Beispiel in C für Windows-Dienste existiert in der MSDN.10 Im deutschsprachigen Freebasic-Portal hat sich bereits jemand die Mühe der Portierung gemacht.11 Dessen Umsetzung lief aber leider nicht in 64 Bit und ließ einige benötigte Funktionen vermissen. Es war aber ein Anfang und diente als Grundgerüst. Die fehlenden Puzzlestücke habe ich mir aus der MSDN ableiten können.
Als IDE eignet sich jeder bessere Codeeditor mit frei konfigurierbarem Build-System. Unter Linux nutze ich Geany12. GTK-Oberflächen erstelle ich visuell mit GNOME Glade13. Weil ich für den servicewatcher mit dem Windows SCM reden muss und für die Konsolenanwendung14 keine UI benötige, habe ich meine gewohnte Umgebung verlassen und alles in einer Windows-VM mit dem in Freebasic erstellten FBEdit15 programmiert.
Wer mit Basic-Dialekten gearbeitet hat findet sich in Freebasic sofort zurecht. Es erfüllt alle Anforderungen, die an objektorientierte16 Sprachen gestellt werden. Wer unbedingt will, kann es auch “Quick and Dirty” imperativ17 wie ein GWBASIC18 mißbrauchen.
Vielleicht ist es meiner Vergangenheit geschuldet, dass ich in den 80er Jahren zuerst mit BASIC19 auf einem Commodore C64 in Berührung kam. Auf meinem ersten Schneider Euro-PC nutzte ich Turbo-Basic bzw. dessen Nachfolger Power-Basic20. In den 90er folgte Visual-Basic, deutlich später VB.NET und Xojo. Bis heute ist mir BASIC in seiner modernen, hochsprachigen Form irgendwie näher als C oder Python.
Kleine Herausforderungen und Tools wie der servicewatcher sind daher willkommen und schnell “herunter” geschrieben und machen ohne viel Tamtam einfach nur ihren Job.
Sub ServiceStopRaw()
Dim ServiceStat As SERVICE_STATUS
Dim hServiceControlManager As HANDLE
Dim hService As HANDLE
hServiceControlManager = OpenSCManager(pg->wsComputerName, BYVAL NULL, SC_MANAGER_CREATE_SERVICE)
If hServiceControlManager Then
hService = OpenService(hServiceControlManager, pg->wsServiceName, SERVICE_ALL_ACCESS)
If hService Then ControlService(hService, SERVICE_CONTROL_STOP, @ServiceStat)
CloseServiceHandle(hServiceControlManager)
EndIf
End Sub
Die eigentliche Fehlerursache der absturzfreudigen Dienste der warenwirtschaftlichen Lösung ist nicht behoben. Die in Freebasic programmierte Lösung lindert lediglich die Schmerzen des Kunden.
Wie es sich gehört hat er den Quellcode in GPL-Lizenz zusammen mit einer kurzen Beschreibung als Git-Repo übergeben bekommen.
In diesem Sinne,
Euer Tomas Jakobs
Update vom 07.08.2024:
Das Repo zu diesem Projekt ist auf Codeberg.org zu finden:
https://codeberg.org/tomas-jakobs/servicewatcher
https://de.wikipedia.org/wiki/Microsoft_Management_Console ↩︎
https://learn.microsoft.com/en-us/archive/msdn-magazine/2016/may/windows-powershell-writing-windows-services-in-powershell ↩︎
https://learn.microsoft.com/de-de/windows/win32/services/the-complete-service-sample ↩︎
https://freebasic-portal.de/code-beispiele/system/windows-service-beispiel-306.html ↩︎
https://en.wikipedia.org/wiki/Object-oriented_programming ↩︎