Harden installer activation against ACC file locks
This commit is contained in:
1 parent
b32cc61e43
commit
08626197be
15 files changed
+389
-23
No files matched your search
@@ -23,7 +23,12 @@ 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.1.2";
|
||||
private const string ProductVersion = "2.1.3";
|
||||
|
||||
/// <summary>
|
||||
/// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen.
|
||||
/// </summary>
|
||||
private static readonly int[] DirectoryMoveRetryDelaysMilliseconds = { 250, 500, 1000, 2000, 3000, 5000, 8000 };
|
||||
|
||||
/// <summary>Maschinenweiter Registrypfad des Windows-Uninstall-Eintrags.</summary>
|
||||
private const string UninstallKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\BizTalkPlatformManagementTool";
|
||||
@@ -43,6 +48,12 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
/// <summary>Optional injizierte Self-Test-Funktion für portable Regressionstests.</summary>
|
||||
private readonly Func<string, bool> selfTestRunner;
|
||||
|
||||
/// <summary>Verschiebt Verzeichnisse produktiv oder über eine injizierte Regressionstest-Funktion.</summary>
|
||||
private readonly Action<string, string> directoryMover;
|
||||
|
||||
/// <summary>Wartet zwischen Move-Versuchen produktiv oder über eine injizierte Regressionstest-Funktion.</summary>
|
||||
private readonly Action<int> retryDelay;
|
||||
|
||||
/// <summary>Zuletzt tatsächlich verwendetes primäres oder temporäres Diagnoseverzeichnis.</summary>
|
||||
private string lastInstallerLogDirectory;
|
||||
|
||||
@@ -90,12 +101,36 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
/// <param name="registerWindowsIntegration"><c>true</c>, wenn Registry und Verknüpfungen verwaltet werden sollen.</param>
|
||||
/// <param name="selfTestRunner">Optionale Testfunktion für Regressionstests; <c>null</c> startet die reale EXE.</param>
|
||||
internal InstallerEngine(string packageDirectory, string installDirectory, string dataDirectory, bool registerWindowsIntegration, Func<string, bool> selfTestRunner)
|
||||
: this(packageDirectory, installDirectory, dataDirectory, registerWindowsIntegration, selfTestRunner, Directory.Move, System.Threading.Thread.Sleep)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialisiert den Installer mit zusätzlich austauschbarer Move- und Wartefunktion.
|
||||
/// </summary>
|
||||
/// <param name="packageDirectory">Das Verzeichnis mit Setup-Payload und Manifest.</param>
|
||||
/// <param name="installDirectory">Das aktive Programmverzeichnis.</param>
|
||||
/// <param name="dataDirectory">Das dauerhafte Daten- und Diagnoseverzeichnis.</param>
|
||||
/// <param name="registerWindowsIntegration"><c>true</c>, wenn Registry und Verknüpfungen verwaltet werden sollen.</param>
|
||||
/// <param name="selfTestRunner">Optionale Testfunktion für Regressionstests; <c>null</c> startet die reale EXE.</param>
|
||||
/// <param name="directoryMover">Austauschbare atomare Verzeichnisverschiebung.</param>
|
||||
/// <param name="retryDelay">Austauschbare Wartefunktion zwischen Wiederholungen.</param>
|
||||
internal InstallerEngine(
|
||||
string packageDirectory,
|
||||
string installDirectory,
|
||||
string dataDirectory,
|
||||
bool registerWindowsIntegration,
|
||||
Func<string, bool> selfTestRunner,
|
||||
Action<string, string> directoryMover,
|
||||
Action<int> retryDelay)
|
||||
{
|
||||
this.packageDirectory = Path.GetFullPath(packageDirectory);
|
||||
this.installDirectory = Path.GetFullPath(installDirectory);
|
||||
this.dataDirectory = Path.GetFullPath(dataDirectory);
|
||||
this.registerWindowsIntegration = registerWindowsIntegration;
|
||||
this.selfTestRunner = selfTestRunner;
|
||||
this.directoryMover = directoryMover ?? throw new ArgumentNullException("directoryMover");
|
||||
this.retryDelay = retryDelay ?? throw new ArgumentNullException("retryDelay");
|
||||
}
|
||||
|
||||
/// <summary>Ruft das feste maschinenweite Programmverzeichnis ab.</summary>
|
||||
@@ -138,6 +173,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
WindowsIntegrationSnapshot integrationSnapshot = null;
|
||||
var backupCreated = false;
|
||||
var activated = false;
|
||||
var activationUsedCopyFallback = false;
|
||||
var integrationMutationStarted = false;
|
||||
var phaseCode = "SETUP-INITIALIZATION";
|
||||
var phase = "Initialisierung";
|
||||
@@ -195,14 +231,34 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
if (hadExistingInstallation)
|
||||
{
|
||||
log.Write("INFO", "event=directory_move role=backup source=\"" + installDirectory + "\" target=\"" + backupDirectory + "\"");
|
||||
Directory.Move(installDirectory, backupDirectory);
|
||||
MoveDirectoryWithRetry(installDirectory, backupDirectory, "backup", log);
|
||||
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");
|
||||
try
|
||||
{
|
||||
MoveDirectoryWithRetry(stagingDirectory, installDirectory, "activate", log);
|
||||
activated = true;
|
||||
log.Write("INFO", "event=directory_move_complete role=activate method=atomic_move");
|
||||
}
|
||||
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.
|
||||
activated = true;
|
||||
ActivateNewInstallationByVerifiedCopy(stagingDirectory, installDirectory, files, ex, log);
|
||||
activationUsedCopyFallback = true;
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
if (hadExistingInstallation || Directory.Exists(installDirectory)) throw;
|
||||
activated = true;
|
||||
ActivateNewInstallationByVerifiedCopy(stagingDirectory, installDirectory, files, ex, log);
|
||||
activationUsedCopyFallback = true;
|
||||
}
|
||||
|
||||
phaseCode = "SETUP-ACTIVATED-SELFTEST";
|
||||
phase = "Aktivierte Version erneut pruefen";
|
||||
@@ -230,10 +286,14 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
phase = "Backup bereinigen";
|
||||
write("Phase 6/6: " + phase + ".");
|
||||
var backupCleanupSucceeded = TryDeleteDirectory(backupDirectory, write);
|
||||
var stagingCleanupSucceeded = TryDeleteDirectory(stagingDirectory, write);
|
||||
var cleanupSucceeded = backupCleanupSucceeded && stagingCleanupSucceeded;
|
||||
log.Write(
|
||||
backupCleanupSucceeded ? "INFO" : "WARN",
|
||||
"event=setup_completed result=" + (backupCleanupSucceeded ? "success" : "success_with_backup_cleanup_warning")
|
||||
cleanupSucceeded ? "INFO" : "WARN",
|
||||
"event=setup_completed result=" + (cleanupSucceeded ? "success" : "success_with_temporary_cleanup_warning")
|
||||
+ " backup_directory_preserved=" + Directory.Exists(backupDirectory)
|
||||
+ " staging_directory_preserved=" + Directory.Exists(stagingDirectory)
|
||||
+ " activation_method=" + (activationUsedCopyFallback ? "verified_copy_fallback" : "atomic_move")
|
||||
+ " 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.");
|
||||
@@ -264,7 +324,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
if (backupCreated && Directory.Exists(backupDirectory))
|
||||
{
|
||||
log.Write("WARN", "event=rollback_step step=restore_backup source=\"" + backupDirectory + "\" target=\"" + installDirectory + "\"");
|
||||
Directory.Move(backupDirectory, installDirectory);
|
||||
MoveDirectoryWithRetry(backupDirectory, installDirectory, "rollback_restore_backup", log);
|
||||
}
|
||||
write(backupCreated ? "Rollback: vorherige Programmversion wiederhergestellt." : "Rollback: unvollstaendige Neuinstallation entfernt.");
|
||||
log.Write("WARN", "event=rollback_step_complete step=files result=success");
|
||||
@@ -353,7 +413,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
if (Directory.Exists(installDirectory))
|
||||
{
|
||||
log.Write("INFO", "event=directory_move role=uninstall_quarantine source=\"" + installDirectory + "\" target=\"" + removalDirectory + "\"");
|
||||
Directory.Move(installDirectory, removalDirectory);
|
||||
MoveDirectoryWithRetry(installDirectory, removalDirectory, "uninstall_quarantine", log);
|
||||
filesMoved = true;
|
||||
log.Write("INFO", "event=directory_move_complete role=uninstall_quarantine");
|
||||
}
|
||||
@@ -375,7 +435,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Move(removalDirectory, installDirectory);
|
||||
MoveDirectoryWithRetry(removalDirectory, installDirectory, "uninstall_rollback_restore", log);
|
||||
log.Write("WARN", "event=uninstall_rollback_step step=restore_program_directory result=success");
|
||||
}
|
||||
catch (Exception rollbackException)
|
||||
@@ -838,6 +898,92 @@ 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.
|
||||
/// </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="moveException">Der vorausgegangene dauerhafte Move-Fehler.</param>
|
||||
/// <param name="log">Das Diagnoseprotokoll des aktuellen Setup-Laufs.</param>
|
||||
private static void ActivateNewInstallationByVerifiedCopy(
|
||||
string stagingDirectory,
|
||||
string targetDirectory,
|
||||
IEnumerable<PackageFile> files,
|
||||
Exception moveException,
|
||||
SetupOperationLog log)
|
||||
{
|
||||
log.Write(
|
||||
"WARN",
|
||||
"event=activation_fallback_started method=verified_copy reason=directory_move_exhausted "
|
||||
+ SetupOperationLog.FormatException(moveException));
|
||||
CopyPayload(stagingDirectory, targetDirectory, files);
|
||||
log.Write("INFO", "event=activation_fallback_complete method=verified_copy target=\"" + targetDirectory + "\"");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wiederholt eine atomare Verzeichnisverschiebung bei kurzzeitigen Windows-Dateisperren.
|
||||
/// </summary>
|
||||
/// <param name="source">Das vorhandene Quellverzeichnis.</param>
|
||||
/// <param name="target">Das noch nicht vorhandene Zielverzeichnis.</param>
|
||||
/// <param name="role">Die maschinenlesbare Rolle für das Diagnoselog.</param>
|
||||
/// <param name="log">Das Diagnoseprotokoll des aktuellen Setup-Laufs.</param>
|
||||
private void MoveDirectoryWithRetry(string source, string target, string role, SetupOperationLog log)
|
||||
{
|
||||
var maximumAttempts = DirectoryMoveRetryDelaysMilliseconds.Length + 1;
|
||||
for (var attempt = 1; ; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
directoryMover(source, target);
|
||||
if (attempt > 1)
|
||||
{
|
||||
log.Write(
|
||||
"INFO",
|
||||
"event=directory_move_recovered role=" + role
|
||||
+ " attempt=" + attempt.ToString(CultureInfo.InvariantCulture)
|
||||
+ " max_attempts=" + maximumAttempts.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
return;
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
if (attempt >= maximumAttempts) throw;
|
||||
WaitBeforeDirectoryMoveRetry(role, attempt, maximumAttempts, ex, log);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
if (attempt >= maximumAttempts) throw;
|
||||
WaitBeforeDirectoryMoveRetry(role, attempt, maximumAttempts, ex, log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Protokolliert und wartet vor einer weiteren Verzeichnisverschiebung.
|
||||
/// </summary>
|
||||
/// <param name="role">Die maschinenlesbare Rolle der Verschiebung.</param>
|
||||
/// <param name="failedAttempt">Die Nummer des fehlgeschlagenen Versuchs.</param>
|
||||
/// <param name="maximumAttempts">Die maximale Gesamtzahl der Versuche.</param>
|
||||
/// <param name="exception">Der als vorübergehend behandelte Dateisystemfehler.</param>
|
||||
/// <param name="log">Das Diagnoseprotokoll des aktuellen Setup-Laufs.</param>
|
||||
private void WaitBeforeDirectoryMoveRetry(string role, int failedAttempt, int maximumAttempts, Exception exception, SetupOperationLog log)
|
||||
{
|
||||
var delayMilliseconds = DirectoryMoveRetryDelaysMilliseconds[failedAttempt - 1];
|
||||
// Direkt nach einem EXE-Self-Test können Loader, Virenscanner oder EDR das
|
||||
// Delete-/Rename-Handle noch kurz halten. Begrenztes Backoff erhält die atomare
|
||||
// Aktivierung; dauerhafte ACL-Fehler werden nach maximal 19,75 Sekunden sichtbar.
|
||||
log.Write(
|
||||
"WARN",
|
||||
"event=directory_move_retry role=" + role
|
||||
+ " failed_attempt=" + failedAttempt.ToString(CultureInfo.InvariantCulture)
|
||||
+ " max_attempts=" + maximumAttempts.ToString(CultureInfo.InvariantCulture)
|
||||
+ " delay_ms=" + delayMilliseconds.ToString(CultureInfo.InvariantCulture)
|
||||
+ " " + SetupOperationLog.FormatException(exception));
|
||||
retryDelay(delayMilliseconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stellt sicher, dass eine betriebsnotwendige Datei im Manifest enthalten ist.
|
||||
/// </summary>
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
AutoSize = true,
|
||||
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
|
||||
Text = "BizTalk Platform Management Tool 2.1.2"
|
||||
Text = "BizTalk Platform Management Tool 2.1.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.1.2.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.2.0")]
|
||||
[assembly: AssemblyVersion("2.1.3.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.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.1.2.0" name="BizTalkPlatformManagementTool.Setup" />
|
||||
<assemblyIdentity version="2.1.3.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.2.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.2.0")]
|
||||
[assembly: AssemblyVersion("2.1.3.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.3.0")]
|
||||
@@ -15,7 +15,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
/// <summary>
|
||||
/// Current tool version written into generated snapshots.
|
||||
/// </summary>
|
||||
public const string Version = "2.1.2-net461";
|
||||
public const string Version = "2.1.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.1.2.0" name="BizTalkPlatformManagementTool" />
|
||||
<assemblyIdentity version="2.1.3.0" name="BizTalkPlatformManagementTool" />
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
|
||||
Reference in new issue
Block a user