|
|
|
@@ -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>
|
|
|
|
|