Document C# codebase and publish 2.1.2
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
|
||||
# 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
|
||||
### Added
|
||||
- Stable setup/uninstall phase codes, environment and file metadata, complete self-test output, exception chains with HRESULT/stacktrace, and per-step rollback diagnostics.
|
||||
|
||||
@@ -27,6 +27,22 @@ Das BizTalk Platform Management Tool unterstützt kontrollierte Wartungsfenster
|
||||
- Regressionstests: `tests/BizTalkPlatformManagementTool.Tests`
|
||||
- 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
|
||||
|
||||
1. Anwendung mit Administratorrechten starten.
|
||||
|
||||
@@ -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.
|
||||
|
||||
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:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
- [Installation](Installation.md)
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
<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)' == '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><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>
|
||||
|
||||
@@ -6,8 +6,14 @@ using BizTalkPlatformManagementTool.Setup;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Packager
|
||||
{
|
||||
/// <summary>Erzeugt aus den Release-Binärdateien das übertragbare Setup-Paket.</summary>
|
||||
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)
|
||||
{
|
||||
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, "Installation.md"), Path.Combine(package, "INSTALLATION.md"));
|
||||
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"));
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var encoded = Convert.ToBase64String(File.ReadAllBytes(source));
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<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.Setup.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
|
||||
@@ -11,28 +11,66 @@ using Microsoft.Win32;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
/// <summary>
|
||||
/// Führt Installation, Update und Deinstallation mit Staging, Validierung und Rollback aus.
|
||||
/// </summary>
|
||||
internal sealed class InstallerEngine
|
||||
{
|
||||
/// <summary>Dateiname der installierten Hauptanwendung.</summary>
|
||||
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 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";
|
||||
|
||||
/// <summary>Verzeichnis der entpackten Setup-Dateien.</summary>
|
||||
private readonly string packageDirectory;
|
||||
|
||||
/// <summary>Aktives maschinenweites Programmverzeichnis.</summary>
|
||||
private readonly string installDirectory;
|
||||
|
||||
/// <summary>Dauerhaftes Datenverzeichnis unter ProgramData.</summary>
|
||||
private readonly string dataDirectory;
|
||||
|
||||
/// <summary>Legt fest, ob Registry, Verknüpfungen und Uninstaller verwaltet werden.</summary>
|
||||
private readonly bool registerWindowsIntegration;
|
||||
|
||||
/// <summary>Optional injizierte Self-Test-Funktion für portable Regressionstests.</summary>
|
||||
private readonly Func<string, bool> selfTestRunner;
|
||||
|
||||
/// <summary>Zuletzt tatsächlich verwendetes primäres oder temporäres Diagnoseverzeichnis.</summary>
|
||||
private string lastInstallerLogDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Enthält den vor einer Mutation gesicherten Zustand der Windows-Integration.
|
||||
/// </summary>
|
||||
private sealed class WindowsIntegrationSnapshot
|
||||
{
|
||||
/// <summary>Ruft ab oder legt fest, ob der Uninstall-Schlüssel vorher existierte.</summary>
|
||||
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; }
|
||||
|
||||
/// <summary>Ruft den vorherigen Inhalt der Desktop-Verknüpfung ab oder legt ihn fest.</summary>
|
||||
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; }
|
||||
|
||||
/// <summary>Ruft den vorherigen Inhalt der Uninstaller-Datei ab oder legt ihn fest.</summary>
|
||||
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)
|
||||
: this(
|
||||
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)
|
||||
{
|
||||
this.packageDirectory = Path.GetFullPath(packageDirectory);
|
||||
@@ -52,19 +98,23 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
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; } }
|
||||
/// <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
|
||||
{
|
||||
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")); } }
|
||||
/// <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)); } }
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var uiReport = report ?? delegate { };
|
||||
@@ -80,6 +130,8 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
|
||||
var sourceApplication = Path.Combine(packageDirectory, "application");
|
||||
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 backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N");
|
||||
var hadExistingInstallation = Directory.Exists(installDirectory);
|
||||
@@ -108,6 +160,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
LogDriveSpace(log, installDirectory);
|
||||
log.WriteFileDetails("setup_executable", Assembly.GetExecutingAssembly().Location);
|
||||
log.WriteFileDetails("existing_application", Path.Combine(installDirectory, ApplicationExeName));
|
||||
// Der vollständige Integrationszustand wird vor der ersten Mutation gesichert.
|
||||
integrationSnapshot = registerWindowsIntegration ? CaptureWindowsIntegration() : null;
|
||||
if (integrationSnapshot != null)
|
||||
log.Write("INFO", "event=integration_snapshot registry_key_existed=" + integrationSnapshot.RegistryKeyExisted
|
||||
@@ -138,6 +191,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
phaseCode = "SETUP-ACTIVATION";
|
||||
phase = "Vorhandene Version sichern und Staging atomar aktivieren";
|
||||
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)
|
||||
{
|
||||
log.Write("INFO", "event=directory_move role=backup source=\"" + installDirectory + "\" target=\"" + backupDirectory + "\"");
|
||||
@@ -197,6 +251,8 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
}
|
||||
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);
|
||||
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)
|
||||
{
|
||||
var uiReport = report ?? delegate { };
|
||||
@@ -289,6 +348,8 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
phaseCode = "UNINSTALL-QUARANTINE";
|
||||
phase = "Programmverzeichnis deaktivieren";
|
||||
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))
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
DeleteFileIfExists(DesktopShortcutPath);
|
||||
@@ -388,6 +458,13 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
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)
|
||||
{
|
||||
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()
|
||||
{
|
||||
var snapshot = new WindowsIntegrationSnapshot
|
||||
@@ -437,6 +518,10 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
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)
|
||||
{
|
||||
if (snapshot == null) return;
|
||||
@@ -457,11 +542,21 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (content == null)
|
||||
@@ -469,11 +564,17 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
DeleteFileIfExists(path);
|
||||
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;
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
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)
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
log.WriteFileDetails(label + "_self_test_executable", executable);
|
||||
@@ -554,10 +661,14 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
using (var process = Process.Start(startInfo))
|
||||
{
|
||||
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 errorRead = process.StandardError.ReadToEndAsync();
|
||||
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";
|
||||
try { process.Kill(); }
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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 + "'.");
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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]";
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (!files.Any(x => string.Equals(x.RelativePath, relativePath, StringComparison.OrdinalIgnoreCase)))
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
|
||||
/// <summary>Ruft den maschinenweiten Pfad der Desktop-Verknüpfung ab.</summary>
|
||||
private static string DesktopShortcutPath
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
get { return Path.Combine(dataDirectory, "Setup", "Uninstall.exe"); }
|
||||
|
||||
@@ -7,16 +7,37 @@ using System.Windows.Forms;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
/// <summary>
|
||||
/// Stellt die Bedienoberfläche für Installation, Update, Deinstallation und Diagnosezugriff bereit.
|
||||
/// </summary>
|
||||
internal sealed class MainForm : Form
|
||||
{
|
||||
/// <summary>Ausführende Installer-Engine.</summary>
|
||||
private readonly InstallerEngine engine;
|
||||
|
||||
/// <summary>Gibt an, ob das Fenster direkt zur Deinstallation geöffnet wurde.</summary>
|
||||
private readonly bool uninstallMode;
|
||||
|
||||
/// <summary>Sichtbare Fortschritts- und Diagnoseausgabe.</summary>
|
||||
private readonly TextBox output = new TextBox();
|
||||
|
||||
/// <summary>Auswahl für die optionale maschinenweite Desktop-Verknüpfung.</summary>
|
||||
private readonly CheckBox desktopShortcut = new CheckBox();
|
||||
|
||||
/// <summary>Schaltfläche für Installation oder Update.</summary>
|
||||
private readonly Button installButton = new Button();
|
||||
|
||||
/// <summary>Schaltfläche für die Deinstallation.</summary>
|
||||
private readonly Button uninstallButton = new Button();
|
||||
|
||||
/// <summary>Verhindert parallele Aktionen und Schließen während einer Operation.</summary>
|
||||
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)
|
||||
{
|
||||
this.engine = engine;
|
||||
@@ -30,6 +51,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
FormClosing += OnFormClosing;
|
||||
}
|
||||
|
||||
/// <summary>Erstellt und verdrahtet die vollständige Setup-Oberfläche.</summary>
|
||||
private void BuildUi()
|
||||
{
|
||||
var root = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(16), RowCount = 5, ColumnCount = 1 };
|
||||
@@ -43,7 +65,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
AutoSize = true,
|
||||
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
|
||||
{
|
||||
@@ -89,6 +111,9 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
SetBusy(true);
|
||||
// Dateisystem-, Registry- und Self-Test-Operationen dürfen die WinForms-Nachrichtenpumpe nicht blockieren.
|
||||
Task.Run(() =>
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (IsDisposed || Disposing) return;
|
||||
@@ -145,6 +175,8 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
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)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
@@ -159,6 +191,9 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
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)
|
||||
{
|
||||
if (!busy) return;
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Text;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
/// <summary>Beschreibt eine durch Länge und SHA-256 abgesicherte Payload-Datei.</summary>
|
||||
public sealed class PackageFile
|
||||
{
|
||||
/// <summary>Gets or sets the normalized payload-relative path.</summary>
|
||||
@@ -18,9 +19,13 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
public string Sha256 { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Erzeugt und validiert das vollständige kryptografische Payload-Manifest.</summary>
|
||||
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)
|
||||
{
|
||||
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.");
|
||||
// Der Mengenvergleich verhindert, dass nicht deklarierte DLLs oder Konfigurationen
|
||||
// unbemerkt mit administrativen Rechten installiert werden.
|
||||
var actualFiles = Directory.GetFiles(applicationDirectory, "*", SearchOption.AllDirectories)
|
||||
.Select(x => NormalizeRelativePath(x.Substring(Path.GetFullPath(applicationDirectory).TrimEnd(Path.DirectorySeparatorChar).Length + 1)))
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
@@ -58,7 +65,9 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
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)
|
||||
{
|
||||
var root = Path.GetFullPath(applicationDirectory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
@@ -70,7 +79,9 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var normalizedRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
@@ -92,6 +106,11 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
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)
|
||||
{
|
||||
path = (path ?? string.Empty).Replace('\\', '/').Trim();
|
||||
|
||||
@@ -3,8 +3,12 @@ using System.Windows.Forms;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
/// <summary>Enthält den Einstiegspunkt des administrativen Windows-Setups.</summary>
|
||||
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]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
|
||||
@@ -8,6 +8,6 @@ using System.Runtime.InteropServices;
|
||||
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
|
||||
[assembly: AssemblyVersion("2.1.1.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.1.0")]
|
||||
[assembly: AssemblyVersion("2.1.2.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.2.0")]
|
||||
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
||||
|
||||
@@ -8,22 +8,38 @@ using System.Text;
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes a durable, single-line diagnostic trace for one setup operation.
|
||||
/// No credential or other secret is accepted by this component.
|
||||
/// Schreibt ein dauerhaftes, einzeiliges Diagnoseprotokoll für genau einen Setup-Lauf.
|
||||
/// Die Komponente übernimmt keine Kennwörter oder andere Geheimnisse.
|
||||
/// </summary>
|
||||
internal sealed class SetupOperationLog
|
||||
{
|
||||
/// <summary>Serialisiert konkurrierende Schreibzugriffe innerhalb des Setup-Prozesses.</summary>
|
||||
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)
|
||||
{
|
||||
FilePath = filePath;
|
||||
}
|
||||
|
||||
/// <summary>Ruft den tatsächlich verwendeten Logpfad ab.</summary>
|
||||
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; }
|
||||
|
||||
/// <summary>Ruft ab, ob das Log im temporären Rückfallverzeichnis liegt.</summary>
|
||||
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)
|
||||
{
|
||||
try
|
||||
@@ -34,6 +50,8 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ohne primäres ProgramData-Log bleibt wenigstens im Benutzer-Temp ein
|
||||
// Diagnosepfad erhalten; die ursprüngliche Ursache wird dort mitgeschrieben.
|
||||
var fallback = CreateInDirectory(
|
||||
Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool", "InstallerLogs"),
|
||||
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)
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
@@ -81,6 +105,11 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath)) return;
|
||||
@@ -97,15 +126,26 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
}
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
@@ -149,6 +194,10 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
try
|
||||
@@ -196,7 +258,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
||||
}
|
||||
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"?>
|
||||
<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">
|
||||
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
|
||||
</trustInfo>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
<DocumentationFile>bin\Release\BizTalkPlatformManagementTool.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
|
||||
@@ -14,9 +14,13 @@ namespace BizTalkPlatformManagementTool
|
||||
/// <summary>
|
||||
/// Starts the application after verifying that BizTalk WMI operations can run elevated.
|
||||
/// </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]
|
||||
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))
|
||||
{
|
||||
return RuntimeSelfTest.Run();
|
||||
@@ -38,6 +42,7 @@ namespace BizTalkPlatformManagementTool
|
||||
}
|
||||
|
||||
bool createdNew;
|
||||
// Der sitzungsbezogene Mutex verhindert konkurrierende Wartungsoperationen desselben Benutzers.
|
||||
using (var mutex = new Mutex(true, @"Local\BizTalkPlatformManagementTool", out createdNew))
|
||||
{
|
||||
if (!createdNew)
|
||||
|
||||
@@ -8,5 +8,5 @@ using System.Runtime.InteropServices;
|
||||
[assembly: AssemblyCopyright("Copyright © 2026")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
|
||||
[assembly: AssemblyVersion("2.1.1.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.1.0")]
|
||||
[assembly: AssemblyVersion("2.1.2.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.2.0")]
|
||||
|
||||
@@ -10,6 +10,10 @@ namespace BizTalkPlatformManagementTool
|
||||
/// </summary>
|
||||
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()
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool.SelfTest." + Guid.NewGuid().ToString("N"));
|
||||
@@ -50,11 +54,16 @@ namespace BizTalkPlatformManagementTool
|
||||
}
|
||||
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)
|
||||
{
|
||||
var snapshot = new BizTalkSnapshot
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
/// <summary>
|
||||
/// Current tool version written into generated snapshots.
|
||||
/// </summary>
|
||||
public const string Version = "2.1.1-net461";
|
||||
public const string Version = "2.1.2-net461";
|
||||
|
||||
/// <summary>
|
||||
/// Fallback application name used when WMI does not expose an application property.
|
||||
@@ -185,6 +185,8 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
SnapshotValidator.EnsureServerMatches(snapshot, 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 item in app.ReceiveLocations.Where(x => x.Enabled))
|
||||
@@ -228,6 +230,8 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
SnapshotValidator.EnsureServerMatches(snapshot, 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))
|
||||
{
|
||||
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)
|
||||
{
|
||||
// Ein blindes Unenlist würde Bound nach Unbound verschieben und damit
|
||||
// einen anderen Zustand als im Snapshot herstellen.
|
||||
plan.Steps.Add(new OperationStep
|
||||
{
|
||||
Kind = "Note",
|
||||
@@ -321,6 +327,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
|
||||
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 + "'");
|
||||
continue;
|
||||
}
|
||||
@@ -611,6 +618,10 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
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)
|
||||
{
|
||||
if (items == null)
|
||||
|
||||
@@ -151,6 +151,8 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
|
||||
try
|
||||
{
|
||||
// Bewusst keine WQL-WHERE-Klausel: BizTalk-Namen können Zeichen enthalten,
|
||||
// die sonst eine fehlerhafte oder anders interpretierte Query erzeugen.
|
||||
ManagementObject match = null;
|
||||
var items = Query(className, false);
|
||||
foreach (var item in items)
|
||||
@@ -256,6 +258,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
break;
|
||||
}
|
||||
// Am Timeout-Ende nur noch die tatsächlich verbleibende Zeit schlafen.
|
||||
Thread.Sleep(remaining < TimeSpan.FromSeconds(delay) ? remaining : TimeSpan.FromSeconds(delay));
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
value = value ?? string.Empty;
|
||||
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;
|
||||
}
|
||||
if (value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) < 0)
|
||||
|
||||
@@ -130,6 +130,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
/// <returns>An HTML-safe value.</returns>
|
||||
private static string Encode(string value)
|
||||
{
|
||||
// Alle aus BizTalk gelesenen Werte werden vor der Aufnahme in HTML neutralisiert.
|
||||
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
|
||||
/// save cannot leave a truncated snapshot or operation plan behind.
|
||||
/// </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)
|
||||
{
|
||||
// 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 backupPath = path + ".bak." + Guid.NewGuid().ToString("N");
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
try
|
||||
@@ -185,7 +199,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
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
|
||||
{
|
||||
// Logging must never interrupt BizTalk operations.
|
||||
// Ein Logfehler darf niemals eine fachliche BizTalk-Operation abbrechen.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,10 +225,14 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
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()
|
||||
{
|
||||
var commonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||
|
||||
@@ -137,6 +137,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
|
||||
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;
|
||||
}
|
||||
foreach (var item in after)
|
||||
|
||||
@@ -9,7 +9,10 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var normalizedLeft = NormalizeServer(left);
|
||||
@@ -82,12 +94,28 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
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)
|
||||
{
|
||||
// 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();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
value = (value ?? string.Empty).Trim().TrimStart('\\');
|
||||
@@ -115,6 +148,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
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)
|
||||
{
|
||||
var index = value.IndexOf('.');
|
||||
|
||||
@@ -373,6 +373,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
var snapshot = _service.CreateSnapshot(options.Server);
|
||||
_service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot);
|
||||
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);
|
||||
ShowPlan(plan);
|
||||
if (!options.DryRun && !ConfirmPreparedPlan("Shutdown", plan, options.Server, planPath))
|
||||
@@ -402,6 +403,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
{
|
||||
var snapshot = JsonFileStore.Load<BizTalkSnapshot>(ResolveStateFile(options));
|
||||
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);
|
||||
ShowPlan(plan);
|
||||
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.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
private bool ConfirmPreparedPlan(string actionName, OperationPlan plan, string server, string planPath)
|
||||
{
|
||||
@@ -683,7 +688,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// The form was closed between the state check and BeginInvoke.
|
||||
// Das Formular wurde zwischen Zustandsprüfung und BeginInvoke geschlossen.
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -695,6 +700,8 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
/// <summary>
|
||||
/// Prevents the form from being disposed while a maintenance operation is active.
|
||||
/// </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)
|
||||
{
|
||||
if (!_isBusy)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="2.1.1.0" name="BizTalkPlatformManagementTool" />
|
||||
<assemblyIdentity version="2.1.2.0" name="BizTalkPlatformManagementTool" />
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
<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 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><Compile Include="Program.cs" /></ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -9,10 +9,16 @@ using BizTalkPlatformManagementTool.Setup;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Enthält die portable Regressionstestsuite ohne Abhängigkeit von einem externen Testframework.
|
||||
/// </summary>
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>Anzahl der im aktuellen Testlauf fehlgeschlagenen Prüfungen.</summary>
|
||||
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()
|
||||
{
|
||||
Run("JsonRoundTripIsBomTolerantAndAtomic", JsonRoundTripIsBomTolerantAndAtomic);
|
||||
@@ -33,12 +39,16 @@ namespace BizTalkPlatformManagementTool.Tests
|
||||
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)
|
||||
{
|
||||
try { test(); Console.WriteLine("PASS " + name); }
|
||||
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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
/// <summary>Prüft Kurzname/FQDN-Kompatibilität und Ablehnung eines fremden Restore-Zielservers.</summary>
|
||||
private static void RestoreRejectsDifferentServer()
|
||||
{
|
||||
var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted);
|
||||
@@ -77,6 +89,7 @@ namespace BizTalkPlatformManagementTool.Tests
|
||||
Expect<InvalidOperationException>(() => SnapshotValidator.EnsureServerMatches(snapshot, "BIZTALK-B"));
|
||||
}
|
||||
|
||||
/// <summary>Prüft die sichere Restore-Reihenfolge und den Schutz gebundener Orchestrierungen.</summary>
|
||||
private static void RestorePlanUsesSafeOrder()
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
/// <summary>Prüft die Neutralisierung formelartiger CSV-Feldwerte.</summary>
|
||||
private static void CsvNeutralizesFormulaValues()
|
||||
{
|
||||
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()
|
||||
{
|
||||
InTemp(directory =>
|
||||
@@ -117,6 +132,7 @@ namespace BizTalkPlatformManagementTool.Tests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Prüft die Aktivierung einer vollständig validierten Neuinstallation.</summary>
|
||||
private static void InstallerActivatesValidatedPayload()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
var result = new BizTalkSnapshot { ToolVersion = "test", CreatedAt = DateTimeOffset.Now.ToString("o"), Server = Environment.MachineName };
|
||||
@@ -270,6 +298,10 @@ namespace BizTalkPlatformManagementTool.Tests
|
||||
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)
|
||||
{
|
||||
var package = Path.Combine(root, "package");
|
||||
@@ -280,6 +312,8 @@ namespace BizTalkPlatformManagementTool.Tests
|
||||
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)
|
||||
{
|
||||
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); }
|
||||
}
|
||||
|
||||
/// <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); }
|
||||
|
||||
/// <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
|
||||
{
|
||||
try { action(); }
|
||||
@@ -296,6 +337,10 @@ namespace BizTalkPlatformManagementTool.Tests
|
||||
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
|
||||
{
|
||||
try { action(); }
|
||||
|
||||
Reference in New Issue
Block a user