Harden installer update activation fallback

This commit is contained in:
2026-08-24 12:53:35 +02:00
parent 4af64d8b41
commit 3219c3f1bc
15 changed files with 196 additions and 31 deletions
+3 -2
View File
@@ -2,7 +2,7 @@
## Projekt und Sicherheitsziel
Das Repository enthält ein .NET-Framework-4.6.1-WinForms-Tool für kontrollierte BizTalk-2020-Wartungsoperationen. Änderungen müssen Dry-run, explizite Freigabe realer Aktionen, sichere Reihenfolgen und wiederherstellbare Installergrenzen erhalten. Die aktuelle Produktversion ist 2.2.2.
Das Repository enthält ein .NET-Framework-4.6.1-WinForms-Tool für kontrollierte BizTalk-2020-Wartungsoperationen. Änderungen müssen Dry-run, explizite Freigabe realer Aktionen, sichere Reihenfolgen und wiederherstellbare Installergrenzen erhalten. Die aktuelle Produktversion ist 2.2.3.
## Installerinvarianten
@@ -10,9 +10,10 @@ Das Repository enthält ein .NET-Framework-4.6.1-WinForms-Tool für kontrolliert
- Eine laufende installierte Anwendung blockiert Update und Deinstallation.
- Ein Update darf die aktive Vorversion erst nach erfolgreichem Staging verändern.
- Das Backup einer bestehenden Installation wird nur atomar per `Directory.Move` erzeugt. Bei dauerhaftem Fehler bleibt die Vorversion aktiv; es gibt keinen In-place-Kopierfallback.
- Nur eine Neuinstallation ohne bestehendes Ziel darf nach ausgeschöpften Move-Retries eine erneut SHA-256-geprüfte Kopie aktivieren.
- Nach ausgeschöpften Aktivierungs-Move-Retries darf eine erneut SHA-256-geprüfte Kopie nur in ein nicht vorhandenes Ziel geschrieben werden: bei einer Neuinstallation oder nachdem eine Update-Vorversion vollständig atomar ins Backup verschoben wurde.
- Nach jeder Aktivierungsart läuft der Self-Test erneut aus dem endgültigen Installationsziel.
- Sobald ein Zielverzeichnis teilweise angelegt sein kann, muss `activated=true` gesetzt sein, damit der Catch-Pfad es entfernt.
- Scheitert der Ziel-Self-Test nach einem Update-Kopierfallback, muss die teilweise neue Version entfernt und das atomare Backup wieder als aktives Verzeichnis eingesetzt werden.
- Windows-Integration wird erst nach bestandenem Ziel-Self-Test verändert und bei Folgefehlern aus dem Snapshot restauriert.
- Diagnose-Logging darf das eigentliche Setup-Ergebnis nie ersetzen.
+9
View File
@@ -1,6 +1,15 @@
# Changelog
## [2.2.3] - 2026-08-24
### Fixed
- A persistent Windows/EDR denial of the final staging-directory rename no longer aborts activation after an existing installation has already been atomically moved to its backup.
- The SHA-256-verified copy fallback now covers both first installation and update activation, while refusing to overwrite an existing target or an update without a completed backup.
### Added
- Regression coverage for successful update copy fallback and complete restoration of the previous version when the post-copy target self-test fails.
- PROD incident analysis and a plain-text operator handoff for the observed 2.1.2 `SETUP-ACTIVATION` failure.
## [2.2.2] - 2026-08-19
### Fixed
- A failure in the optional GUI log sink can no longer escape step-level error handling or stop later independent shutdown/restore steps.
+3 -2
View File
@@ -161,7 +161,7 @@ Die Aktivierung ist transaktional aufgebaut:
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.
Wenn ausschließlich die atomare Staging-Umbenennung einer Neuinstallation dauerhaft gesperrt bleibt, ist ein eng begrenzter Kopierfallback zulässig: Es existiert dabei keine aktive Vorversion, jede Zieldatei wird erneut anhand des Manifests per SHA-256 geprüft und der zweite Self-Test läuft weiterhin aus dem endgültigen Ziel. Updates verwenden diesen Fallback absichtlich nicht, weil die unveränderte alte Installation Vorrang vor einer nicht atomaren In-place-Aktualisierung hat. Jeder Retry und die verwendete Aktivierungsmethode stehen im Setup-Log.
Wenn ausschließlich die atomare Staging-Umbenennung dauerhaft gesperrt bleibt, ist ein eng begrenzter Kopierfallback zulässig: Bei einer Neuinstallation muss das Ziel fehlen; bei einem Update muss die vollständige Vorversion bereits atomar ins Backup verschoben worden sein und das aktive Ziel fehlen. Jede Zieldatei wird erneut anhand des Manifests per SHA-256 geprüft, und der zweite Self-Test läuft weiterhin aus dem endgültigen Ziel. Der Fallback ist nie ein In-place-Update. Ein Teilfehler entfernt die Zielkopie und stellt beim Update das Backup wieder her. Jeder Retry, der Geltungsbereich und die verwendete Aktivierungsmethode stehen im Setup-Log.
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.
@@ -182,7 +182,8 @@ Das äußere ZIP erhält zusätzlich eine SHA-256-Datei und eine Certutil-kompat
- Staging-Fehler ohne Mutation einer bestehenden Installation.
- Erfolgreiche Staging-Aktivierung.
- Erholung nach einer vorübergehenden Rename-Sperre mit korrektem Backoff und Diagnoseereignissen.
- Verifizierter Kopierfallback einer dauerhaft move-gesperrten Neuinstallation.
- Verifizierter Kopierfallback einer dauerhaft move-gesperrten Neuinstallation und eines bereits atomar gesicherten Updates.
- Vollständiger Update-Rollback, wenn der Ziel-Self-Test nach dem Kopierfallback fehlschlägt.
- Begrenzter Abbruch eines dauerhaft move-gesperrten Updates ohne Veränderung der Vorversion.
- Wiederherstellung der Vorversion, wenn der Self-Test nach Aktivierung fehlschlägt.
- Deinstallation durch atomare Umbenennung des Programmverzeichnisses vor der bestmöglichen Bereinigung.
+5 -3
View File
@@ -55,7 +55,9 @@ 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.
Bei einer Neuinstallation ohne bestehendes Ziel darf der Installer nach vollständig ausgeschöpften Move-Versuchen auf eine verifizierte Kopie zurückfallen. Dabei wird jede Datei aus dem bereits geprüften Staging nochmals gegen ihren Manifest-SHA-256 geprüft; anschließend läuft auch der zweite Self-Test aus dem endgültigen Ziel. Dieser Fallback gilt ausdrücklich nicht für Updates: Kann dort die Vorversion nicht atomar ins Backup verschoben werden, bricht das Update unverändert ab.
Kann ausschließlich die Umbenennung des geprüften Staging-Verzeichnisses dauerhaft nicht ausgeführt werden, darf der Installer nach vollständig ausgeschöpften Move-Versuchen auf eine verifizierte Kopie zurückfallen. Dabei wird jede Datei aus dem Staging nochmals gegen ihren Manifest-SHA-256 geprüft; anschließend läuft auch der zweite Self-Test aus dem endgültigen Ziel. Bei einer Neuinstallation muss das Ziel noch fehlen. Bei einem Update ist der Fallback erst zulässig, nachdem die vollständige Vorversion atomar ins Backup verschoben wurde und das Ziel ebenfalls nicht mehr existiert. Er ist damit kein In-place-Update und überschreibt keine aktive Version.
Scheitern Kopie, Hashprüfung oder Ziel-Self-Test, entfernt das Setup das teilweise neue Ziel und stellt beim Update das zuvor atomar erzeugte Backup wieder her. Kann bereits die Vorversion nicht atomar ins Backup verschoben werden, bleibt sie unverändert aktiv und das Update bricht ohne Kopierfallback ab.
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 temporäres Verzeichnis wegen einer Dateisperre nicht gelöscht werden, bleibt die erfolgreiche Installation aktiv und das Log kennzeichnet den Abschluss als `success_with_temporary_cleanup_warning`.
@@ -77,7 +79,7 @@ Die Abschlussmeldung enthält einen stabilen Fehlercode und die betroffene Phase
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.
Bei einer gesperrten Verzeichnisverschiebung erscheinen `event=directory_move_retry` und nach einer Erholung `event=directory_move_recovered`. Eine Neuinstallation mit Kopierfallback protokolliert `event=activation_fallback_started`, `event=activation_fallback_complete` und im Abschluss `activation_method=verified_copy_fallback`. Ein dauerhaft gesperrtes Update verwendet keinen Kopierfallback und endet weiterhin mit `SETUP-ACTIVATION`, bevor die aktive Version verändert wurde.
Bei einer gesperrten Verzeichnisverschiebung erscheinen `event=directory_move_retry` und nach einer Erholung `event=directory_move_recovered`. Ein Kopierfallback protokolliert `event=activation_fallback_started`, `event=activation_fallback_complete`, den Geltungsbereich `scope=new_install` oder `scope=update_after_backup` und im Abschluss `activation_method=verified_copy_fallback`. Kann bei einem Update bereits das Backup nicht erzeugt werden, endet es weiterhin mit `SETUP-ACTIVATION`, bevor die aktive Version verändert wurde.
Für eine Supportanalyse bitte sichern:
@@ -89,7 +91,7 @@ Ein erfolgreicher Installer-Self-Test bestätigt Paket, Programmstart und lokale
## Emergency Restore nach einem Teilabbruch
Version 2.2.2 kann einen Wiederanlauf allein aus einer erhaltenen `before.json` vorbereiten und ausführen. Eine mit Version 2.1.3 erzeugte Datei ist kompatibel; zusätzliche Plan- oder Nachher-Dateien des fehlgeschlagenen Laufs sind nicht erforderlich. Auch ein Fehler der GUI-Logweiterleitung ist vom fachlichen Executor isoliert und kann die späteren Planschritte nicht abbrechen.
Version 2.2.3 kann einen Wiederanlauf allein aus einer erhaltenen `before.json` vorbereiten und ausführen. Eine mit Version 2.1.3 erzeugte Datei ist kompatibel; zusätzliche Plan- oder Nachher-Dateien des fehlgeschlagenen Laufs sind nicht erforderlich. Auch ein Fehler der GUI-Logweiterleitung ist vom fachlichen Executor isoliert und kann die späteren Planschritte nicht abbrechen.
1. Die erhaltene `before.json` außerhalb des Arbeitsverzeichnisses zusätzlich sichern.
2. Anwendung als Administrator starten und denselben Zielserver wählen, der im Snapshot gespeichert ist.
+2 -1
View File
@@ -31,7 +31,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
- Bounded activation retries plus a verified first-install copy fallback for ACC/EDR rename locks
- Bounded activation retries plus a verified copy fallback for first installs and atomically backed-up updates when Windows/EDR blocks only the staging rename
- 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
@@ -115,6 +115,7 @@ Targeted German inline comments explain non-obvious operational decisions such a
- [Dokumentation](Dokumentation.md)
- [Installer stability analysis](docs/Installer-Stabilitaetsanalyse-2026-08-11.md)
- [ACC activation incident analysis](docs/ACC-Installer-Aktivierungsfehler-2026-08-11.md)
- [PROD activation incident analysis and 2.2.3 fix](docs/PROD-Installer-Aktivierungsfehler-2026-08-24.md)
- [ACC runtime shutdown incident and recovery fix](docs/ACC-Runtime-Shutdown-Exception-2026-08-19.md)
- [AI maintainer handoff](AI-README.md)
- [References](REFERENCES.md)
@@ -12,7 +12,7 @@ Vor Version 2.1.0 enthielt das Repository keinen Installer für die C#-Anwendung
- Update nur bei geschlossener produktiver Toolinstanz.
- Staging und Backup als eindeutige Geschwister des Installationsverzeichnisses auf demselben Volume.
- Acht begrenzte Move-Versuche mit 250, 500, 1.000, 2.000, 3.000, 5.000 und 8.000 ms Backoff zwischen den Versuchen.
- SHA-256-verifizierter Kopierfallback ausschließlich für eine Neuinstallation ohne aktive Vorversion; Updates bleiben strikt atomar.
- SHA-256-verifizierter Kopierfallback für eine Neuinstallation ohne Ziel oder für eine Update-Aktivierung nach vollständig atomar gesicherter Vorversion; niemals als In-place-Update.
- Zweiter Self-Test nach Aktivierung und vor Windows-Registrierung.
- Automatisches Datei- und Registrierungsrollback bei Fehlern.
- Dauerhaftes phasenbezogenes Installerlog unter ProgramData.
@@ -26,7 +26,7 @@ Vor Version 2.1.0 enthielt das Repository keinen Installer für die C#-Anwendung
## Lokal verifiziert
- Release-Build aller Projekte mit Mono MSBuild.
- Siebzehn Regressionstests einschließlich manipulierter/zusätzlicher/ausbrechender Payload-Pfade, Staging-Abbruch ohne Mutation, transienter Move-Erholung, verifiziertem Neuinstallationsfallback, begrenztem Updateabbruch, erzwungenem Fehler des zweiten Self-Tests mit Wiederherstellung der Vorversion, Deinstallation über ein Quarantäneverzeichnis, Diagnosekontext, Log-Fallback und Fehlerisolierung der UI-Ausgabe.
- Neunundzwanzig Regressionstests einschließlich manipulierter/zusätzlicher/ausbrechender Payload-Pfade, Staging-Abbruch ohne Mutation, transienter Move-Erholung, verifiziertem Neuinstallations- und Updatefallback, begrenztem Updateabbruch, 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.
@@ -0,0 +1,52 @@
# PROD-Installer-Aktivierungsfehler vom 24.08.2026
## Kurzbefund
Der Screenshot zeigt nicht den aktuellen Repository-Stand, sondern `BizTalk Platform Management Tool 2.1.2`. Der Lauf bestand um 12:40:59 die Staging-Prüfung und scheiterte unmittelbar danach in Phase 3 mit:
```text
SETUP-ACTIVATION
System.InvalidOperationException: Installation/Update fehlgeschlagen
System.IO.IOException: Access to the path
'C:\Program Files\BizTalkPlatformManagementTool.staging.<guid>' is denied.
at System.IO.Directory.InternalMove(...)
```
Die Abschlussmeldung enthält `Kein Rollback erforderlich`. Im 2.1.2-Code bedeutet das, dass weder ein vorhandenes Installationsverzeichnis ins Backup verschoben noch ein neues Ziel aktiviert wurde. Der Fehler trat somit an der ersten Systemmutation auf; Paketkopie und Staging-Self-Test waren bereits erfolgreich.
## Ursachenbewertung
Version 2.1.2 führte für die Aktivierung genau einen `Directory.Move` aus. `Access denied` beim Umbenennen des vollständig erstellten und bereits ausführbaren Staging-Verzeichnisses ist mit einer kurzzeitigen Loader-/Virenscanner-/EDR-Sperre oder einer auf Rename/Delete beschränkten Richtlinie vereinbar. Der Screenshot allein identifiziert den blockierenden Prozess nicht. Er belegt aber:
- kein Manifest- oder SHA-256-Fehler,
- keinen Fehler des WMI-freien Staging-Self-Tests,
- keinen BizTalk-Laufzeitfehler,
- keinen erforderlichen Rollback einer aktiven Version,
- einen Fehler beim Verzeichnis-Rename unter `C:\Program Files`.
Der gleiche 2.1.2-Codepfad war bereits am 11.08.2026 in ACC aufgefallen. Der damals ab 2.1.3 ergänzte Retry- und Neuinstallationsfallback war in der auf dem Screenshot ausgeführten Datei noch nicht enthalten.
## Fix in Version 2.2.3
Version 2.2.3 enthält die bereits vorhandenen acht begrenzten Move-Versuche mit 250, 500, 1.000, 2.000, 3.000, 5.000 und 8.000 ms Backoff. Bleibt ausschließlich der Staging-Rename gesperrt, wird die validierte Payload in das fehlende Ziel kopiert und jede Zieldatei erneut gegen das Manifest geprüft.
Der Fix deckt jetzt beide sicheren Situationen ab:
1. Neuinstallation: Es existiert kein Installationsziel.
2. Update: Die vollständige Vorversion wurde zuvor atomar ins Backup verschoben und das Installationsziel existiert nicht mehr.
Der Fallback überschreibt niemals eine aktive Installation. Bei einem Update bleibt das Backup während Kopie, Hashprüfung und zweitem Self-Test erhalten. Scheitert einer dieser Schritte, entfernt der Installer die teilweise neue Version und stellt das Backup wieder her. Kann schon das Backup nicht atomar erzeugt werden, bleibt die Vorversion unverändert aktiv und es gibt keinen Kopierfallback.
Zwei neue Failure-Injection-Regressionstests belegen den erfolgreichen Updatefallback und die vollständige Wiederherstellung der Vorversion nach einem erzwungen fehlgeschlagenen Ziel-Self-Test.
## Vorgehen in PROD
1. Die 2.1.2-Datei nicht erneut starten.
2. Screenshot und vollständiges `setup-*.log` aus `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs` als Vorfallevidenz sichern.
3. `artifacts\BizTalkPlatformManagementTool-Setup.zip.b64.txt` aus Version 2.2.3 mit `certutil -decode` rekonstruieren.
4. Den SHA-256 des ZIPs mit `BizTalkPlatformManagementTool-Setup.zip.sha256.txt` vergleichen.
5. Das ZIP in einen neuen Ordner entpacken und die dortige `Setup.exe` starten. Der Fenstertitel muss `2.2.3` zeigen.
6. Im Erfolgslog `setup_version=2.2.3.0` und `activation_method=atomic_move` oder `activation_method=verified_copy_fallback` prüfen. Beim Fallback muss zusätzlich `scope=new_install` oder `scope=update_after_backup` protokolliert sein.
7. Danach in der Anwendung zuerst **Diagnose** und anschließend mit aktiviertem **Dry run** den vorgesehenen Betriebsablauf prüfen.
Eine Windows-/BizTalk-/EDR-Abnahme in PROD bleibt erforderlich; die lokale Mono-Suite kann reale ACLs, Endpoint-Schutz, Registry und Verknüpfungen nicht simulieren.
@@ -23,7 +23,7 @@ namespace BizTalkPlatformManagementTool.Setup
private const string ProductName = "BizTalk Platform Management Tool";
/// <summary>Aktuelle Produktversion des Installers und Uninstall-Eintrags.</summary>
private const string ProductVersion = "2.2.2";
private const string ProductVersion = "2.2.3";
/// <summary>
/// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen.
@@ -244,19 +244,18 @@ namespace BizTalkPlatformManagementTool.Setup
}
catch (IOException ex)
{
if (hadExistingInstallation || Directory.Exists(installDirectory)) throw;
// Eine Neuinstallation hat noch keine aktive Vorversion. Deshalb ist ein
// erneut hashgeprüfter Copy-Fallback sicher rückrollbar, ohne die strengere
// atomare Update-Grenze für bestehende Installationen aufzuweichen.
EnsureVerifiedCopyActivationIsSafe(hadExistingInstallation, backupCreated, installDirectory);
// Bei einem Update bleibt die Vorversion vollständig im atomar erzeugten
// Backup erhalten. Dadurch ist auch eine teilweise Zielkopie rückrollbar.
activated = true;
ActivateNewInstallationByVerifiedCopy(stagingDirectory, installDirectory, files, ex, log);
ActivateByVerifiedCopy(stagingDirectory, installDirectory, files, hadExistingInstallation, ex, log);
activationUsedCopyFallback = true;
}
catch (UnauthorizedAccessException ex)
{
if (hadExistingInstallation || Directory.Exists(installDirectory)) throw;
EnsureVerifiedCopyActivationIsSafe(hadExistingInstallation, backupCreated, installDirectory);
activated = true;
ActivateNewInstallationByVerifiedCopy(stagingDirectory, installDirectory, files, ex, log);
ActivateByVerifiedCopy(stagingDirectory, installDirectory, files, hadExistingInstallation, ex, log);
activationUsedCopyFallback = true;
}
@@ -899,27 +898,47 @@ namespace BizTalkPlatformManagementTool.Setup
}
/// <summary>
/// Aktiviert eine Neuinstallation durch eine erneut hashgeprüfte Kopie, wenn Windows
/// ausschließlich die Verzeichnisumbenennung des erfolgreich geprüften Stagings sperrt.
/// Stellt sicher, dass ein Kopierfallback keine aktive oder ungesicherte Vorversion überschreibt.
/// </summary>
/// <param name="hadExistingInstallation"><c>true</c>, wenn beim Start eine Installation vorhanden war.</param>
/// <param name="backupCreated"><c>true</c>, wenn diese Vorversion bereits atomar gesichert wurde.</param>
/// <param name="targetDirectory">Das erwartungsgemäß nicht vorhandene Installationsziel.</param>
private static void EnsureVerifiedCopyActivationIsSafe(
bool hadExistingInstallation,
bool backupCreated,
string targetDirectory)
{
if (Directory.Exists(targetDirectory) || File.Exists(targetDirectory))
throw new InvalidOperationException("Verified copy activation refused because the target path already exists: " + targetDirectory);
if (hadExistingInstallation && !backupCreated)
throw new InvalidOperationException("Verified copy activation refused because the existing installation was not backed up.");
}
/// <summary>
/// Aktiviert eine Neuinstallation oder ein bereits gesichertes Update durch eine erneut
/// hashgeprüfte Kopie, wenn Windows nur die Staging-Umbenennung dauerhaft sperrt.
/// </summary>
/// <param name="stagingDirectory">Das bereits validierte Staging-Verzeichnis.</param>
/// <param name="targetDirectory">Das noch nicht vorhandene Installationsverzeichnis.</param>
/// <param name="files">Die validierten Manifesteinträge.</param>
/// <param name="hadExistingInstallation"><c>true</c> für ein Update mit erhaltenem Backup.</param>
/// <param name="moveException">Der vorausgegangene dauerhafte Move-Fehler.</param>
/// <param name="log">Das Diagnoseprotokoll des aktuellen Setup-Laufs.</param>
private static void ActivateNewInstallationByVerifiedCopy(
private static void ActivateByVerifiedCopy(
string stagingDirectory,
string targetDirectory,
IEnumerable<PackageFile> files,
bool hadExistingInstallation,
Exception moveException,
SetupOperationLog log)
{
var scope = hadExistingInstallation ? "update_after_backup" : "new_install";
log.Write(
"WARN",
"event=activation_fallback_started method=verified_copy reason=directory_move_exhausted "
"event=activation_fallback_started method=verified_copy scope=" + scope + " reason=directory_move_exhausted "
+ SetupOperationLog.FormatException(moveException));
CopyPayload(stagingDirectory, targetDirectory, files);
log.Write("INFO", "event=activation_fallback_complete method=verified_copy target=\"" + targetDirectory + "\"");
log.Write("INFO", "event=activation_fallback_complete method=verified_copy scope=" + scope + " target=\"" + targetDirectory + "\"");
}
/// <summary>
@@ -65,7 +65,7 @@ namespace BizTalkPlatformManagementTool.Setup
{
AutoSize = true,
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
Text = "BizTalk Platform Management Tool 2.2.2"
Text = "BizTalk Platform Management Tool 2.2.3"
});
root.Controls.Add(new Label
{
@@ -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.2.2.0")]
[assembly: AssemblyFileVersion("2.2.2.0")]
[assembly: AssemblyVersion("2.2.3.0")]
[assembly: AssemblyFileVersion("2.2.3.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
@@ -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.2.2.0" name="BizTalkPlatformManagementTool.Setup" />
<assemblyIdentity version="2.2.3.0" name="BizTalkPlatformManagementTool.Setup" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
</trustInfo>
@@ -9,6 +9,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: ComVisible(false)]
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
[assembly: AssemblyVersion("2.2.2.0")]
[assembly: AssemblyFileVersion("2.2.2.0")]
[assembly: AssemblyVersion("2.2.3.0")]
[assembly: AssemblyFileVersion("2.2.3.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
@@ -16,7 +16,7 @@ namespace BizTalkPlatformManagementTool.Services
/// <summary>
/// Current tool version written into generated snapshots.
/// </summary>
public const string Version = "2.2.2-net461";
public const string Version = "2.2.3-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.2.2.0" name="BizTalkPlatformManagementTool" />
<assemblyIdentity version="2.2.3.0" name="BizTalkPlatformManagementTool" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
@@ -41,6 +41,8 @@ namespace BizTalkPlatformManagementTool.Tests
Run("InstallerActivatesValidatedPayload", InstallerActivatesValidatedPayload);
Run("InstallerRetriesTransientActivationMove", InstallerRetriesTransientActivationMove);
Run("InstallerUsesVerifiedCopyFallbackForNewInstall", InstallerUsesVerifiedCopyFallbackForNewInstall);
Run("InstallerUsesVerifiedCopyFallbackForBackedUpUpdate", InstallerUsesVerifiedCopyFallbackForBackedUpUpdate);
Run("InstallerRollsBackFailedUpdateCopyFallback", InstallerRollsBackFailedUpdateCopyFallback);
Run("InstallerStopsAfterBoundedUpdateMoveRetries", InstallerStopsAfterBoundedUpdateMoveRetries);
Run("InstallerDoesNotMutateOnStagingFailure", InstallerDoesNotMutateOnStagingFailure);
Run("InstallerRollsBackFailedActivatedSelfTest", InstallerRollsBackFailedActivatedSelfTest);
@@ -379,10 +381,88 @@ namespace BizTalkPlatformManagementTool.Tests
Assert(log.Contains("event=directory_move_retry role=activate failed_attempt=7"), "final scheduled retry was not diagnosed");
Assert(log.Contains("exception_type=System.UnauthorizedAccessException"), "ACL failure type missing from retry diagnostics");
Assert(log.Contains("event=activation_fallback_complete method=verified_copy"), "copy fallback completion was not diagnosed");
Assert(log.Contains("scope=new_install"), "new-install fallback scope missing from diagnostics");
Assert(log.Contains("activation_method=verified_copy_fallback"), "copy fallback missing from setup summary");
});
}
/// <summary>Prüft den verifizierten Kopierfallback, nachdem eine Update-Vorversion atomar gesichert wurde.</summary>
private static void InstallerUsesVerifiedCopyFallbackForBackedUpUpdate()
{
InTemp(directory =>
{
var package = CreatePackage(directory, "new");
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 data = Path.Combine(directory, "data");
var activationMoveAttempts = 0;
var delays = new List<int>();
var engine = new InstallerEngine(
package,
install,
data,
false,
path => true,
(source, target) =>
{
if (source.IndexOf(".staging.", StringComparison.Ordinal) >= 0)
{
activationMoveAttempts++;
throw new UnauthorizedAccessException("simulated staging rename policy denial");
}
Directory.Move(source, target);
},
delays.Add);
engine.Install(false, null);
Assert(activationMoveAttempts == 8, "update activation did not exhaust the bounded move attempts");
Assert(delays.SequenceEqual(new[] { 250, 500, 1000, 2000, 3000, 5000, 8000 }), "unexpected update fallback retry schedule");
Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "new", "update copy fallback did not activate the new payload");
Assert(!Directory.GetDirectories(directory, "install.backup.*").Any(), "successful update fallback left a backup directory");
var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single());
Assert(log.Contains("event=activation_fallback_complete method=verified_copy scope=update_after_backup"), "update fallback scope missing from diagnostics");
Assert(log.Contains("activation_method=verified_copy_fallback"), "update copy fallback missing from setup summary");
});
}
/// <summary>Prüft das Backup-Rollback, wenn der Ziel-Self-Test nach einem Update-Kopierfallback fehlschlägt.</summary>
private static void InstallerRollsBackFailedUpdateCopyFallback()
{
InTemp(directory =>
{
var package = CreatePackage(directory, "new");
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 selfTestCalls = 0;
var engine = new InstallerEngine(
package,
install,
Path.Combine(directory, "data"),
false,
path => ++selfTestCalls == 1,
(source, target) =>
{
if (source.IndexOf(".staging.", StringComparison.Ordinal) >= 0)
throw new UnauthorizedAccessException("simulated staging rename policy denial");
Directory.Move(source, target);
},
milliseconds => { });
var exception = Capture<InvalidOperationException>(() => engine.Install(false, null));
Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "old", "failed update fallback did not restore the previous payload");
Assert(!Directory.GetDirectories(directory, "install.backup.*").Any(), "rollback left the previous version in a backup directory");
Assert(!Directory.GetDirectories(directory, "install.staging.*").Any(), "rollback left the failed staging directory");
Assert(exception.Message.Contains("Fehlercode=SETUP-ACTIVATED-SELFTEST"), "failed fallback reported the wrong setup phase");
Assert(exception.Message.Contains("Rollback erfolgreich"), "failed fallback did not report successful rollback");
});
}
/// <summary>Prüft, dass ein Update bei dauerhaft gesperrtem Backup atomar und unverändert abbricht.</summary>
private static void InstallerStopsAfterBoundedUpdateMoveRetries()
{