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 cfb15a8..f3849de 100644 Binary files a/docs/BizTalk_Checkmk_Pulse_Architekturueberblick.docx and b/docs/BizTalk_Checkmk_Pulse_Architekturueberblick.docx differ 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,