Fix legacy installer updates and diagnostics
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Binary file not shown.
@@ -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
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<Compile Include="InstallerEngine.cs" />
|
||||
<Compile Include="MainForm.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="SetupOperationLog.cs" />
|
||||
<Compile Include="TaskSchedulerService.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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<string> 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<string>();
|
||||
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<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")
|
||||
{
|
||||
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<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 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 + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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.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;
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user