Fix legacy installer updates and diagnostics

This commit is contained in:
2026-08-11 10:10:28 +02:00
parent 448be8dc49
commit cc1e59916a
15 changed files with 629 additions and 63 deletions
+16
View File
@@ -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 gelöscht. Bei Fehler oder Timeout wird die alte Version wiederhergestellt und
ein frischer Lauf des alten Tasks abgewartet. 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 Der installierte Task selbst läuft mit `RunLevel Limited`. Der Installer
übergibt das Kennwort direkt an die Windows-Task-Scheduler-COM-Schnittstelle; übergibt das Kennwort direkt an die Windows-Task-Scheduler-COM-Schnittstelle;
es wird weder in einer Prozesskommandozeile noch in Config oder Log es wird weder in einer Prozesskommandozeile noch in Config oder Log
+22 -7
View File
@@ -84,6 +84,10 @@ Der Datenaustausch ist bewusst defensiv:
`UNKNOWN`-Snapshot und einen ungleich null lautenden Task-Exitcode. `UNKNOWN`-Snapshot und einen ungleich null lautenden Task-Exitcode.
- Provider und Consumer protokollieren in tägliche Dateien; die Aufbewahrung - Provider und Consumer protokollieren in tägliche Dateien; die Aufbewahrung
ist standardmäßig 30 Tage. 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 - Snapshotgröße, WMI-/SQL-Timeouts, Log-Retention und Stale-Grenze sind
begrenzt und konfigurierbar. begrenzt und konfigurierbar.
@@ -263,19 +267,23 @@ als `UNKNOWN`.
Ein gMSA kann über die Checkbox ohne Kennworteingabe installiert werden; die Ein gMSA kann über die Checkbox ohne Kennworteingabe installiert werden; die
produktive Standardbeschreibung geht vom normalen Servicekonto aus. 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 1. prüft Paket und Staging zwingend auf neun Self-Test-Zeilen, bevor der
wird, 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 2. baut die neue Version in einem Staging-Verzeichnis auf und übernimmt
vorhandene AppSettings; neue Keys erhalten ihre neuen Defaults. Die alten vorhandene AppSettings; neue Keys erhalten ihre neuen Defaults. Die alten
unveränderten Endpoint-Defaults `12`/`500` werden auf `16`/`100` migriert, unveränderten Endpoint-Defaults `12`/`500` werden auf `16`/`100` migriert,
3. vergleicht installierten und neuen Checkmk-Servicevertrag; ein 3. vergleicht installierten und neuen Checkmk-Servicevertrag. Rein additive
unbestätigter Rename stoppt vor jeder Umschaltung, 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, 4. schaltet die Programmdateien per Verzeichniswechsel um,
5. startet einen triggerlosen Provider-Abnahmelauf unter dem echten 5. startet einen triggerlosen Provider-Abnahmelauf unter dem echten
Collector-Konto und erzwingt einen vollständigen Endpoint-Katalogabgleich, 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, mit der Produkt-EXE einen erst nach Installationsbeginn erzeugten Snapshot,
die Collector-Identität, alle neun eindeutigen Services ohne `UNKNOWN` die Collector-Identität, alle neun eindeutigen Services ohne `UNKNOWN`
sowie den frischen, maschinen- und umgebungsgebundenen Endpoint-Katalog, sowie den frischen, maschinen- und umgebungsgebundenen Endpoint-Katalog,
@@ -283,7 +291,8 @@ Der Installer:
werden vorherige Version, Wrapper und Task samt frischem Altsnapshot werden vorherige Version, Wrapper und Task samt frischem Altsnapshot
wiederhergestellt, wiederhergestellt,
8. behält `%ProgramData%\BizTalkCheckmkPulse` mit Endpoint-Katalog, Snapshot 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 9. installiert EXE und Config nach `%ProgramFiles%\BizTalkCheckmkPulse`, setzt
explizite ACLs für Administratoren, Provider und `LocalSystem` und explizite ACLs für Administratoren, Provider und `LocalSystem` und
installiert nur den kleinen `.cmd`-Consumer unter installiert nur den kleinen `.cmd`-Consumer unter
@@ -306,8 +315,14 @@ Provider-Log:
```cmd ```cmd
dir /o-d "%ProgramData%\BizTalkCheckmkPulse\logs" dir /o-d "%ProgramData%\BizTalkCheckmkPulse\logs"
type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log" 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: Snapshot und Consumer:
```cmd ```cmd
+23 -4
View File
@@ -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 monitoring data`. Nur bei bewusst auf `true` gesetztem Opt-in ist anschließend
eine Checkmk Service Discovery erforderlich. eine Checkmk Service Discovery erforderlich.
Ab Version 2.2.3 vergleicht der Installer bei jedem Update die exakten Ab Version 2.2.5 vergleicht der Installer bei jedem Update die exakten
Servicenamen der installierten und der vorbereiteten Version. Neue, entfernte Servicenamen der installierten und der vorbereiteten Version. Rein neue
oder umbenannte Services führen **vor dem Stoppen des Tasks** zu einem Services sind abwärtskompatibel, blockieren das Update nicht und werden als
Sicherheitsstopp. Die Installation darf nur über die Checkbox 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** **Service-Rename ist beabsichtigt; Checkmk Service Discovery ist eingeplant**
fortgesetzt werden. Der Installer zeigt dabei die entfernten und neuen Namen fortgesetzt werden. Der Installer zeigt dabei die entfernten und neuen Namen
an. Eine bloß geänderte Ausgabereihenfolge gilt nicht als Rename. 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: Version 2.2.4 nimmt die umgeschaltete Installation zusätzlich vollständig ab:
- einmaliger, triggerloser Providerlauf unter dem angegebenen Collector-Konto, - 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 Version abgewartet. Fehler in Paket-, Config- oder Rename-Vorprüfung treten vor
jeder Taskänderung auf und lassen die laufende Installation unangetastet. 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 Beim korrigierenden Wechsel von 2.2.1 mit `BizTalk ACC ...` auf stabile
`BizTalk ...`-Namen ist die Änderung beabsichtigt: Checkbox aktivieren und den `BizTalk ...`-Namen ist die Änderung beabsichtigt: Checkbox aktivieren und den
Servicebestand danach per Discovery abgleichen. Sind in Checkmk bereits die Servicebestand danach per Discovery abgleichen. Sind in Checkmk bereits die
@@ -139,7 +139,7 @@ Endpoint-/Namensfix bewusst nicht grüngefärbt.
## ACC-Abnahme nach Installation ## 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 Beim Wechsel von der fehlerhaften 2.2.1-Ausgabe `BizTalk ACC ...` auf die
stabilen Namen die Rename-/Discovery-Checkbox bewusst aktivieren. stabilen Namen die Rename-/Discovery-Checkbox bewusst aktivieren.
2. In der installierten Config bestätigen: 2. In der installierten Config bestätigen:
@@ -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-<UTC>-<PID>.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.
+9 -1
View File
@@ -53,7 +53,7 @@ Verbindlicher Agent-Dump:
## Service Discovery ## 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 unter dem echten Collector-Konto einen frischen Snapshot und bei aktivierter
Endpoint-Prüfung einen frisch synchronisierten Katalog erzeugt hat. Die neun Endpoint-Prüfung einen frisch synchronisierten Katalog erzeugt hat. Die neun
stabilen Services müssen vollständig und ohne `UNKNOWN` vorliegen. Diese lokale 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 `IncludeEnvironmentInServiceName=true` erzeugt umgebungsabhängige Namen und
verlangt den anschließenden Discovery-Abgleich. 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. 1. Provider dreimal erfolgreich laufen lassen.
2. `LastTaskResult=0`, frischen Snapshot und Log prüfen. 2. `LastTaskResult=0`, frischen Snapshot und Log prüfen.
3. Agent-Dump im `LocalSystem`-Kontext prüfen. 3. Agent-Dump im `LocalSystem`-Kontext prüfen.
+7 -6
View File
@@ -306,7 +306,7 @@ def configure_document(document):
p = footer.paragraphs[0] p = footer.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_after = Pt(0) 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) add_page_field(p)
@@ -320,7 +320,7 @@ def build_document():
core.title = "BizTalk Checkmk Pulse Architektur- und Lösungsüberblick" core.title = "BizTalk Checkmk Pulse Architektur- und Lösungsüberblick"
core.subject = "Gesamtarchitektur, Funktionsumfang und Datenaustausch" core.subject = "Gesamtarchitektur, Funktionsumfang und Datenaustausch"
core.author = "BEW" 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 # Cover
banner = document.add_table(rows=1, cols=1) banner = document.add_table(rows=1, cols=1)
@@ -377,9 +377,9 @@ def build_document():
document, document,
("Dokumentmerkmal", "Angabe"), ("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"), ("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"), ("Dokumentzweck", "Architektur- und Leistungsüberblick für technische Stakeholder"),
), ),
(4.2, 13.0), (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, "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, "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, "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_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) 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_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( add_rich_paragraph(
document, 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, before=8,
after=0, after=0,
) )
@@ -43,6 +43,7 @@
<Compile Include="InstallerEngine.cs" /> <Compile Include="InstallerEngine.cs" />
<Compile Include="MainForm.cs" /> <Compile Include="MainForm.cs" />
<Compile Include="Program.cs" /> <Compile Include="Program.cs" />
<Compile Include="SetupOperationLog.cs" />
<Compile Include="TaskSchedulerService.cs" /> <Compile Include="TaskSchedulerService.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+203 -29
View File
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.AccessControl; using System.Security.AccessControl;
@@ -37,26 +38,32 @@ namespace BizTalkCheckmkPulse.Setup
bool allowServiceNameChange, bool allowServiceNameChange,
Action<string> report) Action<string> report)
{ {
Validate(account, password, isGmsa, environmentName); var operationStartedUtc = DateTime.UtcNow;
report = report ?? delegate { }; var uiReport = report ?? delegate { };
var accountSid = (SecurityIdentifier)new NTAccount(account).Translate(typeof(SecurityIdentifier)); var setupLog = SetupOperationLog.Create(runtimeDirectory, "install-or-update");
report("Konto aufgeloest: " + account + " (" + accountSid.Value + ")"); 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 sourceApplication = Path.Combine(packageDirectory, "application");
var sourceExe = Path.Combine(sourceApplication, "BizTalkCheckmkPulse.exe"); var sourceExe = Path.Combine(sourceApplication, "BizTalkCheckmkPulse.exe");
var sourceConfig = sourceExe + ".config"; var sourceConfig = sourceExe + ".config";
var sourceWrapper = Path.Combine(packageDirectory, "biztalk_checkmk_pulse.cmd"); var sourceWrapper = Path.Combine(packageDirectory, "biztalk_checkmk_pulse.cmd");
RequireFile(sourceExe);
RequireFile(sourceConfig);
RequireFile(sourceWrapper);
var targetExe = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe"); var targetExe = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe");
var targetConfig = targetExe + ".config"; var targetConfig = targetExe + ".config";
var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N"); var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N");
var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N"); var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N");
var targetWrapper = Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd"); var targetWrapper = Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd");
var previousWrapper = File.Exists(targetWrapper) ? File.ReadAllBytes(targetWrapper) : null; byte[] previousWrapper = null;
var hadExistingInstallation = Directory.Exists(installDirectory); var hadExistingInstallation = false;
var hadExistingExecutable = File.Exists(targetExe); var hadExistingExecutable = false;
var mutationStarted = false; var mutationStarted = false;
var backupCreated = false; var backupCreated = false;
var filesActivated = false; var filesActivated = false;
@@ -64,14 +71,29 @@ namespace BizTalkCheckmkPulse.Setup
try 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."); report("Paket-Vorpruefung erfolgreich: neun Checkmk-Services.");
if (!isGmsa) if (!isGmsa)
{ {
phase = "Collector-Anmeldung pruefen";
ValidateBatchLogon(account, password); ValidateBatchLogon(account, password);
report("Collector-Anmeldung und 'Log on as a batch job' vor dem Update bestaetigt."); report("Collector-Anmeldung und 'Log on as a batch job' vor dem Update bestaetigt.");
} }
phase = "Update-Staging vorbereiten";
report("Phase: " + phase + ".");
Directory.CreateDirectory(stagingDirectory); Directory.CreateDirectory(stagingDirectory);
var stagedExe = Path.Combine(stagingDirectory, "BizTalkCheckmkPulse.exe"); var stagedExe = Path.Combine(stagingDirectory, "BizTalkCheckmkPulse.exe");
var stagedConfig = stagedExe + ".config"; var stagedConfig = stagedExe + ".config";
@@ -81,38 +103,45 @@ namespace BizTalkCheckmkPulse.Setup
stagedConfig, stagedConfig,
File.Exists(targetConfig) ? targetConfig : null, File.Exists(targetConfig) ? targetConfig : null,
environmentName); environmentName);
var stagedServiceNames = RunSelfTest(stagedExe); var stagedServiceNames = RunSelfTest(stagedExe, 9, "Staging", setupLog);
report("Update-Staging validiert. Umgebung=" + (effectiveEnvironment.Length == 0 ? "(keine)" : effectiveEnvironment) + "."); report("Update-Staging validiert. Umgebung=" + (effectiveEnvironment.Length == 0 ? "(keine)" : effectiveEnvironment) + ".");
if (File.Exists(targetExe)) 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( var serviceNameChange = EnsureServiceNameCompatibility(
installedServiceNames, installedServiceNames,
stagedServiceNames, stagedServiceNames,
allowServiceNameChange); allowServiceNameChange);
report(serviceNameChange.Length == 0 report(serviceNameChange.Length == 0
? "Checkmk-Servicevertrag unveraendert: keine Service Discovery erforderlich." ? "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. // Erst nach vollstaendiger Staging-Pruefung wird der laufende Provider angehalten.
phase = "Vorhandenen Scheduled Task anhalten";
report("Phase: " + phase + ".");
mutationStarted = true; mutationStarted = true;
scheduler.DeleteIfExists(TaskName); scheduler.DeleteIfExists(TaskName);
report("Vorhandener Scheduled Task angehalten und fuer das Update entfernt."); report("Vorhandener Scheduled Task angehalten und fuer das Update entfernt.");
if (hadExistingInstallation) if (hadExistingInstallation)
{ {
phase = "Vorhandene Programmversion sichern";
Directory.Move(installDirectory, backupDirectory); Directory.Move(installDirectory, backupDirectory);
backupCreated = true; backupCreated = true;
} }
phase = "Neue Programmversion aktivieren";
Directory.Move(stagingDirectory, installDirectory); Directory.Move(stagingDirectory, installDirectory);
filesActivated = true; filesActivated = true;
report(hadExistingInstallation report(hadExistingInstallation
? "Programmdateien atomar auf die neue Version umgestellt." ? "Programmdateien atomar auf die neue Version umgestellt."
: "Programmdateien installiert: " + installDirectory); : "Programmdateien installiert: " + installDirectory);
phase = "Runtime-Verzeichnisse und ACLs einrichten";
var dataDirectory = Path.Combine(runtimeDirectory, "data"); var dataDirectory = Path.Combine(runtimeDirectory, "data");
var logDirectory = Path.Combine(runtimeDirectory, "logs"); var logDirectory = Path.Combine(runtimeDirectory, "logs");
Directory.CreateDirectory(runtimeDirectory); Directory.CreateDirectory(runtimeDirectory);
@@ -125,13 +154,16 @@ namespace BizTalkCheckmkPulse.Setup
ApplyDirectoryAcl(logDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.Modify); ApplyDirectoryAcl(logDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.Modify);
report("Least-Privilege-Verzeichnisrechte gesetzt; vorhandene Runtime-Daten bleiben erhalten."); report("Least-Privilege-Verzeichnisrechte gesetzt; vorhandene Runtime-Daten bleiben erhalten.");
phase = "Checkmk Local Check installieren";
Directory.CreateDirectory(checkmkLocalDirectory); Directory.CreateDirectory(checkmkLocalDirectory);
File.Copy(sourceWrapper, targetWrapper, true); File.Copy(sourceWrapper, targetWrapper, true);
report("Checkmk Local Check installiert: " + checkmkLocalDirectory); 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."); report("Installierter Self-Test erfolgreich: neun Checkmk-Services.");
phase = "Provider-Abnahmelauf registrieren und starten";
var runtimeValidationStartedUtc = DateTime.UtcNow; var runtimeValidationStartedUtc = DateTime.UtcNow;
scheduler.RegisterValidationAndStart( scheduler.RegisterValidationAndStart(
TaskName, TaskName,
@@ -142,6 +174,7 @@ namespace BizTalkCheckmkPulse.Setup
isGmsa); isGmsa);
report("Einmaliger Provider-Abnahmelauf mit erzwungenem Endpoint-Katalogabgleich gestartet."); report("Einmaliger Provider-Abnahmelauf mit erzwungenem Endpoint-Katalogabgleich gestartet.");
phase = "Provider-Abnahmelauf abwarten";
var completedRun = scheduler.WaitForSuccessfulRun( var completedRun = scheduler.WaitForSuccessfulRun(
TaskName, TaskName,
runtimeValidationStartedUtc, runtimeValidationStartedUtc,
@@ -149,12 +182,14 @@ namespace BizTalkCheckmkPulse.Setup
report); report);
report("Provider-Abnahmelauf erfolgreich: LastTaskResult=" + completedRun.ExitCode + "."); report("Provider-Abnahmelauf erfolgreich: LastTaskResult=" + completedRun.ExitCode + ".");
phase = "Runtime-Artefakte validieren";
var runtimeValidation = RunRuntimeValidation( var runtimeValidation = RunRuntimeValidation(
targetExe, targetExe,
runtimeValidationStartedUtc, runtimeValidationStartedUtc,
account); account);
report("Post-Install-Runtime-Abnahme erfolgreich: " + runtimeValidation); report("Post-Install-Runtime-Abnahme erfolgreich: " + runtimeValidation);
phase = "Minuetlichen Scheduled Task registrieren";
scheduler.RegisterRecurring( scheduler.RegisterRecurring(
TaskName, TaskName,
targetExe, targetExe,
@@ -165,19 +200,28 @@ namespace BizTalkCheckmkPulse.Setup
report("Validierter minuetlicher Scheduled Task registriert: " + TaskName); report("Validierter minuetlicher Scheduled Task registriert: " + TaskName);
TryDeleteDirectory(backupDirectory, report); TryDeleteDirectory(backupDirectory, report);
report("Installation/Update erfolgreich abgeschlossen.");
} }
catch (Exception installException) 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<string>(); var rollbackFailures = new List<string>();
if (mutationStarted) if (mutationStarted)
{ {
try try
{ {
scheduler.DeleteIfExists(TaskName); scheduler.DeleteIfExists(TaskName);
report("Rollback: Scheduled Task gestoppt und entfernt.");
} }
catch (Exception ex) catch (Exception ex)
{ {
rollbackFailures.Add("Task stoppen: " + ex.Message); rollbackFailures.Add("Task stoppen: " + ex.Message);
setupLog.WriteException("Rollback: Task stoppen", ex);
} }
} }
@@ -192,6 +236,7 @@ namespace BizTalkCheckmkPulse.Setup
catch (Exception ex) catch (Exception ex)
{ {
rollbackFailures.Add("Programmdateien wiederherstellen: " + ex.Message); rollbackFailures.Add("Programmdateien wiederherstellen: " + ex.Message);
setupLog.WriteException("Rollback: Programmdateien wiederherstellen", ex);
} }
} }
@@ -200,10 +245,12 @@ namespace BizTalkCheckmkPulse.Setup
try try
{ {
RestoreWrapper(targetWrapper, previousWrapper); RestoreWrapper(targetWrapper, previousWrapper);
report("Rollback: Checkmk-Wrapper auf den vorherigen Stand zurueckgesetzt.");
} }
catch (Exception ex) catch (Exception ex)
{ {
rollbackFailures.Add("Checkmk-Wrapper wiederherstellen: " + ex.Message); rollbackFailures.Add("Checkmk-Wrapper wiederherstellen: " + ex.Message);
setupLog.WriteException("Rollback: Checkmk-Wrapper wiederherstellen", ex);
} }
} }
@@ -229,19 +276,28 @@ namespace BizTalkCheckmkPulse.Setup
catch (Exception ex) catch (Exception ex)
{ {
rollbackFailures.Add("Scheduled Task wiederherstellen: " + ex.Message); rollbackFailures.Add("Scheduled Task wiederherstellen: " + ex.Message);
setupLog.WriteException("Rollback: Scheduled Task wiederherstellen", ex);
} }
} }
var rollback = rollbackFailures.Count == 0 var rollback = !mutationStarted
? "Keine Systemaenderung vorgenommen; kein Rollback erforderlich."
: rollbackFailures.Count == 0
? "Rollback erfolgreich." ? "Rollback erfolgreich."
: "Rollback unvollstaendig: " + string.Join(" | ", rollbackFailures); : "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( throw new InvalidOperationException(
"Installation/Update fehlgeschlagen. " + rollback + " Ursache: " + installException.Message, "Installation/Update fehlgeschlagen in Phase '" + phase + "'. "
+ rollback + " Ursache: " + installException.Message + " " + logReference,
installException); installException);
} }
finally finally
{ {
TryDeleteDirectory(stagingDirectory, null); TryDeleteDirectory(stagingDirectory, null);
setupLog.Dispose();
} }
} }
@@ -452,8 +508,20 @@ namespace BizTalkCheckmkPulse.Setup
new DirectoryInfo(path).SetAccessControl(security); new DirectoryInfo(path).SetAccessControl(security);
} }
private static IReadOnlyList<string> RunSelfTest(string executable) private static IReadOnlyList<string> 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") var start = new ProcessStartInfo(executable, "--self-test")
{ {
UseShellExecute = false, UseShellExecute = false,
@@ -464,22 +532,72 @@ namespace BizTalkCheckmkPulse.Setup
using (var process = Process.Start(start)) using (var process = Process.Start(start))
{ {
if (process == null) throw new InvalidOperationException("Self-Test konnte nicht gestartet werden."); if (process == null) throw new InvalidOperationException("Self-Test konnte nicht gestartet werden.");
var output = process.StandardOutput.ReadToEnd(); var outputRead = process.StandardOutput.ReadToEndAsync();
var error = process.StandardError.ReadToEnd(); var errorRead = process.StandardError.ReadToEndAsync();
if (!process.WaitForExit(30000)) if (!process.WaitForExit(30000))
{ {
process.Kill(); 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.");
}
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<string> 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 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(); var serviceNames = lines.Select(ExtractServiceName).ToArray();
if (serviceNames.Distinct(StringComparer.Ordinal).Count() != serviceNames.Length) if (serviceNames.Distinct(StringComparer.Ordinal).Count() != serviceNames.Length)
throw new InvalidOperationException("Self-Test fehlgeschlagen: Checkmk-Servicenamen sind nicht eindeutig."); throw new InvalidOperationException(
"Self-Test '" + label + "' fehlgeschlagen: Checkmk-Servicenamen sind nicht eindeutig. "
+ "Services=[" + string.Join(", ", serviceNames) + "].");
return 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( private static string RunRuntimeValidation(
@@ -501,13 +619,21 @@ namespace BizTalkCheckmkPulse.Setup
using (var process = Process.Start(start)) using (var process = Process.Start(start))
{ {
if (process == null) throw new InvalidOperationException("Runtime-Abnahme konnte nicht gestartet werden."); if (process == null) throw new InvalidOperationException("Runtime-Abnahme konnte nicht gestartet werden.");
var output = process.StandardOutput.ReadToEnd().Trim(); var outputRead = process.StandardOutput.ReadToEndAsync();
var error = process.StandardError.ReadToEnd().Trim(); var errorRead = process.StandardError.ReadToEndAsync();
if (!process.WaitForExit(30000)) if (!process.WaitForExit(30000))
{ {
process.Kill(); 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 if (process.ExitCode != 0
|| !output.StartsWith("RUNTIME_VALIDATION_V1 ", StringComparison.Ordinal)) || !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 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 added = staged.Except(installed, StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToArray();
var description = "Entfernt=[" + string.Join(", ", removed) + "]; Neu=[" + string.Join(", ", added) + "]."; 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( throw new InvalidOperationException(
"Sicherheitsstopp: Das Update wuerde Checkmk-Servicenamen aendern. " "Sicherheitsstopp: Das Update wuerde Checkmk-Servicenamen aendern. "
@@ -553,5 +682,50 @@ namespace BizTalkCheckmkPulse.Setup
return description; 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 + ".");
}
}
} }
} }
@@ -6,5 +6,5 @@ using System.Runtime.CompilerServices;
[assembly: AssemblyDescription("PowerShell-free installer and updater for BizTalk Checkmk Pulse")] [assembly: AssemblyDescription("PowerShell-free installer and updater for BizTalk Checkmk Pulse")]
[assembly: AssemblyCompany("BEW")] [assembly: AssemblyCompany("BEW")]
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")] [assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
[assembly: AssemblyVersion("2.2.4.0")] [assembly: AssemblyVersion("2.2.5.0")]
[assembly: AssemblyFileVersion("2.2.4.0")] [assembly: AssemblyFileVersion("2.2.5.0")]
@@ -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
{
/// <summary>
/// Best-effort-Diagnoselog fuer einen einzelnen Setup-Lauf. Das Kennwort
/// wird dieser Klasse nie uebergeben und kann deshalb nicht protokolliert werden.
/// </summary>
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", " ");
}
}
}
@@ -1,4 +1,6 @@
using System; using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
@@ -11,6 +13,7 @@ namespace BizTalkCheckmkPulse.Setup
private const int TaskTriggerTime = 1; private const int TaskTriggerTime = 1;
private const int TaskActionExecute = 0; private const int TaskActionExecute = 0;
private const int TaskInstancesIgnoreNew = 2; private const int TaskInstancesIgnoreNew = 2;
private const int TaskStateReady = 3;
private const int TaskStateRunning = 4; private const int TaskStateRunning = 4;
public void RegisterValidationAndStart( public void RegisterValidationAndStart(
@@ -165,11 +168,20 @@ namespace BizTalkCheckmkPulse.Setup
return new TaskRunResult(lastRunUtc.Value, lastResult); 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) if (report != null && DateTime.UtcNow >= nextReportUtc)
{ {
report( report(
"Warte auf erfolgreichen Providerlauf: state=" + lastState "Warte auf erfolgreichen Providerlauf: state=" + DescribeState(lastState)
+ ", last_result=" + lastResult + ", last_result=" + DescribeResult(lastResult)
+ ", last_run=" + (lastRunUtc.HasValue ? lastRunUtc.Value.ToString("o") : "(noch keiner)") + "."); + ", last_run=" + (lastRunUtc.HasValue ? lastRunUtc.Value.ToString("o") : "(noch keiner)") + ".");
nextReportUtc = DateTime.UtcNow.AddSeconds(15); nextReportUtc = DateTime.UtcNow.AddSeconds(15);
} }
@@ -179,8 +191,8 @@ namespace BizTalkCheckmkPulse.Setup
throw new TimeoutException( throw new TimeoutException(
"Providerlauf wurde innerhalb von " + Math.Ceiling(timeout.TotalSeconds) "Providerlauf wurde innerhalb von " + Math.Ceiling(timeout.TotalSeconds)
+ " Sekunden nicht erfolgreich abgeschlossen. state=" + lastState + " Sekunden nicht erfolgreich abgeschlossen. state=" + DescribeState(lastState)
+ ", last_result=" + lastResult + ", last_result=" + DescribeResult(lastResult)
+ ", last_run=" + (lastRunUtc.HasValue ? lastRunUtc.Value.ToString("o") : "(noch keiner)") + "."); + ", last_run=" + (lastRunUtc.HasValue ? lastRunUtc.Value.ToString("o") : "(noch keiner)") + ".");
} }
finally finally
@@ -197,13 +209,77 @@ namespace BizTalkCheckmkPulse.Setup
DateTime? lastRunUtc, DateTime? lastRunUtc,
DateTime notBeforeUtc) DateTime notBeforeUtc)
{ {
return taskState != TaskStateRunning return taskState == TaskStateReady
&& lastTaskResult == 0 && lastTaskResult == 0
&& lastRunUtc.HasValue && lastRunUtc.HasValue
// Task Scheduler kann LastRunTime mit geringerer Aufloesung als DateTime.UtcNow liefern. // Task Scheduler kann LastRunTime mit geringerer Aufloesung als DateTime.UtcNow liefern.
&& lastRunUtc.Value >= notBeforeUtc.ToUniversalTime().AddSeconds(-2); && 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) public void DeleteIfExists(string taskName)
{ {
dynamic service = null; dynamic service = null;
@@ -6,5 +6,5 @@ using System.Reflection;
[assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")] [assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")]
[assembly: AssemblyCompany("BEW")] [assembly: AssemblyCompany("BEW")]
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")] [assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
[assembly: AssemblyVersion("2.2.4.0")] [assembly: AssemblyVersion("2.2.5.0")]
[assembly: AssemblyFileVersion("2.2.4.0")] [assembly: AssemblyFileVersion("2.2.5.0")]
@@ -42,6 +42,9 @@ namespace BizTalkCheckmkPulse.Tests
Run("DefaultServiceContractIsExact", DefaultServiceContractIsExact); Run("DefaultServiceContractIsExact", DefaultServiceContractIsExact);
Run("InstallerRejectsUnconfirmedServiceRename", InstallerRejectsUnconfirmedServiceRename); Run("InstallerRejectsUnconfirmedServiceRename", InstallerRejectsUnconfirmedServiceRename);
Run("InstallerAllowsConfirmedServiceRename", InstallerAllowsConfirmedServiceRename); Run("InstallerAllowsConfirmedServiceRename", InstallerAllowsConfirmedServiceRename);
Run("InstallerAcceptsLegacyEightServiceSelfTest", InstallerAcceptsLegacyEightServiceSelfTest);
Run("InstallerRequiresNineServicesForCandidate", InstallerRequiresNineServicesForCandidate);
Run("InstallerAllowsAdditiveServiceContract", InstallerAllowsAdditiveServiceContract);
Run("InstallerUpdatePreservesExistingSettings", InstallerUpdatePreservesExistingSettings); Run("InstallerUpdatePreservesExistingSettings", InstallerUpdatePreservesExistingSettings);
Run("RuntimeValidationAcceptsFreshConsistentArtifacts", RuntimeValidationAcceptsFreshConsistentArtifacts); Run("RuntimeValidationAcceptsFreshConsistentArtifacts", RuntimeValidationAcceptsFreshConsistentArtifacts);
Run("RuntimeValidationRejectsPreInstallSnapshot", RuntimeValidationRejectsPreInstallSnapshot); Run("RuntimeValidationRejectsPreInstallSnapshot", RuntimeValidationRejectsPreInstallSnapshot);
@@ -53,6 +56,7 @@ namespace BizTalkCheckmkPulse.Tests
Run("ForceEndpointRefreshRequiresProviderMode", ForceEndpointRefreshRequiresProviderMode); Run("ForceEndpointRefreshRequiresProviderMode", ForceEndpointRefreshRequiresProviderMode);
Run("RuntimeValidationArgumentsAreParsedStrictly", RuntimeValidationArgumentsAreParsedStrictly); Run("RuntimeValidationArgumentsAreParsedStrictly", RuntimeValidationArgumentsAreParsedStrictly);
Run("TaskCompletionRequiresFreshSuccessfulRun", TaskCompletionRequiresFreshSuccessfulRun); Run("TaskCompletionRequiresFreshSuccessfulRun", TaskCompletionRequiresFreshSuccessfulRun);
Run("TaskFailureIsDetectedImmediately", TaskFailureIsDetectedImmediately);
Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines); Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines);
Run("SnapshotRejectsTampering", SnapshotRejectsTampering); Run("SnapshotRejectsTampering", SnapshotRejectsTampering);
Run("SnapshotRejectsStaleData", SnapshotRejectsStaleData); Run("SnapshotRejectsStaleData", SnapshotRejectsStaleData);
@@ -810,6 +814,51 @@ namespace BizTalkCheckmkPulse.Tests
Assert(change.Contains("BizTalk Platform"), "confirmed rename must report new service"); 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) private static string ExtractQuotedServiceName(string line)
{ {
var start = line.IndexOf('"') + 1; var start = line.IndexOf('"') + 1;
@@ -1030,6 +1079,19 @@ namespace BizTalkCheckmkPulse.Tests
3, 0, boundary.AddSeconds(-3), boundary), "old successful task must not pass"); 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( private static RuntimeValidationResult ValidateRuntimeArtifacts(
DateTime boundaryUtc, DateTime boundaryUtc,
DateTime snapshotUtc, DateTime snapshotUtc,