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