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
@@ -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()
{