Document C# codebase and publish 2.1.2

This commit is contained in:
2026-08-11 13:23:01 +02:00
parent bb2d2f8484
commit b32cc61e43
30 changed files with 505 additions and 34 deletions
+9
View File
@@ -1,6 +1,15 @@
# Changelog # Changelog
## [2.1.2] - 2026-08-11
### Added
- Complete XML documentation for types and methods across application, setup, packager and regression projects, including parameters, generic type parameters and return values.
- Focused German inline comments for non-obvious BizTalk ordering, WMI compatibility, persistence, security and installer transaction decisions.
- XML documentation output in every Release project configuration for compiler-side validation.
### Changed
- README, technical documentation and installation/build guidance now describe the source-documentation standard and generated XML developer artifacts.
## [2.1.1] - 2026-08-11 ## [2.1.1] - 2026-08-11
### Added ### Added
- Stable setup/uninstall phase codes, environment and file metadata, complete self-test output, exception chains with HRESULT/stacktrace, and per-step rollback diagnostics. - Stable setup/uninstall phase codes, environment and file metadata, complete self-test output, exception chains with HRESULT/stacktrace, and per-step rollback diagnostics.
+16
View File
@@ -27,6 +27,22 @@ Das BizTalk Platform Management Tool unterstützt kontrollierte Wartungsfenster
- Regressionstests: `tests/BizTalkPlatformManagementTool.Tests` - Regressionstests: `tests/BizTalkPlatformManagementTool.Tests`
- PowerShell-Archiv: `archive/powershell/BizTalkPlatformManagementTool.ps1` - PowerShell-Archiv: `archive/powershell/BizTalkPlatformManagementTool.ps1`
## Code-Dokumentationsstandard
Die vollständige C#-Codebasis in Anwendung, Installer, Packager und Regressionstests ist auf Typ- und Methodenebene mit XML-Dokumentationskommentaren versehen. Methoden dokumentieren ihre Parameter mit `<param>`, generische Typen mit `<typeparam>` und Rückgabewerte mit `<returns>`, soweit jeweils vorhanden. Die Release-Konfiguration jedes Projekts erzeugt zusätzlich eine XML-Dokumentationsdatei im jeweiligen `bin\Release`-Verzeichnis. Dadurch prüft der Compiler Syntax und Referenzen der öffentlichen Dokumentation bei jedem Release-Build.
Deutsche Inline-Kommentare stehen gezielt an Stellen, deren Zweck nicht allein aus dem Code hervorgeht. Dazu zählen insbesondere:
- sichere Shutdown-/Restore-Reihenfolge und Schutz gebundener Orchestrierungen,
- WMI-Auflösung über breite Abfrage mit clientseitigem Filter,
- atomare JSON-Ersetzung auf demselben Volume,
- Neutralisierung formelartiger CSV-Werte,
- Persistenz eines Operationsplans vor der Benutzerbestätigung,
- Staging-, Aktivierungs-, Quarantäne- und Rollbackgrenzen des Installers,
- begrenzte Self-Test-Prozess- und Streambehandlung.
Selbsterklärende Zuweisungen und reine UI-Konstruktion werden nicht zeilenweise kommentiert. Kommentare sollen die fachliche Begründung, Sicherheitsgrenze oder Plattformbesonderheit festhalten und nicht lediglich den unmittelbar sichtbaren Code wiederholen.
## UI Workflow ## UI Workflow
1. Anwendung mit Administratorrechten starten. 1. Anwendung mit Administratorrechten starten.
+2
View File
@@ -98,6 +98,8 @@ scripts\package-release.cmd
`test-release.cmd` baut alle vier Projekte und führt die Regressionstests aus. `package-release.cmd` baut und testet erneut, erzeugt Paket, ZIP, Base64-TXT und SHA-256-Datei und validiert dabei das interne Payload-Manifest. `test-release.cmd` baut alle vier Projekte und führt die Regressionstests aus. `package-release.cmd` baut und testet erneut, erzeugt Paket, ZIP, Base64-TXT und SHA-256-Datei und validiert dabei das interne Payload-Manifest.
Die Release-Konfiguration erzeugt außerdem pro Assembly eine XML-Dokumentationsdatei im jeweiligen `bin\Release`-Verzeichnis. Damit werden XML-Kommentare und `cref`-Referenzen während des Builds compilerseitig geprüft; diese Entwicklerartefakte sind für den Betrieb nicht erforderlich und deshalb nicht Bestandteil der Installer-Payload.
Unter Mono kann der portable Anteil lokal geprüft werden: Unter Mono kann der portable Anteil lokal geprüft werden:
```sh ```sh
+6
View File
@@ -91,6 +91,12 @@ The app targets .NET Framework 4.6.1 for compatibility with customer environment
Use `scripts\test-release.cmd` for the build and regression suite and `scripts\package-release.cmd` for the tested installer ZIP, Certutil-compatible Base64 TXT and SHA-256 file. See [Installation](Installation.md) for decoding and update/rollback details. Use `scripts\test-release.cmd` for the build and regression suite and `scripts\package-release.cmd` for the tested installer ZIP, Certutil-compatible Base64 TXT and SHA-256 file. See [Installation](Installation.md) for decoding and update/rollback details.
## Source Documentation
All C# types and methods in the application, setup, packager and regression project use XML documentation comments. Method contracts include `param`, `typeparam` and `returns` elements where applicable. Release builds generate one XML documentation file per assembly, so malformed or missing public documentation becomes visible during compilation.
Targeted German inline comments explain non-obvious operational decisions such as WMI client-side filtering, shutdown/restore order, atomic file replacement, CSV formula neutralization and installer transaction boundaries. Trivial statements are intentionally not paraphrased in comments; the comments record the reason or safety constraint behind the code.
## Documentation ## Documentation
- [Installation](Installation.md) - [Installation](Installation.md)
@@ -8,7 +8,7 @@
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion><FileAlignment>512</FileAlignment><Deterministic>true</Deterministic> <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion><FileAlignment>512</FileAlignment><Deterministic>true</Deterministic>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "><DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel></PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "><DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel></PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "><DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel></PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "><DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel><DocumentationFile>bin\Release\BizTalkPlatformManagementTool.Packager.xml</DocumentationFile></PropertyGroup>
<ItemGroup><Reference Include="System" /><Reference Include="System.Core" /><Reference Include="System.IO.Compression" /><Reference Include="System.IO.Compression.FileSystem" /><Reference Include="System.Security" /></ItemGroup> <ItemGroup><Reference Include="System" /><Reference Include="System.Core" /><Reference Include="System.IO.Compression" /><Reference Include="System.IO.Compression.FileSystem" /><Reference Include="System.Security" /></ItemGroup>
<ItemGroup><Compile Include="Program.cs" /></ItemGroup> <ItemGroup><Compile Include="Program.cs" /></ItemGroup>
<ItemGroup><ProjectReference Include="..\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj"><Project>{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}</Project><Name>BizTalkPlatformManagementTool.Setup</Name></ProjectReference></ItemGroup> <ItemGroup><ProjectReference Include="..\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj"><Project>{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}</Project><Name>BizTalkPlatformManagementTool.Setup</Name></ProjectReference></ItemGroup>
@@ -6,8 +6,14 @@ using BizTalkPlatformManagementTool.Setup;
namespace BizTalkPlatformManagementTool.Packager namespace BizTalkPlatformManagementTool.Packager
{ {
/// <summary>Erzeugt aus den Release-Binärdateien das übertragbare Setup-Paket.</summary>
internal static class Program internal static class Program
{ {
/// <summary>
/// Erstellt Payload, Manifest, ZIP, Certutil-kompatible Base64-TXT und SHA-256-Datei.
/// </summary>
/// <param name="args">Repository-Wurzel und Build-Konfiguration.</param>
/// <returns>Null bei erfolgreicher Paketierung, andernfalls eins.</returns>
private static int Main(string[] args) private static int Main(string[] args)
{ {
try try
@@ -27,6 +33,7 @@ namespace BizTalkPlatformManagementTool.Packager
Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool", "bin", configuration, "BizTalkPlatformManagementTool.exe.config"), Path.Combine(application, "BizTalkPlatformManagementTool.exe.config")); Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool", "bin", configuration, "BizTalkPlatformManagementTool.exe.config"), Path.Combine(application, "BizTalkPlatformManagementTool.exe.config"));
Copy(Path.Combine(root, "Installation.md"), Path.Combine(package, "INSTALLATION.md")); Copy(Path.Combine(root, "Installation.md"), Path.Combine(package, "INSTALLATION.md"));
PackageManifest.Write(application, Path.Combine(package, "application.manifest")); PackageManifest.Write(application, Path.Combine(package, "application.manifest"));
// Das frisch erzeugte Manifest wird vor dem äußeren ZIP sofort gegen die Payload geprüft.
PackageManifest.ValidateAndRead(application, Path.Combine(package, "application.manifest")); PackageManifest.ValidateAndRead(application, Path.Combine(package, "application.manifest"));
if (File.Exists(zip)) File.Delete(zip); if (File.Exists(zip)) File.Delete(zip);
@@ -46,6 +53,9 @@ namespace BizTalkPlatformManagementTool.Packager
} }
} }
/// <summary>Kopiert eine erforderliche Release-Datei und legt ihr Zielverzeichnis an.</summary>
/// <param name="source">Der vorhandene Quelldateipfad.</param>
/// <param name="target">Der Zieldateipfad innerhalb des Pakets.</param>
private static void Copy(string source, string target) private static void Copy(string source, string target)
{ {
if (!File.Exists(source)) throw new FileNotFoundException("Required package file missing: " + source, source); if (!File.Exists(source)) throw new FileNotFoundException("Required package file missing: " + source, source);
@@ -53,6 +63,9 @@ namespace BizTalkPlatformManagementTool.Packager
File.Copy(source, target, true); File.Copy(source, target, true);
} }
/// <summary>Schreibt eine Datei als Certutil-kompatible Base64-TXT mit 64 Zeichen pro Zeile.</summary>
/// <param name="source">Die binäre Quelldatei.</param>
/// <param name="target">Die zu erzeugende Textdatei.</param>
private static void WriteBase64(string source, string target) private static void WriteBase64(string source, string target)
{ {
var encoded = Convert.ToBase64String(File.ReadAllBytes(source)); var encoded = Convert.ToBase64String(File.ReadAllBytes(source));
@@ -22,6 +22,7 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType><Optimize>true</Optimize> <DebugType>pdbonly</DebugType><Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel> <OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel>
<DocumentationFile>bin\Release\BizTalkPlatformManagementTool.Setup.xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
@@ -11,28 +11,66 @@ using Microsoft.Win32;
namespace BizTalkPlatformManagementTool.Setup namespace BizTalkPlatformManagementTool.Setup
{ {
/// <summary>
/// Führt Installation, Update und Deinstallation mit Staging, Validierung und Rollback aus.
/// </summary>
internal sealed class InstallerEngine internal sealed class InstallerEngine
{ {
/// <summary>Dateiname der installierten Hauptanwendung.</summary>
internal const string ApplicationExeName = "BizTalkPlatformManagementTool.exe"; internal const string ApplicationExeName = "BizTalkPlatformManagementTool.exe";
/// <summary>Anzeigename für Verknüpfungen und Windows-Uninstall-Eintrag.</summary>
private const string ProductName = "BizTalk Platform Management Tool"; private const string ProductName = "BizTalk Platform Management Tool";
private const string ProductVersion = "2.1.1";
/// <summary>Aktuelle Produktversion des Installers und Uninstall-Eintrags.</summary>
private const string ProductVersion = "2.1.2";
/// <summary>Maschinenweiter Registrypfad des Windows-Uninstall-Eintrags.</summary>
private const string UninstallKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\BizTalkPlatformManagementTool"; private const string UninstallKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\BizTalkPlatformManagementTool";
/// <summary>Verzeichnis der entpackten Setup-Dateien.</summary>
private readonly string packageDirectory; private readonly string packageDirectory;
/// <summary>Aktives maschinenweites Programmverzeichnis.</summary>
private readonly string installDirectory; private readonly string installDirectory;
/// <summary>Dauerhaftes Datenverzeichnis unter ProgramData.</summary>
private readonly string dataDirectory; private readonly string dataDirectory;
/// <summary>Legt fest, ob Registry, Verknüpfungen und Uninstaller verwaltet werden.</summary>
private readonly bool registerWindowsIntegration; private readonly bool registerWindowsIntegration;
/// <summary>Optional injizierte Self-Test-Funktion für portable Regressionstests.</summary>
private readonly Func<string, bool> selfTestRunner; private readonly Func<string, bool> selfTestRunner;
/// <summary>Zuletzt tatsächlich verwendetes primäres oder temporäres Diagnoseverzeichnis.</summary>
private string lastInstallerLogDirectory; private string lastInstallerLogDirectory;
/// <summary>
/// Enthält den vor einer Mutation gesicherten Zustand der Windows-Integration.
/// </summary>
private sealed class WindowsIntegrationSnapshot private sealed class WindowsIntegrationSnapshot
{ {
/// <summary>Ruft ab oder legt fest, ob der Uninstall-Schlüssel vorher existierte.</summary>
public bool RegistryKeyExisted { get; set; } public bool RegistryKeyExisted { get; set; }
/// <summary>Ruft die vorherigen Registrywerte einschließlich ihres Typs ab oder legt sie fest.</summary>
public Dictionary<string, Tuple<object, RegistryValueKind>> RegistryValues { get; set; } public Dictionary<string, Tuple<object, RegistryValueKind>> RegistryValues { get; set; }
/// <summary>Ruft den vorherigen Inhalt der Desktop-Verknüpfung ab oder legt ihn fest.</summary>
public byte[] DesktopShortcut { get; set; } public byte[] DesktopShortcut { get; set; }
/// <summary>Ruft den vorherigen Inhalt der Startmenü-Verknüpfung ab oder legt ihn fest.</summary>
public byte[] StartMenuShortcut { get; set; } public byte[] StartMenuShortcut { get; set; }
/// <summary>Ruft den vorherigen Inhalt der Uninstaller-Datei ab oder legt ihn fest.</summary>
public byte[] Uninstaller { get; set; } public byte[] Uninstaller { get; set; }
} }
/// <summary>
/// Initialisiert den Installer mit den maschinenweiten Standardzielpfaden.
/// </summary>
/// <param name="packageDirectory">Das Verzeichnis mit Setup-Payload und Manifest.</param>
public InstallerEngine(string packageDirectory) public InstallerEngine(string packageDirectory)
: this( : this(
packageDirectory, packageDirectory,
@@ -43,6 +81,14 @@ namespace BizTalkPlatformManagementTool.Setup
{ {
} }
/// <summary>
/// Initialisiert den Installer mit expliziten Pfaden und austauschbarer Self-Test-Ausführung.
/// </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>
internal InstallerEngine(string packageDirectory, string installDirectory, string dataDirectory, bool registerWindowsIntegration, Func<string, bool> selfTestRunner) internal InstallerEngine(string packageDirectory, string installDirectory, string dataDirectory, bool registerWindowsIntegration, Func<string, bool> selfTestRunner)
{ {
this.packageDirectory = Path.GetFullPath(packageDirectory); this.packageDirectory = Path.GetFullPath(packageDirectory);
@@ -52,19 +98,23 @@ namespace BizTalkPlatformManagementTool.Setup
this.selfTestRunner = selfTestRunner; this.selfTestRunner = selfTestRunner;
} }
/// <summary>Gets the fixed machine-wide application installation directory.</summary> /// <summary>Ruft das feste maschinenweite Programmverzeichnis ab.</summary>
public string InstallDirectory { get { return installDirectory; } } public string InstallDirectory { get { return installDirectory; } }
/// <summary>Gets the directory containing persistent setup diagnostic logs.</summary> /// <summary>Ruft das zuletzt verwendete beziehungsweise reguläre Installer-Logverzeichnis ab.</summary>
public string InstallerLogDirectory public string InstallerLogDirectory
{ {
get { return lastInstallerLogDirectory ?? Path.Combine(dataDirectory, "InstallerLogs"); } get { return lastInstallerLogDirectory ?? Path.Combine(dataDirectory, "InstallerLogs"); }
} }
/// <summary>Gets whether this setup copy contains a complete install/update payload.</summary> /// <summary>Ruft ab, ob diese Setup-Kopie Payload und Manifest für Installation oder Update enthält.</summary>
public bool HasInstallPayload { get { return Directory.Exists(Path.Combine(packageDirectory, "application")) && File.Exists(Path.Combine(packageDirectory, "application.manifest")); } } 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> /// <summary>Ruft ab, ob die Anwendungs-EXE im Installationsziel vorhanden ist.</summary>
public bool IsInstalled { get { return File.Exists(Path.Combine(installDirectory, ApplicationExeName)); } } public bool IsInstalled { get { return File.Exists(Path.Combine(installDirectory, ApplicationExeName)); } }
/// <summary>Validates, stages and transactionally installs or updates the application.</summary> /// <summary>
/// Validiert und staged die Payload und installiert oder aktualisiert die Anwendung transaktional.
/// </summary>
/// <param name="createDesktopShortcut"><c>true</c>, wenn eine Desktop-Verknüpfung angelegt werden soll.</param>
/// <param name="report">Optionale Fortschrittsausgabe für die Setup-Oberfläche.</param>
public void Install(bool createDesktopShortcut, Action<string> report) public void Install(bool createDesktopShortcut, Action<string> report)
{ {
var uiReport = report ?? delegate { }; var uiReport = report ?? delegate { };
@@ -80,6 +130,8 @@ namespace BizTalkPlatformManagementTool.Setup
var sourceApplication = Path.Combine(packageDirectory, "application"); var sourceApplication = Path.Combine(packageDirectory, "application");
var manifestPath = Path.Combine(packageDirectory, "application.manifest"); var manifestPath = Path.Combine(packageDirectory, "application.manifest");
// Staging und Backup sind Geschwister des Zielverzeichnisses. Dadurch bleiben
// die späteren Directory.Move-Operationen auf demselben Volume atomar.
var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N"); var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N");
var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N"); var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N");
var hadExistingInstallation = Directory.Exists(installDirectory); var hadExistingInstallation = Directory.Exists(installDirectory);
@@ -108,6 +160,7 @@ namespace BizTalkPlatformManagementTool.Setup
LogDriveSpace(log, installDirectory); LogDriveSpace(log, installDirectory);
log.WriteFileDetails("setup_executable", Assembly.GetExecutingAssembly().Location); log.WriteFileDetails("setup_executable", Assembly.GetExecutingAssembly().Location);
log.WriteFileDetails("existing_application", Path.Combine(installDirectory, ApplicationExeName)); log.WriteFileDetails("existing_application", Path.Combine(installDirectory, ApplicationExeName));
// Der vollständige Integrationszustand wird vor der ersten Mutation gesichert.
integrationSnapshot = registerWindowsIntegration ? CaptureWindowsIntegration() : null; integrationSnapshot = registerWindowsIntegration ? CaptureWindowsIntegration() : null;
if (integrationSnapshot != null) if (integrationSnapshot != null)
log.Write("INFO", "event=integration_snapshot registry_key_existed=" + integrationSnapshot.RegistryKeyExisted log.Write("INFO", "event=integration_snapshot registry_key_existed=" + integrationSnapshot.RegistryKeyExisted
@@ -138,6 +191,7 @@ namespace BizTalkPlatformManagementTool.Setup
phaseCode = "SETUP-ACTIVATION"; phaseCode = "SETUP-ACTIVATION";
phase = "Vorhandene Version sichern und Staging atomar aktivieren"; phase = "Vorhandene Version sichern und Staging atomar aktivieren";
write("Phase 3/6: " + phase + ". Ab hier beginnt die Systemaenderung."); write("Phase 3/6: " + phase + ". Ab hier beginnt die Systemaenderung.");
// Erst nach Manifestprüfung und bestandenem Staging-Self-Test wird die aktive Version verändert.
if (hadExistingInstallation) if (hadExistingInstallation)
{ {
log.Write("INFO", "event=directory_move role=backup source=\"" + installDirectory + "\" target=\"" + backupDirectory + "\""); log.Write("INFO", "event=directory_move role=backup source=\"" + installDirectory + "\" target=\"" + backupDirectory + "\"");
@@ -197,6 +251,8 @@ namespace BizTalkPlatformManagementTool.Setup
} }
else else
{ {
// Dateisystem und Windows-Integration werden unabhängig behandelt, damit
// ein Fehler in einem Teil den Diagnosezustand des anderen nicht verdeckt.
log.Write("WARN", "event=rollback_started activated=" + activated + " backup_created=" + backupCreated + " integration_mutation_started=" + integrationMutationStarted); log.Write("WARN", "event=rollback_started activated=" + activated + " backup_created=" + backupCreated + " integration_mutation_started=" + integrationMutationStarted);
try try
{ {
@@ -256,7 +312,10 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>Removes the active program directory and registered Windows integration.</summary> /// <summary>
/// Entfernt das aktive Programmverzeichnis und die registrierte Windows-Integration.
/// </summary>
/// <param name="report">Optionale Fortschrittsausgabe für die Setup-Oberfläche.</param>
public void Uninstall(Action<string> report) public void Uninstall(Action<string> report)
{ {
var uiReport = report ?? delegate { }; var uiReport = report ?? delegate { };
@@ -289,6 +348,8 @@ namespace BizTalkPlatformManagementTool.Setup
phaseCode = "UNINSTALL-QUARANTINE"; phaseCode = "UNINSTALL-QUARANTINE";
phase = "Programmverzeichnis deaktivieren"; phase = "Programmverzeichnis deaktivieren";
write("Phase 2/3: " + phase + "."); write("Phase 2/3: " + phase + ".");
// Die atomare Umbenennung deaktiviert die Anwendung, ohne die einzige
// wiederherstellbare Kopie vor Abschluss der Deinstallation zu löschen.
if (Directory.Exists(installDirectory)) if (Directory.Exists(installDirectory))
{ {
log.Write("INFO", "event=directory_move role=uninstall_quarantine source=\"" + installDirectory + "\" target=\"" + removalDirectory + "\""); log.Write("INFO", "event=directory_move role=uninstall_quarantine source=\"" + installDirectory + "\" target=\"" + removalDirectory + "\"");
@@ -345,6 +406,11 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>
/// Registriert Startmenü, optionale Desktop-Verknüpfung, Uninstaller und Uninstall-Schlüssel.
/// </summary>
/// <param name="targetExe">Der vollständige Pfad der aktivierten Anwendung.</param>
/// <param name="createDesktopShortcut"><c>true</c>, wenn eine Desktop-Verknüpfung gewünscht ist.</param>
private void RegisterWindowsIntegration(string targetExe, bool createDesktopShortcut) private void RegisterWindowsIntegration(string targetExe, bool createDesktopShortcut)
{ {
var programsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName); var programsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName);
@@ -373,6 +439,10 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>
/// Entfernt und verifiziert die vom Setup verwaltete Windows-Integration.
/// </summary>
/// <param name="log">Das Diagnoseprotokoll des aktuellen Setup-Laufs.</param>
private void RemoveWindowsIntegration(SetupOperationLog log) private void RemoveWindowsIntegration(SetupOperationLog log)
{ {
DeleteFileIfExists(DesktopShortcutPath); DeleteFileIfExists(DesktopShortcutPath);
@@ -388,6 +458,13 @@ namespace BizTalkPlatformManagementTool.Setup
log.Write("INFO", "event=windows_integration_removed registry_key=\"HKLM\\" + UninstallKeyPath + "\""); log.Write("INFO", "event=windows_integration_removed registry_key=\"HKLM\\" + UninstallKeyPath + "\"");
} }
/// <summary>
/// Erstellt eine Windows-Verknüpfung über Windows Script Host und gibt COM-Objekte deterministisch frei.
/// </summary>
/// <param name="shortcutPath">Der vollständige Pfad der Verknüpfung.</param>
/// <param name="targetPath">Der Zielpfad der Verknüpfung.</param>
/// <param name="workingDirectory">Das Arbeitsverzeichnis des Ziels.</param>
/// <param name="description">Die in Windows sichtbare Beschreibung.</param>
private static void CreateShortcut(string shortcutPath, string targetPath, string workingDirectory, string description) private static void CreateShortcut(string shortcutPath, string targetPath, string workingDirectory, string description)
{ {
Directory.CreateDirectory(Path.GetDirectoryName(shortcutPath)); Directory.CreateDirectory(Path.GetDirectoryName(shortcutPath));
@@ -414,6 +491,10 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>
/// Liest Registrywerte, Verknüpfungen und Uninstaller vor einer möglichen Mutation ein.
/// </summary>
/// <returns>Ein wiederherstellbarer Snapshot der Windows-Integration.</returns>
private WindowsIntegrationSnapshot CaptureWindowsIntegration() private WindowsIntegrationSnapshot CaptureWindowsIntegration()
{ {
var snapshot = new WindowsIntegrationSnapshot var snapshot = new WindowsIntegrationSnapshot
@@ -437,6 +518,10 @@ namespace BizTalkPlatformManagementTool.Setup
return snapshot; return snapshot;
} }
/// <summary>
/// Stellt einen zuvor erfassten Zustand der Windows-Integration wieder her.
/// </summary>
/// <param name="snapshot">Der wiederherzustellende Integrationszustand.</param>
private void RestoreWindowsIntegration(WindowsIntegrationSnapshot snapshot) private void RestoreWindowsIntegration(WindowsIntegrationSnapshot snapshot)
{ {
if (snapshot == null) return; if (snapshot == null) return;
@@ -457,11 +542,21 @@ namespace BizTalkPlatformManagementTool.Setup
RestoreFile(UninstallerPath, snapshot.Uninstaller); RestoreFile(UninstallerPath, snapshot.Uninstaller);
} }
/// <summary>
/// Liest eine Datei vollständig oder bildet ihr Nichtvorhandensein als <c>null</c> ab.
/// </summary>
/// <param name="path">Der zu lesende Dateipfad.</param>
/// <returns>Der Dateiinhalt oder <c>null</c>, wenn die Datei nicht existiert.</returns>
private static byte[] ReadFileOrNull(string path) private static byte[] ReadFileOrNull(string path)
{ {
return File.Exists(path) ? File.ReadAllBytes(path) : null; return File.Exists(path) ? File.ReadAllBytes(path) : null;
} }
/// <summary>
/// Stellt eine Datei exakt wieder her oder entfernt sie, wenn sie vorher nicht vorhanden war.
/// </summary>
/// <param name="path">Der wiederherzustellende Dateipfad.</param>
/// <param name="content">Der vorherige Inhalt oder <c>null</c> für „nicht vorhanden“.</param>
private static void RestoreFile(string path, byte[] content) private static void RestoreFile(string path, byte[] content)
{ {
if (content == null) if (content == null)
@@ -469,11 +564,17 @@ namespace BizTalkPlatformManagementTool.Setup
DeleteFileIfExists(path); DeleteFileIfExists(path);
return; return;
} }
// Beim Rollback des laufenden Uninstallers kann dessen Datei bereits exakt dem
// Snapshot entsprechen; dann vermeiden wir einen unnötigen Schreibzugriff auf die aktive EXE.
if (File.Exists(path) && File.ReadAllBytes(path).SequenceEqual(content)) return; if (File.Exists(path) && File.ReadAllBytes(path).SequenceEqual(content)) return;
Directory.CreateDirectory(Path.GetDirectoryName(path)); Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllBytes(path, content); File.WriteAllBytes(path, content);
} }
/// <summary>
/// Verhindert Mutation, solange die installierte Anwendung noch ausgeführt wird.
/// </summary>
/// <param name="log">Das Diagnoseprotokoll für Prozessfund und Zugriffsfehler.</param>
private void EnsureApplicationNotRunning(SetupOperationLog log) private void EnsureApplicationNotRunning(SetupOperationLog log)
{ {
var target = Path.Combine(installDirectory, ApplicationExeName); var target = Path.Combine(installDirectory, ApplicationExeName);
@@ -517,6 +618,12 @@ namespace BizTalkPlatformManagementTool.Setup
log.Write("INFO", "event=running_application_check target_exists=true candidate_count=" + candidates.ToString(CultureInfo.InvariantCulture) + " result=not_running"); log.Write("INFO", "event=running_application_check target_exists=true candidate_count=" + candidates.ToString(CultureInfo.InvariantCulture) + " result=not_running");
} }
/// <summary>
/// Führt den WMI-freien Self-Test aus und validiert Exitcode sowie Erfolgstoken.
/// </summary>
/// <param name="executable">Die zu prüfende Anwendungs-EXE.</param>
/// <param name="label">Die Phasenbezeichnung für Log und Fehlermeldung.</param>
/// <param name="log">Das Diagnoseprotokoll für Metadaten und Prozessausgaben.</param>
private void RunAndValidateSelfTest(string executable, string label, SetupOperationLog log) private void RunAndValidateSelfTest(string executable, string label, SetupOperationLog log)
{ {
log.WriteFileDetails(label + "_self_test_executable", executable); log.WriteFileDetails(label + "_self_test_executable", executable);
@@ -554,10 +661,14 @@ namespace BizTalkPlatformManagementTool.Setup
using (var process = Process.Start(startInfo)) using (var process = Process.Start(startInfo))
{ {
if (process == null) throw new InvalidOperationException("Self-Test '" + label + "' konnte nicht gestartet werden."); if (process == null) throw new InvalidOperationException("Self-Test '" + label + "' konnte nicht gestartet werden.");
// Beide Kanäle werden parallel geleert, damit ein voller stdout-/stderr-Puffer
// den Kindprozess nicht blockiert und dadurch einen künstlichen Timeout erzeugt.
var outputRead = process.StandardOutput.ReadToEndAsync(); var outputRead = process.StandardOutput.ReadToEndAsync();
var errorRead = process.StandardError.ReadToEndAsync(); var errorRead = process.StandardError.ReadToEndAsync();
if (!process.WaitForExit(60000)) if (!process.WaitForExit(60000))
{ {
// Auch nach dem Timeout bleiben Prozessende und Stream-Erfassung begrenzt;
// ein nicht beendbarer Kindprozess darf das Setup nicht endlos festhalten.
var killResult = "sent"; var killResult = "sent";
try { process.Kill(); } try { process.Kill(); }
catch (Exception killException) catch (Exception killException)
@@ -610,6 +721,12 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>
/// Liest die erzeugte Windows-Integration zurück und vergleicht sie mit dem Sollzustand.
/// </summary>
/// <param name="targetExe">Der erwartete Zielpfad der Anwendung.</param>
/// <param name="desktopRequested">Der gewünschte Zustand der Desktop-Verknüpfung.</param>
/// <param name="log">Das Diagnoseprotokoll für die validierten Dateien.</param>
private void ValidateWindowsIntegration(string targetExe, bool desktopRequested, SetupOperationLog log) private void ValidateWindowsIntegration(string targetExe, bool desktopRequested, SetupOperationLog log)
{ {
if (!File.Exists(StartMenuShortcutPath)) throw new FileNotFoundException("Start menu shortcut was not created.", StartMenuShortcutPath); if (!File.Exists(StartMenuShortcutPath)) throw new FileNotFoundException("Start menu shortcut was not created.", StartMenuShortcutPath);
@@ -633,6 +750,12 @@ namespace BizTalkPlatformManagementTool.Setup
log.Write("INFO", "event=windows_integration_validated registry_key=\"HKLM\\" + UninstallKeyPath + "\" desktop_shortcut=" + desktopRequested); log.Write("INFO", "event=windows_integration_validated registry_key=\"HKLM\\" + UninstallKeyPath + "\" desktop_shortcut=" + desktopRequested);
} }
/// <summary>
/// Vergleicht einen Registrywert mit seinem erwarteten Zeichenfolgenwert.
/// </summary>
/// <param name="key">Der geöffnete Uninstall-Schlüssel.</param>
/// <param name="name">Der Name des zu prüfenden Registrywerts.</param>
/// <param name="expected">Der erwartete Wert.</param>
private static void RequireRegistryValue(RegistryKey key, string name, string expected) private static void RequireRegistryValue(RegistryKey key, string name, string expected)
{ {
var actual = Convert.ToString(key.GetValue(name, null, RegistryValueOptions.DoNotExpandEnvironmentNames), CultureInfo.InvariantCulture); var actual = Convert.ToString(key.GetValue(name, null, RegistryValueOptions.DoNotExpandEnvironmentNames), CultureInfo.InvariantCulture);
@@ -640,6 +763,13 @@ namespace BizTalkPlatformManagementTool.Setup
throw new InvalidOperationException("Uninstall registry value '" + name + "' is invalid. Expected='" + expected + "', actual='" + actual + "'."); throw new InvalidOperationException("Uninstall registry value '" + name + "' is invalid. Expected='" + expected + "', actual='" + actual + "'.");
} }
/// <summary>
/// Schreibt zuerst dauerhaft ins Log und isoliert anschließend Fehler des UI-Callbacks.
/// </summary>
/// <param name="log">Das dauerhafte Setup-Protokoll.</param>
/// <param name="report">Der optionale UI-Callback.</param>
/// <param name="level">Der Log-Level.</param>
/// <param name="message">Die auszugebende Nachricht.</param>
private static void ReportSafely(SetupOperationLog log, Action<string> report, string level, string message) private static void ReportSafely(SetupOperationLog log, Action<string> report, string level, string message)
{ {
log.Write(level, message); log.Write(level, message);
@@ -653,6 +783,11 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>
/// Protokolliert Dateisystemtyp und freien Speicher des Zielvolumes bestmöglich.
/// </summary>
/// <param name="log">Das Setup-Protokoll.</param>
/// <param name="path">Ein Pfad auf dem zu untersuchenden Volume.</param>
private static void LogDriveSpace(SetupOperationLog log, string path) private static void LogDriveSpace(SetupOperationLog log, string path)
{ {
try try
@@ -671,6 +806,11 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>
/// Verdichtet Prozessausgabe für eine begrenzte Bedienermeldung; das Log bleibt vollständig.
/// </summary>
/// <param name="value">Die rohe Standard- oder Fehlerausgabe.</param>
/// <returns>Eine einzeilige Ausgabe mit maximal 2.000 Zeichen.</returns>
private static string CompactProcessText(string value) private static string CompactProcessText(string value)
{ {
var text = (value ?? string.Empty).Replace("\r", string.Empty).Replace("\n", " | ").Trim(); var text = (value ?? string.Empty).Replace("\r", string.Empty).Replace("\n", " | ").Trim();
@@ -678,6 +818,12 @@ namespace BizTalkPlatformManagementTool.Setup
return text.Length <= maxLength ? text : text.Substring(0, maxLength) + "...[truncated]"; return text.Length <= maxLength ? text : text.Substring(0, maxLength) + "...[truncated]";
} }
/// <summary>
/// Kopiert deklarierte Payload-Dateien und prüft jede Zielkopie erneut per SHA-256.
/// </summary>
/// <param name="sourceRoot">Das validierte Payload-Quellverzeichnis.</param>
/// <param name="targetRoot">Das isolierte Staging-Zielverzeichnis.</param>
/// <param name="files">Die validierten Manifesteinträge.</param>
private static void CopyPayload(string sourceRoot, string targetRoot, IEnumerable<PackageFile> files) private static void CopyPayload(string sourceRoot, string targetRoot, IEnumerable<PackageFile> files)
{ {
Directory.CreateDirectory(targetRoot); Directory.CreateDirectory(targetRoot);
@@ -692,12 +838,23 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>
/// Stellt sicher, dass eine betriebsnotwendige Datei im Manifest enthalten ist.
/// </summary>
/// <param name="files">Die validierten Manifesteinträge.</param>
/// <param name="relativePath">Der erforderliche relative Dateipfad.</param>
private static void RequirePayload(IEnumerable<PackageFile> files, string relativePath) private static void RequirePayload(IEnumerable<PackageFile> files, string relativePath)
{ {
if (!files.Any(x => string.Equals(x.RelativePath, relativePath, StringComparison.OrdinalIgnoreCase))) if (!files.Any(x => string.Equals(x.RelativePath, relativePath, StringComparison.OrdinalIgnoreCase)))
throw new InvalidDataException("Required payload file is missing from the manifest: " + relativePath); throw new InvalidDataException("Required payload file is missing from the manifest: " + relativePath);
} }
/// <summary>
/// Löscht ein temporäres Verzeichnis bestmöglich und meldet eine verbleibende Kopie als Warnung.
/// </summary>
/// <param name="path">Das zu löschende Verzeichnis.</param>
/// <param name="report">Die ausfallsichere Fortschrittsausgabe.</param>
/// <returns><c>true</c>, wenn das Verzeichnis anschließend nicht mehr existiert.</returns>
private static bool TryDeleteDirectory(string path, Action<string> report) private static bool TryDeleteDirectory(string path, Action<string> report)
{ {
try try
@@ -712,21 +869,28 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>
/// Löscht eine Datei, sofern sie vorhanden ist, und lässt echte Löschfehler sichtbar werden.
/// </summary>
/// <param name="path">Der zu löschende Dateipfad.</param>
private static void DeleteFileIfExists(string path) private static void DeleteFileIfExists(string path)
{ {
if (File.Exists(path)) File.Delete(path); if (File.Exists(path)) File.Delete(path);
} }
/// <summary>Ruft den maschinenweiten Pfad der Desktop-Verknüpfung ab.</summary>
private static string DesktopShortcutPath private static string DesktopShortcutPath
{ {
get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), ProductName + ".lnk"); } get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), ProductName + ".lnk"); }
} }
/// <summary>Ruft den maschinenweiten Pfad der Startmenü-Verknüpfung ab.</summary>
private static string StartMenuShortcutPath private static string StartMenuShortcutPath
{ {
get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName, ProductName + ".lnk"); } get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName, ProductName + ".lnk"); }
} }
/// <summary>Ruft den dauerhaften Pfad der Uninstaller-Kopie ab.</summary>
private string UninstallerPath private string UninstallerPath
{ {
get { return Path.Combine(dataDirectory, "Setup", "Uninstall.exe"); } get { return Path.Combine(dataDirectory, "Setup", "Uninstall.exe"); }
@@ -7,16 +7,37 @@ using System.Windows.Forms;
namespace BizTalkPlatformManagementTool.Setup namespace BizTalkPlatformManagementTool.Setup
{ {
/// <summary>
/// Stellt die Bedienoberfläche für Installation, Update, Deinstallation und Diagnosezugriff bereit.
/// </summary>
internal sealed class MainForm : Form internal sealed class MainForm : Form
{ {
/// <summary>Ausführende Installer-Engine.</summary>
private readonly InstallerEngine engine; private readonly InstallerEngine engine;
/// <summary>Gibt an, ob das Fenster direkt zur Deinstallation geöffnet wurde.</summary>
private readonly bool uninstallMode; private readonly bool uninstallMode;
/// <summary>Sichtbare Fortschritts- und Diagnoseausgabe.</summary>
private readonly TextBox output = new TextBox(); private readonly TextBox output = new TextBox();
/// <summary>Auswahl für die optionale maschinenweite Desktop-Verknüpfung.</summary>
private readonly CheckBox desktopShortcut = new CheckBox(); private readonly CheckBox desktopShortcut = new CheckBox();
/// <summary>Schaltfläche für Installation oder Update.</summary>
private readonly Button installButton = new Button(); private readonly Button installButton = new Button();
/// <summary>Schaltfläche für die Deinstallation.</summary>
private readonly Button uninstallButton = new Button(); private readonly Button uninstallButton = new Button();
/// <summary>Verhindert parallele Aktionen und Schließen während einer Operation.</summary>
private bool busy; private bool busy;
/// <summary>
/// Initialisiert das Setup-Fenster für den normalen oder direkten Deinstallationsmodus.
/// </summary>
/// <param name="engine">Die ausführende Installer-Engine.</param>
/// <param name="uninstallMode"><c>true</c>, wenn das Setup über den Uninstall-Eintrag gestartet wurde.</param>
public MainForm(InstallerEngine engine, bool uninstallMode) public MainForm(InstallerEngine engine, bool uninstallMode)
{ {
this.engine = engine; this.engine = engine;
@@ -30,6 +51,7 @@ namespace BizTalkPlatformManagementTool.Setup
FormClosing += OnFormClosing; FormClosing += OnFormClosing;
} }
/// <summary>Erstellt und verdrahtet die vollständige Setup-Oberfläche.</summary>
private void BuildUi() private void BuildUi()
{ {
var root = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(16), RowCount = 5, ColumnCount = 1 }; var root = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(16), RowCount = 5, ColumnCount = 1 };
@@ -43,7 +65,7 @@ namespace BizTalkPlatformManagementTool.Setup
{ {
AutoSize = true, AutoSize = true,
Font = new Font(Font.FontFamily, 14, FontStyle.Bold), Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
Text = "BizTalk Platform Management Tool 2.1.1" Text = "BizTalk Platform Management Tool 2.1.2"
}); });
root.Controls.Add(new Label root.Controls.Add(new Label
{ {
@@ -89,6 +111,9 @@ namespace BizTalkPlatformManagementTool.Setup
else if (!engine.HasInstallPayload) Append("Kein Installationspayload neben Setup.exe gefunden. Dieser Aufruf erlaubt nur die Deinstallation."); else if (!engine.HasInstallPayload) Append("Kein Installationspayload neben Setup.exe gefunden. Dieser Aufruf erlaubt nur die Deinstallation.");
} }
/// <summary>Öffnet das zuletzt verwendete Diagnoseverzeichnis im Windows Explorer.</summary>
/// <param name="sender">Die auslösende Schaltfläche.</param>
/// <param name="e">Die Ereignisargumente.</param>
private void OpenLogs(object sender, EventArgs e) private void OpenLogs(object sender, EventArgs e)
{ {
try try
@@ -102,6 +127,8 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>Startet Installation oder Deinstallation außerhalb des UI-Threads.</summary>
/// <param name="uninstall"><c>true</c> für Deinstallation, <c>false</c> für Installation oder Update.</param>
private void Run(bool uninstall) 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) if (uninstall && MessageBox.Show(this, "BizTalk Platform Management Tool wirklich deinstallieren?", "Deinstallation bestaetigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes)
@@ -109,6 +136,7 @@ namespace BizTalkPlatformManagementTool.Setup
var createDesktopShortcut = desktopShortcut.Checked; var createDesktopShortcut = desktopShortcut.Checked;
SetBusy(true); SetBusy(true);
// Dateisystem-, Registry- und Self-Test-Operationen dürfen die WinForms-Nachrichtenpumpe nicht blockieren.
Task.Run(() => Task.Run(() =>
{ {
try try
@@ -134,6 +162,8 @@ namespace BizTalkPlatformManagementTool.Setup
}); });
} }
/// <summary>Fügt der sichtbaren Ausgabe threadsicher eine zeitgestempelte Nachricht hinzu.</summary>
/// <param name="message">Die anzuzeigende Nachricht.</param>
private void Append(string message) private void Append(string message)
{ {
if (IsDisposed || Disposing) return; if (IsDisposed || Disposing) return;
@@ -145,6 +175,8 @@ namespace BizTalkPlatformManagementTool.Setup
output.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message + Environment.NewLine); output.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message + Environment.NewLine);
} }
/// <summary>Schaltet Steuerelemente und Wartecursor threadsicher in oder aus dem Arbeitszustand.</summary>
/// <param name="value"><c>true</c>, solange eine Setup-Operation läuft.</param>
private void SetBusy(bool value) private void SetBusy(bool value)
{ {
if (InvokeRequired) if (InvokeRequired)
@@ -159,6 +191,9 @@ namespace BizTalkPlatformManagementTool.Setup
UseWaitCursor = value; UseWaitCursor = value;
} }
/// <summary>Verhindert das Schließen des Fensters während einer laufenden Setup-Operation.</summary>
/// <param name="sender">Das zu schließende Setup-Fenster.</param>
/// <param name="e">Die abbrechbaren Argumente des Schließereignisses.</param>
private void OnFormClosing(object sender, FormClosingEventArgs e) private void OnFormClosing(object sender, FormClosingEventArgs e)
{ {
if (!busy) return; if (!busy) return;
@@ -8,6 +8,7 @@ using System.Text;
namespace BizTalkPlatformManagementTool.Setup namespace BizTalkPlatformManagementTool.Setup
{ {
/// <summary>Beschreibt eine durch Länge und SHA-256 abgesicherte Payload-Datei.</summary>
public sealed class PackageFile public sealed class PackageFile
{ {
/// <summary>Gets or sets the normalized payload-relative path.</summary> /// <summary>Gets or sets the normalized payload-relative path.</summary>
@@ -18,9 +19,13 @@ namespace BizTalkPlatformManagementTool.Setup
public string Sha256 { get; set; } public string Sha256 { get; set; }
} }
/// <summary>Erzeugt und validiert das vollständige kryptografische Payload-Manifest.</summary>
public static class PackageManifest public static class PackageManifest
{ {
/// <summary>Reads and cryptographically validates a complete application payload manifest.</summary> /// <summary>Liest das Manifest und validiert jede sowie ausschließlich jede Payload-Datei.</summary>
/// <param name="applicationDirectory">Das Wurzelverzeichnis der Anwendungs-Payload.</param>
/// <param name="manifestPath">Der Pfad des zu prüfenden Manifests.</param>
/// <returns>Die validierten und normalisierten Manifesteinträge.</returns>
public static IList<PackageFile> ValidateAndRead(string applicationDirectory, string manifestPath) public static IList<PackageFile> ValidateAndRead(string applicationDirectory, string manifestPath)
{ {
if (!Directory.Exists(applicationDirectory)) throw new DirectoryNotFoundException("Application payload missing: " + applicationDirectory); if (!Directory.Exists(applicationDirectory)) throw new DirectoryNotFoundException("Application payload missing: " + applicationDirectory);
@@ -49,6 +54,8 @@ namespace BizTalkPlatformManagementTool.Setup
} }
if (files.Count == 0) throw new InvalidDataException("The package manifest does not contain payload files."); if (files.Count == 0) throw new InvalidDataException("The package manifest does not contain payload files.");
// Der Mengenvergleich verhindert, dass nicht deklarierte DLLs oder Konfigurationen
// unbemerkt mit administrativen Rechten installiert werden.
var actualFiles = Directory.GetFiles(applicationDirectory, "*", SearchOption.AllDirectories) var actualFiles = Directory.GetFiles(applicationDirectory, "*", SearchOption.AllDirectories)
.Select(x => NormalizeRelativePath(x.Substring(Path.GetFullPath(applicationDirectory).TrimEnd(Path.DirectorySeparatorChar).Length + 1))) .Select(x => NormalizeRelativePath(x.Substring(Path.GetFullPath(applicationDirectory).TrimEnd(Path.DirectorySeparatorChar).Length + 1)))
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray(); .OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray();
@@ -58,7 +65,9 @@ namespace BizTalkPlatformManagementTool.Setup
return files; return files;
} }
/// <summary>Creates a deterministic manifest covering every application payload file.</summary> /// <summary>Erzeugt ein deterministisch sortiertes Manifest über die vollständige Payload.</summary>
/// <param name="applicationDirectory">Das Wurzelverzeichnis der Anwendungs-Payload.</param>
/// <param name="manifestPath">Der Zielpfad des Manifests.</param>
public static void Write(string applicationDirectory, string manifestPath) public static void Write(string applicationDirectory, string manifestPath)
{ {
var root = Path.GetFullPath(applicationDirectory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; var root = Path.GetFullPath(applicationDirectory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
@@ -70,7 +79,9 @@ namespace BizTalkPlatformManagementTool.Setup
File.WriteAllLines(manifestPath, lines, new UTF8Encoding(false)); File.WriteAllLines(manifestPath, lines, new UTF8Encoding(false));
} }
/// <summary>Calculates the lowercase SHA-256 digest of a file.</summary> /// <summary>Berechnet den kleingeschriebenen SHA-256-Hash einer Datei.</summary>
/// <param name="path">Der Pfad der zu prüfenden Datei.</param>
/// <returns>Der SHA-256-Hash als 64-stellige Hexadezimalzeichenfolge.</returns>
public static string Sha256(string path) public static string Sha256(string path)
{ {
using (var stream = File.OpenRead(path)) using (var stream = File.OpenRead(path))
@@ -83,7 +94,10 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>Resolves a relative payload path and rejects directory traversal.</summary> /// <summary>Löst einen relativen Payload-Pfad auf und weist Directory Traversal zurück.</summary>
/// <param name="root">Das erlaubte Payload-Wurzelverzeichnis.</param>
/// <param name="relative">Der relative Pfad aus dem Manifest.</param>
/// <returns>Der vollständig aufgelöste, innerhalb von <paramref name="root"/> liegende Pfad.</returns>
public static string ResolveContainedPath(string root, string relative) public static string ResolveContainedPath(string root, string relative)
{ {
var normalizedRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; var normalizedRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
@@ -92,6 +106,11 @@ namespace BizTalkPlatformManagementTool.Setup
return result; return result;
} }
/// <summary>
/// Normalisiert Pfadtrenner und weist absolute, leere oder ausbrechende Pfade zurück.
/// </summary>
/// <param name="path">Der zu normalisierende relative Pfad.</param>
/// <returns>Der normalisierte Pfad mit Schrägstrichen.</returns>
private static string NormalizeRelativePath(string path) private static string NormalizeRelativePath(string path)
{ {
path = (path ?? string.Empty).Replace('\\', '/').Trim(); path = (path ?? string.Empty).Replace('\\', '/').Trim();
@@ -3,8 +3,12 @@ using System.Windows.Forms;
namespace BizTalkPlatformManagementTool.Setup namespace BizTalkPlatformManagementTool.Setup
{ {
/// <summary>Enthält den Einstiegspunkt des administrativen Windows-Setups.</summary>
internal static class Program internal static class Program
{ {
/// <summary>Startet die Setup-Oberfläche im Installations- oder Deinstallationsmodus.</summary>
/// <param name="args">Befehlszeilenargumente; <c>--uninstall</c> aktiviert die Deinstallation.</param>
/// <returns>Null nach regulärem Schließen der Oberfläche.</returns>
[STAThread] [STAThread]
private static int Main(string[] args) private static int Main(string[] args)
{ {
@@ -8,6 +8,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyProduct("BizTalk Platform Management Tool")] [assembly: AssemblyProduct("BizTalk Platform Management Tool")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")] [assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
[assembly: AssemblyVersion("2.1.1.0")] [assembly: AssemblyVersion("2.1.2.0")]
[assembly: AssemblyFileVersion("2.1.1.0")] [assembly: AssemblyFileVersion("2.1.2.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")] [assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
@@ -8,22 +8,38 @@ using System.Text;
namespace BizTalkPlatformManagementTool.Setup namespace BizTalkPlatformManagementTool.Setup
{ {
/// <summary> /// <summary>
/// Writes a durable, single-line diagnostic trace for one setup operation. /// Schreibt ein dauerhaftes, einzeiliges Diagnoseprotokoll für genau einen Setup-Lauf.
/// No credential or other secret is accepted by this component. /// Die Komponente übernimmt keine Kennwörter oder andere Geheimnisse.
/// </summary> /// </summary>
internal sealed class SetupOperationLog internal sealed class SetupOperationLog
{ {
/// <summary>Serialisiert konkurrierende Schreibzugriffe innerhalb des Setup-Prozesses.</summary>
private readonly object sync = new object(); private readonly object sync = new object();
/// <summary>
/// Initialisiert ein Protokoll für einen bereits festgelegten Dateipfad.
/// </summary>
/// <param name="filePath">Der Logpfad oder eine leere Zeichenfolge bei vollständig ausgefallenem Logging.</param>
private SetupOperationLog(string filePath) private SetupOperationLog(string filePath)
{ {
FilePath = filePath; FilePath = filePath;
} }
/// <summary>Ruft den tatsächlich verwendeten Logpfad ab.</summary>
public string FilePath { get; private set; } public string FilePath { get; private set; }
/// <summary>Ruft die formatierte Ursache eines Fehlers beim primären Logaufbau ab.</summary>
public string CreationError { get; private set; } public string CreationError { get; private set; }
/// <summary>Ruft ab, ob das Log im temporären Rückfallverzeichnis liegt.</summary>
public bool IsFallback { get; private set; } public bool IsFallback { get; private set; }
/// <summary>
/// Erstellt ein Setup-Protokoll unter ProgramData oder ersatzweise im Temp-Verzeichnis.
/// </summary>
/// <param name="dataDirectory">Das bevorzugte dauerhafte Datenverzeichnis.</param>
/// <param name="operation">Die kurze Operationsbezeichnung für den Dateinamen und Kontextkopf.</param>
/// <returns>Ein verwendbares Protokollobjekt, auch wenn keine Logdatei angelegt werden konnte.</returns>
public static SetupOperationLog Create(string dataDirectory, string operation) public static SetupOperationLog Create(string dataDirectory, string operation)
{ {
try try
@@ -34,6 +50,8 @@ namespace BizTalkPlatformManagementTool.Setup
{ {
try try
{ {
// Ohne primäres ProgramData-Log bleibt wenigstens im Benutzer-Temp ein
// Diagnosepfad erhalten; die ursprüngliche Ursache wird dort mitgeschrieben.
var fallback = CreateInDirectory( var fallback = CreateInDirectory(
Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool", "InstallerLogs"), Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool", "InstallerLogs"),
operation); operation);
@@ -55,6 +73,12 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>
/// Legt eine eindeutig benannte Logdatei an und schreibt den technischen Kontextkopf.
/// </summary>
/// <param name="directory">Das Zielverzeichnis der Logdatei.</param>
/// <param name="operation">Die Bezeichnung des Setup-Laufs.</param>
/// <returns>Das initialisierte Setup-Protokoll.</returns>
private static SetupOperationLog CreateInDirectory(string directory, string operation) private static SetupOperationLog CreateInDirectory(string directory, string operation)
{ {
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
@@ -81,6 +105,11 @@ namespace BizTalkPlatformManagementTool.Setup
return log; return log;
} }
/// <summary>
/// Schreibt eine UTC-zeitgestempelte, einzeilige Nachricht ausfallsicher in die Logdatei.
/// </summary>
/// <param name="level">Der textuelle Log-Level.</param>
/// <param name="message">Die zu protokollierende Nachricht.</param>
public void Write(string level, string message) public void Write(string level, string message)
{ {
if (string.IsNullOrEmpty(FilePath)) return; if (string.IsNullOrEmpty(FilePath)) return;
@@ -97,15 +126,26 @@ namespace BizTalkPlatformManagementTool.Setup
} }
catch catch
{ {
// Diagnostic logging must never replace the actual setup outcome. // Das Diagnose-Logging darf das eigentliche Setup-Ergebnis niemals ersetzen.
} }
} }
/// <summary>
/// Protokolliert einen Fehler mit stabiler Kennung, Phase und vollständiger Exception-Kette.
/// </summary>
/// <param name="errorCode">Die stabile maschinenlesbare Fehlerkennung.</param>
/// <param name="phase">Die lesbare Setup-Phase.</param>
/// <param name="exception">Die zu protokollierende Ausnahme.</param>
public void WriteException(string errorCode, string phase, Exception exception) public void WriteException(string errorCode, string phase, Exception exception)
{ {
Write("ERROR", "event=exception error_code=" + errorCode + " phase=\"" + phase + "\" " + FormatException(exception)); Write("ERROR", "event=exception error_code=" + errorCode + " phase=\"" + phase + "\" " + FormatException(exception));
} }
/// <summary>
/// Protokolliert Existenz, Größe, Zeitstempel, Dateiversion und SHA-256 einer Datei.
/// </summary>
/// <param name="label">Die fachliche Rolle der Datei im Setup.</param>
/// <param name="path">Der zu untersuchende Dateipfad.</param>
public void WriteFileDetails(string label, string path) public void WriteFileDetails(string label, string path)
{ {
try try
@@ -131,6 +171,11 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>
/// Formatiert eine Exception-Kette mit Typ, HRESULT, Nachricht und Stacktrace.
/// </summary>
/// <param name="exception">Die äußerste Ausnahme.</param>
/// <returns>Eine einzeilige Diagnose mit höchstens zwölf Exception-Ebenen.</returns>
internal static string FormatException(Exception exception) internal static string FormatException(Exception exception)
{ {
var result = new StringBuilder(); var result = new StringBuilder();
@@ -149,6 +194,10 @@ namespace BizTalkPlatformManagementTool.Setup
return SingleLine(result.ToString()); return SingleLine(result.ToString());
} }
/// <summary>
/// Ermittelt die aktuelle Windows-Identität ohne einen Diagnosefehler weiterzureichen.
/// </summary>
/// <returns>Der Identitätsname oder <c>(unknown)</c>.</returns>
private static string CurrentIdentity() private static string CurrentIdentity()
{ {
try try
@@ -164,6 +213,10 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>
/// Ermittelt, ob der aktuelle Prozess mit Administratorrechten läuft.
/// </summary>
/// <returns><c>true</c>, <c>false</c> oder <c>unknown</c>.</returns>
private static string IsElevated() private static string IsElevated()
{ {
try try
@@ -179,11 +232,20 @@ namespace BizTalkPlatformManagementTool.Setup
} }
} }
/// <summary>
/// Maskiert Steuerzeichen, damit jeder Logeintrag genau eine physische Zeile belegt.
/// </summary>
/// <param name="value">Der zu normalisierende Text.</param>
/// <returns>Der einzeilige Text.</returns>
private static string SingleLine(string value) private static string SingleLine(string value)
{ {
return (value ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t"); return (value ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t");
} }
/// <summary>
/// Entfernt Setup-Protokolle, deren letzte Änderung mehr als 90 Tage zurückliegt.
/// </summary>
/// <param name="directory">Das zu bereinigende Installer-Logverzeichnis.</param>
private static void CleanupOldLogs(string directory) private static void CleanupOldLogs(string directory)
{ {
try try
@@ -196,7 +258,7 @@ namespace BizTalkPlatformManagementTool.Setup
} }
catch catch
{ {
// Retention cleanup is best-effort. // Die Aufbewahrungsbereinigung ist bestmöglich und blockiert kein Setup.
} }
} }
} }
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="2.1.1.0" name="BizTalkPlatformManagementTool.Setup" /> <assemblyIdentity version="2.1.2.0" name="BizTalkPlatformManagementTool.Setup" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security> <security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
</trustInfo> </trustInfo>
@@ -34,6 +34,7 @@
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit> <Prefer32Bit>false</Prefer32Bit>
<DocumentationFile>bin\Release\BizTalkPlatformManagementTool.xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
@@ -14,9 +14,13 @@ namespace BizTalkPlatformManagementTool
/// <summary> /// <summary>
/// Starts the application after verifying that BizTalk WMI operations can run elevated. /// Starts the application after verifying that BizTalk WMI operations can run elevated.
/// </summary> /// </summary>
/// <param name="args">Befehlszeilenargumente; <c>--self-test</c> startet die WMI-freie Installerprüfung.</param>
/// <returns>Null bei erfolgreichem Abschluss, andernfalls ein prozessgeeigneter Fehlercode.</returns>
[STAThread] [STAThread]
private static int Main(string[] args) private static int Main(string[] args)
{ {
// Der Self-Test muss ohne Administratorprüfung und ohne WinForms-Oberfläche laufen,
// damit der Installer ihn bereits im isolierten Staging-Verzeichnis ausführen kann.
if (args != null && args.Length == 1 && string.Equals(args[0], "--self-test", StringComparison.OrdinalIgnoreCase)) if (args != null && args.Length == 1 && string.Equals(args[0], "--self-test", StringComparison.OrdinalIgnoreCase))
{ {
return RuntimeSelfTest.Run(); return RuntimeSelfTest.Run();
@@ -38,6 +42,7 @@ namespace BizTalkPlatformManagementTool
} }
bool createdNew; bool createdNew;
// Der sitzungsbezogene Mutex verhindert konkurrierende Wartungsoperationen desselben Benutzers.
using (var mutex = new Mutex(true, @"Local\BizTalkPlatformManagementTool", out createdNew)) using (var mutex = new Mutex(true, @"Local\BizTalkPlatformManagementTool", out createdNew))
{ {
if (!createdNew) if (!createdNew)
@@ -8,5 +8,5 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")] [assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
[assembly: AssemblyVersion("2.1.1.0")] [assembly: AssemblyVersion("2.1.2.0")]
[assembly: AssemblyFileVersion("2.1.1.0")] [assembly: AssemblyFileVersion("2.1.2.0")]
@@ -10,6 +10,10 @@ namespace BizTalkPlatformManagementTool
/// </summary> /// </summary>
internal static class RuntimeSelfTest internal static class RuntimeSelfTest
{ {
/// <summary>
/// Prüft Serialisierung, Validierung, Vergleich und Report-Persistenz ohne BizTalk-WMI-Zugriff.
/// </summary>
/// <returns>Null bei erfolgreicher Prüfung, andernfalls eins.</returns>
public static int Run() public static int Run()
{ {
var directory = Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool.SelfTest." + Guid.NewGuid().ToString("N")); var directory = Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool.SelfTest." + Guid.NewGuid().ToString("N"));
@@ -50,11 +54,16 @@ namespace BizTalkPlatformManagementTool
} }
catch catch
{ {
// The self-test result is more important than temporary cleanup. // Ein Bereinigungsfehler darf das bereits feststehende Self-Test-Ergebnis nicht überschreiben.
} }
} }
} }
/// <summary>
/// Erstellt einen minimalen, aber vollständig validierbaren Snapshot für den Self-Test.
/// </summary>
/// <param name="sendPortState">Der rohe BizTalk-Status des enthaltenen Send Ports.</param>
/// <returns>Ein Snapshot mit genau einer Anwendung und einem Send Port.</returns>
private static BizTalkSnapshot SampleSnapshot(int sendPortState) private static BizTalkSnapshot SampleSnapshot(int sendPortState)
{ {
var snapshot = new BizTalkSnapshot var snapshot = new BizTalkSnapshot
@@ -15,7 +15,7 @@ namespace BizTalkPlatformManagementTool.Services
/// <summary> /// <summary>
/// Current tool version written into generated snapshots. /// Current tool version written into generated snapshots.
/// </summary> /// </summary>
public const string Version = "2.1.1-net461"; public const string Version = "2.1.2-net461";
/// <summary> /// <summary>
/// Fallback application name used when WMI does not expose an application property. /// Fallback application name used when WMI does not expose an application property.
@@ -185,6 +185,8 @@ namespace BizTalkPlatformManagementTool.Services
SnapshotValidator.EnsureServerMatches(snapshot, server); SnapshotValidator.EnsureServerMatches(snapshot, server);
var plan = NewPlan(OperationMode.Shutdown, server); var plan = NewPlan(OperationMode.Shutdown, server);
// Eingehenden Verkehr zuerst stoppen, bevor abhängige Verarbeitungsartefakte
// und zuletzt die Host Instances heruntergefahren werden.
foreach (var app in snapshot.Applications) foreach (var app in snapshot.Applications)
{ {
foreach (var item in app.ReceiveLocations.Where(x => x.Enabled)) foreach (var item in app.ReceiveLocations.Where(x => x.Enabled))
@@ -228,6 +230,8 @@ namespace BizTalkPlatformManagementTool.Services
SnapshotValidator.EnsureServerMatches(snapshot, server); SnapshotValidator.EnsureServerMatches(snapshot, server);
var plan = NewPlan(OperationMode.Restore, server); var plan = NewPlan(OperationMode.Restore, server);
// Beim Restore gilt die umgekehrte Abhängigkeitsrichtung: zuerst Laufzeit-Hosts,
// danach ausgehende Verarbeitung und Receive Locations bewusst ganz zum Schluss.
foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted)) foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted))
{ {
var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Start host instance", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Start", null, ArtifactStates.HostStarted); var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Start host instance", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Start", null, ArtifactStates.HostStarted);
@@ -269,6 +273,8 @@ namespace BizTalkPlatformManagementTool.Services
} }
else if (item.OrchestrationStatus == ArtifactStates.OrchestrationBound) else if (item.OrchestrationStatus == ArtifactStates.OrchestrationBound)
{ {
// Ein blindes Unenlist würde Bound nach Unbound verschieben und damit
// einen anderen Zustand als im Snapshot herstellen.
plan.Steps.Add(new OperationStep plan.Steps.Add(new OperationStep
{ {
Kind = "Note", Kind = "Note",
@@ -321,6 +327,7 @@ namespace BizTalkPlatformManagementTool.Services
if (options.DryRun) if (options.DryRun)
{ {
// Dry-run löst das Objekt absichtlich nicht erneut per WMI auf und führt keine Methode aus.
_logger.Info("DRY RUN: " + step.Action + " '" + step.Name + "'"); _logger.Info("DRY RUN: " + step.Action + " '" + step.Name + "'");
continue; continue;
} }
@@ -611,6 +618,10 @@ namespace BizTalkPlatformManagementTool.Services
return UnknownApplication; return UnknownApplication;
} }
/// <summary>
/// Gibt alle von einer WMI-Abfrage übernommenen Objekte deterministisch frei.
/// </summary>
/// <param name="items">Die freizugebenden WMI-Objekte oder <c>null</c>.</param>
private static void DisposeAll(IEnumerable<ManagementObject> items) private static void DisposeAll(IEnumerable<ManagementObject> items)
{ {
if (items == null) if (items == null)
@@ -151,6 +151,8 @@ namespace BizTalkPlatformManagementTool.Services
try try
{ {
// Bewusst keine WQL-WHERE-Klausel: BizTalk-Namen können Zeichen enthalten,
// die sonst eine fehlerhafte oder anders interpretierte Query erzeugen.
ManagementObject match = null; ManagementObject match = null;
var items = Query(className, false); var items = Query(className, false);
foreach (var item in items) foreach (var item in items)
@@ -256,6 +258,7 @@ namespace BizTalkPlatformManagementTool.Services
{ {
break; break;
} }
// Am Timeout-Ende nur noch die tatsächlich verbleibende Zeit schlafen.
Thread.Sleep(remaining < TimeSpan.FromSeconds(delay) ? remaining : TimeSpan.FromSeconds(delay)); Thread.Sleep(remaining < TimeSpan.FromSeconds(delay) ? remaining : TimeSpan.FromSeconds(delay));
} }
@@ -95,6 +95,7 @@ namespace BizTalkPlatformManagementTool.Services
value = value ?? string.Empty; value = value ?? string.Empty;
if (value.Length > 0 && (value[0] == '=' || value[0] == '+' || value[0] == '-' || value[0] == '@' || value[0] == '\t')) if (value.Length > 0 && (value[0] == '=' || value[0] == '+' || value[0] == '-' || value[0] == '@' || value[0] == '\t'))
{ {
// Tabellenkalkulationen dürfen exportierte Namen nicht als Formel ausführen.
value = "'" + value; value = "'" + value;
} }
if (value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) < 0) if (value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) < 0)
@@ -130,6 +130,7 @@ namespace BizTalkPlatformManagementTool.Services
/// <returns>An HTML-safe value.</returns> /// <returns>An HTML-safe value.</returns>
private static string Encode(string value) private static string Encode(string value)
{ {
// Alle aus BizTalk gelesenen Werte werden vor der Aufnahme in HTML neutralisiert.
return WebUtility.HtmlEncode(value ?? string.Empty); return WebUtility.HtmlEncode(value ?? string.Empty);
} }
} }
@@ -123,8 +123,12 @@ namespace BizTalkPlatformManagementTool.Services
/// Writes a file through a same-directory temporary file so an interrupted /// Writes a file through a same-directory temporary file so an interrupted
/// save cannot leave a truncated snapshot or operation plan behind. /// save cannot leave a truncated snapshot or operation plan behind.
/// </summary> /// </summary>
/// <param name="path">Der endgültige Zielpfad.</param>
/// <param name="content">Der vollständig serialisierte Dateiinhalt.</param>
private static void WriteAtomically(string path, string content) private static void WriteAtomically(string path, string content)
{ {
// Temporärdatei und Backup liegen absichtlich im Zielverzeichnis. Dadurch bleiben
// Umbenennung und Austausch auf demselben Volume und können atomar erfolgen.
var temporaryPath = path + ".tmp." + Guid.NewGuid().ToString("N"); var temporaryPath = path + ".tmp." + Guid.NewGuid().ToString("N");
var backupPath = path + ".bak." + Guid.NewGuid().ToString("N"); var backupPath = path + ".bak." + Guid.NewGuid().ToString("N");
try try
@@ -156,6 +160,12 @@ namespace BizTalkPlatformManagementTool.Services
} }
} }
/// <summary>
/// Ersetzt eine vorhandene Datei über Umbenennungen, wenn <see cref="File.Replace(string, string, string, bool)"/> nicht unterstützt wird.
/// </summary>
/// <param name="path">Der endgültige Zielpfad.</param>
/// <param name="temporaryPath">Die vollständig geschriebene Temporärdatei.</param>
/// <param name="backupPath">Der temporäre Sicherungspfad der vorherigen Datei.</param>
private static void ReplaceWithRenameFallback(string path, string temporaryPath, string backupPath) private static void ReplaceWithRenameFallback(string path, string temporaryPath, string backupPath)
{ {
File.Move(path, backupPath); File.Move(path, backupPath);
@@ -174,6 +184,10 @@ namespace BizTalkPlatformManagementTool.Services
} }
} }
/// <summary>
/// Löscht eine temporäre Datei bestmöglich, ohne das primäre Speicherergebnis zu verändern.
/// </summary>
/// <param name="path">Der zu löschende Dateipfad.</param>
private static void TryDelete(string path) private static void TryDelete(string path)
{ {
try try
@@ -185,7 +199,7 @@ namespace BizTalkPlatformManagementTool.Services
} }
catch catch
{ {
// Temporary cleanup is best-effort and must not hide the save result. // Die Bereinigung ist nachrangig und darf einen erfolgreichen Schreibvorgang nicht verdecken.
} }
} }
} }
@@ -197,7 +197,7 @@ namespace BizTalkPlatformManagementTool.Services
} }
catch catch
{ {
// Logging must never interrupt BizTalk operations. // Ein Logfehler darf niemals eine fachliche BizTalk-Operation abbrechen.
} }
} }
@@ -225,10 +225,14 @@ namespace BizTalkPlatformManagementTool.Services
} }
catch catch
{ {
// Log retention cleanup is best-effort. // Die Aufbewahrungsbereinigung ist bestmöglich und beeinflusst den aktuellen Lauf nicht.
} }
} }
/// <summary>
/// Ermittelt das bevorzugte maschinenweite Logverzeichnis mit Rückfall auf das EXE-Verzeichnis.
/// </summary>
/// <returns>Ein verwendbares Verzeichnis für die täglichen Laufzeitlogs.</returns>
private static string ResolveLogDirectory() private static string ResolveLogDirectory()
{ {
var commonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); var commonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
@@ -137,6 +137,7 @@ namespace BizTalkPlatformManagementTool.Services
foreach (var item in before) foreach (var item in before)
{ {
// Hostname allein ist gruppenweit nicht eindeutig; der Server gehört zur Identität.
beforeMap[SnapshotValidator.ArtifactKey(item.Server, item.InstanceName)] = item; beforeMap[SnapshotValidator.ArtifactKey(item.Server, item.InstanceName)] = item;
} }
foreach (var item in after) foreach (var item in after)
@@ -9,7 +9,10 @@ namespace BizTalkPlatformManagementTool.Services
/// </summary> /// </summary>
public static class SnapshotValidator public static class SnapshotValidator
{ {
/// <summary>Normalizes optional collections and rejects missing or duplicate artifact identities.</summary> /// <summary>
/// Normalisiert optionale Sammlungen und weist fehlende oder doppelte Artefaktidentitäten zurück.
/// </summary>
/// <param name="snapshot">Der zu normalisierende und zu validierende Snapshot.</param>
public static void Validate(BizTalkSnapshot snapshot) public static void Validate(BizTalkSnapshot snapshot)
{ {
if (snapshot == null) if (snapshot == null)
@@ -55,7 +58,11 @@ namespace BizTalkPlatformManagementTool.Services
} }
} }
/// <summary>Validates a snapshot and ensures it belongs to the requested operation server.</summary> /// <summary>
/// Validiert einen Snapshot und stellt sicher, dass er zum angeforderten Zielserver gehört.
/// </summary>
/// <param name="snapshot">Der als Operationsgrundlage verwendete Snapshot.</param>
/// <param name="targetServer">Der für die Operation ausgewählte BizTalk-Server.</param>
public static void EnsureServerMatches(BizTalkSnapshot snapshot, string targetServer) public static void EnsureServerMatches(BizTalkSnapshot snapshot, string targetServer)
{ {
Validate(snapshot); Validate(snapshot);
@@ -69,7 +76,12 @@ namespace BizTalkPlatformManagementTool.Services
} }
} }
/// <summary>Compares server names while accepting short-name/FQDN variants of the same host.</summary> /// <summary>
/// Vergleicht Servernamen und akzeptiert Kurzname und FQDN desselben Hosts als identisch.
/// </summary>
/// <param name="left">Der erste Servername.</param>
/// <param name="right">Der zweite Servername.</param>
/// <returns><c>true</c>, wenn beide Namen denselben Server bezeichnen; andernfalls <c>false</c>.</returns>
public static bool ServerNamesEqual(string left, string right) public static bool ServerNamesEqual(string left, string right)
{ {
var normalizedLeft = NormalizeServer(left); var normalizedLeft = NormalizeServer(left);
@@ -82,12 +94,28 @@ namespace BizTalkPlatformManagementTool.Services
return string.Equals(ShortName(normalizedLeft), ShortName(normalizedRight), StringComparison.OrdinalIgnoreCase); return string.Equals(ShortName(normalizedLeft), ShortName(normalizedRight), StringComparison.OrdinalIgnoreCase);
} }
/// <summary>Builds the collision-safe identity used for application artifacts.</summary> /// <summary>
/// Erstellt die kollisionsarme Identität für anwendungsbezogene BizTalk-Artefakte.
/// </summary>
/// <param name="application">Der Name der BizTalk-Anwendung.</param>
/// <param name="name">Der Artefaktname.</param>
/// <returns>Ein zusammengesetzter Schlüssel aus Anwendung und Artefaktname.</returns>
public static string ArtifactKey(string application, string name) public static string ArtifactKey(string application, string name)
{ {
// Das nicht druckbare Trennzeichen kann in normalen BizTalk-Namen nicht mit der
// sichtbaren Verkettung von Anwendung und Artefakt verwechselt werden.
return (application ?? string.Empty).Trim() + "\u001f" + (name ?? string.Empty).Trim(); return (application ?? string.Empty).Trim() + "\u001f" + (name ?? string.Empty).Trim();
} }
/// <summary>
/// Prüft eine Artefaktsammlung auf leere Namen und doppelte Identitäten.
/// </summary>
/// <typeparam name="T">Der Typ des zu prüfenden Snapshot-Artefakts.</typeparam>
/// <param name="application">Die besitzende BizTalk-Anwendung.</param>
/// <param name="type">Die lesbare Artefaktbezeichnung für Fehlermeldungen.</param>
/// <param name="values">Die zu prüfenden Artefakte.</param>
/// <param name="getName">Funktion zum Ermitteln des Artefaktnamens.</param>
/// <param name="keys">Die bereits bekannten Identitäten dieses Artefakttyps.</param>
private static void ValidateArtifacts<T>(string application, string type, IEnumerable<T> values, Func<T, string> getName, HashSet<string> keys) private static void ValidateArtifacts<T>(string application, string type, IEnumerable<T> values, Func<T, string> getName, HashSet<string> keys)
{ {
foreach (var value in values) foreach (var value in values)
@@ -105,6 +133,11 @@ namespace BizTalkPlatformManagementTool.Services
} }
} }
/// <summary>
/// Normalisiert lokale Serveraliasnamen auf den tatsächlichen Rechnernamen.
/// </summary>
/// <param name="value">Der eingegebene Servername.</param>
/// <returns>Der getrimmte und normalisierte Servername.</returns>
private static string NormalizeServer(string value) private static string NormalizeServer(string value)
{ {
value = (value ?? string.Empty).Trim().TrimStart('\\'); value = (value ?? string.Empty).Trim().TrimStart('\\');
@@ -115,6 +148,11 @@ namespace BizTalkPlatformManagementTool.Services
return value; return value;
} }
/// <summary>
/// Entfernt den DNS-Suffix eines Servernamens.
/// </summary>
/// <param name="value">Ein normalisierter Kurzname oder FQDN.</param>
/// <returns>Der Hostanteil vor dem ersten Punkt.</returns>
private static string ShortName(string value) private static string ShortName(string value)
{ {
var index = value.IndexOf('.'); var index = value.IndexOf('.');
@@ -373,6 +373,7 @@ namespace BizTalkPlatformManagementTool.Ui
var snapshot = _service.CreateSnapshot(options.Server); var snapshot = _service.CreateSnapshot(options.Server);
_service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot); _service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot);
var plan = _service.CreateShutdownPlan(snapshot, options.Server); var plan = _service.CreateShutdownPlan(snapshot, options.Server);
// Der exakte, frisch erzeugte Plan wird vor Bestätigung und jeder Laufzeitänderung gespeichert.
var planPath = _service.SavePlan(options.OutputDirectory, "shutdown-plan.json", plan); var planPath = _service.SavePlan(options.OutputDirectory, "shutdown-plan.json", plan);
ShowPlan(plan); ShowPlan(plan);
if (!options.DryRun && !ConfirmPreparedPlan("Shutdown", plan, options.Server, planPath)) if (!options.DryRun && !ConfirmPreparedPlan("Shutdown", plan, options.Server, planPath))
@@ -402,6 +403,7 @@ namespace BizTalkPlatformManagementTool.Ui
{ {
var snapshot = JsonFileStore.Load<BizTalkSnapshot>(ResolveStateFile(options)); var snapshot = JsonFileStore.Load<BizTalkSnapshot>(ResolveStateFile(options));
var plan = _service.CreateRestorePlan(snapshot, options.Server); var plan = _service.CreateRestorePlan(snapshot, options.Server);
// Auch beim Restore bestätigt der Bediener genau den bereits auditierbar gespeicherten Plan.
var planPath = _service.SavePlan(options.OutputDirectory, "restore-plan.json", plan); var planPath = _service.SavePlan(options.OutputDirectory, "restore-plan.json", plan);
ShowPlan(plan); ShowPlan(plan);
if (!options.DryRun && !ConfirmPreparedPlan("Restore", plan, options.Server, planPath)) if (!options.DryRun && !ConfirmPreparedPlan("Restore", plan, options.Server, planPath))
@@ -503,6 +505,9 @@ namespace BizTalkPlatformManagementTool.Ui
/// Confirms a fully prepared runtime-changing plan immediately before execution. /// Confirms a fully prepared runtime-changing plan immediately before execution.
/// </summary> /// </summary>
/// <param name="actionName">The action name displayed in the confirmation dialog.</param> /// <param name="actionName">The action name displayed in the confirmation dialog.</param>
/// <param name="plan">Der vollständig vorbereitete und gespeicherte Operationsplan.</param>
/// <param name="server">Der Zielserver der geplanten Änderung.</param>
/// <param name="planPath">Der Pfad der bereits gespeicherten Plandatei.</param>
/// <returns>True when the action may continue; otherwise false.</returns> /// <returns>True when the action may continue; otherwise false.</returns>
private bool ConfirmPreparedPlan(string actionName, OperationPlan plan, string server, string planPath) private bool ConfirmPreparedPlan(string actionName, OperationPlan plan, string server, string planPath)
{ {
@@ -683,7 +688,7 @@ namespace BizTalkPlatformManagementTool.Ui
} }
catch (InvalidOperationException) catch (InvalidOperationException)
{ {
// The form was closed between the state check and BeginInvoke. // Das Formular wurde zwischen Zustandsprüfung und BeginInvoke geschlossen.
} }
} }
else else
@@ -695,6 +700,8 @@ namespace BizTalkPlatformManagementTool.Ui
/// <summary> /// <summary>
/// Prevents the form from being disposed while a maintenance operation is active. /// Prevents the form from being disposed while a maintenance operation is active.
/// </summary> /// </summary>
/// <param name="sender">Das Formular, das geschlossen werden soll.</param>
/// <param name="e">Die abbrechbaren Argumente des Schließereignisses.</param>
private void MainFormClosing(object sender, FormClosingEventArgs e) private void MainFormClosing(object sender, FormClosingEventArgs e)
{ {
if (!_isBusy) if (!_isBusy)
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="2.1.1.0" name="BizTalkPlatformManagementTool" /> <assemblyIdentity version="2.1.2.0" name="BizTalkPlatformManagementTool" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security> <security>
<requestedPrivileges> <requestedPrivileges>
@@ -3,7 +3,7 @@
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" /> <Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
<PropertyGroup><Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration><Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform><ProjectGuid>{318F4307-F62C-47C9-9B90-F0C9BF2F812A}</ProjectGuid><OutputType>Exe</OutputType><RootNamespace>BizTalkPlatformManagementTool.Tests</RootNamespace><AssemblyName>BizTalkPlatformManagementTool.Tests</AssemblyName><TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion><FileAlignment>512</FileAlignment><Deterministic>true</Deterministic></PropertyGroup> <PropertyGroup><Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration><Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform><ProjectGuid>{318F4307-F62C-47C9-9B90-F0C9BF2F812A}</ProjectGuid><OutputType>Exe</OutputType><RootNamespace>BizTalkPlatformManagementTool.Tests</RootNamespace><AssemblyName>BizTalkPlatformManagementTool.Tests</AssemblyName><TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion><FileAlignment>512</FileAlignment><Deterministic>true</Deterministic></PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "><DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel></PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "><DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel></PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "><DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel></PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "><DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel><DocumentationFile>bin\Release\BizTalkPlatformManagementTool.Tests.xml</DocumentationFile></PropertyGroup>
<ItemGroup><Reference Include="System" /><Reference Include="System.Core" /></ItemGroup> <ItemGroup><Reference Include="System" /><Reference Include="System.Core" /></ItemGroup>
<ItemGroup><Compile Include="Program.cs" /></ItemGroup> <ItemGroup><Compile Include="Program.cs" /></ItemGroup>
<ItemGroup> <ItemGroup>
@@ -9,10 +9,16 @@ using BizTalkPlatformManagementTool.Setup;
namespace BizTalkPlatformManagementTool.Tests namespace BizTalkPlatformManagementTool.Tests
{ {
/// <summary>
/// Enthält die portable Regressionstestsuite ohne Abhängigkeit von einem externen Testframework.
/// </summary>
internal static class Program internal static class Program
{ {
/// <summary>Anzahl der im aktuellen Testlauf fehlgeschlagenen Prüfungen.</summary>
private static int failures; private static int failures;
/// <summary>Führt alle Regressionstests aus und liefert einen CI-tauglichen Exitcode.</summary>
/// <returns>Null, wenn alle Tests bestanden wurden; andernfalls eins.</returns>
private static int Main() private static int Main()
{ {
Run("JsonRoundTripIsBomTolerantAndAtomic", JsonRoundTripIsBomTolerantAndAtomic); Run("JsonRoundTripIsBomTolerantAndAtomic", JsonRoundTripIsBomTolerantAndAtomic);
@@ -33,12 +39,16 @@ namespace BizTalkPlatformManagementTool.Tests
return failures == 0 ? 0 : 1; return failures == 0 ? 0 : 1;
} }
/// <summary>Führt einen einzelnen Test isoliert aus und protokolliert sein Ergebnis.</summary>
/// <param name="name">Der stabile Testname für die Konsolenausgabe.</param>
/// <param name="test">Die auszuführende Testfunktion.</param>
private static void Run(string name, Action test) private static void Run(string name, Action test)
{ {
try { test(); Console.WriteLine("PASS " + name); } try { test(); Console.WriteLine("PASS " + name); }
catch (Exception ex) { failures++; Console.Error.WriteLine("FAIL " + name + ": " + ex); } catch (Exception ex) { failures++; Console.Error.WriteLine("FAIL " + name + ": " + ex); }
} }
/// <summary>Prüft BOM-tolerantes Lesen und rückstandsfreies atomisches JSON-Schreiben.</summary>
private static void JsonRoundTripIsBomTolerantAndAtomic() private static void JsonRoundTripIsBomTolerantAndAtomic()
{ {
InTemp(directory => InTemp(directory =>
@@ -58,6 +68,7 @@ namespace BizTalkPlatformManagementTool.Tests
}); });
} }
/// <summary>Prüft, dass gleichnamige Artefakte verschiedener Anwendungen getrennt verglichen werden.</summary>
private static void DiffUsesApplicationAndNameIdentity() private static void DiffUsesApplicationAndNameIdentity()
{ {
var before = Snapshot("APP-A", "SHARED", ArtifactStates.SendPortStarted); var before = Snapshot("APP-A", "SHARED", ArtifactStates.SendPortStarted);
@@ -69,6 +80,7 @@ namespace BizTalkPlatformManagementTool.Tests
Assert(diff.ArtifactDifferences[0].Application == "APP-A", "wrong application was compared"); Assert(diff.ArtifactDifferences[0].Application == "APP-A", "wrong application was compared");
} }
/// <summary>Prüft Kurzname/FQDN-Kompatibilität und Ablehnung eines fremden Restore-Zielservers.</summary>
private static void RestoreRejectsDifferentServer() private static void RestoreRejectsDifferentServer()
{ {
var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted); var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted);
@@ -77,6 +89,7 @@ namespace BizTalkPlatformManagementTool.Tests
Expect<InvalidOperationException>(() => SnapshotValidator.EnsureServerMatches(snapshot, "BIZTALK-B")); Expect<InvalidOperationException>(() => SnapshotValidator.EnsureServerMatches(snapshot, "BIZTALK-B"));
} }
/// <summary>Prüft die sichere Restore-Reihenfolge und den Schutz gebundener Orchestrierungen.</summary>
private static void RestorePlanUsesSafeOrder() private static void RestorePlanUsesSafeOrder()
{ {
var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted); var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted);
@@ -90,6 +103,7 @@ namespace BizTalkPlatformManagementTool.Tests
Assert(!bound.Execute && bound.Kind == "Note", "bound orchestration must remain unchanged"); Assert(!bound.Execute && bound.Kind == "Note", "bound orchestration must remain unchanged");
} }
/// <summary>Prüft die Neutralisierung formelartiger CSV-Feldwerte.</summary>
private static void CsvNeutralizesFormulaValues() private static void CsvNeutralizesFormulaValues()
{ {
InTemp(directory => InTemp(directory =>
@@ -102,6 +116,7 @@ namespace BizTalkPlatformManagementTool.Tests
}); });
} }
/// <summary>Prüft, dass eine nach Manifestbildung veränderte Payload abgelehnt wird.</summary>
private static void PackageManifestRejectsTampering() private static void PackageManifestRejectsTampering()
{ {
InTemp(directory => InTemp(directory =>
@@ -117,6 +132,7 @@ namespace BizTalkPlatformManagementTool.Tests
}); });
} }
/// <summary>Prüft die Aktivierung einer vollständig validierten Neuinstallation.</summary>
private static void InstallerActivatesValidatedPayload() private static void InstallerActivatesValidatedPayload()
{ {
InTemp(directory => InTemp(directory =>
@@ -131,6 +147,7 @@ namespace BizTalkPlatformManagementTool.Tests
}); });
} }
/// <summary>Prüft die Ablehnung nicht deklarierter Dateien und ausbrechender Manifestpfade.</summary>
private static void PackageManifestRejectsUndeclaredAndTraversalFiles() private static void PackageManifestRejectsUndeclaredAndTraversalFiles()
{ {
InTemp(directory => InTemp(directory =>
@@ -149,6 +166,7 @@ namespace BizTalkPlatformManagementTool.Tests
}); });
} }
/// <summary>Prüft, dass ein Staging-Fehler die aktive Installation nicht mutiert.</summary>
private static void InstallerDoesNotMutateOnStagingFailure() private static void InstallerDoesNotMutateOnStagingFailure()
{ {
InTemp(directory => InTemp(directory =>
@@ -172,6 +190,7 @@ namespace BizTalkPlatformManagementTool.Tests
}); });
} }
/// <summary>Prüft die Wiederherstellung der Vorversion nach fehlgeschlagenem aktiviertem Self-Test.</summary>
private static void InstallerRollsBackFailedActivatedSelfTest() private static void InstallerRollsBackFailedActivatedSelfTest()
{ {
InTemp(directory => InTemp(directory =>
@@ -191,6 +210,7 @@ namespace BizTalkPlatformManagementTool.Tests
}); });
} }
/// <summary>Prüft die Deinstallation über ein atomar umbenanntes Quarantäneverzeichnis.</summary>
private static void InstallerUninstallRemovesProgramDirectory() private static void InstallerUninstallRemovesProgramDirectory()
{ {
InTemp(directory => InTemp(directory =>
@@ -205,6 +225,7 @@ namespace BizTalkPlatformManagementTool.Tests
}); });
} }
/// <summary>Prüft technischen Kontext, Fehlercode, HRESULT und innere Ausnahme im Setup-Log.</summary>
private static void InstallerDiagnosticLogContainsContextAndExceptionChain() private static void InstallerDiagnosticLogContainsContextAndExceptionChain()
{ {
InTemp(directory => InTemp(directory =>
@@ -225,6 +246,7 @@ namespace BizTalkPlatformManagementTool.Tests
}); });
} }
/// <summary>Prüft, dass ein fehlerhafter UI-Callback die Installation nicht beeinflusst.</summary>
private static void InstallerSurvivesUiReportFailure() private static void InstallerSurvivesUiReportFailure()
{ {
InTemp(directory => InTemp(directory =>
@@ -240,6 +262,7 @@ namespace BizTalkPlatformManagementTool.Tests
}); });
} }
/// <summary>Prüft den Diagnose-Log-Fallback bei einem nicht verwendbaren ProgramData-Pfad.</summary>
private static void InstallerDiagnosticLogFallsBackToTemp() private static void InstallerDiagnosticLogFallsBackToTemp()
{ {
InTemp(directory => InTemp(directory =>
@@ -261,6 +284,11 @@ namespace BizTalkPlatformManagementTool.Tests
}); });
} }
/// <summary>Erstellt einen minimalen Snapshot für Vergleiche und Planprüfungen.</summary>
/// <param name="application">Der Name der Testanwendung.</param>
/// <param name="port">Der Name des Test-Send-Ports.</param>
/// <param name="state">Der rohe Send-Port-Status.</param>
/// <returns>Ein Snapshot mit einer Anwendung und einem Send Port.</returns>
private static BizTalkSnapshot Snapshot(string application, string port, int state) private static BizTalkSnapshot Snapshot(string application, string port, int state)
{ {
var result = new BizTalkSnapshot { ToolVersion = "test", CreatedAt = DateTimeOffset.Now.ToString("o"), Server = Environment.MachineName }; var result = new BizTalkSnapshot { ToolVersion = "test", CreatedAt = DateTimeOffset.Now.ToString("o"), Server = Environment.MachineName };
@@ -270,6 +298,10 @@ namespace BizTalkPlatformManagementTool.Tests
return result; return result;
} }
/// <summary>Erzeugt eine minimale, manifestierte Installer-Payload.</summary>
/// <param name="root">Das temporäre Testwurzelverzeichnis.</param>
/// <param name="payload">Der simulierte Inhalt der Anwendungs-EXE.</param>
/// <returns>Das Verzeichnis des erzeugten Testpakets.</returns>
private static string CreatePackage(string root, string payload) private static string CreatePackage(string root, string payload)
{ {
var package = Path.Combine(root, "package"); var package = Path.Combine(root, "package");
@@ -280,6 +312,8 @@ namespace BizTalkPlatformManagementTool.Tests
return package; return package;
} }
/// <summary>Führt einen Test in einem eindeutigen temporären Verzeichnis mit garantierter Bereinigung aus.</summary>
/// <param name="action">Die Testfunktion, die den temporären Pfad erhält.</param>
private static void InTemp(Action<string> action) private static void InTemp(Action<string> action)
{ {
var directory = Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool.Tests." + Guid.NewGuid().ToString("N")); var directory = Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool.Tests." + Guid.NewGuid().ToString("N"));
@@ -288,7 +322,14 @@ namespace BizTalkPlatformManagementTool.Tests
finally { if (Directory.Exists(directory)) Directory.Delete(directory, true); } finally { if (Directory.Exists(directory)) Directory.Delete(directory, true); }
} }
/// <summary>Bricht den Test ab, wenn eine erwartete Bedingung nicht erfüllt ist.</summary>
/// <param name="condition">Die erwartete Bedingung.</param>
/// <param name="message">Die Fehlermeldung bei nicht erfüllter Bedingung.</param>
private static void Assert(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); } private static void Assert(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); }
/// <summary>Prüft, dass eine Aktion eine bestimmte Ausnahme auslöst.</summary>
/// <typeparam name="T">Der erwartete Ausnahmetyp.</typeparam>
/// <param name="action">Die auszuführende Aktion.</param>
private static void Expect<T>(Action action) where T : Exception private static void Expect<T>(Action action) where T : Exception
{ {
try { action(); } try { action(); }
@@ -296,6 +337,10 @@ namespace BizTalkPlatformManagementTool.Tests
throw new InvalidOperationException("Expected exception " + typeof(T).Name); throw new InvalidOperationException("Expected exception " + typeof(T).Name);
} }
/// <summary>Führt eine Aktion aus und gibt die erwartete Ausnahme für weitere Prüfungen zurück.</summary>
/// <typeparam name="T">Der erwartete Ausnahmetyp.</typeparam>
/// <param name="action">Die auszuführende Aktion.</param>
/// <returns>Die von der Aktion ausgelöste Ausnahme.</returns>
private static T Capture<T>(Action action) where T : Exception private static T Capture<T>(Action action) where T : Exception
{ {
try { action(); } try { action(); }