From cc1e59916ab29fa1cc44192af3c6d655abd6a170 Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Tue, 11 Aug 2026 10:10:28 +0200 Subject: [PATCH] Fix legacy installer updates and diagnostics --- Dokumentation.md | 16 ++ README.md | 29 ++- deployment/INSTALLATION.md | 27 +- ...ndpoint-Reachability-Analyse-2026-08-10.md | 2 +- ...k_Checkmk_Pulse_Architekturueberblick.docx | Bin 277162 -> 277253 bytes .../Installer-Self-Test-Analyse-2026-08-11.md | 68 +++++ docs/Integration.md | 10 +- scripts/generate-architecture-overview.py | 13 +- .../BizTalkCheckmkPulse.Setup.csproj | 1 + .../InstallerEngine.cs | 244 +++++++++++++++--- .../Properties/AssemblyInfo.cs | 4 +- .../SetupOperationLog.cs | 126 +++++++++ .../TaskSchedulerService.cs | 86 +++++- .../Properties/AssemblyInfo.cs | 4 +- tests/BizTalkCheckmkPulse.Tests/Program.cs | 62 +++++ 15 files changed, 629 insertions(+), 63 deletions(-) create mode 100644 docs/Installer-Self-Test-Analyse-2026-08-11.md create mode 100644 src/BizTalkCheckmkPulse.Setup/SetupOperationLog.cs diff --git a/Dokumentation.md b/Dokumentation.md index 08226e6..f6ff9c1 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -419,6 +419,22 @@ enthalten. Erst dann wird der normale Minutentask registriert und das Backup gelöscht. Bei Fehler oder Timeout wird die alte Version wiederhergestellt und ein frischer Lauf des alten Tasks abgewartet. +Version 2.2.5 korrigiert die versionsübergreifende Vorprüfung. Die neue +Paket-, Staging- und Zielversion muss weiterhin exakt neun gültige +Self-Test-Zeilen liefern. Für die installierte Ausgangsversion wird dagegen +deren vollständiger, eindeutiger und ausschließlich grüner Servicevertrag +akzeptiert, auch wenn er aus einer älteren Version mit acht Services stammt. +Rein additive Services blockieren das Update nicht; entfernte Namen und damit +auch Renames bleiben vor der Umschaltung bestätigungspflichtig. + +Parallel erzeugt jeder Installations-/Updatelauf ein separates +`%ProgramData%\BizTalkCheckmkPulse\logs\setup-*.log`. Protokolliert werden +Phase, Setup-/Binary-Version, Pfad und Größe, Self-Test-stdout/-stderr, +Exitcodes, Taskstatus, Exception-Kette, letzte Provider-Logzeilen bei einem +Abnahmefehler und das Rollback-Ergebnis. Das Collector-Kennwort wird der +Logging-Komponente nie übergeben. Ein frisch abgeschlossener Task mit +Fehlercode wird sofort erkannt; der Fehler erscheint dezimal und hexadezimal. + Der installierte Task selbst läuft mit `RunLevel Limited`. Der Installer übergibt das Kennwort direkt an die Windows-Task-Scheduler-COM-Schnittstelle; es wird weder in einer Prozesskommandozeile noch in Config oder Log diff --git a/README.md b/README.md index 210fb86..ce69357 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,10 @@ Der Datenaustausch ist bewusst defensiv: `UNKNOWN`-Snapshot und einen ungleich null lautenden Task-Exitcode. - Provider und Consumer protokollieren in tägliche Dateien; die Aufbewahrung ist standardmäßig 30 Tage. +- Jeder Installations-/Updatelauf schreibt zusätzlich ein eigenes + `setup-*.log` mit Phase, Binary-Version, Self-Test-Ausgabe, + Task-Scheduler-Status, Exception-Kette und Rollback-Ergebnis. Kennwörter + werden nicht an das Logging übergeben. - Snapshotgröße, WMI-/SQL-Timeouts, Log-Retention und Stale-Grenze sind begrenzt und konfigurierbar. @@ -263,19 +267,23 @@ als `UNKNOWN`. Ein gMSA kann über die Checkbox ohne Kennworteingabe installiert werden; die produktive Standardbeschreibung geht vom normalen Servicekonto aus. -Der Installer: +Der Installer (ab Version 2.2.5): -1. prüft Paket und neun Self-Test-Zeilen, bevor der bestehende Task angehalten - wird, +1. prüft Paket und Staging zwingend auf neun Self-Test-Zeilen, bevor der + bestehende Task angehalten wird. Der Servicevertrag der installierten + Legacy-Version darf weniger gültige Zeilen enthalten, 2. baut die neue Version in einem Staging-Verzeichnis auf und übernimmt vorhandene AppSettings; neue Keys erhalten ihre neuen Defaults. Die alten unveränderten Endpoint-Defaults `12`/`500` werden auf `16`/`100` migriert, -3. vergleicht installierten und neuen Checkmk-Servicevertrag; ein - unbestätigter Rename stoppt vor jeder Umschaltung, +3. vergleicht installierten und neuen Checkmk-Servicevertrag. Rein additive + Services werden zugelassen und als Discovery-Hinweis gemeldet; entfernte + oder umbenannte Services stoppen ohne ausdrückliche Bestätigung vor jeder + Umschaltung, 4. schaltet die Programmdateien per Verzeichniswechsel um, 5. startet einen triggerlosen Provider-Abnahmelauf unter dem echten Collector-Konto und erzwingt einen vollständigen Endpoint-Katalogabgleich, -6. wartet höchstens vier Minuten auf `LastTaskResult=0` und validiert danach +6. wartet höchstens vier Minuten auf `LastTaskResult=0`, bricht bei einem + bereits abgeschlossenen Fehlerlauf sofort mit Dezimal-/Hexcode ab und validiert danach mit der Produkt-EXE einen erst nach Installationsbeginn erzeugten Snapshot, die Collector-Identität, alle neun eindeutigen Services ohne `UNKNOWN` sowie den frischen, maschinen- und umgebungsgebundenen Endpoint-Katalog, @@ -283,7 +291,8 @@ Der Installer: werden vorherige Version, Wrapper und Task samt frischem Altsnapshot wiederhergestellt, 8. behält `%ProgramData%\BizTalkCheckmkPulse` mit Endpoint-Katalog, Snapshot - und Logs bei, + und Logs bei. Das laufbezogene Setup-Log enthält auch die vollständige + Self-Test-Ausgabe sowie bei Abnahmefehlern die letzten Provider-Logzeilen, 9. installiert EXE und Config nach `%ProgramFiles%\BizTalkCheckmkPulse`, setzt explizite ACLs für Administratoren, Provider und `LocalSystem` und installiert nur den kleinen `.cmd`-Consumer unter @@ -306,8 +315,14 @@ Provider-Log: ```cmd dir /o-d "%ProgramData%\BizTalkCheckmkPulse\logs" type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log" +type "%ProgramData%\BizTalkCheckmkPulse\logs\setup-*.log" ``` +Die Analyse des in Version 2.2.4 beobachteten Abbruchs mit +`Exitcode=0, Zeilen=8` ist in +[docs/Installer-Self-Test-Analyse-2026-08-11.md](docs/Installer-Self-Test-Analyse-2026-08-11.md) +dokumentiert. + Snapshot und Consumer: ```cmd diff --git a/deployment/INSTALLATION.md b/deployment/INSTALLATION.md index 34066ab..bf13d42 100644 --- a/deployment/INSTALLATION.md +++ b/deployment/INSTALLATION.md @@ -28,14 +28,23 @@ nach dem nächsten Agentenlauf das 2.2.1-Fehlerbild `Item not found in monitoring data`. Nur bei bewusst auf `true` gesetztem Opt-in ist anschließend eine Checkmk Service Discovery erforderlich. -Ab Version 2.2.3 vergleicht der Installer bei jedem Update die exakten -Servicenamen der installierten und der vorbereiteten Version. Neue, entfernte -oder umbenannte Services führen **vor dem Stoppen des Tasks** zu einem -Sicherheitsstopp. Die Installation darf nur über die Checkbox +Ab Version 2.2.5 vergleicht der Installer bei jedem Update die exakten +Servicenamen der installierten und der vorbereiteten Version. Rein neue +Services sind abwärtskompatibel, blockieren das Update nicht und werden als +Hinweis für die anschließende Service Discovery angezeigt. Entfernte oder +umbenannte Services führen **vor dem Stoppen des Tasks** zu einem +Sicherheitsstopp. Die Installation darf dann nur über die Checkbox **Service-Rename ist beabsichtigt; Checkmk Service Discovery ist eingeplant** fortgesetzt werden. Der Installer zeigt dabei die entfernten und neuen Namen an. Eine bloß geänderte Ausgabereihenfolge gilt nicht als Rename. +Version 2.2.5 akzeptiert außerdem den gültigen Self-Test einer installierten +Legacy-Version mit weniger Services. Paket, Staging und aktivierte Zielversion +müssen weiterhin exakt neun Services liefern. Damit kann insbesondere eine +Version mit acht Services auf die Version mit dem zusätzlichen Service +`BizTalk Endpoint Reachability` aktualisiert werden. Der frühere Abbruch +`Exitcode=0, Zeilen=8` betraf die Altversion und war kein Fehler im neuen Paket. + Version 2.2.4 nimmt die umgeschaltete Installation zusätzlich vollständig ab: - einmaliger, triggerloser Providerlauf unter dem angegebenen Collector-Konto, @@ -55,6 +64,16 @@ Programmversion samt Task wiederhergestellt und ein frischer Lauf der alten Version abgewartet. Fehler in Paket-, Config- oder Rename-Vorprüfung treten vor jeder Taskänderung auf und lassen die laufende Installation unangetastet. +Jeder Installations-/Updatelauf schreibt ein separates Diagnoselog nach +`%ProgramData%\BizTalkCheckmkPulse\logs\setup-*.log`. Es enthält die aktuelle +Phase, Pfad und Version jeder geprüften EXE, Exitcode, vollständiges stdout und +stderr des Self-Tests, Taskstatus samt Dezimal-/Hexcode, Exception-Kette und +Rollback-Ergebnis. Bei einem Runtime-Abnahmefehler werden zusätzlich die +letzten Provider-Logzeilen übernommen. Das Collector-Kennwort wird weder an +das Logging übergeben noch protokolliert. Ein abgeschlossener Providerlauf mit +Fehlercode beendet die Abnahme sofort; nur ein noch laufender Task wird bis zur +Timeoutgrenze abgewartet. + Beim korrigierenden Wechsel von 2.2.1 mit `BizTalk ACC ...` auf stabile `BizTalk ...`-Namen ist die Änderung beabsichtigt: Checkbox aktivieren und den Servicebestand danach per Discovery abgleichen. Sind in Checkmk bereits die diff --git a/docs/ACC-Endpoint-Reachability-Analyse-2026-08-10.md b/docs/ACC-Endpoint-Reachability-Analyse-2026-08-10.md index d9d367f..9d5ebb8 100644 --- a/docs/ACC-Endpoint-Reachability-Analyse-2026-08-10.md +++ b/docs/ACC-Endpoint-Reachability-Analyse-2026-08-10.md @@ -139,7 +139,7 @@ Endpoint-/Namensfix bewusst nicht grüngefärbt. ## ACC-Abnahme nach Installation -1. Setup 2.2.4 als Update ausführen; Umgebung `ACC` kann bestehen bleiben. +1. Setup 2.2.5 als Update ausführen; Umgebung `ACC` kann bestehen bleiben. Beim Wechsel von der fehlerhaften 2.2.1-Ausgabe `BizTalk ACC ...` auf die stabilen Namen die Rename-/Discovery-Checkbox bewusst aktivieren. 2. In der installierten Config bestätigen: diff --git a/docs/BizTalk_Checkmk_Pulse_Architekturueberblick.docx b/docs/BizTalk_Checkmk_Pulse_Architekturueberblick.docx index cfb15a840c7cd6d828b23c83ac446a6f317a58db..f3849dec767bd027dc669d98b1cd9912679f70b7 100644 GIT binary patch delta 11300 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 diff --git a/docs/Installer-Self-Test-Analyse-2026-08-11.md b/docs/Installer-Self-Test-Analyse-2026-08-11.md new file mode 100644 index 0000000..e01b739 --- /dev/null +++ b/docs/Installer-Self-Test-Analyse-2026-08-11.md @@ -0,0 +1,68 @@ +# Analyse: Update-Abbruch mit `Exitcode=0, Zeilen=8` + +## Beobachtung + +Der Installer meldete nach erfolgreicher Paketprüfung, Anmeldung und +Staging-Validierung: + +```text +Installation/Update fehlgeschlagen. Rollback erfolgreich. +Ursache: Self-Test fehlgeschlagen. Exitcode=0, Zeilen=8. +``` + +Die Reihenfolge im Statusfenster zeigt, dass das neue Paket und das Staging +bereits erfolgreich neun Services geliefert hatten. Der danach ausgeführte +Self-Test gehörte zur noch installierten Ausgangsversion. + +## Ursache + +Version 2.2.4 verwendete dieselbe starre Erwartung von exakt neun Zeilen für +beide Seiten des Updatevergleichs. Eine ältere, intakte Installation vor +Einführung von `BizTalk Endpoint Reachability` liefert jedoch acht gültige +Checkmk-Zeilen und Exitcode 0. Der Updater deutete diese erwartete +Versionsdifferenz fälschlich als defekten Self-Test. Der Abbruch erfolgte vor +dem Stoppen des Tasks und vor dem Austausch von Dateien. Die Meldung +`Rollback erfolgreich` war deshalb technisch irreführend; es war keine +Systemänderung zurückzusetzen. + +## Korrektur in Version 2.2.5 + +- Paket, Staging und aktivierte Zielversion müssen weiterhin exakt neun + eindeutige `OK`-Zeilen liefern. +- Die installierte Ausgangsversion darf eine andere positive Anzahl + eindeutiger `OK`-Zeilen liefern. Damit wird ihr tatsächlicher + versionsspezifischer Servicevertrag gelesen. +- Rein neu hinzukommende Services sind updatekompatibel. Sie werden als + Discovery-Hinweis gemeldet, blockieren aber die Umschaltung nicht. +- Entfernte Namen – und damit auch Renames – bleiben ohne aktivierte + Discovery-Bestätigung ein Sicherheitsstopp. +- Tritt ein Fehler vor der ersten Systemänderung auf, lautet der Status jetzt + `kein Rollback erforderlich`. + +## Erweiterte Diagnose + +Jeder Lauf erzeugt +`%ProgramData%\BizTalkCheckmkPulse\logs\setup--.log`. Das Log enthält: + +- aktuelle Installationsphase und Setup-Version, +- Pfad, Dateiversion und Größe jeder geprüften EXE, +- erwartete und tatsächliche Zeilenzahl, Exitcode sowie vollständiges stdout + und stderr jedes Self-Tests, +- Task-Scheduler-State und `LastTaskResult` dezimal und hexadezimal, +- vollständige Exception-Kette mit HRESULT und Stacktrace, +- bei Runtime-Abnahmefehlern bis zu 40 letzte Provider-Logzeilen, +- Ergebnis jedes Rollback-Schritts. + +Das Kennwort wird ausschließlich an `LogonUser` und die +Task-Scheduler-COM-Schnittstelle übergeben. Die Logging-Komponente erhält es +nicht. + +## Regressionstests + +Automatisiert geprüft werden insbesondere: + +- acht gültige Zeilen einer Legacy-Ausgangsversion werden akzeptiert, +- acht Zeilen eines neuen Stagings werden weiterhin abgewiesen, +- eine reine Service-Erweiterung wird ohne Rename-Bestätigung akzeptiert, +- ein frisch beendeter Task mit Fehlercode wird sofort erkannt und der Code + enthält zusätzlich die hexadezimale Darstellung. diff --git a/docs/Integration.md b/docs/Integration.md index cfb7bea..f8c97fb 100644 --- a/docs/Integration.md +++ b/docs/Integration.md @@ -53,7 +53,7 @@ Verbindlicher Agent-Dump: ## Service Discovery -Das Setup 2.2.4 schließt erst erfolgreich ab, nachdem der erste Providerlauf +Das Setup ab 2.2.4 schließt erst erfolgreich ab, nachdem der erste Providerlauf unter dem echten Collector-Konto einen frischen Snapshot und – bei aktivierter Endpoint-Prüfung – einen frisch synchronisierten Katalog erzeugt hat. Die neun stabilen Services müssen vollständig und ohne `UNKNOWN` vorliegen. Diese lokale @@ -64,6 +64,14 @@ Servicenamen aber standardmäßig nicht. Nur das bewusste Opt-in `IncludeEnvironmentInServiceName=true` erzeugt umgebungsabhängige Namen und verlangt den anschließenden Discovery-Abgleich. +Ab Setup 2.2.5 darf der gültige Servicevertrag der installierten +Ausgangsversion weniger als neun Services enthalten. So ist das Update einer +Legacy-Version mit acht Services möglich; nur Paket, Staging und Zielversion +müssen exakt neun Services liefern. Additive Services werden gemeldet, aber +nicht blockiert. Entfernte oder umbenannte Services erfordern weiterhin die +bewusste Bestätigung im Setup. Laufbezogene Details stehen in +`%ProgramData%\BizTalkCheckmkPulse\logs\setup-*.log`. + 1. Provider dreimal erfolgreich laufen lassen. 2. `LastTaskResult=0`, frischen Snapshot und Log prüfen. 3. Agent-Dump im `LocalSystem`-Kontext prüfen. diff --git a/scripts/generate-architecture-overview.py b/scripts/generate-architecture-overview.py index e5a7f73..3adc066 100644 --- a/scripts/generate-architecture-overview.py +++ b/scripts/generate-architecture-overview.py @@ -306,7 +306,7 @@ def configure_document(document): p = footer.paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_after = Pt(0) - format_run(p.add_run("Version 2.2.4 | Stand 10.08.2026 | Seite "), size=7.5, color=MUTED) + format_run(p.add_run("Version 2.2.5 | Stand 11.08.2026 | Seite "), size=7.5, color=MUTED) add_page_field(p) @@ -320,7 +320,7 @@ def build_document(): core.title = "BizTalk Checkmk Pulse – Architektur- und Lösungsüberblick" core.subject = "Gesamtarchitektur, Funktionsumfang und Datenaustausch" core.author = "BEW" - core.comments = "Externe Lösungsübersicht zum implementierten Stand 2.2.4" + core.comments = "Externe Lösungsübersicht zum implementierten Stand 2.2.5" # Cover banner = document.add_table(rows=1, cols=1) @@ -377,9 +377,9 @@ def build_document(): document, ("Dokumentmerkmal", "Angabe"), ( - ("Lösungsstand", "BizTalk Checkmk Pulse 2.2.4"), + ("Lösungsstand", "BizTalk Checkmk Pulse 2.2.5"), ("Zielplattform", "Microsoft BizTalk Server 2020 · Windows Server 2019 · Checkmk 2.4"), - ("Dokumentstand", "10. August 2026"), + ("Dokumentstand", "11. August 2026"), ("Dokumentzweck", "Architektur- und Leistungsüberblick für technische Stakeholder"), ), (4.2, 13.0), @@ -543,7 +543,8 @@ def build_document(): add_bullet(document, "Getrennte Tageslogs für Provider und Consumer; Standardaufbewahrung 30 Tage.") add_bullet(document, "Erwartet inaktive Receive Locations und Send Ports können exakt allowlisted werden und bleiben als Messwert sichtbar.") add_bullet(document, "Endpoint-Prüfung standardmäßig mit maximal 100 Zielen, 16 parallelen Probes und 3 Sekunden Timeout je Ziel.") - add_bullet(document, "Der Installer vergleicht bei Updates die exakten Checkmk-Servicenamen und stoppt unbestätigte Änderungen, bevor Task oder Programmdateien angefasst werden.") + add_bullet(document, "Der Installer validiert neue Versionen auf exakt neun Services, akzeptiert vollständige Legacy-Verträge mit weniger Services und stoppt entfernte oder umbenannte Servicenamen vor jeder Umschaltung.") + add_bullet(document, "Ein laufbezogenes Setup-Log protokolliert Phase, Binary-Version, Self-Test-Ausgabe, Taskcodes, Exception-Kette und Rollback ohne Kennwort.") add_bullet(document, "Nach der Umschaltung erzwingt das Setup einen frischen Katalogabgleich und akzeptiert die Installation erst nach einem erfolgreichen, identitäts- und integritätsgeprüften Providerlauf ohne UNKNOWN in den neun stabilen Services.") add_heading(document, "Sicherheitsmodell", level=2) @@ -573,7 +574,7 @@ def build_document(): add_callout(document, "Kernaussage: ", "Ein privilegierter Read-only-Collector sammelt, ein unprivilegierter Consumer validiert und übergibt – Checkmk erhält neun stabile Services statt direkten Zugriff auf BizTalk oder SQL.", fill=LIGHT_GREEN, border=GREEN) add_rich_paragraph( document, - (("Dokumentbasis: Implementierung und Projektdokumentation von BizTalk Checkmk Pulse 2.2.4 sowie Checkmk User Guide „Local checks“, abgerufen am 10. August 2026.", {"size": 7.8, "color": MUTED}),), + (("Dokumentbasis: Implementierung und Projektdokumentation von BizTalk Checkmk Pulse 2.2.5 sowie Checkmk User Guide „Local checks“, abgerufen am 10. August 2026.", {"size": 7.8, "color": MUTED}),), before=8, after=0, ) diff --git a/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj b/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj index ada08f1..97452b1 100644 --- a/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj +++ b/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj @@ -43,6 +43,7 @@ + diff --git a/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs b/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs index 27d1099..e5f8c16 100644 --- a/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs +++ b/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; +using System.Globalization; using System.Linq; using System.Runtime.InteropServices; using System.Security.AccessControl; @@ -37,26 +38,32 @@ namespace BizTalkCheckmkPulse.Setup bool allowServiceNameChange, Action report) { - Validate(account, password, isGmsa, environmentName); - report = report ?? delegate { }; - var accountSid = (SecurityIdentifier)new NTAccount(account).Translate(typeof(SecurityIdentifier)); - report("Konto aufgeloest: " + account + " (" + accountSid.Value + ")"); + var operationStartedUtc = DateTime.UtcNow; + var uiReport = report ?? delegate { }; + var setupLog = SetupOperationLog.Create(runtimeDirectory, "install-or-update"); + report = delegate(string message) + { + setupLog.Write("INFO", message); + uiReport(message); + }; + if (setupLog.FilePath.Length > 0) + report("Diagnoselog: " + setupLog.FilePath); + else + uiReport("WARNUNG: Diagnoselog konnte nicht angelegt werden: " + setupLog.CreationError); + var phase = "Initialisierung"; var sourceApplication = Path.Combine(packageDirectory, "application"); var sourceExe = Path.Combine(sourceApplication, "BizTalkCheckmkPulse.exe"); var sourceConfig = sourceExe + ".config"; var sourceWrapper = Path.Combine(packageDirectory, "biztalk_checkmk_pulse.cmd"); - RequireFile(sourceExe); - RequireFile(sourceConfig); - RequireFile(sourceWrapper); var targetExe = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe"); var targetConfig = targetExe + ".config"; var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N"); var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N"); var targetWrapper = Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd"); - var previousWrapper = File.Exists(targetWrapper) ? File.ReadAllBytes(targetWrapper) : null; - var hadExistingInstallation = Directory.Exists(installDirectory); - var hadExistingExecutable = File.Exists(targetExe); + byte[] previousWrapper = null; + var hadExistingInstallation = false; + var hadExistingExecutable = false; var mutationStarted = false; var backupCreated = false; var filesActivated = false; @@ -64,14 +71,29 @@ namespace BizTalkCheckmkPulse.Setup try { - RunSelfTest(sourceExe); + phase = "Eingaben und Paket pruefen"; + report("Phase: " + phase + ". Paket=" + packageDirectory + "."); + Validate(account, password, isGmsa, environmentName); + var accountSid = (SecurityIdentifier)new NTAccount(account).Translate(typeof(SecurityIdentifier)); + report("Konto aufgeloest: " + account + " (" + accountSid.Value + ")"); + RequireFile(sourceExe); + RequireFile(sourceConfig); + RequireFile(sourceWrapper); + previousWrapper = File.Exists(targetWrapper) ? File.ReadAllBytes(targetWrapper) : null; + hadExistingInstallation = Directory.Exists(installDirectory); + hadExistingExecutable = File.Exists(targetExe); + + RunSelfTest(sourceExe, 9, "Paket", setupLog); report("Paket-Vorpruefung erfolgreich: neun Checkmk-Services."); if (!isGmsa) { + phase = "Collector-Anmeldung pruefen"; ValidateBatchLogon(account, password); report("Collector-Anmeldung und 'Log on as a batch job' vor dem Update bestaetigt."); } + phase = "Update-Staging vorbereiten"; + report("Phase: " + phase + "."); Directory.CreateDirectory(stagingDirectory); var stagedExe = Path.Combine(stagingDirectory, "BizTalkCheckmkPulse.exe"); var stagedConfig = stagedExe + ".config"; @@ -81,38 +103,45 @@ namespace BizTalkCheckmkPulse.Setup stagedConfig, File.Exists(targetConfig) ? targetConfig : null, environmentName); - var stagedServiceNames = RunSelfTest(stagedExe); + var stagedServiceNames = RunSelfTest(stagedExe, 9, "Staging", setupLog); report("Update-Staging validiert. Umgebung=" + (effectiveEnvironment.Length == 0 ? "(keine)" : effectiveEnvironment) + "."); if (File.Exists(targetExe)) { - var installedServiceNames = RunSelfTest(targetExe); + phase = "Installierten Servicevertrag vergleichen"; + report("Phase: " + phase + ". Legacy-Versionen mit weniger stabilen Services werden als Updatequelle akzeptiert."); + var installedServiceNames = RunSelfTest(targetExe, null, "Installierte Version", setupLog); var serviceNameChange = EnsureServiceNameCompatibility( installedServiceNames, stagedServiceNames, allowServiceNameChange); report(serviceNameChange.Length == 0 ? "Checkmk-Servicevertrag unveraendert: keine Service Discovery erforderlich." - : "Checkmk-Service-Rename ausdruecklich bestaetigt; Service Discovery erforderlich. " + serviceNameChange); + : "Checkmk-Servicevertrag geaendert; Service Discovery erforderlich. " + serviceNameChange); } // Erst nach vollstaendiger Staging-Pruefung wird der laufende Provider angehalten. + phase = "Vorhandenen Scheduled Task anhalten"; + report("Phase: " + phase + "."); mutationStarted = true; scheduler.DeleteIfExists(TaskName); report("Vorhandener Scheduled Task angehalten und fuer das Update entfernt."); if (hadExistingInstallation) { + phase = "Vorhandene Programmversion sichern"; Directory.Move(installDirectory, backupDirectory); backupCreated = true; } + phase = "Neue Programmversion aktivieren"; Directory.Move(stagingDirectory, installDirectory); filesActivated = true; report(hadExistingInstallation ? "Programmdateien atomar auf die neue Version umgestellt." : "Programmdateien installiert: " + installDirectory); + phase = "Runtime-Verzeichnisse und ACLs einrichten"; var dataDirectory = Path.Combine(runtimeDirectory, "data"); var logDirectory = Path.Combine(runtimeDirectory, "logs"); Directory.CreateDirectory(runtimeDirectory); @@ -125,13 +154,16 @@ namespace BizTalkCheckmkPulse.Setup ApplyDirectoryAcl(logDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.Modify); report("Least-Privilege-Verzeichnisrechte gesetzt; vorhandene Runtime-Daten bleiben erhalten."); + phase = "Checkmk Local Check installieren"; Directory.CreateDirectory(checkmkLocalDirectory); File.Copy(sourceWrapper, targetWrapper, true); report("Checkmk Local Check installiert: " + checkmkLocalDirectory); - RunSelfTest(targetExe); + phase = "Installierte Programmversion pruefen"; + RunSelfTest(targetExe, 9, "Aktivierte Version", setupLog); report("Installierter Self-Test erfolgreich: neun Checkmk-Services."); + phase = "Provider-Abnahmelauf registrieren und starten"; var runtimeValidationStartedUtc = DateTime.UtcNow; scheduler.RegisterValidationAndStart( TaskName, @@ -142,6 +174,7 @@ namespace BizTalkCheckmkPulse.Setup isGmsa); report("Einmaliger Provider-Abnahmelauf mit erzwungenem Endpoint-Katalogabgleich gestartet."); + phase = "Provider-Abnahmelauf abwarten"; var completedRun = scheduler.WaitForSuccessfulRun( TaskName, runtimeValidationStartedUtc, @@ -149,12 +182,14 @@ namespace BizTalkCheckmkPulse.Setup report); report("Provider-Abnahmelauf erfolgreich: LastTaskResult=" + completedRun.ExitCode + "."); + phase = "Runtime-Artefakte validieren"; var runtimeValidation = RunRuntimeValidation( targetExe, runtimeValidationStartedUtc, account); report("Post-Install-Runtime-Abnahme erfolgreich: " + runtimeValidation); + phase = "Minuetlichen Scheduled Task registrieren"; scheduler.RegisterRecurring( TaskName, targetExe, @@ -165,19 +200,28 @@ namespace BizTalkCheckmkPulse.Setup report("Validierter minuetlicher Scheduled Task registriert: " + TaskName); TryDeleteDirectory(backupDirectory, report); + report("Installation/Update erfolgreich abgeschlossen."); } catch (Exception installException) { + setupLog.WriteException(phase, installException); + if (phase.StartsWith("Provider-", StringComparison.Ordinal) + || phase.StartsWith("Runtime-", StringComparison.Ordinal)) + { + WriteRecentProviderLog(setupLog, runtimeDirectory, operationStartedUtc); + } var rollbackFailures = new List(); if (mutationStarted) { try { scheduler.DeleteIfExists(TaskName); + report("Rollback: Scheduled Task gestoppt und entfernt."); } catch (Exception ex) { rollbackFailures.Add("Task stoppen: " + ex.Message); + setupLog.WriteException("Rollback: Task stoppen", ex); } } @@ -192,6 +236,7 @@ namespace BizTalkCheckmkPulse.Setup catch (Exception ex) { rollbackFailures.Add("Programmdateien wiederherstellen: " + ex.Message); + setupLog.WriteException("Rollback: Programmdateien wiederherstellen", ex); } } @@ -200,10 +245,12 @@ namespace BizTalkCheckmkPulse.Setup try { RestoreWrapper(targetWrapper, previousWrapper); + report("Rollback: Checkmk-Wrapper auf den vorherigen Stand zurueckgesetzt."); } catch (Exception ex) { rollbackFailures.Add("Checkmk-Wrapper wiederherstellen: " + ex.Message); + setupLog.WriteException("Rollback: Checkmk-Wrapper wiederherstellen", ex); } } @@ -229,19 +276,28 @@ namespace BizTalkCheckmkPulse.Setup catch (Exception ex) { rollbackFailures.Add("Scheduled Task wiederherstellen: " + ex.Message); + setupLog.WriteException("Rollback: Scheduled Task wiederherstellen", ex); } } - var rollback = rollbackFailures.Count == 0 - ? "Rollback erfolgreich." - : "Rollback unvollstaendig: " + string.Join(" | ", rollbackFailures); + var rollback = !mutationStarted + ? "Keine Systemaenderung vorgenommen; kein Rollback erforderlich." + : rollbackFailures.Count == 0 + ? "Rollback erfolgreich." + : "Rollback unvollstaendig: " + string.Join(" | ", rollbackFailures); + setupLog.Write("ERROR", rollback); + var logReference = setupLog.FilePath.Length == 0 + ? "Diagnoselog nicht verfuegbar (" + setupLog.CreationError + ")." + : "Diagnoselog: " + setupLog.FilePath; throw new InvalidOperationException( - "Installation/Update fehlgeschlagen. " + rollback + " Ursache: " + installException.Message, + "Installation/Update fehlgeschlagen in Phase '" + phase + "'. " + + rollback + " Ursache: " + installException.Message + " " + logReference, installException); } finally { TryDeleteDirectory(stagingDirectory, null); + setupLog.Dispose(); } } @@ -452,8 +508,20 @@ namespace BizTalkCheckmkPulse.Setup new DirectoryInfo(path).SetAccessControl(security); } - private static IReadOnlyList RunSelfTest(string executable) + private static IReadOnlyList RunSelfTest( + string executable, + int? expectedServiceCount, + string label, + SetupOperationLog setupLog) { + var version = FileVersionInfo.GetVersionInfo(executable).FileVersion ?? "(unbekannt)"; + setupLog.Write( + "INFO", + "Self-Test startet. label=" + label + + ", path=" + executable + + ", file_version=" + version + + ", size_bytes=" + new FileInfo(executable).Length + + ", expected_lines=" + (expectedServiceCount.HasValue ? expectedServiceCount.Value.ToString(CultureInfo.InvariantCulture) : "legacy-compatible") + "."); var start = new ProcessStartInfo(executable, "--self-test") { UseShellExecute = false, @@ -464,24 +532,74 @@ namespace BizTalkCheckmkPulse.Setup using (var process = Process.Start(start)) { if (process == null) throw new InvalidOperationException("Self-Test konnte nicht gestartet werden."); - var output = process.StandardOutput.ReadToEnd(); - var error = process.StandardError.ReadToEnd(); + var outputRead = process.StandardOutput.ReadToEndAsync(); + var errorRead = process.StandardError.ReadToEndAsync(); if (!process.WaitForExit(30000)) { process.Kill(); - throw new InvalidOperationException("Self-Test hat das Zeitlimit ueberschritten."); + process.WaitForExit(); + System.Threading.Tasks.Task.WaitAll(outputRead, errorRead); + var timeoutOutput = outputRead.Result; + var timeoutError = errorRead.Result; + setupLog.Write("ERROR", "Self-Test Timeout. label=" + label + ", stdout=" + timeoutOutput + ", stderr=" + timeoutError + "."); + throw new InvalidOperationException("Self-Test '" + label + "' hat das Zeitlimit von 30 Sekunden ueberschritten."); } - var lines = output.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); - if (process.ExitCode != 0 || lines.Length != 9 || lines.Any(x => !x.StartsWith("0 ", StringComparison.Ordinal))) - throw new InvalidOperationException("Self-Test fehlgeschlagen. Exitcode=" + process.ExitCode + ", Zeilen=" + lines.Length + ". " + error); - - var serviceNames = lines.Select(ExtractServiceName).ToArray(); - if (serviceNames.Distinct(StringComparer.Ordinal).Count() != serviceNames.Length) - throw new InvalidOperationException("Self-Test fehlgeschlagen: Checkmk-Servicenamen sind nicht eindeutig."); - return serviceNames; + System.Threading.Tasks.Task.WaitAll(outputRead, errorRead); + var output = outputRead.Result; + var error = errorRead.Result; + setupLog.Write( + "INFO", + "Self-Test beendet. label=" + label + + ", exit_code=" + process.ExitCode + + ", stdout=" + output + + ", stderr=" + error + "."); + return ParseSelfTestOutput(output, error, process.ExitCode, expectedServiceCount, label); } } + internal static IReadOnlyList ParseSelfTestOutput( + string output, + string error, + int exitCode, + int? expectedServiceCount, + string label) + { + var lines = (output ?? string.Empty).Split( + new[] { "\r\n", "\n" }, + StringSplitOptions.RemoveEmptyEntries); + var expected = expectedServiceCount.HasValue + ? expectedServiceCount.Value.ToString(CultureInfo.InvariantCulture) + : "mindestens 1 (Legacy-kompatibel)"; + if (exitCode != 0 + || lines.Length == 0 + || expectedServiceCount.HasValue && lines.Length != expectedServiceCount.Value + || lines.Any(x => !x.StartsWith("0 ", StringComparison.Ordinal))) + { + throw new InvalidOperationException( + "Self-Test '" + label + "' fehlgeschlagen. Exitcode=" + exitCode + + ", Zeilen=" + lines.Length + ", Erwartet=" + expected + + ", Stdout=" + CompactProcessText(output) + + ", Stderr=" + CompactProcessText(error) + "."); + } + + var serviceNames = lines.Select(ExtractServiceName).ToArray(); + if (serviceNames.Distinct(StringComparer.Ordinal).Count() != serviceNames.Length) + throw new InvalidOperationException( + "Self-Test '" + label + "' fehlgeschlagen: Checkmk-Servicenamen sind nicht eindeutig. " + + "Services=[" + string.Join(", ", serviceNames) + "]."); + return serviceNames; + } + + private static string CompactProcessText(string value) + { + var text = (value ?? string.Empty) + .Replace("\r", string.Empty) + .Replace("\n", " | ") + .Trim(); + const int maxLength = 2000; + return text.Length <= maxLength ? text : text.Substring(0, maxLength) + "...[gekuerzt]"; + } + private static string RunRuntimeValidation( string executable, DateTime notBeforeUtc, @@ -501,13 +619,21 @@ namespace BizTalkCheckmkPulse.Setup using (var process = Process.Start(start)) { if (process == null) throw new InvalidOperationException("Runtime-Abnahme konnte nicht gestartet werden."); - var output = process.StandardOutput.ReadToEnd().Trim(); - var error = process.StandardError.ReadToEnd().Trim(); + var outputRead = process.StandardOutput.ReadToEndAsync(); + var errorRead = process.StandardError.ReadToEndAsync(); if (!process.WaitForExit(30000)) { process.Kill(); - throw new TimeoutException("Runtime-Abnahme hat das Zeitlimit ueberschritten."); + process.WaitForExit(); + System.Threading.Tasks.Task.WaitAll(outputRead, errorRead); + throw new TimeoutException( + "Runtime-Abnahme hat das Zeitlimit von 30 Sekunden ueberschritten. Stdout=" + + CompactProcessText(outputRead.Result) + + ", Stderr=" + CompactProcessText(errorRead.Result) + "."); } + System.Threading.Tasks.Task.WaitAll(outputRead, errorRead); + var output = outputRead.Result.Trim(); + var error = errorRead.Result.Trim(); if (process.ExitCode != 0 || !output.StartsWith("RUNTIME_VALIDATION_V1 ", StringComparison.Ordinal)) @@ -542,7 +668,10 @@ namespace BizTalkCheckmkPulse.Setup var removed = installed.Except(staged, StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToArray(); var added = staged.Except(installed, StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToArray(); var description = "Entfernt=[" + string.Join(", ", removed) + "]; Neu=[" + string.Join(", ", added) + "]."; - if (!allowServiceNameChange) + // Reine Erweiterungen lassen bestehende Checkmk-Services unangetastet und + // muessen Legacy-Updates deshalb nicht blockieren. Entfernte Namen sind + // dagegen ein Rename oder ein Vertragsbruch und bleiben bestaetigungspflichtig. + if (removed.Length > 0 && !allowServiceNameChange) { throw new InvalidOperationException( "Sicherheitsstopp: Das Update wuerde Checkmk-Servicenamen aendern. " @@ -553,5 +682,50 @@ namespace BizTalkCheckmkPulse.Setup return description; } + + private static void WriteRecentProviderLog( + SetupOperationLog setupLog, + string runtimeRoot, + DateTime operationStartedUtc) + { + try + { + var directory = Path.Combine(runtimeRoot, "logs"); + if (!Directory.Exists(directory)) + { + setupLog.Write("INFO", "Provider-Logverzeichnis existiert nicht: " + directory + "."); + return; + } + + var file = Directory.GetFiles(directory, "biztalk-checkmk-pulse-*.log") + .OrderByDescending(File.GetLastWriteTimeUtc) + .FirstOrDefault(); + if (file == null) + { + setupLog.Write("INFO", "Kein Provider-Log fuer die Fehlerdiagnose gefunden."); + return; + } + + var lastWriteUtc = File.GetLastWriteTimeUtc(file); + if (lastWriteUtc < operationStartedUtc.AddSeconds(-5)) + { + setupLog.Write( + "WARN", + "Neuestes Provider-Log ist aelter als dieser Setup-Lauf und wird nicht als Fehlerkontext uebernommen. " + + "path=" + file + ", last_write_utc=" + lastWriteUtc.ToString("o") + "."); + return; + } + + var lines = File.ReadAllLines(file); + var tail = lines.Skip(Math.Max(0, lines.Length - 40)); + setupLog.Write( + "ERROR", + "Letzte Provider-Logzeilen. path=" + file + ", tail=" + string.Join(" || ", tail) + "."); + } + catch (Exception ex) + { + setupLog.Write("WARN", "Provider-Log konnte nicht fuer die Diagnose gelesen werden: " + ex.Message + "."); + } + } } } diff --git a/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs b/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs index 86d791e..be9fa57 100644 --- a/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs +++ b/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs @@ -6,5 +6,5 @@ using System.Runtime.CompilerServices; [assembly: AssemblyDescription("PowerShell-free installer and updater for BizTalk Checkmk Pulse")] [assembly: AssemblyCompany("BEW")] [assembly: AssemblyProduct("BizTalk Checkmk Pulse")] -[assembly: AssemblyVersion("2.2.4.0")] -[assembly: AssemblyFileVersion("2.2.4.0")] +[assembly: AssemblyVersion("2.2.5.0")] +[assembly: AssemblyFileVersion("2.2.5.0")] diff --git a/src/BizTalkCheckmkPulse.Setup/SetupOperationLog.cs b/src/BizTalkCheckmkPulse.Setup/SetupOperationLog.cs new file mode 100644 index 0000000..57d3a20 --- /dev/null +++ b/src/BizTalkCheckmkPulse.Setup/SetupOperationLog.cs @@ -0,0 +1,126 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Security.Principal; +using System.Text; + +namespace BizTalkCheckmkPulse.Setup +{ + /// + /// Best-effort-Diagnoselog fuer einen einzelnen Setup-Lauf. Das Kennwort + /// wird dieser Klasse nie uebergeben und kann deshalb nicht protokolliert werden. + /// + internal sealed class SetupOperationLog : IDisposable + { + private readonly object sync = new object(); + private StreamWriter writer; + + private SetupOperationLog(string filePath, StreamWriter writer) + { + FilePath = filePath; + this.writer = writer; + } + + public string FilePath { get; private set; } + + public static SetupOperationLog Create(string runtimeDirectory, string operation) + { + try + { + var directory = Path.Combine(runtimeDirectory, "logs"); + Directory.CreateDirectory(directory); + var path = Path.Combine( + directory, + "setup-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmssfff", CultureInfo.InvariantCulture) + + "-" + Process.GetCurrentProcess().Id + ".log"); + var stream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read); + var log = new SetupOperationLog(path, new StreamWriter(stream, new UTF8Encoding(false)) { AutoFlush = true }); + log.Write("INFO", "Setup-Log gestartet. operation=" + operation + + ", setup_version=" + typeof(SetupOperationLog).Assembly.GetName().Version + + ", os=" + Environment.OSVersion.VersionString + + ", process_bitness=" + (Environment.Is64BitProcess ? "64" : "32") + + ", identity=" + CurrentIdentity() + "."); + return log; + } + catch (Exception ex) + { + return new SetupOperationLog(string.Empty, null) + { + CreationError = ex.Message + }; + } + } + + public string CreationError { get; private set; } + + public void Write(string level, string message) + { + lock (sync) + { + if (writer == null) return; + writer.WriteLine( + DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture) + + " level=" + level + + " " + SingleLine(message)); + } + } + + public void WriteException(string phase, Exception exception) + { + Write("ERROR", "Fehler in Phase '" + phase + "'. " + FormatException(exception)); + } + + internal static string FormatException(Exception exception) + { + var result = new StringBuilder(); + var current = exception; + var depth = 0; + while (current != null && depth < 10) + { + if (depth > 0) result.Append(" | inner[").Append(depth).Append("] "); + result.Append(current.GetType().FullName) + .Append(" hresult=0x") + .Append(current.HResult.ToString("X8", CultureInfo.InvariantCulture)) + .Append(": ") + .Append(current.Message); + if (!string.IsNullOrWhiteSpace(current.StackTrace)) + result.Append(" stack=").Append(current.StackTrace); + current = current.InnerException; + depth++; + } + return SingleLine(result.ToString()); + } + + public void Dispose() + { + lock (sync) + { + if (writer == null) return; + writer.Dispose(); + writer = null; + } + } + + private static string CurrentIdentity() + { + try + { + var identity = WindowsIdentity.GetCurrent(); + return identity == null ? "(unknown)" : identity.Name; + } + catch (Exception) + { + return "(unknown)"; + } + } + + private static string SingleLine(string value) + { + return (value ?? string.Empty) + .Replace("\r", "\\r") + .Replace("\n", "\\n") + .Replace("\t", " "); + } + } +} diff --git a/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs b/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs index 7d3a20a..0518c09 100644 --- a/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs +++ b/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs @@ -1,4 +1,6 @@ using System; +using System.ComponentModel; +using System.Globalization; using System.Runtime.InteropServices; using System.Threading; @@ -11,6 +13,7 @@ namespace BizTalkCheckmkPulse.Setup private const int TaskTriggerTime = 1; private const int TaskActionExecute = 0; private const int TaskInstancesIgnoreNew = 2; + private const int TaskStateReady = 3; private const int TaskStateRunning = 4; public void RegisterValidationAndStart( @@ -165,11 +168,20 @@ namespace BizTalkCheckmkPulse.Setup return new TaskRunResult(lastRunUtc.Value, lastResult); } + if (IsFreshCompletedFailure(lastState, lastResult, lastRunUtc, notBeforeUtc)) + { + throw new InvalidOperationException( + "Providerlauf ist fehlgeschlagen. state=" + DescribeState(lastState) + + ", last_result=" + DescribeResult(lastResult) + + ", last_run=" + lastRunUtc.Value.ToString("o") + + ". Provider- und Setup-Log pruefen."); + } + if (report != null && DateTime.UtcNow >= nextReportUtc) { report( - "Warte auf erfolgreichen Providerlauf: state=" + lastState - + ", last_result=" + lastResult + "Warte auf erfolgreichen Providerlauf: state=" + DescribeState(lastState) + + ", last_result=" + DescribeResult(lastResult) + ", last_run=" + (lastRunUtc.HasValue ? lastRunUtc.Value.ToString("o") : "(noch keiner)") + "."); nextReportUtc = DateTime.UtcNow.AddSeconds(15); } @@ -179,8 +191,8 @@ namespace BizTalkCheckmkPulse.Setup throw new TimeoutException( "Providerlauf wurde innerhalb von " + Math.Ceiling(timeout.TotalSeconds) - + " Sekunden nicht erfolgreich abgeschlossen. state=" + lastState - + ", last_result=" + lastResult + + " Sekunden nicht erfolgreich abgeschlossen. state=" + DescribeState(lastState) + + ", last_result=" + DescribeResult(lastResult) + ", last_run=" + (lastRunUtc.HasValue ? lastRunUtc.Value.ToString("o") : "(noch keiner)") + "."); } finally @@ -197,13 +209,77 @@ namespace BizTalkCheckmkPulse.Setup DateTime? lastRunUtc, DateTime notBeforeUtc) { - return taskState != TaskStateRunning + return taskState == TaskStateReady && lastTaskResult == 0 && lastRunUtc.HasValue // Task Scheduler kann LastRunTime mit geringerer Aufloesung als DateTime.UtcNow liefern. && lastRunUtc.Value >= notBeforeUtc.ToUniversalTime().AddSeconds(-2); } + internal static bool IsFreshCompletedFailure( + int taskState, + int lastTaskResult, + DateTime? lastRunUtc, + DateTime notBeforeUtc) + { + return taskState == TaskStateReady + && lastTaskResult != 0 + && lastRunUtc.HasValue + && lastRunUtc.Value >= notBeforeUtc.ToUniversalTime().AddSeconds(-2); + } + + internal static string DescribeResult(int result) + { + var unsigned = unchecked((uint)result); + string description; + switch (unsigned) + { + case 0: description = "Erfolgreich"; break; + case 1: description = "Provider-Laufzeitfehler; Provider-Log pruefen"; break; + case 2: description = "Provider bereits aktiv oder Snapshot-I/O fehlgeschlagen"; break; + case 0x00041300: description = "Task ist bereit"; break; + case 0x00041301: description = "Task wird ausgefuehrt"; break; + case 0x00041302: description = "Task ist deaktiviert"; break; + case 0x00041303: description = "Task wurde noch nicht ausgefuehrt"; break; + case 0x00041306: description = "Letzter Tasklauf wurde beendet"; break; + default: + if ((unsigned & 0xFFFF0000u) == 0x80070000u) + { + try + { + description = new Win32Exception((int)(unsigned & 0xFFFFu)).Message; + } + catch (Exception) + { + description = "Windows-/Task-Scheduler-Fehler"; + } + } + else + { + description = "Anwendungs- oder Task-Scheduler-Code"; + } + break; + } + return result.ToString(CultureInfo.InvariantCulture) + + " (0x" + unsigned.ToString("X8", CultureInfo.InvariantCulture) + ")" + + " '" + description + "'"; + } + + private static string DescribeState(int state) + { + string name; + switch (state) + { + case 0: name = "Unknown"; break; + case 1: name = "Disabled"; break; + case 2: name = "Queued"; break; + case 3: name = "Ready"; break; + case 4: name = "Running"; break; + default: name = "Unbekannt"; break; + } + return state.ToString(CultureInfo.InvariantCulture) + " (" + name + ")"; + } + public void DeleteIfExists(string taskName) { dynamic service = null; diff --git a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs index 13c46a6..4971c3e 100644 --- a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs +++ b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs @@ -6,5 +6,5 @@ using System.Reflection; [assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")] [assembly: AssemblyCompany("BEW")] [assembly: AssemblyProduct("BizTalk Checkmk Pulse")] -[assembly: AssemblyVersion("2.2.4.0")] -[assembly: AssemblyFileVersion("2.2.4.0")] +[assembly: AssemblyVersion("2.2.5.0")] +[assembly: AssemblyFileVersion("2.2.5.0")] diff --git a/tests/BizTalkCheckmkPulse.Tests/Program.cs b/tests/BizTalkCheckmkPulse.Tests/Program.cs index 2f6c6ac..55575ad 100644 --- a/tests/BizTalkCheckmkPulse.Tests/Program.cs +++ b/tests/BizTalkCheckmkPulse.Tests/Program.cs @@ -42,6 +42,9 @@ namespace BizTalkCheckmkPulse.Tests Run("DefaultServiceContractIsExact", DefaultServiceContractIsExact); Run("InstallerRejectsUnconfirmedServiceRename", InstallerRejectsUnconfirmedServiceRename); Run("InstallerAllowsConfirmedServiceRename", InstallerAllowsConfirmedServiceRename); + Run("InstallerAcceptsLegacyEightServiceSelfTest", InstallerAcceptsLegacyEightServiceSelfTest); + Run("InstallerRequiresNineServicesForCandidate", InstallerRequiresNineServicesForCandidate); + Run("InstallerAllowsAdditiveServiceContract", InstallerAllowsAdditiveServiceContract); Run("InstallerUpdatePreservesExistingSettings", InstallerUpdatePreservesExistingSettings); Run("RuntimeValidationAcceptsFreshConsistentArtifacts", RuntimeValidationAcceptsFreshConsistentArtifacts); Run("RuntimeValidationRejectsPreInstallSnapshot", RuntimeValidationRejectsPreInstallSnapshot); @@ -53,6 +56,7 @@ namespace BizTalkCheckmkPulse.Tests Run("ForceEndpointRefreshRequiresProviderMode", ForceEndpointRefreshRequiresProviderMode); Run("RuntimeValidationArgumentsAreParsedStrictly", RuntimeValidationArgumentsAreParsedStrictly); Run("TaskCompletionRequiresFreshSuccessfulRun", TaskCompletionRequiresFreshSuccessfulRun); + Run("TaskFailureIsDetectedImmediately", TaskFailureIsDetectedImmediately); Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines); Run("SnapshotRejectsTampering", SnapshotRejectsTampering); Run("SnapshotRejectsStaleData", SnapshotRejectsStaleData); @@ -810,6 +814,51 @@ namespace BizTalkCheckmkPulse.Tests Assert(change.Contains("BizTalk Platform"), "confirmed rename must report new service"); } + private static void InstallerAcceptsLegacyEightServiceSelfTest() + { + var output = string.Join("\r\n", Enumerable.Range(1, 8) + .Select(x => "0 \"BizTalk Legacy " + x + "\" - Self test OK")); + var services = BizTalkCheckmkPulse.Setup.InstallerEngine.ParseSelfTestOutput( + output, + string.Empty, + 0, + null, + "Installierte Version"); + AssertEqual(8, services.Count, "legacy self-test service count"); + } + + private static void InstallerRequiresNineServicesForCandidate() + { + var output = string.Join("\n", Enumerable.Range(1, 8) + .Select(x => "0 \"BizTalk Candidate " + x + "\" - Self test OK")); + try + { + BizTalkCheckmkPulse.Setup.InstallerEngine.ParseSelfTestOutput( + output, + string.Empty, + 0, + 9, + "Staging"); + throw new InvalidOperationException("candidate with eight services was accepted"); + } + catch (InvalidOperationException ex) + { + Assert(ex.Message.Contains("Zeilen=8"), "candidate failure must report actual line count"); + Assert(ex.Message.Contains("Erwartet=9"), "candidate failure must report expected line count"); + Assert(ex.Message.Contains("Stdout="), "candidate failure must include stdout"); + } + } + + private static void InstallerAllowsAdditiveServiceContract() + { + var change = BizTalkCheckmkPulse.Setup.InstallerEngine.EnsureServiceNameCompatibility( + new[] { "BizTalk Platform", "BizTalk Event Log" }, + new[] { "BizTalk Platform", "BizTalk Endpoint Reachability", "BizTalk Event Log" }, + false); + Assert(change.Contains("Entfernt=[]"), "additive contract must not remove a service"); + Assert(change.Contains("BizTalk Endpoint Reachability"), "additive contract must report the new service"); + } + private static string ExtractQuotedServiceName(string line) { var start = line.IndexOf('"') + 1; @@ -1030,6 +1079,19 @@ namespace BizTalkCheckmkPulse.Tests 3, 0, boundary.AddSeconds(-3), boundary), "old successful task must not pass"); } + private static void TaskFailureIsDetectedImmediately() + { + var boundary = DateTime.UtcNow; + Assert(BizTalkCheckmkPulse.Setup.TaskSchedulerService.IsFreshCompletedFailure( + 3, 8, boundary.AddSeconds(1), boundary), "fresh failed task must be detected"); + Assert(!BizTalkCheckmkPulse.Setup.TaskSchedulerService.IsFreshCompletedFailure( + 4, 8, boundary.AddSeconds(1), boundary), "running task must not be treated as completed failure"); + Assert(!BizTalkCheckmkPulse.Setup.TaskSchedulerService.IsFreshCompletedFailure( + 3, 8, boundary.AddSeconds(-3), boundary), "old task failure must not fail a new validation run"); + var description = BizTalkCheckmkPulse.Setup.TaskSchedulerService.DescribeResult(8); + Assert(description.Contains("0x00000008"), "task result must include hexadecimal code"); + } + private static RuntimeValidationResult ValidateRuntimeArtifacts( DateTime boundaryUtc, DateTime snapshotUtc,