Improve installer diagnostics and rollback reporting

This commit is contained in:
2026-08-11 11:52:34 +02:00
parent a74528e5c6
commit bb2d2f8484
14 changed files with 746 additions and 97 deletions
+13
View File
@@ -1,6 +1,19 @@
# Changelog
## [2.1.1] - 2026-08-11
### Added
- Stable setup/uninstall phase codes, environment and file metadata, complete self-test output, exception chains with HRESULT/stacktrace, and per-step rollback diagnostics.
- Installer-log fallback from ProgramData to Temp plus a setup button for opening diagnostic logs.
- Post-write verification of shortcuts, uninstall executable and essential uninstall-registry values.
- Regression coverage for pre-mutation rollback wording, diagnostic context, log fallback and UI-report isolation.
### Changed
- Pre-mutation failures now explicitly report that no rollback was required; completed rollbacks are only reported as successful when every restoration step succeeded.
- Self-test timeout handling is bounded after termination is requested and records kill/capture status.
- Technical installer acceptance is explicitly separated from BizTalk WMI/environment validation.
- Setup and uninstall preserve undeletable backup/quarantine data and record a successful completion with a cleanup warning.
## [2.1.0] - 2026-08-11
### Added
- Transactional Windows setup/updater with SHA-256 payload manifest, isolated staging, pre/post activation self-tests, rollback, shortcuts and uninstall registration.
+11 -1
View File
@@ -115,7 +115,14 @@ Die Aktivierung ist transaktional aufgebaut:
6. Staging auf demselben Volume als produktives Verzeichnis aktivieren.
7. Aktivierte EXE erneut per `--self-test` prüfen.
8. Erst danach Startmenü, optionale Desktop-Verknüpfung und Windows-Uninstall-Eintrag schreiben.
9. Bei einem Fehler die neue Version entfernen und das Backup einschließlich Windows-Integration wiederherstellen.
9. Windows-Integration durch Rücklesen der Verknüpfungen und der wesentlichen Registrywerte verifizieren.
10. Bei einem Fehler nach der ersten Mutation die neue Version entfernen und das Backup einschließlich Windows-Integration wiederherstellen.
Fehler vor der ersten Mutation werden als **Kein Rollback erforderlich** ausgewiesen. Ein Rollback wird nur als erfolgreich bezeichnet, wenn seine einzelnen Schritte ohne Fehler abgeschlossen wurden; ein verbliebenes Backup ohne aktives Programmverzeichnis verletzt eine zusätzlich geprüfte Invariante und wird als unvollständiger Rollback gemeldet. Bereinigungsprobleme an einem nicht mehr benötigten Backup oder Deinstallations-Quarantäneverzeichnis werden als erfolgreiche Operation mit Warnung protokolliert und zerstören die wiederherstellbaren Dateien nicht blind.
Das dauerhafte Setup-Log enthält pro Lauf einen Kontextkopf mit Version, Betriebssystem, 32-/64-Bit-Kontext, CLR, Rechner, Identität und Elevation. Hinzu kommen Pfade, freier Speicher, Dateiversion/-größe/-SHA-256, vollständige Self-Test-Ausgaben mit Exitcode, Exception-Ketten mit HRESULT und Stacktrace sowie jeder Rollbackschritt. Stabile `SETUP-*`- und `UNINSTALL-*`-Fehlercodes ermöglichen die Zuordnung unabhängig vom lokalisierten Meldungstext. Die Aufbewahrung beträgt 90 Tage. Wenn ProgramData für die Diagnose nicht verfügbar ist, wird ein Temp-Fallback verwendet und sichtbar gemeldet; die Schaltfläche **Diagnoselogs öffnen** öffnet den regulären Ablageort.
Der Self-Test ist eine technische Installationsabnahme ohne BizTalk-WMI. Ein fachlicher Umgebungszustand wie fehlender WMI-Zugriff oder `Unknown` führt deshalb nicht zum Installer-Rollback. Nach erfolgreicher Installation bleiben **Diagnose** und ein Dry-run gegen das Zielsystem verpflichtende Abnahmeschritte.
Das äußere ZIP erhält zusätzlich eine SHA-256-Datei und eine Certutil-kompatible Base64-TXT-Datei. Diese äußere Prüfsumme erkennt Übertragungsfehler; sie ist keine digitale Herausgebersignatur. Details und Befehle stehen in `Installation.md`.
@@ -133,6 +140,9 @@ Das äußere ZIP erhält zusätzlich eine SHA-256-Datei und eine Certutil-kompat
- Erfolgreiche Staging-Aktivierung.
- Wiederherstellung der Vorversion, wenn der Self-Test nach Aktivierung fehlschlägt.
- Deinstallation durch atomare Umbenennung des Programmverzeichnisses vor der bestmöglichen Bereinigung.
- Eindeutige Fehlerphase ohne irreführende Rollbackmeldung bei einem Staging-Fehler.
- Vollständiger Diagnosekontext mit Exception-Kette und Temp-Fallback für das Setup-Log.
- Unabhängigkeit der Installation von Fehlern der UI-Fortschrittsanzeige.
Der portable Build, die Tests, der Anwendungsselftest und die Paketkonsistenz sind lokal unter Mono prüfbar. Die endgültige Freigabe erfordert zusätzlich einen Windows-Test von UAC, Registry, Verknüpfungen und Setup-Rollback sowie einen repräsentativen BizTalk-2020-Test von Diagnose, Dry-run, Shutdown und Restore.
+29 -2
View File
@@ -39,7 +39,8 @@ Installationsziele:
- Programm: `%ProgramFiles%\BizTalkPlatformManagementTool`
- Laufzeitlogs: `%ProgramData%\BizTalkPlatformManagementTool\Logs`
- Installerlogs und Uninstaller: `%ProgramData%\BizTalkPlatformManagementTool`
- Installerlogs: `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs`
- Uninstaller: `%ProgramData%\BizTalkPlatformManagementTool\Setup\Uninstall.exe`
- Startmenü: `BizTalk Platform Management Tool`
## Update und Rollback
@@ -54,7 +55,33 @@ Der Installer verändert eine bestehende Installation erst nach erfolgreicher Pa
6. Die aktivierte EXE führt den Self-Test erneut aus.
7. Erst danach werden Verknüpfungen und Windows-Uninstall-Eintrag aktualisiert.
Schlägt ein Schritt nach Beginn der Umschaltung fehl, entfernt das Setup die neue Version und stellt das Backup wieder her. Staging und Backup werden anschließend bestmöglich bereinigt. Das genaue Phasenprotokoll steht unter `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs`.
Schlägt ein Schritt nach Beginn der Umschaltung fehl, entfernt das Setup die neue Version und stellt das Backup wieder her. Tritt der Fehler bereits bei Paketprüfung oder Staging auf, meldet das Setup ausdrücklich **Kein Rollback erforderlich**, weil die aktive Installation unverändert blieb. Staging und Backup werden anschließend bestmöglich bereinigt. Kann ein nicht mehr benötigtes Backup wegen einer Dateisperre nicht gelöscht werden, bleibt die erfolgreiche Installation aktiv und das Log kennzeichnet den Abschluss als `success_with_backup_cleanup_warning`.
## Installer-Diagnose
Jeder Installations-, Update- und Deinstallationslauf erzeugt ein eigenes UTF-8-Log. Über **Diagnoselogs öffnen** kann der Ordner direkt aus dem Setup geöffnet werden. Die Logs werden 90 Tage aufbewahrt. Ist ProgramData nicht beschreibbar, versucht das Setup ein Fallback unter `%TEMP%\BizTalkPlatformManagementTool\InstallerLogs` und zeigt dessen Pfad an. Kann auch dort kein Log erzeugt werden, bleibt der eigentliche Fehler in der Setup-Meldung sichtbar.
Die Abschlussmeldung enthält einen stabilen Fehlercode und die betroffene Phase:
| Fehlercode | Bedeutung |
|---|---|
| `SETUP-INITIALIZATION` | Daten-/Logverzeichnis oder Ausgangszustand konnte nicht vorbereitet werden |
| `SETUP-PACKAGE-VALIDATION` | Manifest, Payload, SHA-256 oder Prüfung auf laufende Anwendung fehlgeschlagen |
| `SETUP-STAGING-VALIDATION` | Kopie ins Staging oder erster Self-Test fehlgeschlagen; aktive Installation unverändert |
| `SETUP-ACTIVATION` | Sichern oder Aktivieren der Programmdateien fehlgeschlagen |
| `SETUP-ACTIVATED-SELFTEST` | Zweiter Self-Test nach der Aktivierung fehlgeschlagen |
| `SETUP-WINDOWS-INTEGRATION` | Verknüpfung, Uninstaller oder Registry-Registrierung/Verifikation fehlgeschlagen |
| `UNINSTALL-*` | Die entsprechende Initialisierungs-, Prozess-, Quarantäne- oder Integrationsphase der Deinstallation fehlgeschlagen |
Das Log enthält Betriebssystem, Prozess-/OS-Bitness, .NET-Version, Rechner, Windows-Identität und Elevation sowie Installationspfade, freien Speicher, Dateigrößen, Versionen und SHA-256. Für Self-Tests werden Laufzeit, Exitcode dezimal und hexadezimal sowie vollständiges `stdout` und `stderr` geschrieben. Bei Fehlern folgen Exception-Kette, HRESULT und Stacktrace; jeder Rollbackschritt und sein Ergebnis werden separat protokolliert. Passwörter oder Verbindungsdaten werden vom Installer nicht verarbeitet oder geloggt.
Für eine Supportanalyse bitte sichern:
1. Vollständige Fehlermeldung einschließlich Fehlercode und Phase.
2. Das jüngste `setup-*.log` aus dem vom Setup angezeigten Pfad.
3. Ob Neuinstallation, Update oder Deinstallation ausgeführt wurde und ob Virenscanner/Endpoint Protection eine Datei blockiert hat.
Ein erfolgreicher Installer-Self-Test bestätigt Paket, Programmstart und lokale Kernfunktionen. Der fachliche BizTalk-Zustand ist bewusst kein Rollbackkriterium: WMI-Erreichbarkeit, Berechtigungen und Umgebungszustand danach separat über **Diagnose** und einen Dry-run prüfen. So wird beispielsweise ein erreichbarer Installer nicht wegen eines fachlichen `Unknown`-Zustands zurückgerollt.
## Deinstallation
+3
View File
@@ -26,6 +26,7 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
- Clear and Close actions in the main toolbar
- No compile-time dependency on BizTalk ExplorerOM assemblies
- Transactional Windows installer/updater with SHA-256 payload validation and rollback
- Installer diagnostics with stable phase codes, complete child-process output, exception chains and a ProgramData-to-Temp log fallback
- WMI-free runtime self-test plus automated regression test executable
## Safe Usage
@@ -80,6 +81,8 @@ Snapshot and plan JSON files are written as UTF-8 without BOM. Loading is tolera
JSON snapshots and plans are written through a same-directory temporary file and atomic replacement. Snapshot comparison keys artifacts by application plus name, preventing collisions between equal artifact names in different applications. CSV fields that could be interpreted as spreadsheet formulas are neutralized.
Installer, update and uninstall diagnostics are retained for 90 days under `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs`. The setup UI can open that directory directly. If the primary log cannot be created, setup reports and uses a `%TEMP%\BizTalkPlatformManagementTool\InstallerLogs` fallback. See [Installation](Installation.md#installer-diagnose) for phase codes and the support checklist.
## Build
Open `BizTalkPlatformManagementTool.sln` in Visual Studio on Windows with the .NET Framework 4.6.1 Developer Pack installed, then build the `Release|Any CPU` configuration.
@@ -14,18 +14,31 @@ Vor Version 2.1.0 enthielt das Repository keinen Installer für die C#-Anwendung
- Zweiter Self-Test nach Aktivierung und vor Windows-Registrierung.
- Automatisches Datei- und Registrierungsrollback bei Fehlern.
- Dauerhaftes phasenbezogenes Installerlog unter ProgramData.
- Temp-Fallback, falls das primäre Installerlog nicht angelegt werden kann.
- Stabile Fehlercodes, vollständige Self-Test-Ausgaben, Exception-Ketten, HRESULTs und einzeln protokollierte Rollbackschritte.
- Explizite Unterscheidung zwischen Fehlern vor der ersten Mutation und tatsächlich ausgeführtem Rollback.
- Rücklesende Verifikation von Verknüpfungen, Uninstaller und zentralen Registrywerten.
- Deinstallation über Windows-Uninstall-Eintrag; Diagnoseprotokolle bleiben erhalten.
- Äußere ZIP-Prüfsumme und Certutil-kompatible Base64-TXT für kontrollierte Übertragung.
## Lokal verifiziert
- Release-Build aller Projekte mit Mono MSBuild.
- Elf Regressionstests einschließlich manipulierter/zusätzlicher/ausbrechender Payload-Pfade, Staging-Abbruch ohne Mutation, erzwungenem Fehler des zweiten Self-Tests mit Wiederherstellung der Vorversion und Deinstallation über ein Quarantäneverzeichnis.
- Vierzehn Regressionstests einschließlich manipulierter/zusätzlicher/ausbrechender Payload-Pfade, Staging-Abbruch ohne Mutation, erzwungenem Fehler des zweiten Self-Tests mit Wiederherstellung der Vorversion, Deinstallation über ein Quarantäneverzeichnis, Diagnosekontext, Log-Fallback und Fehlerisolierung der UI-Ausgabe.
- WMI-freier Self-Test der produktiven EXE.
- Erstellung des Installationsordners, ZIPs, Base64-TXTs und der SHA-256-Datei.
- Rückdekodierung der Base64-TXT und Bytevergleich mit dem ZIP.
- Erneute Prüfung des internen Manifests nach der Paketierung.
## Zweite Prüfung nach den Deployment-Erfahrungen des Schwesterprojekts
Die erneute Prüfung orientierte sich an den dokumentierten Schwierigkeiten aus `biztalk-checkmk-pulse`. Zwei Erkenntnisse wurden bewusst übertragen:
1. Ein Fehler vor der ersten Änderung darf nicht als erfolgreicher Rollback beschrieben werden. Der Installer meldet daher eindeutig, dass kein Rollback erforderlich war und protokolliert `mutation_started=false`.
2. Technische Installationsfähigkeit und fachlicher Laufzeitzustand sind getrennte Abnahmen. Der WMI-freie Self-Test entscheidet über Aktivierung/Rollback; BizTalk-WMI, Berechtigungen und ein möglicher `Unknown`-Zustand werden erst danach mit **Diagnose** und Dry-run bewertet.
Zusätzlich wurden die Informationen erfasst, die bei einem schwer reproduzierbaren Windows-Deployment typischerweise fehlen: genaue Setup-/Dateiversionen und Hashes, vollständiges `stdout`/`stderr`, dezimaler und hexadezimaler Exitcode, Timeout-/Kill-Ergebnis, Identität/Elevation, Exception-Kette/HRESULT/Stacktrace und jeder Rollbackschritt. Der Self-Test wartet nach einem Timeout höchstens weitere fünf Sekunden auf Prozessende und Ausgabekanäle; ein nicht beendbarer Prozess kann das Setup somit nicht unbegrenzt blockieren.
## Noch auf Windows/BizTalk zu validieren
Die lokale Linux-/Mono-Verifikation kann folgende Windows-spezifische Punkte nicht abschließend beweisen:
@@ -35,6 +48,7 @@ Die lokale Linux-/Mono-Verifikation kann folgende Windows-spezifische Punkte nic
3. 64-Bit-Uninstall-Eintrag und Aufruf über Apps & Features.
4. Updateblockade bei laufender installierter GUI.
5. Reales Rollback bei Dateisperren, Virenscannerzugriff oder Registryfehlern.
6. Diagnose und Laufzeitoperationen gegen `root\MicrosoftBizTalkServer` auf BizTalk Server 2020.
6. ProgramData-Fehler und Temp-Log-Fallback unter realen Windows-ACLs.
7. Diagnose und Laufzeitoperationen gegen `root\MicrosoftBizTalkServer` auf BizTalk Server 2020.
Bis diese Punkte repräsentativ geprüft sind, ist der Installer lokal automatisiert gehärtet, aber noch nicht als vollständig produktionsvalidiert auf Windows/BizTalk zu bezeichnen.
@@ -15,13 +15,14 @@ namespace BizTalkPlatformManagementTool.Setup
{
internal const string ApplicationExeName = "BizTalkPlatformManagementTool.exe";
private const string ProductName = "BizTalk Platform Management Tool";
private const string ProductVersion = "2.1.0";
private const string ProductVersion = "2.1.1";
private const string UninstallKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\BizTalkPlatformManagementTool";
private readonly string packageDirectory;
private readonly string installDirectory;
private readonly string dataDirectory;
private readonly bool registerWindowsIntegration;
private readonly Func<string, bool> selfTestRunner;
private string lastInstallerLogDirectory;
private sealed class WindowsIntegrationSnapshot
{
@@ -38,7 +39,7 @@ namespace BizTalkPlatformManagementTool.Setup
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "BizTalkPlatformManagementTool"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "BizTalkPlatformManagementTool"),
true,
RunApplicationSelfTest)
null)
{
}
@@ -48,11 +49,16 @@ namespace BizTalkPlatformManagementTool.Setup
this.installDirectory = Path.GetFullPath(installDirectory);
this.dataDirectory = Path.GetFullPath(dataDirectory);
this.registerWindowsIntegration = registerWindowsIntegration;
this.selfTestRunner = selfTestRunner ?? throw new ArgumentNullException("selfTestRunner");
this.selfTestRunner = selfTestRunner;
}
/// <summary>Gets the fixed machine-wide application installation directory.</summary>
public string InstallDirectory { get { return installDirectory; } }
/// <summary>Gets the directory containing persistent setup diagnostic logs.</summary>
public string InstallerLogDirectory
{
get { return lastInstallerLogDirectory ?? Path.Combine(dataDirectory, "InstallerLogs"); }
}
/// <summary>Gets whether this setup copy contains a complete install/update payload.</summary>
public bool HasInstallPayload { get { return Directory.Exists(Path.Combine(packageDirectory, "application")) && File.Exists(Path.Combine(packageDirectory, "application.manifest")); } }
/// <summary>Gets whether the application executable is present at the install target.</summary>
@@ -61,47 +67,94 @@ namespace BizTalkPlatformManagementTool.Setup
/// <summary>Validates, stages and transactionally installs or updates the application.</summary>
public void Install(bool createDesktopShortcut, Action<string> report)
{
report = report ?? delegate { };
Directory.CreateDirectory(dataDirectory);
var uiReport = report ?? delegate { };
var log = SetupOperationLog.Create(dataDirectory, "install-update");
Action<string> write = message => { log.Write("INFO", message); report(message); };
write("Diagnoselog: " + (log.FilePath.Length == 0 ? "nicht verfuegbar" : log.FilePath));
Action<string> write = message => ReportSafely(log, uiReport, "INFO", message);
if (!string.IsNullOrEmpty(log.CreationError))
write((log.IsFallback ? "WARNUNG: Primaeres Diagnoselog nicht verfuegbar; Temp-Fallback wird verwendet: " : "WARNUNG: Diagnoselog konnte nicht angelegt werden: ") + log.CreationError);
if (log.FilePath.Length != 0)
{
lastInstallerLogDirectory = Path.GetDirectoryName(log.FilePath);
write("Diagnoselog: " + log.FilePath);
}
var sourceApplication = Path.Combine(packageDirectory, "application");
var manifestPath = Path.Combine(packageDirectory, "application.manifest");
var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N");
var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N");
var hadExistingInstallation = Directory.Exists(installDirectory);
var integrationSnapshot = registerWindowsIntegration ? CaptureWindowsIntegration() : null;
WindowsIntegrationSnapshot integrationSnapshot = null;
var backupCreated = false;
var activated = false;
var integrationMutationStarted = false;
var phaseCode = "SETUP-INITIALIZATION";
var phase = "Initialisierung";
try
{
write("Phase 1/6: Paketmanifest und SHA-256 pruefen.");
Directory.CreateDirectory(dataDirectory);
log.Write(
"INFO",
"event=setup_context package_directory=\"" + packageDirectory
+ "\" source_application=\"" + sourceApplication
+ "\" manifest=\"" + manifestPath
+ "\" install_directory=\"" + installDirectory
+ "\" data_directory=\"" + dataDirectory
+ "\" staging_directory=\"" + stagingDirectory
+ "\" backup_directory=\"" + backupDirectory
+ "\" existing_installation=" + hadExistingInstallation
+ " desktop_shortcut_requested=" + createDesktopShortcut
+ " windows_integration=" + registerWindowsIntegration);
LogDriveSpace(log, installDirectory);
log.WriteFileDetails("setup_executable", Assembly.GetExecutingAssembly().Location);
log.WriteFileDetails("existing_application", Path.Combine(installDirectory, ApplicationExeName));
integrationSnapshot = registerWindowsIntegration ? CaptureWindowsIntegration() : null;
if (integrationSnapshot != null)
log.Write("INFO", "event=integration_snapshot registry_key_existed=" + integrationSnapshot.RegistryKeyExisted
+ " registry_value_count=" + integrationSnapshot.RegistryValues.Count
+ " desktop_shortcut=" + (integrationSnapshot.DesktopShortcut != null)
+ " start_menu_shortcut=" + (integrationSnapshot.StartMenuShortcut != null)
+ " uninstaller=" + (integrationSnapshot.Uninstaller != null));
phaseCode = "SETUP-PACKAGE-VALIDATION";
phase = "Paketmanifest und SHA-256 pruefen";
write("Phase 1/6: " + phase + ".");
var files = PackageManifest.ValidateAndRead(sourceApplication, manifestPath);
RequirePayload(files, ApplicationExeName);
RequirePayload(files, ApplicationExeName + ".config");
EnsureApplicationNotRunning();
log.WriteFileDetails("package_manifest", manifestPath);
foreach (var file in files)
log.Write("INFO", "event=payload_validated relative_path=\"" + file.RelativePath + "\" size_bytes=" + file.Length + " sha256=" + file.Sha256);
EnsureApplicationNotRunning(log);
write("Phase 2/6: Update in isoliertes Staging kopieren.");
phaseCode = "SETUP-STAGING-VALIDATION";
phase = "Update in isoliertes Staging kopieren und pruefen";
write("Phase 2/6: " + phase + ".");
CopyPayload(sourceApplication, stagingDirectory, files);
var stagedExe = Path.Combine(stagingDirectory, ApplicationExeName);
if (!selfTestRunner(stagedExe)) throw new InvalidOperationException("Der WMI-freie Self-Test der Staging-Version ist fehlgeschlagen.");
RunAndValidateSelfTest(stagedExe, "staging", log);
write("Staging-Self-Test erfolgreich.");
write("Phase 3/6: Vorhandene Version sichern und Staging atomar aktivieren.");
phaseCode = "SETUP-ACTIVATION";
phase = "Vorhandene Version sichern und Staging atomar aktivieren";
write("Phase 3/6: " + phase + ". Ab hier beginnt die Systemaenderung.");
if (hadExistingInstallation)
{
log.Write("INFO", "event=directory_move role=backup source=\"" + installDirectory + "\" target=\"" + backupDirectory + "\"");
Directory.Move(installDirectory, backupDirectory);
backupCreated = true;
log.Write("INFO", "event=directory_move_complete role=backup");
}
log.Write("INFO", "event=directory_move role=activate source=\"" + stagingDirectory + "\" target=\"" + installDirectory + "\"");
Directory.Move(stagingDirectory, installDirectory);
activated = true;
log.Write("INFO", "event=directory_move_complete role=activate");
write("Phase 4/6: Aktivierte Version erneut pruefen.");
phaseCode = "SETUP-ACTIVATED-SELFTEST";
phase = "Aktivierte Version erneut pruefen";
write("Phase 4/6: " + phase + ".");
var targetExe = Path.Combine(installDirectory, ApplicationExeName);
if (!selfTestRunner(targetExe)) throw new InvalidOperationException("Der Self-Test der aktivierten Version ist fehlgeschlagen.");
RunAndValidateSelfTest(targetExe, "activated", log);
File.WriteAllText(
Path.Combine(installDirectory, "install-state.txt"),
"ProductVersion=" + ProductVersion + Environment.NewLine
@@ -109,88 +162,186 @@ namespace BizTalkPlatformManagementTool.Setup
+ "ManifestSha256=" + PackageManifest.Sha256(manifestPath) + Environment.NewLine,
new UTF8Encoding(false));
write("Phase 5/6: Windows-Integration registrieren.");
phaseCode = "SETUP-WINDOWS-INTEGRATION";
phase = "Windows-Integration registrieren und verifizieren";
write("Phase 5/6: " + phase + ".");
if (registerWindowsIntegration)
{
integrationMutationStarted = true;
RegisterWindowsIntegration(targetExe, createDesktopShortcut);
ValidateWindowsIntegration(targetExe, createDesktopShortcut, log);
}
write("Phase 6/6: Backup bereinigen.");
TryDeleteDirectory(backupDirectory, write);
phaseCode = "SETUP-CLEANUP";
phase = "Backup bereinigen";
write("Phase 6/6: " + phase + ".");
var backupCleanupSucceeded = TryDeleteDirectory(backupDirectory, write);
log.Write(
backupCleanupSucceeded ? "INFO" : "WARN",
"event=setup_completed result=" + (backupCleanupSucceeded ? "success" : "success_with_backup_cleanup_warning")
+ " backup_directory_preserved=" + Directory.Exists(backupDirectory)
+ " install_directory=\"" + installDirectory + "\"");
write("Installation/Update erfolgreich abgeschlossen: " + installDirectory);
write("Technische Installationsabnahme erfolgreich. BizTalk-WMI wurde bewusst nicht als Installerkriterium verwendet; bitte anschliessend in der Anwendung 'Diagnose' und einen Dry-run ausfuehren.");
}
catch (Exception ex)
{
log.Write("ERROR", ex.ToString());
log.WriteException(phaseCode, phase, ex);
var rollbackErrors = new List<string>();
try
var mutationStarted = backupCreated || activated || integrationMutationStarted;
var fileRollbackSucceeded = true;
if (!mutationStarted)
{
if (activated && Directory.Exists(installDirectory)) Directory.Delete(installDirectory, true);
if (backupCreated && Directory.Exists(backupDirectory)) Directory.Move(backupDirectory, installDirectory);
write(backupCreated ? "Rollback: vorherige Programmversion wiederhergestellt." : "Rollback: unvollstaendige Neuinstallation entfernt.");
write("Kein Rollback erforderlich: Der Fehler trat vor der ersten Systemaenderung auf.");
log.Write("ERROR", "event=rollback_summary result=not_required mutation_started=false");
}
catch (Exception rollbackException)
else
{
rollbackErrors.Add(rollbackException.Message);
log.Write("ERROR", "Rollback files: " + rollbackException);
}
try
{
if (registerWindowsIntegration)
log.Write("WARN", "event=rollback_started activated=" + activated + " backup_created=" + backupCreated + " integration_mutation_started=" + integrationMutationStarted);
try
{
RestoreWindowsIntegration(integrationSnapshot);
if (activated && Directory.Exists(installDirectory))
{
log.Write("WARN", "event=rollback_step step=remove_activated_directory path=\"" + installDirectory + "\"");
Directory.Delete(installDirectory, true);
}
if (backupCreated && Directory.Exists(backupDirectory))
{
log.Write("WARN", "event=rollback_step step=restore_backup source=\"" + backupDirectory + "\" target=\"" + installDirectory + "\"");
Directory.Move(backupDirectory, installDirectory);
}
write(backupCreated ? "Rollback: vorherige Programmversion wiederhergestellt." : "Rollback: unvollstaendige Neuinstallation entfernt.");
log.Write("WARN", "event=rollback_step_complete step=files result=success");
}
catch (Exception rollbackException)
{
fileRollbackSucceeded = false;
rollbackErrors.Add("Programmdateien: " + rollbackException.Message);
log.WriteException("SETUP-ROLLBACK-FILES", "Rollback Programmdateien", rollbackException);
}
try
{
if (registerWindowsIntegration && integrationSnapshot != null)
{
log.Write("WARN", "event=rollback_step step=restore_windows_integration");
RestoreWindowsIntegration(integrationSnapshot);
log.Write("WARN", "event=rollback_step_complete step=windows_integration result=success");
}
}
catch (Exception rollbackException)
{
rollbackErrors.Add("Windows-Integration: " + rollbackException.Message);
log.WriteException("SETUP-ROLLBACK-INTEGRATION", "Rollback Windows-Integration", rollbackException);
}
}
catch (Exception rollbackException)
{
rollbackErrors.Add(rollbackException.Message);
log.Write("ERROR", "Rollback registration: " + rollbackException);
}
TryDeleteDirectory(stagingDirectory, write);
TryDeleteDirectory(backupDirectory, write);
var suffix = rollbackErrors.Count == 0 ? " Rollback erfolgreich." : " Rollback-Fehler: " + string.Join(" | ", rollbackErrors);
throw new InvalidOperationException("Installation/Update fehlgeschlagen." + suffix + " Ursache: " + ex.Message, ex);
if (fileRollbackSucceeded && !Directory.Exists(installDirectory) && Directory.Exists(backupDirectory))
{
fileRollbackSucceeded = false;
rollbackErrors.Add("Programmdateien: Backup blieb ohne aktives Installationsverzeichnis bestehen: " + backupDirectory);
log.Write("ERROR", "event=rollback_invariant_failed active_directory=false backup_directory=true backup_path=\"" + backupDirectory + "\"");
}
var rollback = !mutationStarted
? "Kein Rollback erforderlich."
: rollbackErrors.Count == 0
? "Rollback erfolgreich."
: "Rollback unvollstaendig: " + string.Join(" | ", rollbackErrors);
log.Write("ERROR", "event=rollback_summary result=\"" + rollback + "\" backup_preserved=" + Directory.Exists(backupDirectory));
throw new InvalidOperationException(
"Installation/Update fehlgeschlagen. Fehlercode=" + phaseCode + ". Phase='" + phase + "'. "
+ rollback + " Ursache: " + ex.Message + " Diagnoselog: " + (log.FilePath.Length == 0 ? "nicht verfuegbar" : log.FilePath),
ex);
}
}
/// <summary>Removes the active program directory and registered Windows integration.</summary>
public void Uninstall(Action<string> report)
{
report = report ?? delegate { };
Directory.CreateDirectory(dataDirectory);
var uiReport = report ?? delegate { };
var log = SetupOperationLog.Create(dataDirectory, "uninstall");
Action<string> write = message => { log.Write("INFO", message); report(message); };
var integrationSnapshot = registerWindowsIntegration ? CaptureWindowsIntegration() : null;
Action<string> write = message => ReportSafely(log, uiReport, "INFO", message);
if (!string.IsNullOrEmpty(log.CreationError))
write((log.IsFallback ? "WARNUNG: Primaeres Diagnoselog nicht verfuegbar; Temp-Fallback wird verwendet: " : "WARNUNG: Diagnoselog konnte nicht angelegt werden: ") + log.CreationError);
if (log.FilePath.Length != 0)
{
lastInstallerLogDirectory = Path.GetDirectoryName(log.FilePath);
write("Diagnoselog: " + log.FilePath);
}
WindowsIntegrationSnapshot integrationSnapshot = null;
var removalDirectory = installDirectory + ".removed." + Guid.NewGuid().ToString("N");
var filesMoved = false;
var phaseCode = "UNINSTALL-INITIALIZATION";
var phase = "Initialisierung";
try
{
EnsureApplicationNotRunning();
Directory.CreateDirectory(dataDirectory);
log.Write("INFO", "event=uninstall_context install_directory=\"" + installDirectory + "\" removal_directory=\"" + removalDirectory + "\" data_directory=\"" + dataDirectory + "\"");
log.WriteFileDetails("installed_application", Path.Combine(installDirectory, ApplicationExeName));
integrationSnapshot = registerWindowsIntegration ? CaptureWindowsIntegration() : null;
phaseCode = "UNINSTALL-RUNNING-APPLICATION";
phase = "Laufende Anwendung ausschliessen";
write("Phase 1/3: " + phase + ".");
EnsureApplicationNotRunning(log);
phaseCode = "UNINSTALL-QUARANTINE";
phase = "Programmverzeichnis deaktivieren";
write("Phase 2/3: " + phase + ".");
if (Directory.Exists(installDirectory))
{
log.Write("INFO", "event=directory_move role=uninstall_quarantine source=\"" + installDirectory + "\" target=\"" + removalDirectory + "\"");
Directory.Move(installDirectory, removalDirectory);
filesMoved = true;
log.Write("INFO", "event=directory_move_complete role=uninstall_quarantine");
}
if (registerWindowsIntegration) RemoveWindowsIntegration();
TryDeleteDirectory(removalDirectory, write);
phaseCode = "UNINSTALL-WINDOWS-INTEGRATION";
phase = "Windows-Integration entfernen";
write("Phase 3/3: " + phase + ".");
if (registerWindowsIntegration) RemoveWindowsIntegration(log);
var cleanupSucceeded = TryDeleteDirectory(removalDirectory, write);
log.Write("INFO", "event=uninstall_completed result=" + (cleanupSucceeded ? "success" : "success_with_quarantine_cleanup_warning")
+ " removal_directory_preserved=" + Directory.Exists(removalDirectory));
write("Deinstallation erfolgreich. Installer-Logs bleiben erhalten: " + Path.Combine(dataDirectory, "InstallerLogs"));
}
catch (Exception ex)
{
log.Write("ERROR", ex.ToString());
log.WriteException(phaseCode, phase, ex);
var rollbackErrors = new List<string>();
if (filesMoved && !Directory.Exists(installDirectory) && Directory.Exists(removalDirectory))
{
try { Directory.Move(removalDirectory, installDirectory); }
catch (Exception rollbackException) { log.Write("ERROR", "Uninstall rollback files: " + rollbackException); }
try
{
Directory.Move(removalDirectory, installDirectory);
log.Write("WARN", "event=uninstall_rollback_step step=restore_program_directory result=success");
}
catch (Exception rollbackException)
{
rollbackErrors.Add("Programmverzeichnis: " + rollbackException.Message);
log.WriteException("UNINSTALL-ROLLBACK-FILES", "Deinstallationsrollback Programmverzeichnis", rollbackException);
}
}
if (registerWindowsIntegration)
if (registerWindowsIntegration && integrationSnapshot != null)
{
try { RestoreWindowsIntegration(integrationSnapshot); }
catch (Exception rollbackException) { log.Write("ERROR", "Uninstall rollback registration: " + rollbackException); }
try
{
RestoreWindowsIntegration(integrationSnapshot);
log.Write("WARN", "event=uninstall_rollback_step step=restore_windows_integration result=success");
}
catch (Exception rollbackException)
{
rollbackErrors.Add("Windows-Integration: " + rollbackException.Message);
log.WriteException("UNINSTALL-ROLLBACK-INTEGRATION", "Deinstallationsrollback Windows-Integration", rollbackException);
}
}
throw;
var rollback = rollbackErrors.Count == 0 ? "Rollback erfolgreich." : "Rollback unvollstaendig: " + string.Join(" | ", rollbackErrors);
log.Write("ERROR", "event=uninstall_rollback_summary result=\"" + rollback + "\"");
throw new InvalidOperationException(
"Deinstallation fehlgeschlagen. Fehlercode=" + phaseCode + ". Phase='" + phase + "'. " + rollback
+ " Ursache: " + ex.Message + " Diagnoselog: " + (log.FilePath.Length == 0 ? "nicht verfuegbar" : log.FilePath),
ex);
}
}
@@ -222,13 +373,19 @@ namespace BizTalkPlatformManagementTool.Setup
}
}
private void RemoveWindowsIntegration()
private void RemoveWindowsIntegration(SetupOperationLog log)
{
TryDeleteFile(DesktopShortcutPath);
DeleteFileIfExists(DesktopShortcutPath);
var programsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName);
TryDeleteFile(Path.Combine(programsDirectory, ProductName + ".lnk"));
DeleteFileIfExists(Path.Combine(programsDirectory, ProductName + ".lnk"));
if (Directory.Exists(programsDirectory) && Directory.GetFileSystemEntries(programsDirectory).Length == 0) Directory.Delete(programsDirectory);
Registry.LocalMachine.DeleteSubKeyTree(UninstallKeyPath, false);
using (var remainingKey = Registry.LocalMachine.OpenSubKey(UninstallKeyPath, false))
{
if (File.Exists(DesktopShortcutPath) || File.Exists(StartMenuShortcutPath) || remainingKey != null)
throw new InvalidOperationException("Windows integration removal verification failed.");
}
log.Write("INFO", "event=windows_integration_removed registry_key=\"HKLM\\" + UninstallKeyPath + "\"");
}
private static void CreateShortcut(string shortcutPath, string targetPath, string workingDirectory, string description)
@@ -309,22 +466,47 @@ namespace BizTalkPlatformManagementTool.Setup
{
if (content == null)
{
TryDeleteFile(path);
DeleteFileIfExists(path);
return;
}
if (File.Exists(path) && File.ReadAllBytes(path).SequenceEqual(content)) return;
Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllBytes(path, content);
}
private void EnsureApplicationNotRunning()
private void EnsureApplicationNotRunning(SetupOperationLog log)
{
var target = Path.Combine(installDirectory, ApplicationExeName);
if (!File.Exists(target)) return;
if (!File.Exists(target))
{
log.Write("INFO", "event=running_application_check target_exists=false target=\"" + target + "\"");
return;
}
var candidates = 0;
foreach (var process in Process.GetProcessesByName(Path.GetFileNameWithoutExtension(ApplicationExeName)))
{
try
{
if (string.Equals(Path.GetFullPath(process.MainModule.FileName), Path.GetFullPath(target), StringComparison.OrdinalIgnoreCase))
candidates++;
var processId = process.Id;
log.Write("INFO", "event=running_application_candidate_detected pid=" + processId.ToString(CultureInfo.InvariantCulture));
string processPath;
try
{
processPath = process.MainModule.FileName;
}
catch (Exception ex)
{
log.WriteException("SETUP-RUNNING-APPLICATION-INSPECTION", "Prozesspfad fuer PID " + processId + " pruefen", ex);
throw new InvalidOperationException(
"Ein gleichnamiger Prozess (PID " + processId + ") konnte nicht sicher geprueft werden. Beenden Sie ihn oder pruefen Sie Berechtigungen und versuchen Sie es erneut.",
ex);
}
var matches = string.Equals(Path.GetFullPath(processPath), Path.GetFullPath(target), StringComparison.OrdinalIgnoreCase);
log.Write("INFO", "event=running_application_candidate pid=" + processId.ToString(CultureInfo.InvariantCulture)
+ " session_id=" + process.SessionId.ToString(CultureInfo.InvariantCulture)
+ " path=\"" + processPath + "\" matches_target=" + matches);
if (matches)
throw new InvalidOperationException(ProductName + " is still running. Close it before installation or removal.");
}
finally
@@ -332,28 +514,170 @@ namespace BizTalkPlatformManagementTool.Setup
process.Dispose();
}
}
log.Write("INFO", "event=running_application_check target_exists=true candidate_count=" + candidates.ToString(CultureInfo.InvariantCulture) + " result=not_running");
}
private static bool RunApplicationSelfTest(string executable)
private void RunAndValidateSelfTest(string executable, string label, SetupOperationLog log)
{
log.WriteFileDetails(label + "_self_test_executable", executable);
var stopwatch = Stopwatch.StartNew();
if (selfTestRunner != null)
{
try
{
var successful = selfTestRunner(executable);
stopwatch.Stop();
log.Write(
successful ? "INFO" : "ERROR",
"event=self_test_completed label=" + label
+ " runner=injected elapsed_ms=" + stopwatch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture)
+ " result=" + (successful ? "success" : "failure"));
if (!successful) throw new InvalidOperationException("Der WMI-freie Self-Test '" + label + "' ist fehlgeschlagen.");
return;
}
catch (Exception ex)
{
stopwatch.Stop();
log.WriteException("SETUP-SELFTEST-INJECTED", "Self-Test " + label, ex);
throw;
}
}
var startInfo = new ProcessStartInfo(executable, "--self-test")
{
WorkingDirectory = Path.GetDirectoryName(executable),
UseShellExecute = false,
CreateNoWindow = true
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
using (var process = Process.Start(startInfo))
{
if (process == null) return false;
if (process == null) throw new InvalidOperationException("Self-Test '" + label + "' konnte nicht gestartet werden.");
var outputRead = process.StandardOutput.ReadToEndAsync();
var errorRead = process.StandardError.ReadToEndAsync();
if (!process.WaitForExit(60000))
{
try { process.Kill(); } catch { }
return false;
var killResult = "sent";
try { process.Kill(); }
catch (Exception killException)
{
killResult = "failed";
log.WriteException("SETUP-SELFTEST-KILL", "Self-Test " + label + " nach Timeout beenden", killException);
}
var exitedAfterKill = false;
try { exitedAfterKill = process.WaitForExit(5000); } catch { }
var streamsCompleted = false;
try
{
streamsCompleted = System.Threading.Tasks.Task.WaitAll(new System.Threading.Tasks.Task[] { outputRead, errorRead }, 5000);
}
catch (Exception captureException)
{
log.WriteException("SETUP-SELFTEST-CAPTURE", "Self-Test " + label + " Ausgabe nach Timeout erfassen", captureException);
}
stopwatch.Stop();
log.Write(
"ERROR",
"event=self_test_timeout label=" + label
+ " timeout_ms=60000 elapsed_ms=" + stopwatch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture)
+ " kill_result=" + killResult
+ " exited_after_kill=" + exitedAfterKill
+ " streams_completed=" + streamsCompleted
+ " stdout=\"" + (outputRead.Status == System.Threading.Tasks.TaskStatus.RanToCompletion ? outputRead.Result : "[capture incomplete]")
+ "\" stderr=\"" + (errorRead.Status == System.Threading.Tasks.TaskStatus.RanToCompletion ? errorRead.Result : "[capture incomplete]") + "\"");
throw new TimeoutException("Self-Test '" + label + "' hat das Zeitlimit von 60 Sekunden ueberschritten.");
}
System.Threading.Tasks.Task.WaitAll(outputRead, errorRead);
stopwatch.Stop();
var output = outputRead.Result;
var error = errorRead.Result;
log.Write(
process.ExitCode == 0 ? "INFO" : "ERROR",
"event=self_test_completed label=" + label
+ " runner=process elapsed_ms=" + stopwatch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture)
+ " exit_code=" + process.ExitCode.ToString(CultureInfo.InvariantCulture)
+ " exit_code_hex=0x" + process.ExitCode.ToString("X8", CultureInfo.InvariantCulture)
+ " stdout=\"" + output + "\" stderr=\"" + error + "\"");
if (process.ExitCode != 0 || output.IndexOf("SELF_TEST_OK", StringComparison.Ordinal) < 0)
{
throw new InvalidOperationException(
"Self-Test '" + label + "' fehlgeschlagen. Exitcode=" + process.ExitCode
+ " (0x" + process.ExitCode.ToString("X8", CultureInfo.InvariantCulture) + ")"
+ ", Stdout=" + CompactProcessText(output)
+ ", Stderr=" + CompactProcessText(error) + ".");
}
return process.ExitCode == 0;
}
}
private void ValidateWindowsIntegration(string targetExe, bool desktopRequested, SetupOperationLog log)
{
if (!File.Exists(StartMenuShortcutPath)) throw new FileNotFoundException("Start menu shortcut was not created.", StartMenuShortcutPath);
if (desktopRequested != File.Exists(DesktopShortcutPath))
throw new InvalidOperationException("Desktop shortcut state does not match the setup selection. Requested=" + desktopRequested + ".");
if (!File.Exists(UninstallerPath)) throw new FileNotFoundException("Uninstaller was not installed.", UninstallerPath);
using (var key = Registry.LocalMachine.OpenSubKey(UninstallKeyPath, false))
{
if (key == null) throw new InvalidOperationException("Windows uninstall registry key is missing after registration.");
RequireRegistryValue(key, "DisplayName", ProductName);
RequireRegistryValue(key, "DisplayVersion", ProductVersion);
RequireRegistryValue(key, "InstallLocation", installDirectory);
RequireRegistryValue(key, "DisplayIcon", targetExe);
RequireRegistryValue(key, "UninstallString", "\"" + UninstallerPath + "\" --uninstall");
}
log.WriteFileDetails("registered_uninstaller", UninstallerPath);
log.WriteFileDetails("start_menu_shortcut", StartMenuShortcutPath);
if (desktopRequested) log.WriteFileDetails("desktop_shortcut", DesktopShortcutPath);
log.Write("INFO", "event=windows_integration_validated registry_key=\"HKLM\\" + UninstallKeyPath + "\" desktop_shortcut=" + desktopRequested);
}
private static void RequireRegistryValue(RegistryKey key, string name, string expected)
{
var actual = Convert.ToString(key.GetValue(name, null, RegistryValueOptions.DoNotExpandEnvironmentNames), CultureInfo.InvariantCulture);
if (!string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Uninstall registry value '" + name + "' is invalid. Expected='" + expected + "', actual='" + actual + "'.");
}
private static void ReportSafely(SetupOperationLog log, Action<string> report, string level, string message)
{
log.Write(level, message);
try
{
report(message);
}
catch (Exception ex)
{
log.Write("WARN", "event=ui_report_failed " + SetupOperationLog.FormatException(ex));
}
}
private static void LogDriveSpace(SetupOperationLog log, string path)
{
try
{
var root = Path.GetPathRoot(Path.GetFullPath(path));
var drive = new DriveInfo(root);
log.Write(
"INFO",
"event=drive_space root=\"" + root + "\" format=\"" + drive.DriveFormat + "\""
+ " available_bytes=" + drive.AvailableFreeSpace.ToString(CultureInfo.InvariantCulture)
+ " total_bytes=" + drive.TotalSize.ToString(CultureInfo.InvariantCulture));
}
catch (Exception ex)
{
log.Write("WARN", "event=drive_space_failed path=\"" + path + "\" " + SetupOperationLog.FormatException(ex));
}
}
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) + "...[truncated]";
}
private static void CopyPayload(string sourceRoot, string targetRoot, IEnumerable<PackageFile> files)
{
Directory.CreateDirectory(targetRoot);
@@ -374,15 +698,23 @@ namespace BizTalkPlatformManagementTool.Setup
throw new InvalidDataException("Required payload file is missing from the manifest: " + relativePath);
}
private static void TryDeleteDirectory(string path, Action<string> report)
private static bool TryDeleteDirectory(string path, Action<string> report)
{
try { if (Directory.Exists(path)) Directory.Delete(path, true); }
catch (Exception ex) { report("WARNUNG: Verzeichnis konnte nicht bereinigt werden: " + path + " - " + ex.Message); }
try
{
if (Directory.Exists(path)) Directory.Delete(path, true);
return !Directory.Exists(path);
}
catch (Exception ex)
{
report("WARNUNG: Verzeichnis konnte nicht bereinigt werden: " + path + " - " + ex.Message);
return false;
}
}
private static void TryDeleteFile(string path)
private static void DeleteFileIfExists(string path)
{
try { if (File.Exists(path)) File.Delete(path); } catch { }
if (File.Exists(path)) File.Delete(path);
}
private static string DesktopShortcutPath
@@ -1,5 +1,7 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
@@ -41,7 +43,7 @@ namespace BizTalkPlatformManagementTool.Setup
{
AutoSize = true,
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
Text = "BizTalk Platform Management Tool 2.1.0"
Text = "BizTalk Platform Management Tool 2.1.1"
});
root.Controls.Add(new Label
{
@@ -66,6 +68,8 @@ namespace BizTalkPlatformManagementTool.Setup
var buttons = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft };
var closeButton = new Button { Text = "Schliessen", AutoSize = true };
closeButton.Click += (sender, args) => Close();
var logsButton = new Button { Text = "Diagnoselogs oeffnen", AutoSize = true };
logsButton.Click += OpenLogs;
installButton.Text = engine.IsInstalled ? "Update installieren" : "Installieren";
installButton.AutoSize = true;
installButton.Enabled = engine.HasInstallPayload && !uninstallMode;
@@ -75,6 +79,7 @@ namespace BizTalkPlatformManagementTool.Setup
uninstallButton.Enabled = engine.IsInstalled;
uninstallButton.Click += (sender, args) => Run(true);
buttons.Controls.Add(closeButton);
buttons.Controls.Add(logsButton);
buttons.Controls.Add(uninstallButton);
buttons.Controls.Add(installButton);
root.Controls.Add(buttons);
@@ -84,6 +89,19 @@ namespace BizTalkPlatformManagementTool.Setup
else if (!engine.HasInstallPayload) Append("Kein Installationspayload neben Setup.exe gefunden. Dieser Aufruf erlaubt nur die Deinstallation.");
}
private void OpenLogs(object sender, EventArgs e)
{
try
{
Directory.CreateDirectory(engine.InstallerLogDirectory);
Process.Start("explorer.exe", engine.InstallerLogDirectory);
}
catch (Exception ex)
{
MessageBox.Show(this, "Diagnoseordner konnte nicht geoeffnet werden: " + ex.Message + "\n\n" + engine.InstallerLogDirectory, "Diagnoselogs", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
private void Run(bool uninstall)
{
if (uninstall && MessageBox.Show(this, "BizTalk Platform Management Tool wirklich deinstallieren?", "Deinstallation bestaetigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes)
@@ -8,6 +8,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
[assembly: ComVisible(false)]
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
[assembly: AssemblyVersion("2.1.0.0")]
[assembly: AssemblyFileVersion("2.1.0.0")]
[assembly: AssemblyVersion("2.1.1.0")]
[assembly: AssemblyFileVersion("2.1.1.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
@@ -1,10 +1,16 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Security.Principal;
using System.Text;
namespace BizTalkPlatformManagementTool.Setup
{
/// <summary>
/// Writes a durable, single-line diagnostic trace for one setup operation.
/// No credential or other secret is accepted by this component.
/// </summary>
internal sealed class SetupOperationLog
{
private readonly object sync = new object();
@@ -15,33 +21,182 @@ namespace BizTalkPlatformManagementTool.Setup
}
public string FilePath { get; private set; }
public string CreationError { get; private set; }
public bool IsFallback { get; private set; }
public static SetupOperationLog Create(string dataDirectory, string operation)
{
try
{
var directory = Path.Combine(dataDirectory, "InstallerLogs");
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, "setup-" + DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) + "-" + operation + ".log");
return new SetupOperationLog(path);
return CreateInDirectory(Path.Combine(dataDirectory, "InstallerLogs"), operation);
}
catch
catch (Exception primaryException)
{
return new SetupOperationLog(string.Empty);
try
{
var fallback = CreateInDirectory(
Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool", "InstallerLogs"),
operation);
fallback.CreationError = FormatException(primaryException);
fallback.IsFallback = true;
fallback.Write(
"WARN",
"event=primary_log_creation_failed requested_data_directory=\"" + dataDirectory + "\" "
+ fallback.CreationError);
return fallback;
}
catch (Exception fallbackException)
{
return new SetupOperationLog(string.Empty)
{
CreationError = "primary={" + FormatException(primaryException) + "} fallback={" + FormatException(fallbackException) + "}"
};
}
}
}
private static SetupOperationLog CreateInDirectory(string directory, string operation)
{
Directory.CreateDirectory(directory);
CleanupOldLogs(directory);
var path = Path.Combine(
directory,
"setup-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmssfff", CultureInfo.InvariantCulture)
+ "-" + Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture)
+ "-" + operation + ".log");
using (new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read)) { }
var log = new SetupOperationLog(path);
log.Write(
"INFO",
"event=setup_started operation=" + operation
+ " setup_version=" + typeof(SetupOperationLog).Assembly.GetName().Version
+ " os=\"" + Environment.OSVersion.VersionString + "\""
+ " process_bitness=" + (Environment.Is64BitProcess ? "64" : "32")
+ " os_bitness=" + (Environment.Is64BitOperatingSystem ? "64" : "32")
+ " clr=" + Environment.Version
+ " machine=\"" + Environment.MachineName + "\""
+ " identity=\"" + CurrentIdentity() + "\""
+ " elevated=" + IsElevated()
+ " base_directory=\"" + AppDomain.CurrentDomain.BaseDirectory + "\"");
return log;
}
public void Write(string level, string message)
{
if (string.IsNullOrEmpty(FilePath)) return;
var line = "[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) + "][" + level + "] " + (message ?? string.Empty) + Environment.NewLine;
var line = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)
+ " level=" + (level ?? "INFO").ToUpperInvariant()
+ " " + SingleLine(message)
+ Environment.NewLine;
try
{
lock (sync) File.AppendAllText(FilePath, line, new UTF8Encoding(false));
lock (sync)
{
File.AppendAllText(FilePath, line, new UTF8Encoding(false));
}
}
catch
{
// Setup logging must not hide the actual installation result.
// Diagnostic logging must never replace the actual setup outcome.
}
}
public void WriteException(string errorCode, string phase, Exception exception)
{
Write("ERROR", "event=exception error_code=" + errorCode + " phase=\"" + phase + "\" " + FormatException(exception));
}
public void WriteFileDetails(string label, string path)
{
try
{
if (!File.Exists(path))
{
Write("INFO", "event=file_details label=\"" + label + "\" path=\"" + path + "\" exists=false");
return;
}
var info = new FileInfo(path);
var version = FileVersionInfo.GetVersionInfo(path).FileVersion ?? "(unknown)";
Write(
"INFO",
"event=file_details label=\"" + label + "\" path=\"" + path + "\" exists=true"
+ " size_bytes=" + info.Length.ToString(CultureInfo.InvariantCulture)
+ " modified_utc=" + info.LastWriteTimeUtc.ToString("o", CultureInfo.InvariantCulture)
+ " file_version=\"" + version + "\""
+ " sha256=" + PackageManifest.Sha256(path));
}
catch (Exception ex)
{
Write("WARN", "event=file_details_failed label=\"" + label + "\" path=\"" + path + "\" " + FormatException(ex));
}
}
internal static string FormatException(Exception exception)
{
var result = new StringBuilder();
var current = exception;
var depth = 0;
while (current != null && depth < 12)
{
if (depth > 0) result.Append(" | inner[").Append(depth).Append("] ");
result.Append("exception_type=").Append(current.GetType().FullName)
.Append(" hresult=0x").Append(current.HResult.ToString("X8", CultureInfo.InvariantCulture))
.Append(" message=\"").Append(current.Message).Append('"');
if (!string.IsNullOrWhiteSpace(current.StackTrace)) result.Append(" stack=\"").Append(current.StackTrace).Append('"');
current = current.InnerException;
depth++;
}
return SingleLine(result.ToString());
}
private static string CurrentIdentity()
{
try
{
using (var identity = WindowsIdentity.GetCurrent())
{
return identity == null ? "(unknown)" : identity.Name;
}
}
catch
{
return "(unknown)";
}
}
private static string IsElevated()
{
try
{
using (var identity = WindowsIdentity.GetCurrent())
{
return identity != null && new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator) ? "true" : "false";
}
}
catch
{
return "unknown";
}
}
private static string SingleLine(string value)
{
return (value ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t");
}
private static void CleanupOldLogs(string directory)
{
try
{
var cutoff = DateTime.UtcNow.AddDays(-90);
foreach (var file in Directory.GetFiles(directory, "setup-*.log"))
{
if (File.GetLastWriteTimeUtc(file) < cutoff) File.Delete(file);
}
}
catch
{
// Retention cleanup is best-effort.
}
}
}
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="2.1.0.0" name="BizTalkPlatformManagementTool.Setup" />
<assemblyIdentity version="2.1.1.0" name="BizTalkPlatformManagementTool.Setup" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
</trustInfo>
@@ -8,5 +8,5 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: ComVisible(false)]
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
[assembly: AssemblyVersion("2.1.0.0")]
[assembly: AssemblyFileVersion("2.1.0.0")]
[assembly: AssemblyVersion("2.1.1.0")]
[assembly: AssemblyFileVersion("2.1.1.0")]
@@ -15,7 +15,7 @@ namespace BizTalkPlatformManagementTool.Services
/// <summary>
/// Current tool version written into generated snapshots.
/// </summary>
public const string Version = "2.1.0-net461";
public const string Version = "2.1.1-net461";
/// <summary>
/// Fallback application name used when WMI does not expose an application property.
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="2.1.0.0" name="BizTalkPlatformManagementTool" />
<assemblyIdentity version="2.1.1.0" name="BizTalkPlatformManagementTool" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
@@ -25,6 +26,9 @@ namespace BizTalkPlatformManagementTool.Tests
Run("InstallerDoesNotMutateOnStagingFailure", InstallerDoesNotMutateOnStagingFailure);
Run("InstallerRollsBackFailedActivatedSelfTest", InstallerRollsBackFailedActivatedSelfTest);
Run("InstallerUninstallRemovesProgramDirectory", InstallerUninstallRemovesProgramDirectory);
Run("InstallerDiagnosticLogContainsContextAndExceptionChain", InstallerDiagnosticLogContainsContextAndExceptionChain);
Run("InstallerDiagnosticLogFallsBackToTemp", InstallerDiagnosticLogFallsBackToTemp);
Run("InstallerSurvivesUiReportFailure", InstallerSurvivesUiReportFailure);
Console.WriteLine(failures == 0 ? "ALL TESTS PASSED" : failures + " TEST(S) FAILED");
return failures == 0 ? 0 : 1;
}
@@ -153,10 +157,18 @@ namespace BizTalkPlatformManagementTool.Tests
var install = Path.Combine(directory, "install"); Directory.CreateDirectory(install);
File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName), "old");
File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName + ".config"), "old-config");
var engine = new InstallerEngine(package, install, Path.Combine(directory, "data"), false, path => false);
Expect<InvalidOperationException>(() => engine.Install(false, null));
var data = Path.Combine(directory, "data");
var messages = new List<string>();
var engine = new InstallerEngine(package, install, data, false, path => false);
var exception = Capture<InvalidOperationException>(() => engine.Install(false, messages.Add));
Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "old", "staging failure modified the installed payload");
Assert(!Directory.GetDirectories(directory, "install.backup.*").Any(), "backup was created before staging passed");
Assert(exception.Message.Contains("Fehlercode=SETUP-STAGING-VALIDATION"), "staging error code missing");
Assert(exception.Message.Contains("Kein Rollback erforderlich"), "pre-mutation failure reported a rollback");
Assert(messages.Any(x => x.Contains("Kein Rollback erforderlich")), "operator output did not distinguish pre-mutation failure");
var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single());
Assert(log.Contains("event=self_test_completed label=staging"), "staging self-test result missing from log");
Assert(log.Contains("event=rollback_summary result=not_required"), "no-rollback evidence missing from log");
});
}
@@ -170,10 +182,12 @@ namespace BizTalkPlatformManagementTool.Tests
File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName + ".config"), "old-config");
var calls = 0;
var engine = new InstallerEngine(package, install, Path.Combine(directory, "data"), false, path => ++calls == 1);
Expect<InvalidOperationException>(() => engine.Install(false, null));
var exception = Capture<InvalidOperationException>(() => engine.Install(false, null));
Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "old", "previous payload was not restored");
Assert(!Directory.GetDirectories(directory, "install.staging.*").Any(), "staging directory remained");
Assert(!Directory.GetDirectories(directory, "install.backup.*").Any(), "backup directory remained");
Assert(exception.Message.Contains("Fehlercode=SETUP-ACTIVATED-SELFTEST"), "activated self-test error code missing");
Assert(exception.Message.Contains("Rollback erfolgreich"), "successful rollback not reported");
});
}
@@ -191,6 +205,62 @@ namespace BizTalkPlatformManagementTool.Tests
});
}
private static void InstallerDiagnosticLogContainsContextAndExceptionChain()
{
InTemp(directory =>
{
var log = SetupOperationLog.Create(directory, "diagnostic-test");
Assert(!string.IsNullOrEmpty(log.FilePath), "diagnostic log was not created");
Exception failure;
try { throw new InvalidOperationException("outer", new IOException("inner")); }
catch (Exception ex) { failure = ex; }
log.WriteException("TEST-CODE", "Test phase", failure);
var content = File.ReadAllText(log.FilePath);
Assert(content.Contains("event=setup_started"), "setup context header missing");
Assert(content.Contains("process_bitness="), "process bitness missing");
Assert(content.Contains("identity="), "identity missing");
Assert(content.Contains("error_code=TEST-CODE"), "error code missing");
Assert(content.Contains("inner[1]"), "inner exception missing");
Assert(content.Contains("hresult=0x"), "HRESULT missing");
});
}
private static void InstallerSurvivesUiReportFailure()
{
InTemp(directory =>
{
var package = CreatePackage(directory, "new");
var install = Path.Combine(directory, "install");
var data = Path.Combine(directory, "data");
var engine = new InstallerEngine(package, install, data, false, path => true);
engine.Install(false, message => { throw new InvalidOperationException("UI unavailable"); });
Assert(File.Exists(Path.Combine(install, InstallerEngine.ApplicationExeName)), "UI report failure interrupted installation");
var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single());
Assert(log.Contains("event=ui_report_failed"), "UI report failure was not diagnosed");
});
}
private static void InstallerDiagnosticLogFallsBackToTemp()
{
InTemp(directory =>
{
var blockedDataDirectory = Path.Combine(directory, "blocked-data-directory");
File.WriteAllText(blockedDataDirectory, "not a directory");
var log = SetupOperationLog.Create(blockedDataDirectory, "fallback-test");
try
{
Assert(log.IsFallback, "diagnostic log did not use the Temp fallback");
Assert(!string.IsNullOrEmpty(log.CreationError), "primary logging failure was not retained");
Assert(File.Exists(log.FilePath), "fallback diagnostic log was not created");
Assert(File.ReadAllText(log.FilePath).Contains("event=primary_log_creation_failed"), "fallback reason missing from log");
}
finally
{
if (File.Exists(log.FilePath)) File.Delete(log.FilePath);
}
});
}
private static BizTalkSnapshot Snapshot(string application, string port, int state)
{
var result = new BizTalkSnapshot { ToolVersion = "test", CreatedAt = DateTimeOffset.Now.ToString("o"), Server = Environment.MachineName };
@@ -225,5 +295,12 @@ namespace BizTalkPlatformManagementTool.Tests
catch (T) { return; }
throw new InvalidOperationException("Expected exception " + typeof(T).Name);
}
private static T Capture<T>(Action action) where T : Exception
{
try { action(); }
catch (T ex) { return ex; }
throw new InvalidOperationException("Expected exception " + typeof(T).Name);
}
}
}