diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a59e549 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = crlf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.{md,json}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes index 7f5bd70..0c42f3c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1 @@ - * text=auto eol=crlf diff --git a/.gitignore b/.gitignore index 5709a8f..1c7bf17 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ bin/ obj/ # Logs / Outputs +artifacts/ *.log *.json *.html diff --git a/BizTalkPlatformManagementTool.sln b/BizTalkPlatformManagementTool.sln index 0f2dfdf..7ffee91 100644 --- a/BizTalkPlatformManagementTool.sln +++ b/BizTalkPlatformManagementTool.sln @@ -4,6 +4,12 @@ VisualStudioVersion = 15.0.28307.168 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool", "src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj", "{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Tests", "tests\BizTalkPlatformManagementTool.Tests\BizTalkPlatformManagementTool.Tests.csproj", "{B2AD340B-859F-413B-8811-7FBA4BD2BC02}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Setup", "src\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj", "{38E61630-11AB-4F8D-B421-F8A88831C4BD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Packager", "src\BizTalkPlatformManagementTool.Packager\BizTalkPlatformManagementTool.Packager.csproj", "{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -14,6 +20,18 @@ Global {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Debug|Any CPU.Build.0 = Debug|Any CPU {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.ActiveCfg = Release|Any CPU {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.Build.0 = Release|Any CPU + {B2AD340B-859F-413B-8811-7FBA4BD2BC02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2AD340B-859F-413B-8811-7FBA4BD2BC02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2AD340B-859F-413B-8811-7FBA4BD2BC02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2AD340B-859F-413B-8811-7FBA4BD2BC02}.Release|Any CPU.Build.0 = Release|Any CPU + {38E61630-11AB-4F8D-B421-F8A88831C4BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38E61630-11AB-4F8D-B421-F8A88831C4BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38E61630-11AB-4F8D-B421-F8A88831C4BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38E61630-11AB-4F8D-B421-F8A88831C4BD}.Release|Any CPU.Build.0 = Release|Any CPU + {AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CHANGELOG.md b/CHANGELOG.md index 1394fa5..0d91bb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,55 @@ - # Changelog -## [Unreleased] - 2026-04-27 +## [Unreleased] - 2026-08-06 + +### Added + +- Überarbeitete deutschsprachige GUI mit einheitlichem Workflow und DPI-Skalierung. +- .NET-Installer mit sicherem Update, Rollback und Deinstallation. +- Release-Packager für ZIP, Base64-TXT und SHA-256. +- Regressionstestprojekt für Plan-, Vergleichs-, Validierungs- und Dateilogik. + ### Changed + +- Restore startet ausschließlich Artefakte, die im Vorher-Snapshot aktiv waren. +- Vorhandene Vorher-Snapshots werden beim Herunterfahren wiederverwendet. +- WMI-Auflösung nutzt zusätzliche Qualifier und bricht bei Mehrdeutigkeit ab. +- Snapshot-Vergleich verwendet zusammengesetzte Artefaktidentitäten. +- JSON-Dateien werden atomar und größenbegrenzt geschrieben. +- Logs und Wartungsdateien liegen unter `%ProgramData%` und überleben Updates. +- Transport-URIs werden nicht mehr in neuen Snapshots persistiert. + +## [2.0.0] - 2026-04-27 + +### Changed + - Projekt auf `BizTalkPlatformManagementTool` umbenannt. - Solution, Projektdatei, Namespace, Assemblyname und Standard-Ausgabepfad angepasst. - Dokumentation aktualisiert und `Installation.md` ergänzt. ## [1.3.0] - 2026-02-02 + ### Added -- HTML Report mit farblicher Kennzeichnung (grün/rot) -- Farbige Statusausgabe auf STDOUT -- Remote‑Server Parameter (-Server) und Ausgabeverzeichnis (-OutDir) -- Verbesserte Diff‑Erkennung (neu/entfernt) + +- HTML-Report mit farblicher Kennzeichnung +- farbige Statusausgabe +- Remote-Server-Parameter und Ausgabeverzeichnis +- verbesserte Diff-Erkennung für neue und entfernte Artefakte ## [1.2.0] - 2026-02-02 + ### Added -- Remote‑fähige Snapshot‑Funktion, Logging (Konsole + Datei) + +- Remote-fähige Snapshot-Funktion und Logging ## [1.1.0] - 2026-02-02 + ### Added -- Textuelle Diff‑Ausgabe auf STDOUT + HTML Report + +- textuelle Diff-Ausgabe und HTML-Report ## [1.0.0] - 2026-02-02 + ### Added -- Grundfunktion: Snapshot & Diff (Receive Locations, Send Ports, Orchestrations) + +- Snapshot und Diff für Receive Locations, Send Ports und Orchestrierungen diff --git a/Dokumentation.md b/Dokumentation.md index e92d8a8..571e609 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -1,105 +1,159 @@ -# BizTalk Platform Management Tool Dokumentation +# Technische Dokumentation -**Stand:** 2026-04-27 -**Implementierung:** C# WinForms, .NET Framework 4.6.1 -**Archivierte PowerShell-Version:** `archive/powershell/BizTalkPlatformManagementTool.ps1` +**Stand:** 2026-08-06 -## Zweck +**Version:** 2.1.0 -Das BizTalk Platform Management Tool unterstützt kontrollierte Wartungsfenster für Microsoft BizTalk Server 2020: +**Technik:** C# WinForms, .NET Framework 4.6.1 -- Exakten Laufzeitstatus vor einer Wartung erfassen. -- Receive Locations, Orchestrations, Send Ports und Host Instances in definierter Reihenfolge herunterfahren. -- Den Laufzeitstatus aus einer gespeicherten `before.json` wiederherstellen. -- Den Status nach der Wartung erfassen und beide Snapshots vergleichen. -- Plan- und Ergebnisdateien für Review, Audit und Nachvollziehbarkeit schreiben. +## Ziel und Sicherheitsprinzip -## Projektstruktur +Das Tool unterstützt geplante BizTalk-Wartungen, ohne den fachlich gewünschten +Ausgangszustand zu verlieren. Der Vorher-Snapshot ist die einzige Quelle für +den Restore. Das bedeutet insbesondere: -- Solution: `BizTalkPlatformManagementTool.sln` -- Projekt: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj` -- UI: `src/BizTalkPlatformManagementTool/Ui/MainForm.cs` -- WMI-Zugriff: `src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs` -- Operationslogik: `src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs` -- PowerShell-Archiv: `archive/powershell/BizTalkPlatformManagementTool.ps1` +- Nur Artefakte, die im Snapshot aktiv waren, werden heruntergefahren. +- Nur Artefakte, die im Snapshot aktiv waren, werden wieder gestartet. +- Zuvor gestoppte, gebundene oder deaktivierte Artefakte werden beim Restore nicht verändert. +- Ein vorhandener Vorher-Snapshot wird durch **Herunterfahren** nicht ersetzt. +- Snapshot und Zielserver müssen übereinstimmen; Kurzname und FQDN desselben Servers werden akzeptiert. +- Bei mehrdeutiger WMI-Auflösung wird abgebrochen, statt möglicherweise das falsche Objekt zu verändern. -## UI Workflow +## Oberfläche -1. Anwendung mit Administratorrechten starten. -2. BizTalk-Servernamen eintragen. -3. Ausgabeverzeichnis wählen. -4. **Dry run** aktiviert lassen. -5. Mit **Diagnose** den Zugriff auf `root\MicrosoftBizTalkServer` prüfen. -6. Mit **Snapshot Before** die Datei `before.json` erzeugen. -7. Mit **Shutdown** den Plan `shutdown-plan.json` erzeugen und prüfen. -8. **Dry run** erst deaktivieren, wenn der Plan korrekt ist. -9. Nach der Wartung mit **Restore** aus `before.json` wiederherstellen. -10. Mit **Snapshot After** und **Compare** die Umgebung validieren. +Die Oberfläche führt von links nach rechts durch sechs Schritte: -Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des letzten Snapshots und zeigt `Started`, `Stopped`, `Partial` oder `Unknown`. **Clear** leert die sichtbaren Ergebnis- und Log-Grids, loescht aber keine Dateien. **Close** beendet die Anwendung. +1. **Verbindung prüfen** +2. **Zustand erfassen** +3. **Herunterfahren** +4. **Wiederherstellen** +5. **Nachher erfassen** +6. **Vergleichen** -## Sicherheitsdesign +Kopfbereich, Status-Badge, einheitliche Eingabefelder, gleich große +Workflow-Schaltflächen, farblich getrennte destruktive und wiederherstellende +Aktionen sowie responsive Ergebnis- und Logtabellen sorgen für eine klare +Bedienung auch bei kleineren Bildschirmgrößen und DPI-Skalierung. -- Dry-run ist standardmäßig aktiviert. -- Beim Start wird geprüft, ob die Anwendung mit Administratorrechten läuft. Ohne erhöhte Rechte wird eine Fehlermeldung angezeigt und die Anwendung beendet. -- Echte Shutdown-/Restore-Aktionen verlangen bei deaktiviertem Dry-run eine zusätzliche Bestätigung. -- Jede Operation schreibt Einträge in das sichtbare Operation Log. -- Zusätzlich wird neben der EXE eine tägliche Logdatei `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben. -- Logdateien werden rollierend für den aktuellen Tag plus vier vorherige Tage vorgehalten. -- Der Kopfbereich zeigt den zuletzt erkannten Umgebungsstatus aus den Host Instances. -- Operationspläne werden vor Laufzeitänderungen gespeichert. -- WMI-Methodenrückgaben werden geprüft. -- Wartezeiten nutzen konfigurierbare Timeout- und Polling-Werte. -- Host Instances auf anderen Servern werden übersprungen und als Warnung protokolliert. -- Echte Shutdown-/Restore-Schritte protokollieren WMI-Klasse, Schlüssel, Zielobjekt und Methode, damit Fehler wie WMI-Query- oder Methodenfehler eindeutig zugeordnet werden können. +Der Dry-run ist standardmäßig aktiviert. Bei echten Änderungen zeigt die +Statusleiste einen roten Hinweis und jede Änderung verlangt eine zusätzliche +Bestätigung mit Zielserver und verwendeter Zustandsdatei. Während einer +Operation sind Eingaben gesperrt; das Fenster kann nicht versehentlich +geschlossen werden. -## Shutdown-Reihenfolge +## Erfasster Zustand -1. Receive Locations deaktivieren, die im Snapshot aktiviert waren. -2. Orchestrations stoppen, die gestartet waren. -3. Send Ports stoppen, die gestartet waren. -4. Host Instances stoppen, die auf dem ausgewählten Server gestartet waren. +Über `root\MicrosoftBizTalkServer` werden gelesen: -## Restore-Reihenfolge +| WMI-Klasse | Relevanter Zustand | +| --- | --- | +| `MSBTS_ReceiveLocation` | `IsDisabled`, Name, Receive Port, Adapter | +| `MSBTS_SendPort` | `Status`, Name, Adaptertyp | +| `MSBTS_Orchestration` | `OrchestrationStatus`, Name und Assembly-Identität | +| `MSBTS_HostInstance` | `ServiceState`, Hostname und ausführender Server | -1. Host Instances starten, die auf dem ausgewählten Server zuvor gestartet waren. -2. Send Ports wiederherstellen. -3. Orchestrations wiederherstellen, soweit dies sicher möglich ist. -4. Receive Locations zuletzt wiederherstellen. +Transportadressen werden bewusst nicht persistiert. Sie sind für Start/Stop +nicht erforderlich und können Benutzerinformationen, Kennwörter oder andere +sensible Bestandteile enthalten. -## Restore-Grenzen +## Zustandswerte -- Send Ports werden auf `Started`, `Stopped` oder `Bound` zurückgesetzt. -- Orchestrations mit Status `Started`, `Stopped` oder `Unbound` werden direkt behandelt. -- Orchestrations mit Status `Bound` bleiben unverändert und werden zur manuellen Prüfung markiert, weil ein blindes Unenlist zu `Unbound` führen kann. -- Receive Locations werden zuletzt aktiviert, damit eingehender Verkehr erst nach Host Instances und abhängigen Artefakten wieder aufgenommen wird. +- Send Port: `1=Bound`, `2=Stopped`, `3=Started` +- Orchestrierung: `1=Unbound`, `2=Bound`, `3=Stopped`, `4=Started` +- Hostinstanz: `1=Stopped`, `2=StartPending`, `3=StopPending`, `4=Running` +- Receive Location: `Enabled = !IsDisabled` -## Ausgabedateien +Weitere Host-Servicezustände werden als unbekannt angezeigt und nicht +automatisch als gestartet behandelt. -- Snapshots: `before.json`, `after.json` -- Operationspläne: `shutdown-plan.json`, `restore-plan.json` -- Nachher-Snapshots: `shutdown-after.json`, `restore-after.json` -- Diff: `diff.json`, `diff.csv`, `diff.html` -- Snapshot-Reports: `*.csv`, `*.hosts.csv`, `*.html` -- Laufzeitlogs neben der EXE: `BizTalkPlatformManagementTool-yyyy-MM-dd.log` +## Shutdown-Plan -## Fehleranalyse +1. Alle im Snapshot aktivierten Receive Locations deaktivieren. +2. Alle im Snapshot gestarteten Orchestrierungen kontrolliert stoppen. +3. Alle im Snapshot gestarteten Send Ports stoppen. +4. Alle im Snapshot gestarteten Hostinstanzen des ausgewählten Servers stoppen. -Bei echten Shutdown- und Restore-Aktionen wird jeder Schritt vor der Ausführung mit Artefakttyp, WMI-Klasse, Schlüsselproperty, Schlüsselwert und Methodenname protokolliert. Die Objektauflösung verwendet eine breite `SELECT * FROM `-Abfrage und filtert danach im Prozess auf den Schlüsselwert. Dadurch können Host-Instance-Namen und andere BizTalk-Namen mit Sonderzeichen keine ungültige WMI-WQL-`WHERE`-Query mehr erzeugen. +Hostinstanzen anderer BizTalk-Knoten erscheinen als übersprungene Hinweise. +Damit kann derselbe Gruppen-Snapshot nachvollziehbar bleiben, ohne remote +Hostinstanzen versehentlich vom falschen Knoten aus zu verändern. -Snapshot- und Plan-JSON-Dateien werden als UTF-8 ohne BOM geschrieben. Beim Laden werden vorhandene Dateien mit UTF-8-BOM oder durch Encoding-Konvertierung sichtbar gewordenem BOM-Marker toleriert. +## Restore-Plan -## Status Mapping +1. Zuvor gestartete Hostinstanzen starten. +2. Zuvor gestartete Send Ports starten. +3. Zuvor gestartete Orchestrierungen starten. +4. Zuvor aktivierte Receive Locations zuletzt aktivieren. -- `MSBTS_SendPort.Status`: `1=Bound`, `2=Stopped`, `3=Started` -- `MSBTS_Orchestration.OrchestrationStatus`: `1=Unbound`, `2=Bound`, `3=Stopped`, `4=Started` -- `MSBTS_ReceiveLocation`: `Enabled = !IsDisabled` -- `MSBTS_HostInstance.ServiceState`: `1=Stopped`, `2=StartPending`, `3=StopPending`, `4=Started` +Gestoppte oder gebundene Send Ports, gestoppte/gebundene/ungebundene +Orchestrierungen, deaktivierte Receive Locations und gestoppte Hostinstanzen +erzeugen keine Restore-Aktion. -## Anforderungen +## Eindeutige WMI-Auflösung -- Windows Server oder Administrationshost mit .NET Framework 4.6.1 -- BizTalk Server 2020 oder installierte BizTalk Administration Tools -- Berechtigungen auf den WMI-Namespace `root\MicrosoftBizTalkServer` -- Lokale Administratorrechte und Start der Anwendung mit **Als Administrator ausführen** -- Visual Studio mit .NET Framework 4.6.1 Developer Pack für Builds +WMI-Abfragen werden ohne dynamisch zusammengesetzte WQL-`WHERE`-Klauseln +ausgeführt. Die Filterung erfolgt im Prozess und behandelt Sonderzeichen in +Namen sicher. Zusätzlich werden verwendet: + +- Receive Location: `Name` plus `ReceivePortName` +- Orchestrierung: `Name` plus `AssemblyName` +- Hostinstanz: `InstanceName` plus `RunningServer` + +Die vollständige Assembly-Identität einer Orchestrierung wird im Snapshot und +beim Vergleich berücksichtigt. Liefert WMI trotz der Qualifier mehrere +Treffer, wird die Operation mit einer eindeutigen Fehlermeldung abgebrochen. +Jeder Schritt prüft vor dem Methodenaufruf den aktuellen Zustand. Bereits +erreichte Zielzustände werden protokolliert und übersprungen; dadurch können +teilweise ausgeführte Pläne nach einer Fehlerkorrektur sicher erneut gestartet +werden. + +## Dateisicherheit + +JSON-Dateien werden zunächst vollständig in eine temporäre Datei geschrieben, +auf den Datenträger gespült und anschließend atomar ersetzt. Leere Dateien und +Dateien über 16 MiB werden verworfen. UTF-8 mit und ohne BOM sowie der sichtbare +BOM-Marker älterer Konvertierungen werden beim Lesen toleriert. + +Vergleiche verwenden zusammengesetzte Identitäten. Gleichnamige Artefakte in +unterschiedlichen Anwendungen, Receive Ports oder Assembly-Versionen werden +nicht miteinander vermischt. + +## Verzeichnisse + +```text +%ProgramFiles%\BizTalkPlatformManagementTool\ + BizTalkPlatformManagementTool.exe + BizTalkPlatformManagementTool.exe.config + +%ProgramData%\BizTalkPlatformManagementTool\ + Wartungen\ + Logs\ +``` + +Die Trennung verhindert, dass ein Update Zustandsdateien überschreibt, und +ermöglicht normale Logrotation außerhalb des geschützten Programmverzeichnisses. +Logs werden für den aktuellen Tag und vier vorherige Tage aufbewahrt. + +## Installer und Rollback + +Der .NET-Installer benötigt keine PowerShell. Er validiert die Assembly, baut +ein Staging-Verzeichnis auf, verschiebt eine bestehende Installation in ein +Backup und aktiviert anschließend die neue Version. Bei einem Fehler wird das +Backup zurückverschoben. Startmenü- und optionale Desktop-Verknüpfung werden +systemweit angelegt. + +Der Release-Packager erzeugt den Installationsordner, ein ZIP, eine +Base64-Transportdatei und eine SHA-256-Datei. + +## Tests + +Die Regressionstests prüfen: + +- Restore enthält ausschließlich zuvor aktive Artefakte. +- Shutdown enthält ausschließlich zuvor aktive Artefakte. +- Receive Locations erhalten ihren zusätzlichen WMI-Qualifier. +- Vergleiche trennen gleichnamige Artefakte verschiedener Anwendungen. +- fremde Server-Snapshots werden abgewiesen. +- atomarer JSON-Ersatz hinterlässt keine temporären Dateien. + +Ein echter WMI-Integrationstest muss auf einem BizTalk-2020-System im Dry-run +und anschließend in einem freigegebenen Wartungsfenster erfolgen. diff --git a/Installation.md b/Installation.md index 2985f19..410bfe8 100644 --- a/Installation.md +++ b/Installation.md @@ -1,40 +1,59 @@ -# Installation +# Installation und Update ## Voraussetzungen -- Windows Server 2019/2022 oder ein Windows-Administrationshost -- Microsoft BizTalk Server 2020 oder BizTalk Administration Tools -- .NET Framework 4.6.1 Runtime -- Für Builds: Visual Studio mit .NET Framework 4.6.1 Developer Pack -- Zugriff auf den WMI-Namespace `root\MicrosoftBizTalkServer` -- Ausreichende Rechte zum Lesen und Ändern von BizTalk-Artefakten -- Lokale Administratorrechte und Start der EXE mit **Als Administrator ausführen** -- Schreibrechte im Verzeichnis der EXE für die tägliche Logdatei +- Windows Server 2019/2022 oder Administrationshost +- Microsoft BizTalk Server 2020 oder installierte BizTalk Administration Tools +- .NET Framework 4.6.1 oder neuer +- lokales Administratorkonto +- Mitgliedschaft in der Gruppe **BizTalk Server Administrators** +- WMI-Zugriff auf `root\MicrosoftBizTalkServer` -## Build +## Installation -1. Repository öffnen. -2. `BizTalkPlatformManagementTool.sln` in Visual Studio öffnen. -3. Konfiguration `Release|Any CPU` auswählen. -4. Solution bauen. -5. Das Ergebnis liegt unter `src\BizTalkPlatformManagementTool\bin\Release\`. +1. `BizTalkPlatformManagementTool-Setup.zip` vollständig entpacken. +2. `Setup.exe` starten; die UAC-Anforderung bestätigen. +3. Optional die systemweite Desktop-Verknüpfung auswählen. +4. **Installieren / aktualisieren** wählen. -## Deployment +Installiert wird nach `%ProgramFiles%\BizTalkPlatformManagementTool`. +Persistente Wartungszustände und Logs liegen getrennt unter +`%ProgramData%\BizTalkPlatformManagementTool`. -1. Den Release-Ordner auf einen BizTalk-Server oder einen Administrationshost kopieren. -2. Sicherstellen, dass der ausführende Benutzer WMI-Zugriff auf `root\MicrosoftBizTalkServer` hat. -3. `BizTalkPlatformManagementTool.exe` mit **Als Administrator ausführen** starten. -4. Als Ausgabeverzeichnis einen Ordner wählen, in dem Plan-, Snapshot- und Report-Dateien abgelegt werden dürfen. -5. Prüfen, dass im EXE-Verzeichnis `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben werden kann. Logs werden für maximal fünf Tage vorgehalten. +## Sicheres Update + +Der Installer validiert das Paket, kopiert die neue Version zunächst in ein +Staging-Verzeichnis und aktiviert sie erst danach. Eine vorhandene Installation +wird vorübergehend umbenannt. Schlägt das Update fehl, wird die vorherige +Programmversion wiederhergestellt. Wartungszustände und Logs unter +`%ProgramData%` bleiben unberührt. + +Vor dem Update muss das Management Tool geschlossen sein. Laufende +Wartungsoperationen dürfen nicht durch ein Update unterbrochen werden. + +## Deinstallation + +Im Setup kann die Anwendung wieder deinstalliert werden. Standardmäßig bleiben +Wartungszustände und Logs erhalten. Die Option kann deaktiviert werden, wenn +auch diese Daten bewusst entfernt werden sollen. + +## Transport-TXT zurückwandeln + +Die Datei `BizTalkPlatformManagementTool-Setup.zip.b64.txt` kann auf dem +Zielsystem beispielsweise mit `certutil.exe` zurückgewandelt werden: + +```cmd +certutil.exe -decode BizTalkPlatformManagementTool-Setup.zip.b64.txt BizTalkPlatformManagementTool-Setup.zip +``` + +Anschließend die SHA-256-Prüfsumme mit +`BizTalkPlatformManagementTool-Setup.zip.sha256.txt` vergleichen. ## Erster Funktionstest -1. Anwendung mit **Als Administrator ausführen** starten. -2. Zielserver eintragen oder den vorgeschlagenen lokalen Server verwenden. -3. **Dry run** aktiviert lassen. -4. **Diagnose** ausführen. -5. **Snapshot Before** ausführen und prüfen, ob `before.json` sowie CSV/HTML-Reports erzeugt wurden. - -## Produktive Nutzung - -Vor produktiven Änderungen immer zuerst einen Dry-run ausführen und die erzeugten `shutdown-plan.json` beziehungsweise `restore-plan.json` prüfen. Dry-run erst deaktivieren, wenn der Plan fachlich und technisch korrekt ist. +1. Tool über Startmenü oder Desktop-Verknüpfung starten. +2. Dry-run aktiviert lassen. +3. **Verbindung prüfen** ausführen. +4. **Zustand erfassen** ausführen. +5. JSON-, CSV- und HTML-Dateien im Arbeitsordner prüfen. +6. **Herunterfahren** im Dry-run ausführen und den Plan kontrollieren. diff --git a/README.md b/README.md index b6d1208..ac7553d 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,105 @@ # BizTalk Platform Management Tool -WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations during maintenance windows. The tool captures runtime snapshots, creates shutdown and restore plans, executes those plans in a safe order, and writes diff reports for validation. +Grafisches .NET-Werkzeug für kontrollierte Wartungsfenster einer Microsoft +BizTalk-Server-2020-Umgebung. Es erfasst den Laufzeitzustand, fährt ausschließlich +die zuvor aktiven Artefakte in sicherer Reihenfolge herunter und startet nach +dem Patchen ausschließlich diese Artefakte wieder. -## Current State +## Wichtigste Eigenschaften -- Application: C# WinForms -- Target framework: .NET Framework 4.6.1 -- Solution: `BizTalkPlatformManagementTool.sln` -- Project: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj` -- Primary namespace and assembly: `BizTalkPlatformManagementTool` -- Legacy PowerShell archive: `archive/powershell/BizTalkPlatformManagementTool.ps1` +- übersichtliche deutschsprachige WinForms-Oberfläche mit klarer Sechs-Schritt-Führung +- Dry-run standardmäßig aktiv +- Vorher-Snapshot als verbindliche Wiederherstellungsgrundlage +- vorhandener Vorher-Snapshot wird beim Herunterfahren nicht überschrieben +- servergebundene Validierung verhindert die Verwendung einer Zustandsdatei für den falschen BizTalk-Server +- Receive Locations, Send Ports, Orchestrierungen und Hostinstanzen +- kontrollierte Reihenfolge mit Zustandsprüfung und konfigurierbarem Timeout +- nur vorher aktive Artefakte werden heruntergefahren und wieder gestartet +- WMI-Objekte werden eindeutig aufgelöst; mehrdeutige Treffer führen sicher zum Abbruch +- wiederholbare Ausführung: bereits erreichte Zielzustände werden übersprungen +- atomar geschriebene JSON-Dateien mit Größenbegrenzung +- keine Transport-URIs, Benutzernamen oder Kennwörter im Snapshot +- CSV-/HTML-Berichte und Vorher-/Nachher-Vergleich +- tägliche Logs unter `%ProgramData%\BizTalkPlatformManagementTool\Logs` +- eigenständiger .NET-Installer für Installation, Update und Deinstallation +- ZIP-, SHA-256- und Base64-TXT-Paket werden beim Release-Build erzeugt -## Features +## Sicherer Wartungsablauf -- Snapshot before and after downtime as JSON, CSV and HTML -- Diff between `before.json` and `after.json` -- Controlled shutdown from the current runtime state -- Controlled restore from `before.json` -- Host instance handling for the selected BizTalk server -- Dry-run mode enabled by default -- WMI access through `root\MicrosoftBizTalkServer` -- Startup check for administrator rights -- Detailed operation logging in the GUI and daily rolling log files next to the executable -- Environment status indicator based on host instance state -- Clear and Close actions in the main toolbar -- No compile-time dependency on BizTalk ExplorerOM assemblies +1. Tool als Administrator starten. +2. Zielserver und Arbeitsordner prüfen. +3. **Verbindung prüfen** ausführen. +4. Mit aktiviertem Dry-run **Zustand erfassen**. +5. **Herunterfahren** im Dry-run ausführen und `shutdown-plan.json` prüfen. +6. Dry-run deaktivieren und **Herunterfahren** bestätigen. +7. Server manuell patchen. +8. Dieselbe Vorher-Zustandsdatei auswählen. +9. **Wiederherstellen** zunächst im Dry-run prüfen und danach real ausführen. +10. **Nachher erfassen** und **Vergleichen**. -## Safe Usage +Die Zustandsdatei ist sicherheitskritisch: Sie definiert exakt, welche +Artefakte wieder gestartet werden. Bewusst gestoppte, gebundene oder +deaktivierte Artefakte bleiben beim Restore unangetastet. -1. Start the app with **Run as administrator** on a BizTalk server or a management host with WMI access. -2. Keep **Dry run** enabled. -3. Click **Diagnose** to verify WMI access. -4. Click **Snapshot Before**. -5. Click **Shutdown** and review `shutdown-plan.json`. -6. Disable **Dry run** only when the plan is correct. -7. After maintenance, click **Restore** using the saved `before.json`. -8. Click **Snapshot After** and **Compare**. +## Reihenfolge -The environment indicator shows `Started`, `Stopped`, `Partial` or `Unknown` from the most recent snapshot. `Clear` removes the visible status and operation log grids; it does not delete files. +Herunterfahren: -The application checks administrator rights during startup. If it is not elevated, it shows an error message and exits because BizTalk WMI operations require an elevated administrator process. +1. zuvor aktivierte Receive Locations deaktivieren +2. zuvor gestartete Orchestrierungen stoppen +3. zuvor gestartete Send Ports stoppen +4. zuvor gestartete Hostinstanzen des ausgewählten Servers stoppen -## Operation Order +Wiederherstellen: -Shutdown: -- Disable receive locations that were enabled. -- Stop orchestrations that were started. -- Stop send ports that were started. -- Stop host instances that were started on the selected server. +1. zuvor gestartete Hostinstanzen des ausgewählten Servers starten +2. zuvor gestartete Send Ports starten +3. zuvor gestartete Orchestrierungen starten +4. zuvor aktivierte Receive Locations zuletzt aktivieren -Restore: -- Start host instances that were previously started. -- Restore send ports to Started, Stopped or Bound. -- Restore orchestrations where safe. -- Restore receive locations last. +## Dateien -Orchestrations that were `Bound` are deliberately left unchanged during restore to avoid accidentally making them `Unbound`. +Standard-Arbeitsbereich: -## Outputs +```text +%ProgramData%\BizTalkPlatformManagementTool\ + Wartungen\ + Wartungsstatus-YYYYMMDD-HHmmss.json + shutdown-plan.json + shutdown-after.json + restore-plan.json + restore-after.json + after.json + diff.json / diff.csv / diff.html + Logs\ + BizTalkPlatformManagementTool-YYYY-MM-DD.log +``` -- `before.json`, `after.json` -- `shutdown-plan.json`, `restore-plan.json` -- `shutdown-after.json`, `restore-after.json` -- `diff.json`, `diff.csv`, `diff.html` -- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html` -- Runtime logs next to the executable: `BizTalkPlatformManagementTool-yyyy-MM-dd.log` +Zu jedem Snapshot entstehen CSV- und HTML-Begleitberichte. Vollständige +Transportadressen werden nicht gespeichert, weil sie Zugangsdaten oder andere +sensible URI-Bestandteile enthalten können und für die Wiederherstellung nicht +benötigt werden. -Log files are retained for the current day plus the previous four days. Older `BizTalkPlatformManagementTool-*.log` files are removed on startup. +## Build und Tests -## Troubleshooting +Voraussetzung ist das .NET Framework 4.6.1 Developer Pack: -The Operation Log shows the WMI class, key property, key value and method for real shutdown and restore steps. WMI objects are resolved with a broad `SELECT * FROM ` query and a client-side key filter so names containing special characters do not break the WMI query parser. +```cmd +msbuild BizTalkPlatformManagementTool.sln /t:Rebuild /p:Configuration=Release /p:Platform="Any CPU" +tests\BizTalkPlatformManagementTool.Tests\bin\Release\BizTalkPlatformManagementTool.Tests.exe +``` -Snapshot and plan JSON files are written as UTF-8 without BOM. Loading is tolerant of existing files that contain a UTF-8 BOM or a visible BOM marker from previous encoding conversions. +Der Release-Build erzeugt automatisch: -## Build +```text +artifacts\BizTalkPlatformManagementTool-Setup\ +artifacts\BizTalkPlatformManagementTool-Setup.zip +artifacts\BizTalkPlatformManagementTool-Setup.zip.b64.txt +artifacts\BizTalkPlatformManagementTool-Setup.zip.sha256.txt +``` -Open `BizTalkPlatformManagementTool.sln` in Visual Studio on Windows with the .NET Framework 4.6.1 Developer Pack installed, then build the `Release|Any CPU` configuration. - -The app targets .NET Framework 4.6.1 for compatibility with customer environments that do not have newer .NET Framework developer packs installed. - -## Documentation +## Dokumentation - [Installation](Installation.md) -- [Dokumentation](Dokumentation.md) -- [References](REFERENCES.md) +- [Technische Dokumentation](Dokumentation.md) +- [Microsoft-Referenzen](REFERENCES.md) diff --git a/REFERENCES.md b/REFERENCES.md index c52a97f..7756d84 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -12,8 +12,11 @@ - MSBTS_Orchestration.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-stop-method-wmi - MSBTS_Orchestration.Unenlist (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-unenlist-method-wmi - MSBTS_ReceiveLocation (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi +- MSBTS_ReceiveLocation.Name / zusammengesetzter Schlüssel: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-name-property-wmi - MSBTS_ReceiveLocation.Enable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-enable-method-wmi - MSBTS_ReceiveLocation.Disable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-disable-method-wmi - MSBTS_HostInstance (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-wmi - MSBTS_HostInstance.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-start-method-wmi - MSBTS_HostInstance.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-stop-method-wmi +- MSBTS_HostInstance.ServiceState (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-servicestate-property-wmi +- MSBTS_Orchestration.AssemblyName / zusammengesetzter Schlüssel: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-assemblyname-property-wmi diff --git a/src/BizTalkPlatformManagementTool.Packager/BizTalkPlatformManagementTool.Packager.csproj b/src/BizTalkPlatformManagementTool.Packager/BizTalkPlatformManagementTool.Packager.csproj new file mode 100644 index 0000000..788b1f1 --- /dev/null +++ b/src/BizTalkPlatformManagementTool.Packager/BizTalkPlatformManagementTool.Packager.csproj @@ -0,0 +1,33 @@ + + + + + Debug + AnyCPU + {AF67FD76-8F5D-42DB-9D52-20EF6332D1F9} + Exe + BizTalkPlatformManagementTool.Packager + BizTalkPlatformManagementTool.Packager + v4.6.1 + 512 + true + + + truefullfalsebin\Debug\DEBUG;TRACE4 + + + pdbonlytruebin\Release\TRACE4 + + + + + + + {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}BizTalkPlatformManagementToolfalse + {38E61630-11AB-4F8D-B421-F8A88831C4BD}BizTalkPlatformManagementTool.Setupfalse + + + + + + diff --git a/src/BizTalkPlatformManagementTool.Packager/Program.cs b/src/BizTalkPlatformManagementTool.Packager/Program.cs new file mode 100644 index 0000000..67ec089 --- /dev/null +++ b/src/BizTalkPlatformManagementTool.Packager/Program.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Security.Cryptography; +using System.Text; + +namespace BizTalkPlatformManagementTool.Packager +{ + internal static class Program + { + private static int Main(string[] args) + { + try + { + if (args.Length != 2) throw new ArgumentException("Usage: Packager.exe "); + var root = Path.GetFullPath(args[0]); + var configuration = args[1]; + var artifacts = Path.Combine(root, "artifacts"); + var package = Path.Combine(artifacts, "BizTalkPlatformManagementTool-Setup"); + var zip = package + ".zip"; + var base64 = zip + ".b64.txt"; + var checksum = zip + ".sha256.txt"; + RecreateDirectory(package); + Directory.CreateDirectory(Path.Combine(package, "application")); + Directory.CreateDirectory(Path.Combine(package, "documentation")); + + var applicationOutput = Path.Combine(root, "src", "BizTalkPlatformManagementTool", "bin", configuration); + var setupOutput = Path.Combine(root, "src", "BizTalkPlatformManagementTool.Setup", "bin", configuration); + CopyRequired(Path.Combine(applicationOutput, "BizTalkPlatformManagementTool.exe"), Path.Combine(package, "application", "BizTalkPlatformManagementTool.exe")); + CopyRequired(Path.Combine(applicationOutput, "BizTalkPlatformManagementTool.exe.config"), Path.Combine(package, "application", "BizTalkPlatformManagementTool.exe.config")); + CopyRequired(Path.Combine(setupOutput, "BizTalkPlatformManagementTool.Setup.exe"), Path.Combine(package, "Setup.exe")); + CopyRequired(Path.Combine(root, "README.md"), Path.Combine(package, "documentation", "README.md")); + CopyRequired(Path.Combine(root, "Dokumentation.md"), Path.Combine(package, "documentation", "Dokumentation.md")); + CopyRequired(Path.Combine(root, "Installation.md"), Path.Combine(package, "documentation", "Installation.md")); + + DeleteIfExists(zip); + DeleteIfExists(base64); + DeleteIfExists(checksum); + ZipFile.CreateFromDirectory(package, zip, CompressionLevel.Optimal, false); + WriteBase64(zip, base64); + var hash = ComputeSha256(zip); + File.WriteAllText(checksum, hash + " " + Path.GetFileName(zip) + "\r\n", new UTF8Encoding(false)); + Console.WriteLine("Installationsordner: " + package); + Console.WriteLine("Installations-ZIP: " + zip); + Console.WriteLine("Transport-TXT: " + base64); + Console.WriteLine("SHA-256: " + hash); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine("Packaging fehlgeschlagen: " + ex.Message); + return 1; + } + } + + private static void WriteBase64(string source, string target) + { + var encoded = Convert.ToBase64String(File.ReadAllBytes(source)); + using (var writer = new StreamWriter(target, false, new UTF8Encoding(false))) + { + for (var offset = 0; offset < encoded.Length; offset += 120) + writer.WriteLine(encoded.Substring(offset, Math.Min(120, encoded.Length - offset))); + } + } + + private static string ComputeSha256(string path) + { + using (var algorithm = SHA256.Create()) + using (var stream = File.OpenRead(path)) + { + return BitConverter.ToString(algorithm.ComputeHash(stream)).Replace("-", string.Empty).ToLowerInvariant(); + } + } + + private static void CopyRequired(string source, string destination) + { + if (!File.Exists(source)) throw new FileNotFoundException("Datei fehlt: " + source, source); + File.Copy(source, destination, true); + } + + private static void RecreateDirectory(string path) + { + if (Directory.Exists(path)) Directory.Delete(path, true); + Directory.CreateDirectory(path); + } + + private static void DeleteIfExists(string path) + { + if (File.Exists(path)) File.Delete(path); + } + } +} diff --git a/src/BizTalkPlatformManagementTool.Setup/BizTalkPlatformManagementTool.Setup.csproj b/src/BizTalkPlatformManagementTool.Setup/BizTalkPlatformManagementTool.Setup.csproj new file mode 100644 index 0000000..ddae233 --- /dev/null +++ b/src/BizTalkPlatformManagementTool.Setup/BizTalkPlatformManagementTool.Setup.csproj @@ -0,0 +1,32 @@ + + + + + Debug + AnyCPU + {38E61630-11AB-4F8D-B421-F8A88831C4BD} + WinExe + BizTalkPlatformManagementTool.Setup + BizTalkPlatformManagementTool.Setup + v4.6.1 + app.manifest + 512 + true + + + truefullfalsebin\Debug\DEBUG;TRACE4 + + + pdbonlytruebin\Release\TRACE4 + + + + + + + + + {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}BizTalkPlatformManagementToolfalse + + + diff --git a/src/BizTalkPlatformManagementTool.Setup/InstallerEngine.cs b/src/BizTalkPlatformManagementTool.Setup/InstallerEngine.cs new file mode 100644 index 0000000..cb3733b --- /dev/null +++ b/src/BizTalkPlatformManagementTool.Setup/InstallerEngine.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; + +namespace BizTalkPlatformManagementTool.Setup +{ + internal sealed class InstallerEngine + { + private const string ProductName = "BizTalkPlatformManagementTool"; + private readonly string _packageDirectory; + private readonly string _installDirectory; + private readonly string _runtimeDirectory; + + public InstallerEngine(string packageDirectory) + { + _packageDirectory = Path.GetFullPath(packageDirectory); + _installDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), ProductName); + _runtimeDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), ProductName); + } + + public bool IsInstalled + { + get { return File.Exists(Path.Combine(_installDirectory, "BizTalkPlatformManagementTool.exe")); } + } + + public void Install(bool createDesktopShortcut, Action report) + { + report = report ?? delegate { }; + var source = Path.Combine(_packageDirectory, "application"); + var sourceExe = Path.Combine(source, "BizTalkPlatformManagementTool.exe"); + RequireFile(sourceExe); + RequireFile(sourceExe + ".config"); + var assembly = AssemblyName.GetAssemblyName(sourceExe); + report("Paket geprüft: Version " + assembly.Version + "."); + + var staging = _installDirectory + ".staging." + Guid.NewGuid().ToString("N"); + var backup = _installDirectory + ".backup." + Guid.NewGuid().ToString("N"); + var hadInstallation = Directory.Exists(_installDirectory); + var backupCreated = false; + var activated = false; + try + { + CopyDirectory(source, staging); + AssemblyName.GetAssemblyName(Path.Combine(staging, "BizTalkPlatformManagementTool.exe")); + report("Neue Version im Staging-Verzeichnis validiert."); + + if (hadInstallation) + { + Directory.Move(_installDirectory, backup); + backupCreated = true; + } + + Directory.Move(staging, _installDirectory); + activated = true; + Directory.CreateDirectory(Path.Combine(_runtimeDirectory, "Wartungen")); + Directory.CreateDirectory(Path.Combine(_runtimeDirectory, "Logs")); + CreateShortcuts(createDesktopShortcut); + report(hadInstallation ? "Programmdateien sicher aktualisiert." : "Programmdateien installiert."); + report("Arbeitsdaten bleiben unter " + _runtimeDirectory + "."); + TryDeleteDirectory(backup); + } + catch (Exception ex) + { + var rollbackErrors = new List(); + try + { + if (activated && Directory.Exists(_installDirectory)) Directory.Delete(_installDirectory, true); + if (backupCreated && Directory.Exists(backup)) Directory.Move(backup, _installDirectory); + } + catch (Exception rollback) + { + rollbackErrors.Add(rollback.Message); + } + + throw new InvalidOperationException( + "Installation/Update fehlgeschlagen. " + + (rollbackErrors.Count == 0 ? "Die vorherige Version wurde wiederhergestellt. " : "Rollbackfehler: " + string.Join(" | ", rollbackErrors) + ". ") + + "Ursache: " + ex.Message, + ex); + } + finally + { + TryDeleteDirectory(staging); + } + } + + public void Uninstall(bool keepRuntimeData, Action report) + { + report = report ?? delegate { }; + DeleteShortcuts(); + if (Directory.Exists(_installDirectory)) Directory.Delete(_installDirectory, true); + if (!keepRuntimeData && Directory.Exists(_runtimeDirectory)) Directory.Delete(_runtimeDirectory, true); + report(keepRuntimeData + ? "Programm entfernt; Wartungszustände und Logs wurden beibehalten." + : "Programm und Arbeitsdaten wurden entfernt."); + } + + private void CreateShortcuts(bool desktop) + { + var target = Path.Combine(_installDirectory, "BizTalkPlatformManagementTool.exe"); + var programs = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), "JR IT Services"); + Directory.CreateDirectory(programs); + WriteInternetShortcut(Path.Combine(programs, "BizTalk Platform Management Tool.url"), target); + var desktopPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), "BizTalk Platform Management Tool.url"); + if (desktop) WriteInternetShortcut(desktopPath, target); else if (File.Exists(desktopPath)) File.Delete(desktopPath); + } + + private static void WriteInternetShortcut(string path, string target) + { + var uri = new Uri(target).AbsoluteUri; + var content = "[InternetShortcut]\r\nURL=" + uri + "\r\nIconFile=" + target + "\r\nIconIndex=0\r\n"; + File.WriteAllText(path, content, new UTF8Encoding(false)); + } + + private static void DeleteShortcuts() + { + var start = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), "JR IT Services", "BizTalk Platform Management Tool.url"); + var desktop = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), "BizTalk Platform Management Tool.url"); + if (File.Exists(start)) File.Delete(start); + if (File.Exists(desktop)) File.Delete(desktop); + } + + private static void CopyDirectory(string source, string destination) + { + Directory.CreateDirectory(destination); + foreach (var file in Directory.GetFiles(source)) + File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), false); + foreach (var directory in Directory.GetDirectories(source)) + CopyDirectory(directory, Path.Combine(destination, Path.GetFileName(directory))); + } + + private static void RequireFile(string path) + { + if (!File.Exists(path)) throw new FileNotFoundException("Installationspaket ist unvollständig. Datei fehlt: " + path, path); + } + + private static void TryDeleteDirectory(string path) + { + if (!Directory.Exists(path)) return; + try { Directory.Delete(path, true); } + catch { } + } + } +} diff --git a/src/BizTalkPlatformManagementTool.Setup/MainForm.cs b/src/BizTalkPlatformManagementTool.Setup/MainForm.cs new file mode 100644 index 0000000..5b95020 --- /dev/null +++ b/src/BizTalkPlatformManagementTool.Setup/MainForm.cs @@ -0,0 +1,148 @@ +using System; +using System.Drawing; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace BizTalkPlatformManagementTool.Setup +{ + internal sealed class MainForm : Form + { + private readonly InstallerEngine _engine; + private readonly CheckBox _desktop = new CheckBox(); + private readonly CheckBox _keepData = new CheckBox(); + private readonly Button _install = new Button(); + private readonly Button _uninstall = new Button(); + private readonly TextBox _status = new TextBox(); + + public MainForm(InstallerEngine engine) + { + _engine = engine; + Text = "BizTalk Platform Management Tool – Setup"; + ClientSize = new Size(680, 500); + MinimumSize = new Size(696, 539); + StartPosition = FormStartPosition.CenterScreen; + AutoScaleMode = AutoScaleMode.Dpi; + Font = new Font("Segoe UI", 9F); + BackColor = Color.White; + BuildUi(); + } + + private void BuildUi() + { + var header = new Panel { Dock = DockStyle.Top, Height = 112, BackColor = Color.FromArgb(28, 67, 102) }; + header.Controls.Add(new Label { Text = "BizTalk Platform Management Tool", Location = new Point(24, 20), Size = new Size(620, 34), ForeColor = Color.White, Font = new Font(Font.FontFamily, 17F, FontStyle.Bold) }); + header.Controls.Add(new Label { Text = "Installieren oder sicher auf eine neue Version aktualisieren", Location = new Point(26, 62), Size = new Size(620, 25), ForeColor = Color.FromArgb(220, 232, 242) }); + Controls.Add(header); + + var body = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(24, 18, 24, 18), ColumnCount = 2, RowCount = 5 }; + body.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); + body.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); + body.RowStyles.Add(new RowStyle(SizeType.Absolute, 48)); + body.RowStyles.Add(new RowStyle(SizeType.Absolute, 36)); + body.RowStyles.Add(new RowStyle(SizeType.Absolute, 36)); + body.RowStyles.Add(new RowStyle(SizeType.Absolute, 54)); + body.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + Controls.Add(body); + + body.Controls.Add(new Label + { + Text = _engine.IsInstalled + ? "Eine vorhandene Installation wurde erkannt. Arbeitsdaten bleiben beim Update erhalten." + : "Bereit zur Installation. Setup muss aus dem vollständig entpackten Paket gestartet werden.", + Dock = DockStyle.Fill, + TextAlign = ContentAlignment.MiddleLeft + }, 0, 0); + body.SetColumnSpan(body.GetControlFromPosition(0, 0), 2); + + _desktop.Text = "Desktop-Verknüpfung erstellen"; + _desktop.Checked = true; + _desktop.Dock = DockStyle.Fill; + body.Controls.Add(_desktop, 0, 1); + body.SetColumnSpan(_desktop, 2); + _keepData.Text = "Wartungszustände und Logs bei Deinstallation behalten"; + _keepData.Checked = true; + _keepData.Dock = DockStyle.Fill; + body.Controls.Add(_keepData, 0, 2); + body.SetColumnSpan(_keepData, 2); + + ConfigureButton(_install, "Installieren / aktualisieren", Color.FromArgb(31, 111, 181)); + ConfigureButton(_uninstall, "Deinstallieren", Color.FromArgb(120, 55, 55)); + _install.Click += async delegate { await InstallAsync(); }; + _uninstall.Click += async delegate { await UninstallAsync(); }; + body.Controls.Add(_install, 0, 3); + body.Controls.Add(_uninstall, 1, 3); + + _status.Dock = DockStyle.Fill; + _status.Multiline = true; + _status.ReadOnly = true; + _status.ScrollBars = ScrollBars.Vertical; + _status.BackColor = Color.FromArgb(247, 249, 252); + _status.BorderStyle = BorderStyle.FixedSingle; + body.Controls.Add(_status, 0, 4); + body.SetColumnSpan(_status, 2); + _uninstall.Enabled = _engine.IsInstalled; + } + + private async Task InstallAsync() + { + SetBusy(true); + try + { + await Task.Run(() => _engine.Install(_desktop.Checked, Report)); + Report("Installation erfolgreich abgeschlossen."); + MessageBox.Show(this, "Installation/Update erfolgreich abgeschlossen.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + Report("FEHLER: " + ex.Message); + MessageBox.Show(this, ex.Message, "Installation fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally { SetBusy(false); } + } + + private async Task UninstallAsync() + { + if (MessageBox.Show(this, "BizTalk Platform Management Tool wirklich deinstallieren?", Text, MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes) return; + SetBusy(true); + try + { + await Task.Run(() => _engine.Uninstall(_keepData.Checked, Report)); + MessageBox.Show(this, "Deinstallation abgeschlossen.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + Report("FEHLER: " + ex.Message); + MessageBox.Show(this, ex.Message, "Deinstallation fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally { SetBusy(false); } + } + + private void Report(string message) + { + if (InvokeRequired) { BeginInvoke(new Action(Report), message); return; } + _status.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message + Environment.NewLine); + } + + private void SetBusy(bool busy) + { + if (InvokeRequired) { BeginInvoke(new Action(SetBusy), busy); return; } + _install.Enabled = !busy; + _uninstall.Enabled = !busy && _engine.IsInstalled; + _desktop.Enabled = !busy; + _keepData.Enabled = !busy; + UseWaitCursor = busy; + } + + private static void ConfigureButton(Button button, string text, Color color) + { + button.Text = text; + button.Dock = DockStyle.Fill; + button.Margin = new Padding(4, 7, 4, 7); + button.FlatStyle = FlatStyle.Flat; + button.FlatAppearance.BorderSize = 0; + button.BackColor = color; + button.ForeColor = Color.White; + button.Font = new Font("Segoe UI", 9F, FontStyle.Bold); + } + } +} diff --git a/src/BizTalkPlatformManagementTool.Setup/Program.cs b/src/BizTalkPlatformManagementTool.Setup/Program.cs new file mode 100644 index 0000000..269936d --- /dev/null +++ b/src/BizTalkPlatformManagementTool.Setup/Program.cs @@ -0,0 +1,16 @@ +using System; +using System.Windows.Forms; + +namespace BizTalkPlatformManagementTool.Setup +{ + internal static class Program + { + [STAThread] + private static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new MainForm(new InstallerEngine(AppDomain.CurrentDomain.BaseDirectory))); + } + } +} diff --git a/src/BizTalkPlatformManagementTool.Setup/app.manifest b/src/BizTalkPlatformManagementTool.Setup/app.manifest new file mode 100644 index 0000000..f2af1ea --- /dev/null +++ b/src/BizTalkPlatformManagementTool.Setup/app.manifest @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj b/src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj index c84037e..475805a 100644 --- a/src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj +++ b/src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj @@ -12,6 +12,7 @@ 512 true true + app.manifest AnyCPU @@ -46,11 +47,13 @@ + + @@ -62,6 +65,7 @@ + diff --git a/src/BizTalkPlatformManagementTool/Models/ArtifactStates.cs b/src/BizTalkPlatformManagementTool/Models/ArtifactStates.cs index 6809fa3..8f61d49 100644 --- a/src/BizTalkPlatformManagementTool/Models/ArtifactStates.cs +++ b/src/BizTalkPlatformManagementTool/Models/ArtifactStates.cs @@ -18,17 +18,17 @@ namespace BizTalkPlatformManagementTool.Models public static string FormatReceiveLocation(bool enabled) { - return enabled ? "Enabled" : "Disabled"; + return enabled ? "Aktiviert" : "Deaktiviert"; } public static string FormatSendPort(int status) { switch (status) { - case SendPortStarted: return "Started"; - case SendPortStopped: return "Stopped"; - case SendPortBound: return "Bound"; - default: return "Unknown (" + status + ")"; + case SendPortStarted: return "Gestartet"; + case SendPortStopped: return "Gestoppt"; + case SendPortBound: return "Gebunden"; + default: return "Unbekannt (" + status + ")"; } } @@ -36,11 +36,11 @@ namespace BizTalkPlatformManagementTool.Models { switch (status) { - case OrchestrationStarted: return "Started"; - case OrchestrationStopped: return "Stopped"; - case OrchestrationBound: return "Bound"; - case OrchestrationUnbound: return "Unbound"; - default: return "Unknown (" + status + ")"; + case OrchestrationStarted: return "Gestartet"; + case OrchestrationStopped: return "Gestoppt"; + case OrchestrationBound: return "Gebunden"; + case OrchestrationUnbound: return "Nicht gebunden"; + default: return "Unbekannt (" + status + ")"; } } @@ -48,11 +48,11 @@ namespace BizTalkPlatformManagementTool.Models { switch (state) { - case HostStarted: return "Started"; - case HostStartPending: return "StartPending"; - case HostStopPending: return "StopPending"; - case HostStopped: return "Stopped"; - default: return "Unknown (" + state + ")"; + case HostStarted: return "Gestartet"; + case HostStartPending: return "Wird gestartet"; + case HostStopPending: return "Wird gestoppt"; + case HostStopped: return "Gestoppt"; + default: return "Unbekannt (" + state + ")"; } } } diff --git a/src/BizTalkPlatformManagementTool/Models/BizTalkSnapshot.cs b/src/BizTalkPlatformManagementTool/Models/BizTalkSnapshot.cs index d769252..87cfd51 100644 --- a/src/BizTalkPlatformManagementTool/Models/BizTalkSnapshot.cs +++ b/src/BizTalkPlatformManagementTool/Models/BizTalkSnapshot.cs @@ -103,6 +103,18 @@ namespace BizTalkPlatformManagementTool.Models [DataMember(Order = 3)] public int OrchestrationStatus { get; set; } + + [DataMember(Order = 4, EmitDefaultValue = false)] + public string AssemblyName { get; set; } + + [DataMember(Order = 5, EmitDefaultValue = false)] + public string AssemblyVersion { get; set; } + + [DataMember(Order = 6, EmitDefaultValue = false)] + public string AssemblyCulture { get; set; } + + [DataMember(Order = 7, EmitDefaultValue = false)] + public string AssemblyPublicKeyToken { get; set; } } [DataContract] diff --git a/src/BizTalkPlatformManagementTool/Models/OperationModels.cs b/src/BizTalkPlatformManagementTool/Models/OperationModels.cs index 0ff62a4..c4606a5 100644 --- a/src/BizTalkPlatformManagementTool/Models/OperationModels.cs +++ b/src/BizTalkPlatformManagementTool/Models/OperationModels.cs @@ -80,6 +80,12 @@ namespace BizTalkPlatformManagementTool.Models [DataMember(Order = 13)] public string Warning { get; set; } + + [DataMember(Order = 14, EmitDefaultValue = false)] + public string QualifierProperty { get; set; } + + [DataMember(Order = 15, EmitDefaultValue = false)] + public string QualifierValue { get; set; } } public sealed class OperationOptions diff --git a/src/BizTalkPlatformManagementTool/Program.cs b/src/BizTalkPlatformManagementTool/Program.cs index 4882bd6..48a5c74 100644 --- a/src/BizTalkPlatformManagementTool/Program.cs +++ b/src/BizTalkPlatformManagementTool/Program.cs @@ -12,14 +12,19 @@ namespace BizTalkPlatformManagementTool { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); + Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); + Application.ThreadException += delegate(object sender, System.Threading.ThreadExceptionEventArgs args) + { + MessageBox.Show("Unerwarteter Fehler: " + args.Exception.Message, "BizTalk Platform Management Tool", MessageBoxButtons.OK, MessageBoxIcon.Error); + }; if (!IsRunningAsAdministrator()) { MessageBox.Show( - "BizTalk Platform Management Tool must be started with administrator rights.\r\n\r\n" + - "Please close the application and start it again with 'Run as administrator'. " + - "Without elevated rights, BizTalk WMI operations in root\\MicrosoftBizTalkServer will fail.", - "Administrator Rights Required", + "Das BizTalk Platform Management Tool benötigt Administratorrechte.\r\n\r\n" + + "Bitte starten Sie die Anwendung über ‚Als Administrator ausführen‘ erneut. " + + "Ohne erhöhte Rechte sind BizTalk-WMI-Operationen nicht zuverlässig möglich.", + "Administratorrechte erforderlich", MessageBoxButtons.OK, MessageBoxIcon.Error); return; diff --git a/src/BizTalkPlatformManagementTool/Properties/AssemblyInfo.cs b/src/BizTalkPlatformManagementTool/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..8465c88 --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Properties/AssemblyInfo.cs @@ -0,0 +1,12 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("BizTalk Platform Management Tool")] +[assembly: AssemblyDescription("Kontrollierte BizTalk-Wartung mit Zustandswiederherstellung")] +[assembly: AssemblyCompany("JR IT Services")] +[assembly: AssemblyProduct("BizTalk Platform Management Tool")] +[assembly: AssemblyCopyright("Copyright © 2026")] +[assembly: ComVisible(false)] +[assembly: Guid("DA63FBDA-4B7F-4D93-B63E-652F26A94796")] +[assembly: AssemblyVersion("2.1.0.0")] +[assembly: AssemblyFileVersion("2.1.0.0")] diff --git a/src/BizTalkPlatformManagementTool/Services/AppPaths.cs b/src/BizTalkPlatformManagementTool/Services/AppPaths.cs new file mode 100644 index 0000000..b136b17 --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Services/AppPaths.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; + +namespace BizTalkPlatformManagementTool.Services +{ + public static class AppPaths + { + public const string ProductName = "BizTalkPlatformManagementTool"; + + public static string RuntimeDirectory + { + get + { + return Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + ProductName); + } + } + + public static string WorkspaceDirectory + { + get { return Path.Combine(RuntimeDirectory, "Wartungen"); } + } + + public static string LogDirectory + { + get { return Path.Combine(RuntimeDirectory, "Logs"); } + } + } +} diff --git a/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs b/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs index 762a325..cdd7b61 100644 --- a/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs +++ b/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs @@ -9,8 +9,8 @@ namespace BizTalkPlatformManagementTool.Services { public sealed class BizTalkOperationService { - public const string Version = "2.0.0-net461"; - private const string UnknownApplication = "(Unknown Application)"; + public const string Version = "2.1.0-net461"; + private const string UnknownApplication = "(Unbekannte Anwendung)"; private readonly OperationLogger _logger; @@ -24,7 +24,14 @@ namespace BizTalkPlatformManagementTool.Services using (var client = CreateClient(server)) { var ports = client.Query("MSBTS_SendPort"); - _logger.Success("WMI/CIM diagnostic succeeded. Send ports visible: " + ports.Count); + try + { + _logger.Success("WMI-Diagnose erfolgreich. Sichtbare Send Ports: " + ports.Count); + } + finally + { + foreach (var port in ports) port.Dispose(); + } } } @@ -42,56 +49,74 @@ namespace BizTalkPlatformManagementTool.Services var apps = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var item in client.Query("MSBTS_ReceiveLocation")) { - var appName = GetApplicationName(item); - var app = GetApplication(apps, appName); - app.ReceiveLocations.Add(new ReceiveLocationState + using (item) { - Application = app.Application, - Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty), - ReceivePortName = BizTalkWmiClient.SafeGetString(item, "ReceivePortName", string.Empty), - Enabled = !BizTalkWmiClient.SafeGetBoolean(item, "IsDisabled", true), - AdapterName = BizTalkWmiClient.SafeGetString(item, "AdapterName", string.Empty), - Address = BizTalkWmiClient.SafeGetString(item, "InboundTransportURL", string.Empty) - }); + var appName = GetApplicationName(item); + var app = GetApplication(apps, appName); + app.ReceiveLocations.Add(new ReceiveLocationState + { + Application = app.Application, + Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty), + ReceivePortName = BizTalkWmiClient.SafeGetString(item, "ReceivePortName", string.Empty), + Enabled = !BizTalkWmiClient.SafeGetBoolean(item, "IsDisabled", true), + AdapterName = BizTalkWmiClient.SafeGetString(item, "AdapterName", string.Empty), + // Transport-URIs koennen Zugangsdaten enthalten und werden fuer die + // Zustandswiederherstellung nicht benoetigt. Daher nicht persistieren. + Address = string.Empty + }); + } } foreach (var item in client.Query("MSBTS_SendPort")) { - var appName = GetApplicationName(item); - var app = GetApplication(apps, appName); - app.SendPorts.Add(new SendPortState + using (item) { - Application = app.Application, - Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty), - Status = BizTalkWmiClient.SafeGetInt32(item, "Status", 0), - PrimaryTransportType = BizTalkWmiClient.SafeGetString(item, "PTTransportType", string.Empty), - PrimaryTransportAddress = BizTalkWmiClient.SafeGetString(item, "PTAddress", string.Empty) - }); + var appName = GetApplicationName(item); + var app = GetApplication(apps, appName); + app.SendPorts.Add(new SendPortState + { + Application = app.Application, + Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty), + Status = BizTalkWmiClient.SafeGetInt32(item, "Status", 0), + PrimaryTransportType = BizTalkWmiClient.SafeGetString(item, "PTTransportType", string.Empty), + PrimaryTransportAddress = string.Empty + }); + } } foreach (var item in client.Query("MSBTS_Orchestration")) { - var appName = GetApplicationName(item); - var app = GetApplication(apps, appName); - app.Orchestrations.Add(new OrchestrationState + using (item) { - Application = app.Application, - Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty), - OrchestrationStatus = BizTalkWmiClient.SafeGetInt32(item, "OrchestrationStatus", 0) - }); + var appName = GetApplicationName(item); + var app = GetApplication(apps, appName); + app.Orchestrations.Add(new OrchestrationState + { + Application = app.Application, + Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty), + OrchestrationStatus = BizTalkWmiClient.SafeGetInt32(item, "OrchestrationStatus", 0), + AssemblyName = BizTalkWmiClient.SafeGetString(item, "AssemblyName", string.Empty), + AssemblyVersion = BizTalkWmiClient.SafeGetString(item, "AssemblyVersion", string.Empty), + AssemblyCulture = BizTalkWmiClient.SafeGetString(item, "AssemblyCulture", string.Empty), + AssemblyPublicKeyToken = BizTalkWmiClient.SafeGetString(item, "AssemblyPublicKeyToken", string.Empty) + }); + } } foreach (var item in client.Query("MSBTS_HostInstance")) { - var state = BizTalkWmiClient.SafeGetInt32(item, "ServiceState", 0); - snapshot.HostInstances.Add(new HostInstanceState + using (item) { - InstanceName = BizTalkWmiClient.SafeGetString(item, "InstanceName", BizTalkWmiClient.SafeGetString(item, "Name", string.Empty)), - HostName = BizTalkWmiClient.SafeGetString(item, "HostName", string.Empty), - Server = BizTalkWmiClient.SafeGetString(item, "RunningServer", string.Empty), - RawState = state, - StateText = ArtifactStates.FormatHostInstance(state) - }); + var state = BizTalkWmiClient.SafeGetInt32(item, "ServiceState", 0); + snapshot.HostInstances.Add(new HostInstanceState + { + InstanceName = BizTalkWmiClient.SafeGetString(item, "InstanceName", BizTalkWmiClient.SafeGetString(item, "Name", string.Empty)), + HostName = BizTalkWmiClient.SafeGetString(item, "HostName", string.Empty), + Server = BizTalkWmiClient.SafeGetString(item, "RunningServer", string.Empty), + RawState = state, + StateText = ArtifactStates.FormatHostInstance(state) + }); + } } foreach (var app in apps.Values.OrderBy(a => a.Application)) @@ -103,7 +128,7 @@ namespace BizTalkPlatformManagementTool.Services } snapshot.HostInstances = snapshot.HostInstances.OrderBy(x => x.Server).ThenBy(x => x.InstanceName).ToList(); - _logger.Success("Snapshot created. Applications: " + snapshot.Applications.Count + ", host instances: " + snapshot.HostInstances.Count); + _logger.Success("Snapshot erstellt. Anwendungen: " + snapshot.Applications.Count + ", Hostinstanzen: " + snapshot.HostInstances.Count); return snapshot; } } @@ -116,27 +141,32 @@ namespace BizTalkPlatformManagementTool.Services { foreach (var item in app.ReceiveLocations.Where(x => x.Enabled)) { - plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, null)); + var step = Step("ReceiveLocation", app.Application, item.Name, null, "Receive Location deaktivieren", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, null); + SetQualifier(step, "ReceivePortName", item.ReceivePortName); + plan.Steps.Add(step); } foreach (var item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted)) { - plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Stop orchestration", "MSBTS_Orchestration", "Name", item.Name, "Stop", new[] { 1, 1 }, ArtifactStates.OrchestrationStopped)); + var step = Step("Orchestration", app.Application, item.Name, null, "Orchestrierung stoppen", "MSBTS_Orchestration", "Name", item.Name, "Stop", new[] { 1, 1 }, ArtifactStates.OrchestrationStopped); + SetQualifier(step, "AssemblyName", item.AssemblyName); + plan.Steps.Add(step); } foreach (var item in app.SendPorts.Where(x => x.Status == ArtifactStates.SendPortStarted)) { - plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Stop send port", "MSBTS_SendPort", "Name", item.Name, "Stop", null, ArtifactStates.SendPortStopped)); + plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Send Port stoppen", "MSBTS_SendPort", "Name", item.Name, "Stop", null, ArtifactStates.SendPortStopped)); } } foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted)) { - var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Stop host instance", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Stop", null, ArtifactStates.HostStopped); - if (!string.IsNullOrEmpty(item.Server) && !string.Equals(item.Server, server, StringComparison.OrdinalIgnoreCase)) + var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Hostinstanz stoppen", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Stop", null, ArtifactStates.HostStopped); + SetQualifier(step, "RunningServer", item.Server); + if (!string.IsNullOrEmpty(item.Server) && !SameServer(item.Server, server)) { step.Execute = false; - step.Warning = "Host instance belongs to server '" + item.Server + "'. Run this action on that server."; + step.Warning = "Die Hostinstanz gehört zum Server '" + item.Server + "'. Aktion dort ausführen."; } plan.Steps.Add(step); } @@ -150,64 +180,37 @@ namespace BizTalkPlatformManagementTool.Services 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); - if (!string.IsNullOrEmpty(item.Server) && !string.Equals(item.Server, server, StringComparison.OrdinalIgnoreCase)) + var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Hostinstanz starten", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Start", null, ArtifactStates.HostStarted); + SetQualifier(step, "RunningServer", item.Server); + if (!string.IsNullOrEmpty(item.Server) && !SameServer(item.Server, server)) { step.Execute = false; - step.Warning = "Host instance belongs to server '" + item.Server + "'. Run this action on that server."; + step.Warning = "Die Hostinstanz gehört zum Server '" + item.Server + "'. Aktion dort ausführen."; } plan.Steps.Add(step); } foreach (var app in snapshot.Applications) { - foreach (var item in app.SendPorts) + // Restore startet ausschliesslich Artefakte, die im Vorher-Snapshot aktiv waren. + // Bewusst gestoppte/gebundene Artefakte bleiben unangetastet. + foreach (var item in app.SendPorts.Where(x => x.Status == ArtifactStates.SendPortStarted)) { - if (item.Status == ArtifactStates.SendPortStarted) - { - plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Start send port", "MSBTS_SendPort", "Name", item.Name, "Start", null, ArtifactStates.SendPortStarted)); - } - else if (item.Status == ArtifactStates.SendPortStopped) - { - plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Ensure send port is stopped", "MSBTS_SendPort", "Name", item.Name, "StopOrEnlist", null, ArtifactStates.SendPortStopped)); - } - else if (item.Status == ArtifactStates.SendPortBound) - { - plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Ensure send port is bound", "MSBTS_SendPort", "Name", item.Name, "UnEnlist", null, ArtifactStates.SendPortBound)); - } + plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Send Port starten", "MSBTS_SendPort", "Name", item.Name, "Start", null, ArtifactStates.SendPortStarted)); } - foreach (var item in app.Orchestrations) + foreach (var item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted)) { - if (item.OrchestrationStatus == ArtifactStates.OrchestrationStarted) - { - plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Start orchestration", "MSBTS_Orchestration", "Name", item.Name, "Start", new[] { 1, 1, 1 }, ArtifactStates.OrchestrationStarted)); - } - else if (item.OrchestrationStatus == ArtifactStates.OrchestrationStopped) - { - plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Ensure orchestration is stopped", "MSBTS_Orchestration", "Name", item.Name, "StopIfStarted", new[] { 1, 1 }, ArtifactStates.OrchestrationStopped)); - } - else if (item.OrchestrationStatus == ArtifactStates.OrchestrationBound) - { - plan.Steps.Add(new OperationStep - { - Kind = "Note", - Application = app.Application, - Name = item.Name, - Action = "Orchestration was Bound in the snapshot; leaving unchanged to avoid making it Unbound.", - Execute = false, - Warning = "Manual check recommended." - }); - } - else if (item.OrchestrationStatus == ArtifactStates.OrchestrationUnbound) - { - plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Unenlist orchestration", "MSBTS_Orchestration", "Name", item.Name, "UnenlistService", new[] { 1 }, ArtifactStates.OrchestrationUnbound)); - } + var step = Step("Orchestration", app.Application, item.Name, null, "Orchestrierung starten", "MSBTS_Orchestration", "Name", item.Name, "Start", new[] { 1, 1, 1 }, ArtifactStates.OrchestrationStarted); + SetQualifier(step, "AssemblyName", item.AssemblyName); + plan.Steps.Add(step); } - foreach (var item in app.ReceiveLocations) + foreach (var item in app.ReceiveLocations.Where(x => x.Enabled)) { - plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, item.Enabled ? "Enable receive location" : "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, item.Enabled ? "Enable" : "Disable", null, null)); + var step = Step("ReceiveLocation", app.Application, item.Name, null, "Receive Location aktivieren", "MSBTS_ReceiveLocation", "Name", item.Name, "Enable", null, null); + SetQualifier(step, "ReceivePortName", item.ReceivePortName); + plan.Steps.Add(step); } } @@ -235,7 +238,7 @@ namespace BizTalkPlatformManagementTool.Services try { _logger.Info("Executing step: " + DescribeStep(step)); - using (var instance = client.FindByProperty(step.WmiClass, step.KeyProperty, step.KeyValue)) + using (var instance = client.FindByProperties(step.WmiClass, step.KeyProperty, step.KeyValue, step.QualifierProperty, step.QualifierValue)) { if (instance == null) { @@ -262,6 +265,31 @@ namespace BizTalkPlatformManagementTool.Services return path; } + public string SaveSnapshot(string path, BizTalkSnapshot snapshot) + { + SnapshotStore.SaveSnapshotSet(path, snapshot); + _logger.Success("Zustand gespeichert: " + path); + return path; + } + + public static void ValidateSnapshot(BizTalkSnapshot snapshot, string expectedServer) + { + if (snapshot == null) + { + throw new InvalidDataException("Die Zustandsdatei enthält keinen Snapshot."); + } + + if (snapshot.Applications == null || snapshot.HostInstances == null) + { + throw new InvalidDataException("Die Zustandsdatei ist unvollständig."); + } + + if (!SameServer(snapshot.Server, expectedServer)) + { + throw new InvalidDataException("Die Zustandsdatei gehört zum Server '" + snapshot.Server + "', ausgewählt ist jedoch '" + expectedServer + "'."); + } + } + public string SavePlan(string outputDirectory, string fileName, OperationPlan plan) { Directory.CreateDirectory(outputDirectory); @@ -282,6 +310,12 @@ namespace BizTalkPlatformManagementTool.Services private void ExecuteStep(BizTalkWmiClient client, ManagementObject instance, OperationStep step, OperationOptions options) { + if (IsTargetReached(instance, step)) + { + _logger.Success("Bereits im Zielzustand: " + step.Name); + return; + } + if (string.Equals(step.MethodName, "StopOrEnlist", StringComparison.OrdinalIgnoreCase)) { var status = BizTalkWmiClient.SafeGetInt32(instance, "Status", 0); @@ -318,6 +352,33 @@ namespace BizTalkPlatformManagementTool.Services WaitForTarget(client, step, options); } + private static bool IsTargetReached(ManagementObject instance, OperationStep step) + { + if (instance == null || step == null) + { + return false; + } + + if (step.Kind == "ReceiveLocation") + { + var shouldBeEnabled = string.Equals(step.MethodName, "Enable", StringComparison.OrdinalIgnoreCase); + return !BizTalkWmiClient.SafeGetBoolean(instance, "IsDisabled", true) == shouldBeEnabled; + } + + if (!step.TargetState.HasValue) + { + return false; + } + + var property = step.Kind == "SendPort" + ? "Status" + : step.Kind == "Orchestration" + ? "OrchestrationStatus" + : step.Kind == "HostInstance" ? "ServiceState" : string.Empty; + return property.Length > 0 + && BizTalkWmiClient.SafeGetInt32(instance, property, 0) == step.TargetState.Value; + } + private void WaitForTarget(BizTalkWmiClient client, OperationStep step, OperationOptions options) { if (!step.TargetState.HasValue) @@ -325,22 +386,22 @@ namespace BizTalkPlatformManagementTool.Services if (step.Kind == "ReceiveLocation") { var shouldBeEnabled = string.Equals(step.MethodName, "Enable", StringComparison.OrdinalIgnoreCase); - client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => !BizTalkWmiClient.SafeGetBoolean(o, "IsDisabled", true) == shouldBeEnabled, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds); + client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, step.QualifierProperty, step.QualifierValue, o => !BizTalkWmiClient.SafeGetBoolean(o, "IsDisabled", true) == shouldBeEnabled, step.Action + " abgeschlossen", options.WaitTimeoutSeconds, options.PollIntervalSeconds); } return; } if (step.Kind == "SendPort") { - client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "Status", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds); + client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, step.QualifierProperty, step.QualifierValue, o => BizTalkWmiClient.SafeGetInt32(o, "Status", 0) == step.TargetState.Value, step.Action + " abgeschlossen", options.WaitTimeoutSeconds, options.PollIntervalSeconds); } else if (step.Kind == "Orchestration") { - client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "OrchestrationStatus", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds); + client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, step.QualifierProperty, step.QualifierValue, o => BizTalkWmiClient.SafeGetInt32(o, "OrchestrationStatus", 0) == step.TargetState.Value, step.Action + " abgeschlossen", options.WaitTimeoutSeconds, options.PollIntervalSeconds); } else if (step.Kind == "HostInstance") { - client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "ServiceState", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds); + client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, step.QualifierProperty, step.QualifierValue, o => BizTalkWmiClient.SafeGetInt32(o, "ServiceState", 0) == step.TargetState.Value, step.Action + " abgeschlossen", options.WaitTimeoutSeconds, options.PollIntervalSeconds); } } @@ -380,6 +441,24 @@ namespace BizTalkPlatformManagementTool.Services }; } + private static void SetQualifier(OperationStep step, string property, string value) + { + if (step == null || string.IsNullOrWhiteSpace(value)) + { + return; + } + + step.QualifierProperty = property; + step.QualifierValue = value; + } + + private static bool SameServer(string left, string right) + { + var first = (left ?? string.Empty).Trim().TrimStart('\\').Split('.')[0]; + var second = (right ?? string.Empty).Trim().TrimStart('\\').Split('.')[0]; + return first.Length > 0 && string.Equals(first, second, StringComparison.OrdinalIgnoreCase); + } + private static object[] ToObjects(int[] values) { if (values == null || values.Length == 0) diff --git a/src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs b/src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs index 4cfef11..dff00c7 100644 --- a/src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs +++ b/src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs @@ -71,10 +71,15 @@ namespace BizTalkPlatformManagementTool.Services public ManagementObject FindByProperty(string className, string propertyName, string value) { - return FindByProperty(className, propertyName, value, true); + return FindByProperties(className, propertyName, value, null, null, true); } - private ManagementObject FindByProperty(string className, string propertyName, string value, bool logLookup) + public ManagementObject FindByProperties(string className, string propertyName, string value, string qualifierProperty, string qualifierValue) + { + return FindByProperties(className, propertyName, value, qualifierProperty, qualifierValue, true); + } + + private ManagementObject FindByProperties(string className, string propertyName, string value, string qualifierProperty, string qualifierValue, bool logLookup) { EnsureConnected(); @@ -84,6 +89,14 @@ namespace BizTalkPlatformManagementTool.Services "{0} = \"{1}\"", propertyName, value ?? string.Empty); + if (!string.IsNullOrWhiteSpace(qualifierProperty)) + { + filter += string.Format( + CultureInfo.InvariantCulture, + " AND {0} = \"{1}\"", + qualifierProperty, + qualifierValue ?? string.Empty); + } if (logLookup) { _logger.Info("Resolving WMI object: " + queryText + " with client-side filter " + filter); @@ -91,26 +104,37 @@ namespace BizTalkPlatformManagementTool.Services try { - ManagementObject match = null; var items = Query(className, false); + var matches = new List(); foreach (var item in items) { - if (MatchesProperty(item, className, propertyName, value)) + if (MatchesProperty(item, className, propertyName, value) + && (string.IsNullOrWhiteSpace(qualifierProperty) + || MatchesProperty(item, className, qualifierProperty, qualifierValue))) { - match = item; - break; + matches.Add(item); } } foreach (var item in items) { - if (!object.ReferenceEquals(item, match)) + if (!matches.Contains(item)) { item.Dispose(); } } - return match; + if (matches.Count > 1) + { + foreach (var item in matches) + { + item.Dispose(); + } + + throw new InvalidOperationException("WMI lookup is ambiguous. Class: " + className + ", filter: " + filter + ", matches: " + matches.Count + "."); + } + + return matches.Count == 0 ? null : matches[0]; } catch (ManagementException ex) { @@ -147,14 +171,14 @@ namespace BizTalkPlatformManagementTool.Services return returnCode; } - public void WaitForState(string className, string keyProperty, string keyValue, Func isReached, string description, int timeoutSeconds, int pollIntervalSeconds) + public void WaitForState(string className, string keyProperty, string keyValue, string qualifierProperty, string qualifierValue, Func isReached, string description, int timeoutSeconds, int pollIntervalSeconds) { var deadline = DateTime.UtcNow.AddSeconds(Math.Max(1, timeoutSeconds)); var delay = Math.Max(1, pollIntervalSeconds); while (DateTime.UtcNow <= deadline) { - using (var current = FindByProperty(className, keyProperty, keyValue, false)) + using (var current = FindByProperties(className, keyProperty, keyValue, qualifierProperty, qualifierValue, false)) { if (current != null && isReached(current)) { diff --git a/src/BizTalkPlatformManagementTool/Services/JsonFileStore.cs b/src/BizTalkPlatformManagementTool/Services/JsonFileStore.cs index 4818aec..dabb7b7 100644 --- a/src/BizTalkPlatformManagementTool/Services/JsonFileStore.cs +++ b/src/BizTalkPlatformManagementTool/Services/JsonFileStore.cs @@ -7,6 +7,7 @@ namespace BizTalkPlatformManagementTool.Services { public static class JsonFileStore { + private const long MaximumJsonBytes = 16L * 1024L * 1024L; private static readonly Encoding Utf8NoBom = new UTF8Encoding(false); public static void Save(string path, T value) @@ -17,11 +18,41 @@ namespace BizTalkPlatformManagementTool.Services UseSimpleDictionaryFormat = true }); - using (var stream = new MemoryStream()) + var fullPath = Path.GetFullPath(path); + var temporaryPath = fullPath + ".tmp." + System.Guid.NewGuid().ToString("N"); + try { - serializer.WriteObject(stream, value); - var json = Utf8NoBom.GetString(stream.ToArray()); - File.WriteAllText(path, json, Utf8NoBom); + using (var stream = new MemoryStream()) + { + serializer.WriteObject(stream, value); + var payload = stream.ToArray(); + if (payload.LongLength > MaximumJsonBytes) + { + throw new InvalidDataException("Die JSON-Ausgabe überschreitet die Sicherheitsgrenze von 16 MiB."); + } + + using (var file = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) + { + file.Write(payload, 0, payload.Length); + file.Flush(true); + } + } + + if (File.Exists(fullPath)) + { + File.Replace(temporaryPath, fullPath, null); + } + else + { + File.Move(temporaryPath, fullPath); + } + } + finally + { + if (File.Exists(temporaryPath)) + { + File.Delete(temporaryPath); + } } } @@ -32,6 +63,16 @@ namespace BizTalkPlatformManagementTool.Services UseSimpleDictionaryFormat = true }); + var information = new FileInfo(path); + if (!information.Exists) + { + throw new FileNotFoundException("Die JSON-Datei wurde nicht gefunden.", path); + } + if (information.Length == 0 || information.Length > MaximumJsonBytes) + { + throw new InvalidDataException("Die JSON-Datei ist leer oder überschreitet die Sicherheitsgrenze von 16 MiB: " + path); + } + var json = NormalizeJson(File.ReadAllText(path, Encoding.UTF8)); using (var stream = new MemoryStream(Utf8NoBom.GetBytes(json))) { @@ -41,7 +82,7 @@ namespace BizTalkPlatformManagementTool.Services } catch (SerializationException ex) { - throw new InvalidDataException("Failed to load JSON file '" + path + "'. " + DescribeJsonStart(json), ex); + throw new InvalidDataException("Die JSON-Datei '" + path + "' konnte nicht geladen werden. " + DescribeJsonStart(json), ex); } } } @@ -70,17 +111,17 @@ namespace BizTalkPlatformManagementTool.Services { if (string.IsNullOrWhiteSpace(json)) { - return "The file is empty or contains only whitespace."; + return "Die Datei ist leer oder enthält nur Leerzeichen."; } var trimmed = json.TrimStart(); var first = trimmed.Length == 0 ? '\0' : trimmed[0]; if (first != '{' && first != '[') { - return "The first JSON character is '" + first + "' instead of '{' or '['."; + return "Das erste JSON-Zeichen ist '" + first + "' statt '{' oder '['."; } - return "The file starts with valid JSON syntax but could not be deserialized into the expected model."; + return "Die Datei beginnt mit gültiger JSON-Syntax, passt aber nicht zum erwarteten Datenmodell."; } } } diff --git a/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs b/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs index 882d09d..87c91f1 100644 --- a/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs +++ b/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs @@ -34,7 +34,7 @@ namespace BizTalkPlatformManagementTool.Services public OperationLogger(Action sink) { _sink = sink; - _logDirectory = AppDomain.CurrentDomain.BaseDirectory; + _logDirectory = ResolveLogDirectory(); CleanupOldLogs(); } @@ -132,5 +132,30 @@ namespace BizTalkPlatformManagementTool.Services // Log retention cleanup is best-effort. } } + + private static string ResolveLogDirectory() + { + var candidates = new[] + { + AppPaths.LogDirectory, + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), AppPaths.ProductName, "Logs"), + AppDomain.CurrentDomain.BaseDirectory + }; + + foreach (var candidate in candidates) + { + try + { + Directory.CreateDirectory(candidate); + return candidate; + } + catch + { + // Das Logging darf die Bedienoberflaeche nicht am Start hindern. + } + } + + return AppDomain.CurrentDomain.BaseDirectory; + } } } diff --git a/src/BizTalkPlatformManagementTool/Services/SnapshotComparer.cs b/src/BizTalkPlatformManagementTool/Services/SnapshotComparer.cs index 9ee419d..bf34c6a 100644 --- a/src/BizTalkPlatformManagementTool/Services/SnapshotComparer.cs +++ b/src/BizTalkPlatformManagementTool/Services/SnapshotComparer.cs @@ -8,6 +8,8 @@ namespace BizTalkPlatformManagementTool.Services { public static SnapshotDiff Compare(BizTalkSnapshot before, BizTalkSnapshot after) { + if (before == null) throw new ArgumentNullException("before"); + if (after == null) throw new ArgumentNullException("after"); var diff = new SnapshotDiff(); CompareReceiveLocations(diff, FlattenReceiveLocations(before), FlattenReceiveLocations(after)); @@ -97,11 +99,11 @@ namespace BizTalkPlatformManagementTool.Services foreach (var item in before) { - beforeMap[item.InstanceName ?? string.Empty] = item; + beforeMap[Key(item.Server, item.InstanceName)] = item; } foreach (var item in after) { - afterMap[item.InstanceName ?? string.Empty] = item; + afterMap[Key(item.Server, item.InstanceName)] = item; } foreach (var pair in afterMap) @@ -145,7 +147,7 @@ namespace BizTalkPlatformManagementTool.Services { foreach (var item in app.ReceiveLocations) { - map[item.Name ?? string.Empty] = item; + map[Key(item.Application, item.ReceivePortName, item.Name)] = item; } } return map; @@ -158,7 +160,7 @@ namespace BizTalkPlatformManagementTool.Services { foreach (var item in app.SendPorts) { - map[item.Name ?? string.Empty] = item; + map[Key(item.Application, item.Name)] = item; } } return map; @@ -171,10 +173,15 @@ namespace BizTalkPlatformManagementTool.Services { foreach (var item in app.Orchestrations) { - map[item.Name ?? string.Empty] = item; + map[Key(item.Application, item.Name, item.AssemblyName, item.AssemblyVersion, item.AssemblyCulture, item.AssemblyPublicKeyToken)] = item; } } return map; } + + private static string Key(params string[] parts) + { + return string.Join("|", Array.ConvertAll(parts, value => (value ?? string.Empty).Trim().ToUpperInvariant())); + } } } diff --git a/src/BizTalkPlatformManagementTool/Ui/MainForm.cs b/src/BizTalkPlatformManagementTool/Ui/MainForm.cs index 17d52dd..a883e28 100644 --- a/src/BizTalkPlatformManagementTool/Ui/MainForm.cs +++ b/src/BizTalkPlatformManagementTool/Ui/MainForm.cs @@ -11,6 +11,11 @@ namespace BizTalkPlatformManagementTool.Ui { public sealed class MainForm : Form { + private static readonly Color BrandColor = Color.FromArgb(28, 67, 102); + private static readonly Color AccentColor = Color.FromArgb(31, 111, 181); + private static readonly Color DangerColor = Color.FromArgb(178, 45, 45); + private static readonly Color SuccessColor = Color.FromArgb(32, 122, 72); + private readonly TextBox _serverTextBox = new TextBox(); private readonly TextBox _outputTextBox = new TextBox(); private readonly TextBox _stateFileTextBox = new TextBox(); @@ -19,33 +24,39 @@ namespace BizTalkPlatformManagementTool.Ui private readonly CheckBox _dryRunCheckBox = new CheckBox(); private readonly DataGridView _statusGrid = new DataGridView(); private readonly DataGridView _logGrid = new DataGridView(); - private readonly StatusStrip _statusStrip = new StatusStrip(); private readonly ToolStripStatusLabel _statusLabel = new ToolStripStatusLabel(); + private readonly ToolStripStatusLabel _modeLabel = new ToolStripStatusLabel(); private readonly Label _environmentStatusLabel = new Label(); + private readonly ProgressBar _progressBar = new ProgressBar(); + private readonly ToolTip _toolTip = new ToolTip(); private readonly OperationLogger _logger; private readonly BizTalkOperationService _service; private Button _diagnoseButton; private Button _beforeButton; - private Button _afterButton; - private Button _compareButton; private Button _shutdownButton; private Button _restoreButton; + private Button _afterButton; + private Button _compareButton; private Button _clearButton; private Button _closeButton; + private bool _busy; public MainForm() { Text = "BizTalk Platform Management Tool"; - Width = 1180; - Height = 760; - MinimumSize = new Size(980, 640); + ClientSize = new Size(1240, 800); + MinimumSize = new Size(1040, 700); StartPosition = FormStartPosition.CenterScreen; + AutoScaleMode = AutoScaleMode.Dpi; + Font = new Font("Segoe UI", 9F); + BackColor = Color.FromArgb(245, 247, 250); _logger = new OperationLogger(AppendLog); _service = new BizTalkOperationService(_logger); BuildUi(); - _logger.Info("Log file: " + _logger.LogFilePath); + FormClosing += MainFormClosing; + _logger.Info("Protokolldatei: " + _logger.LogFilePath); } private void BuildUi() @@ -54,134 +65,215 @@ namespace BizTalkPlatformManagementTool.Ui { Dock = DockStyle.Fill, ColumnCount = 1, - RowCount = 4, - Padding = new Padding(10) + RowCount = 5, + Padding = new Padding(0), + BackColor = BackColor }; + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 86)); + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 176)); root.RowStyles.Add(new RowStyle(SizeType.Absolute, 92)); - root.RowStyles.Add(new RowStyle(SizeType.Absolute, 54)); root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); - root.RowStyles.Add(new RowStyle(SizeType.Absolute, 24)); + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 30)); Controls.Add(root); - root.Controls.Add(BuildSettingsPanel(), 0, 0); - root.Controls.Add(BuildActionPanel(), 0, 1); - root.Controls.Add(BuildTabs(), 0, 2); + root.Controls.Add(BuildHeader(), 0, 0); + root.Controls.Add(BuildSettingsPanel(), 0, 1); + root.Controls.Add(BuildWorkflowPanel(), 0, 2); + root.Controls.Add(BuildTabs(), 0, 3); + root.Controls.Add(BuildStatusStrip(), 0, 4); + } - _statusStrip.Items.Add(_statusLabel); - _statusLabel.Text = "Ready. Dry run is enabled."; - root.Controls.Add(_statusStrip, 0, 3); + private Control BuildHeader() + { + var header = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 2, + RowCount = 2, + Padding = new Padding(24, 14, 24, 12), + BackColor = BrandColor + }; + header.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); + header.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 250)); + header.RowStyles.Add(new RowStyle(SizeType.Absolute, 34)); + header.RowStyles.Add(new RowStyle(SizeType.Absolute, 24)); + + var title = new Label + { + Text = "BizTalk-Wartung sicher steuern", + Dock = DockStyle.Fill, + Font = new Font(Font.FontFamily, 17F, FontStyle.Bold), + ForeColor = Color.White, + TextAlign = ContentAlignment.MiddleLeft + }; + var subtitle = new Label + { + Text = "Zustand erfassen · kontrolliert herunterfahren · exakt wiederherstellen", + Dock = DockStyle.Fill, + ForeColor = Color.FromArgb(220, 232, 242), + TextAlign = ContentAlignment.MiddleLeft + }; + + _environmentStatusLabel.Dock = DockStyle.Fill; + _environmentStatusLabel.Margin = new Padding(16, 4, 0, 4); + _environmentStatusLabel.Padding = new Padding(8, 0, 8, 0); + _environmentStatusLabel.Font = new Font(Font, FontStyle.Bold); + _environmentStatusLabel.TextAlign = ContentAlignment.MiddleCenter; + UpdateEnvironmentStatus(null); + + header.Controls.Add(title, 0, 0); + header.Controls.Add(subtitle, 0, 1); + header.Controls.Add(_environmentStatusLabel, 1, 0); + header.SetRowSpan(_environmentStatusLabel, 2); + return header; } private Control BuildSettingsPanel() { - var panel = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 10, RowCount = 2 }; - panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 64)); - panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 30)); - panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 64)); - panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 45)); - panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 96)); - panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 70)); - panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 76)); - panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 72)); - panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 76)); - panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 220)); - panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 40)); - panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 40)); + var group = new GroupBox + { + Text = "Ziel und Wartungsdateien", + Dock = DockStyle.Fill, + Margin = new Padding(18, 14, 18, 6), + Padding = new Padding(12, 10, 12, 10), + BackColor = Color.White + }; + var panel = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 6, RowCount = 3 }; + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 118)); + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 48)); + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 106)); + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 118)); + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 52)); + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 106)); + panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 42)); + panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 42)); + panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 42)); + group.Controls.Add(panel); _serverTextBox.Text = Environment.MachineName; ConfigureInput(_serverTextBox); - _outputTextBox.Text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "BizTalkPlatformManagementTool"); + _outputTextBox.Text = AppPaths.WorkspaceDirectory; ConfigureInput(_outputTextBox); - _stateFileTextBox.Text = "before.json"; + _stateFileTextBox.Text = "Wartungsstatus-" + DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".json"; ConfigureInput(_stateFileTextBox); _timeoutInput.Minimum = 30; _timeoutInput.Maximum = 3600; _timeoutInput.Value = 300; ConfigureInput(_timeoutInput); - _pollInput.Minimum = 1; _pollInput.Maximum = 60; _pollInput.Value = 5; ConfigureInput(_pollInput); - _dryRunCheckBox.Text = "Dry run"; + _dryRunCheckBox.Text = "Sicherheitsmodus: nur planen (Dry-run)"; _dryRunCheckBox.Checked = true; _dryRunCheckBox.Dock = DockStyle.Fill; - _dryRunCheckBox.Margin = new Padding(8, 7, 8, 7); - _dryRunCheckBox.TextAlign = ContentAlignment.MiddleLeft; + _dryRunCheckBox.Margin = new Padding(5, 8, 8, 6); + _dryRunCheckBox.Font = new Font(Font, FontStyle.Bold); + _dryRunCheckBox.ForeColor = AccentColor; + _dryRunCheckBox.CheckedChanged += delegate { UpdateModeIndicator(); }; - var browseButton = new Button { Text = "Browse...", Dock = DockStyle.Fill, Margin = new Padding(8, 5, 8, 5) }; - browseButton.Click += BrowseButtonClick; + var browseOutput = SmallButton("Ordner …", BrowseOutputClick); + var browseState = SmallButton("Datei …", BrowseStateClick); - _environmentStatusLabel.Dock = DockStyle.Fill; - _environmentStatusLabel.Margin = new Padding(12, 5, 0, 5); - _environmentStatusLabel.TextAlign = ContentAlignment.MiddleCenter; - _environmentStatusLabel.BorderStyle = BorderStyle.FixedSingle; - UpdateEnvironmentStatus(null); - - panel.Controls.Add(Label("Server"), 0, 0); + panel.Controls.Add(FieldLabel("BizTalk-Server"), 0, 0); panel.Controls.Add(_serverTextBox, 1, 0); - panel.Controls.Add(Label("Output"), 2, 0); - panel.Controls.Add(_outputTextBox, 3, 0); - panel.Controls.Add(browseButton, 4, 0); - panel.Controls.Add(Label("Timeout"), 5, 0); - panel.Controls.Add(_timeoutInput, 6, 0); + panel.Controls.Add(FieldLabel("Timeout (Sek.)"), 3, 0); + panel.Controls.Add(_timeoutInput, 4, 0); - panel.Controls.Add(Label("State"), 0, 1); - panel.Controls.Add(_stateFileTextBox, 1, 1); - panel.SetColumnSpan(_stateFileTextBox, 2); - panel.Controls.Add(_dryRunCheckBox, 3, 1); - panel.Controls.Add(Label("Poll sec."), 7, 1); - panel.Controls.Add(_pollInput, 8, 1); - panel.Controls.Add(_environmentStatusLabel, 9, 0); - panel.SetRowSpan(_environmentStatusLabel, 2); + panel.Controls.Add(FieldLabel("Arbeitsordner"), 0, 1); + panel.Controls.Add(_outputTextBox, 1, 1); + panel.Controls.Add(browseOutput, 2, 1); + panel.Controls.Add(FieldLabel("Prüfintervall (Sek.)"), 3, 1); + panel.Controls.Add(_pollInput, 4, 1); - return panel; + panel.Controls.Add(FieldLabel("Vorher-Zustand"), 0, 2); + panel.Controls.Add(_stateFileTextBox, 1, 2); + panel.Controls.Add(browseState, 2, 2); + panel.Controls.Add(_dryRunCheckBox, 3, 2); + panel.SetColumnSpan(_dryRunCheckBox, 3); + + _toolTip.SetToolTip(_stateFileTextBox, "Diese Datei bestimmt exakt, welche Artefakte später wieder gestartet werden."); + _toolTip.SetToolTip(_dryRunCheckBox, "Im Dry-run werden Pläne und Protokolle erzeugt, aber keine BizTalk-Zustände geändert."); + return group; } - private Control BuildActionPanel() + private Control BuildWorkflowPanel() { - var panel = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.LeftToRight, Padding = new Padding(0, 8, 0, 0), WrapContents = false }; - _diagnoseButton = ActionButton("Diagnose", DiagnoseClick); - _beforeButton = ActionButton("Snapshot Before", BeforeClick); - _afterButton = ActionButton("Snapshot After", AfterClick); - _compareButton = ActionButton("Compare", CompareClick); - _shutdownButton = ActionButton("Shutdown", ShutdownClick); - _restoreButton = ActionButton("Restore", RestoreClick); - _clearButton = ActionButton("Clear", ClearClick); - _closeButton = ActionButton("Close", CloseClick); + var outer = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 2, + RowCount = 1, + Margin = new Padding(18, 4, 18, 8), + Padding = new Padding(18, 4, 18, 8) + }; + outer.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); + outer.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 190)); - panel.Controls.Add(_diagnoseButton); - panel.Controls.Add(_beforeButton); - panel.Controls.Add(_afterButton); - panel.Controls.Add(_compareButton); - panel.Controls.Add(_shutdownButton); - panel.Controls.Add(_restoreButton); - panel.Controls.Add(_clearButton); - panel.Controls.Add(_closeButton); - return panel; + var workflow = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 6, RowCount = 2 }; + for (var i = 0; i < 6; i++) workflow.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 16.666F)); + workflow.RowStyles.Add(new RowStyle(SizeType.Absolute, 22)); + workflow.RowStyles.Add(new RowStyle(SizeType.Absolute, 48)); + + var captions = new[] { "Vorprüfung", "Vor Wartung", "Wartung", "Nach Wartung", "Kontrolle", "Abschluss" }; + foreach (var caption in captions.Select((value, index) => new { value, index })) + { + workflow.Controls.Add(new Label + { + Text = caption.value, + Dock = DockStyle.Fill, + ForeColor = Color.DimGray, + TextAlign = ContentAlignment.MiddleCenter + }, caption.index, 0); + } + + _diagnoseButton = WorkflowButton("1 Verbindung prüfen", AccentColor, DiagnoseClick); + _beforeButton = WorkflowButton("2 Zustand erfassen", AccentColor, BeforeClick); + _shutdownButton = WorkflowButton("3 Herunterfahren", DangerColor, ShutdownClick); + _restoreButton = WorkflowButton("4 Wiederherstellen", SuccessColor, RestoreClick); + _afterButton = WorkflowButton("5 Nachher erfassen", AccentColor, AfterClick); + _compareButton = WorkflowButton("6 Vergleichen", AccentColor, CompareClick); + workflow.Controls.Add(_diagnoseButton, 0, 1); + workflow.Controls.Add(_beforeButton, 1, 1); + workflow.Controls.Add(_shutdownButton, 2, 1); + workflow.Controls.Add(_restoreButton, 3, 1); + workflow.Controls.Add(_afterButton, 4, 1); + workflow.Controls.Add(_compareButton, 5, 1); + + var utility = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2, RowCount = 1, Padding = new Padding(12, 22, 0, 0) }; + utility.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); + utility.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); + _clearButton = SmallButton("Leeren", ClearClick); + _closeButton = SmallButton("Schließen", CloseClick); + utility.Controls.Add(_clearButton, 0, 0); + utility.Controls.Add(_closeButton, 1, 0); + + outer.Controls.Add(workflow, 0, 0); + outer.Controls.Add(utility, 1, 0); + return outer; } private Control BuildTabs() { - var tabs = new TabControl { Dock = DockStyle.Fill }; - var statusPage = new TabPage("Status / Results"); - var logPage = new TabPage("Operation Log"); + var tabs = new TabControl { Dock = DockStyle.Fill, Margin = new Padding(18, 0, 18, 8), Padding = new Point(14, 5) }; + var statusPage = new TabPage("Status und Ausführungsplan") { Padding = new Padding(4) }; + var logPage = new TabPage("Aktivitätsprotokoll") { Padding = new Padding(4) }; ConfigureGrid(_statusGrid); - _statusGrid.Columns.Add("Scope", "Scope"); - _statusGrid.Columns.Add("Application", "Application"); - _statusGrid.Columns.Add("Name", "Name"); - _statusGrid.Columns.Add("Status", "Status"); - _statusGrid.Columns.Add("Details", "Details"); + AddGridColumn(_statusGrid, "Scope", "Bereich", 130, false); + AddGridColumn(_statusGrid, "Application", "Anwendung", 190, false); + AddGridColumn(_statusGrid, "Name", "Name", 250, true); + AddGridColumn(_statusGrid, "Status", "Status / Aktion", 180, false); + AddGridColumn(_statusGrid, "Details", "Details", 300, true); + _statusGrid.CellFormatting += StatusGridCellFormatting; statusPage.Controls.Add(_statusGrid); ConfigureGrid(_logGrid); - _logGrid.Columns.Add("Time", "Time"); - _logGrid.Columns.Add("Level", "Level"); - _logGrid.Columns.Add("Message", "Message"); - _logGrid.Columns[2].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; + AddGridColumn(_logGrid, "Time", "Zeit", 72, false); + AddGridColumn(_logGrid, "Level", "Stufe", 85, false); + AddGridColumn(_logGrid, "Message", "Meldung", 600, true); logPage.Controls.Add(_logGrid); tabs.TabPages.Add(statusPage); @@ -189,19 +281,38 @@ namespace BizTalkPlatformManagementTool.Ui return tabs; } + private Control BuildStatusStrip() + { + var strip = new StatusStrip { SizingGrip = false, BackColor = Color.White }; + _statusLabel.Text = "Bereit."; + _statusLabel.Spring = true; + _statusLabel.TextAlign = ContentAlignment.MiddleLeft; + _progressBar.Style = ProgressBarStyle.Marquee; + _progressBar.MarqueeAnimationSpeed = 25; + _progressBar.Visible = false; + var progressHost = new ToolStripControlHost(_progressBar) { AutoSize = false, Width = 120, Height = 16 }; + strip.Items.Add(_statusLabel); + strip.Items.Add(progressHost); + strip.Items.Add(_modeLabel); + UpdateModeIndicator(); + return strip; + } + private void DiagnoseClick(object sender, EventArgs e) { - var server = _serverTextBox.Text.Trim(); - RunAsync("Diagnosing WMI access...", () => _service.Diagnose(server)); + var server = GetOptions().Server; + RunAsync("WMI-Zugriff wird geprüft …", () => _service.Diagnose(server)); } private void BeforeClick(object sender, EventArgs e) { var options = GetOptions(); - RunAsync("Creating before snapshot...", () => + var path = ResolveStateFile(options); + if (File.Exists(path) && !ConfirmOverwrite(path)) return; + RunAsync("Vorher-Zustand wird erfasst …", () => { var snapshot = _service.CreateSnapshot(options.Server); - _service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot); + _service.SaveSnapshot(path, snapshot); ShowSnapshot(snapshot); }); } @@ -209,7 +320,7 @@ namespace BizTalkPlatformManagementTool.Ui private void AfterClick(object sender, EventArgs e) { var options = GetOptions(); - RunAsync("Creating after snapshot...", () => + RunAsync("Nachher-Zustand wird erfasst …", () => { var snapshot = _service.CreateSnapshot(options.Server); _service.SaveSnapshot(options.OutputDirectory, "after.json", snapshot); @@ -220,10 +331,10 @@ namespace BizTalkPlatformManagementTool.Ui private void CompareClick(object sender, EventArgs e) { var options = GetOptions(); - RunAsync("Comparing before and after snapshots...", () => + RunAsync("Vorher- und Nachher-Zustand werden verglichen …", () => { - var before = JsonFileStore.Load(Path.Combine(options.OutputDirectory, "before.json")); - var after = JsonFileStore.Load(Path.Combine(options.OutputDirectory, "after.json")); + var before = LoadAndValidateSnapshot(ResolveStateFile(options), options.Server); + var after = LoadAndValidateSnapshot(Path.Combine(options.OutputDirectory, "after.json"), options.Server); var diff = SnapshotComparer.Compare(before, after); _service.SaveDiff(options.OutputDirectory, "diff.json", diff); ShowDiff(diff); @@ -232,16 +343,23 @@ namespace BizTalkPlatformManagementTool.Ui private void ShutdownClick(object sender, EventArgs e) { - if (!ConfirmDangerousAction("Shutdown")) - { - return; - } - var options = GetOptions(); - RunAsync("Preparing shutdown...", () => + if (!ConfirmDangerousAction("herunterfahren", options)) return; + RunAsync("Herunterfahren wird vorbereitet …", () => { - var snapshot = _service.CreateSnapshot(options.Server); - _service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot); + var statePath = ResolveStateFile(options); + BizTalkSnapshot snapshot; + if (File.Exists(statePath)) + { + snapshot = LoadAndValidateSnapshot(statePath, options.Server); + _logger.Info("Vorhandener Vorher-Zustand wird unverändert wiederverwendet: " + statePath); + } + else + { + snapshot = _service.CreateSnapshot(options.Server); + _service.SaveSnapshot(statePath, snapshot); + } + var plan = _service.CreateShutdownPlan(snapshot, options.Server); _service.SavePlan(options.OutputDirectory, "shutdown-plan.json", plan); ShowPlan(plan); @@ -257,15 +375,11 @@ namespace BizTalkPlatformManagementTool.Ui private void RestoreClick(object sender, EventArgs e) { - if (!ConfirmDangerousAction("Restore")) - { - return; - } - var options = GetOptions(); - RunAsync("Preparing restore...", () => + if (!ConfirmDangerousAction("wiederherstellen", options)) return; + RunAsync("Wiederherstellung wird vorbereitet …", () => { - var snapshot = JsonFileStore.Load(ResolveStateFile(options)); + var snapshot = LoadAndValidateSnapshot(ResolveStateFile(options), options.Server); var plan = _service.CreateRestorePlan(snapshot, options.Server); _service.SavePlan(options.OutputDirectory, "restore-plan.json", plan); ShowPlan(plan); @@ -284,7 +398,7 @@ namespace BizTalkPlatformManagementTool.Ui _statusGrid.Rows.Clear(); _logGrid.Rows.Clear(); UpdateEnvironmentStatus(null); - _statusLabel.Text = "Ready."; + _statusLabel.Text = "Bereit."; } private void CloseClick(object sender, EventArgs e) @@ -300,13 +414,14 @@ namespace BizTalkPlatformManagementTool.Ui try { work(); - _logger.Success("Operation completed."); - SetBusy(false, "Ready."); + _logger.Success("Vorgang abgeschlossen."); + SetBusy(false, "Bereit."); } catch (Exception ex) { _logger.Error(FormatException(ex)); - SetBusy(false, "Failed: " + ex.Message); + SetBusy(false, "Fehler: " + ex.Message); + InvokeIfRequired(() => MessageBox.Show(this, FormatException(ex), "Vorgang fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error)); } }); } @@ -314,11 +429,8 @@ namespace BizTalkPlatformManagementTool.Ui private OperationOptions GetOptions() { var server = string.IsNullOrWhiteSpace(_serverTextBox.Text) ? Environment.MachineName : _serverTextBox.Text.Trim(); - var output = string.IsNullOrWhiteSpace(_outputTextBox.Text) - ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "BizTalkPlatformManagementTool") - : _outputTextBox.Text.Trim(); - var stateFile = string.IsNullOrWhiteSpace(_stateFileTextBox.Text) ? "before.json" : _stateFileTextBox.Text.Trim(); - + var output = string.IsNullOrWhiteSpace(_outputTextBox.Text) ? AppPaths.WorkspaceDirectory : _outputTextBox.Text.Trim(); + var stateFile = string.IsNullOrWhiteSpace(_stateFileTextBox.Text) ? "Wartungsstatus.json" : _stateFileTextBox.Text.Trim(); return new OperationOptions { Server = server, @@ -330,25 +442,34 @@ namespace BizTalkPlatformManagementTool.Ui }; } - private string ResolveStateFile(OperationOptions options) + private static BizTalkSnapshot LoadAndValidateSnapshot(string path, string server) + { + var snapshot = JsonFileStore.Load(path); + BizTalkOperationService.ValidateSnapshot(snapshot, server); + return snapshot; + } + + private static string ResolveStateFile(OperationOptions options) { return Path.IsPathRooted(options.StateFile) ? options.StateFile : Path.Combine(options.OutputDirectory, options.StateFile); } - private bool ConfirmDangerousAction(string actionName) + private bool ConfirmDangerousAction(string verb, OperationOptions options) { - if (_dryRunCheckBox.Checked) - { - return true; - } + if (options.DryRun) return true; + var message = "Die BizTalk-Umgebung auf '" + options.Server + "' wird jetzt tatsächlich " + verb + ".\r\n\r\n" + + "Maßgeblicher Vorher-Zustand:\r\n" + ResolveStateFile(options) + "\r\n\r\nFortfahren?"; + return MessageBox.Show(this, message, "BizTalk-Laufzeit ändern", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.Yes; + } - var result = MessageBox.Show( - actionName + " will change the BizTalk runtime state on server '" + _serverTextBox.Text + "'. Continue?", - "Confirm BizTalk Runtime Change", + private bool ConfirmOverwrite(string path) + { + return MessageBox.Show(this, + "Die Zustandsdatei existiert bereits und ist die Grundlage einer möglichen Wiederherstellung:\r\n\r\n" + path + "\r\n\r\nWirklich überschreiben?", + "Vorher-Zustand schützen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, - MessageBoxDefaultButton.Button2); - return result == DialogResult.Yes; + MessageBoxDefaultButton.Button2) == DialogResult.Yes; } private void ShowSnapshot(BizTalkSnapshot snapshot) @@ -359,22 +480,14 @@ namespace BizTalkPlatformManagementTool.Ui foreach (var app in snapshot.Applications) { foreach (var item in app.ReceiveLocations) - { - _statusGrid.Rows.Add("ReceiveLocation", app.Application, item.Name, ArtifactStates.FormatReceiveLocation(item.Enabled), item.AdapterName + " " + item.Address); - } + _statusGrid.Rows.Add("Receive Location", app.Application, item.Name, ArtifactStates.FormatReceiveLocation(item.Enabled), item.AdapterName); foreach (var item in app.SendPorts) - { - _statusGrid.Rows.Add("SendPort", app.Application, item.Name, ArtifactStates.FormatSendPort(item.Status), item.PrimaryTransportType + " " + item.PrimaryTransportAddress); - } + _statusGrid.Rows.Add("Send Port", app.Application, item.Name, ArtifactStates.FormatSendPort(item.Status), item.PrimaryTransportType); foreach (var item in app.Orchestrations) - { - _statusGrid.Rows.Add("Orchestration", app.Application, item.Name, ArtifactStates.FormatOrchestration(item.OrchestrationStatus), string.Empty); - } + _statusGrid.Rows.Add("Orchestrierung", app.Application, item.Name, ArtifactStates.FormatOrchestration(item.OrchestrationStatus), item.AssemblyName); } foreach (var item in snapshot.HostInstances) - { - _statusGrid.Rows.Add("HostInstance", string.Empty, item.InstanceName, item.StateText, item.HostName + " on " + item.Server); - } + _statusGrid.Rows.Add("Hostinstanz", string.Empty, item.InstanceName, item.StateText, item.HostName + " auf " + item.Server); UpdateEnvironmentStatus(snapshot); }); } @@ -385,13 +498,11 @@ namespace BizTalkPlatformManagementTool.Ui { _statusGrid.Rows.Clear(); foreach (var item in diff.ArtifactDifferences) - { - _statusGrid.Rows.Add("Diff", item.Application, item.Name, item.Before + " -> " + item.After, item.ArtifactType); - } + _statusGrid.Rows.Add("Abweichung", item.Application, item.Name, item.Before + " → " + item.After, item.ArtifactType); foreach (var item in diff.HostInstanceDifferences) - { - _statusGrid.Rows.Add("HostDiff", string.Empty, item.InstanceName, item.Before + " -> " + item.After, item.Server); - } + _statusGrid.Rows.Add("Host-Abweichung", string.Empty, item.InstanceName, item.Before + " → " + item.After, item.Server); + if (diff.ArtifactDifferences.Count == 0 && diff.HostInstanceDifferences.Count == 0) + _statusGrid.Rows.Add("Vergleich", string.Empty, "Keine Abweichungen", "OK", "Vorher- und Nachher-Zustand stimmen überein."); }); } @@ -401,9 +512,7 @@ namespace BizTalkPlatformManagementTool.Ui { _statusGrid.Rows.Clear(); foreach (var step in plan.Steps) - { - _statusGrid.Rows.Add(step.Kind, step.Application, step.Name, step.Execute ? step.Action : "Skipped", step.Warning ?? step.MethodName); - } + _statusGrid.Rows.Add(step.Kind, step.Application, step.Name, step.Execute ? step.Action : "Übersprungen", step.Warning ?? step.MethodName); }); } @@ -413,19 +522,10 @@ namespace BizTalkPlatformManagementTool.Ui { var index = _logGrid.Rows.Add(entry.Timestamp.ToString("HH:mm:ss"), entry.Level.ToString(), entry.Message); var row = _logGrid.Rows[index]; - if (entry.Level == LogLevel.Error) - { - row.DefaultCellStyle.ForeColor = Color.DarkRed; - } - else if (entry.Level == LogLevel.Warning) - { - row.DefaultCellStyle.ForeColor = Color.DarkOrange; - } - else if (entry.Level == LogLevel.Success) - { - row.DefaultCellStyle.ForeColor = Color.DarkGreen; - } - _logGrid.FirstDisplayedScrollingRowIndex = Math.Max(0, _logGrid.Rows.Count - 1); + if (entry.Level == LogLevel.Error) row.DefaultCellStyle.ForeColor = DangerColor; + else if (entry.Level == LogLevel.Warning) row.DefaultCellStyle.ForeColor = Color.DarkOrange; + else if (entry.Level == LogLevel.Success) row.DefaultCellStyle.ForeColor = SuccessColor; + if (_logGrid.Rows.Count > 0) _logGrid.FirstDisplayedScrollingRowIndex = _logGrid.Rows.Count - 1; }); } @@ -433,55 +533,121 @@ namespace BizTalkPlatformManagementTool.Ui { InvokeIfRequired(() => { - _diagnoseButton.Enabled = !busy; - _beforeButton.Enabled = !busy; - _afterButton.Enabled = !busy; - _compareButton.Enabled = !busy; - _shutdownButton.Enabled = !busy; - _restoreButton.Enabled = !busy; - _clearButton.Enabled = !busy; - _closeButton.Enabled = !busy; + _busy = busy; + foreach (var button in new[] { _diagnoseButton, _beforeButton, _shutdownButton, _restoreButton, _afterButton, _compareButton, _clearButton, _closeButton }) + button.Enabled = !busy; + _serverTextBox.Enabled = !busy; + _outputTextBox.Enabled = !busy; + _stateFileTextBox.Enabled = !busy; + _dryRunCheckBox.Enabled = !busy; + _progressBar.Visible = busy; _statusLabel.Text = status; }); } - private void BrowseButtonClick(object sender, EventArgs e) + private void BrowseOutputClick(object sender, EventArgs e) { using (var dialog = new FolderBrowserDialog()) { - dialog.SelectedPath = Directory.Exists(_outputTextBox.Text) ? _outputTextBox.Text : Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); - if (dialog.ShowDialog(this) == DialogResult.OK) - { - _outputTextBox.Text = dialog.SelectedPath; - } + dialog.Description = "Arbeitsordner für Snapshots, Pläne und Berichte wählen"; + dialog.SelectedPath = Directory.Exists(_outputTextBox.Text) ? _outputTextBox.Text : AppPaths.WorkspaceDirectory; + if (dialog.ShowDialog(this) == DialogResult.OK) _outputTextBox.Text = dialog.SelectedPath; } } + private void BrowseStateClick(object sender, EventArgs e) + { + using (var dialog = new OpenFileDialog()) + { + dialog.Title = "Vorher-Zustand auswählen"; + dialog.Filter = "BizTalk-Zustand (*.json)|*.json|Alle Dateien (*.*)|*.*"; + dialog.CheckFileExists = true; + dialog.InitialDirectory = Directory.Exists(_outputTextBox.Text) ? _outputTextBox.Text : AppPaths.WorkspaceDirectory; + if (dialog.ShowDialog(this) == DialogResult.OK) _stateFileTextBox.Text = dialog.FileName; + } + } + + private void UpdateEnvironmentStatus(BizTalkSnapshot snapshot) + { + if (snapshot == null || snapshot.HostInstances == null || snapshot.HostInstances.Count == 0) + { + SetEnvironmentBadge("STATUS UNBEKANNT", Color.FromArgb(225, 230, 236), BrandColor); + return; + } + + var total = snapshot.HostInstances.Count; + var started = snapshot.HostInstances.Count(x => x.RawState == ArtifactStates.HostStarted); + var pending = snapshot.HostInstances.Count(x => x.RawState == ArtifactStates.HostStartPending || x.RawState == ArtifactStates.HostStopPending); + if (started == total) SetEnvironmentBadge("GESTARTET " + started + "/" + total, Color.FromArgb(214, 242, 225), Color.FromArgb(20, 92, 52)); + else if (started == 0 && pending == 0) SetEnvironmentBadge("GESTOPPT 0/" + total, Color.FromArgb(250, 220, 220), Color.FromArgb(130, 24, 24)); + else SetEnvironmentBadge("TEILWEISE " + started + "/" + total, Color.FromArgb(255, 237, 194), Color.FromArgb(128, 80, 0)); + } + + private void SetEnvironmentBadge(string text, Color backColor, Color foreColor) + { + _environmentStatusLabel.Text = text; + _environmentStatusLabel.BackColor = backColor; + _environmentStatusLabel.ForeColor = foreColor; + } + + private void UpdateModeIndicator() + { + _modeLabel.Text = _dryRunCheckBox.Checked ? " DRY-RUN AKTIV " : " ECHTE ÄNDERUNGEN "; + _modeLabel.BackColor = _dryRunCheckBox.Checked ? Color.FromArgb(221, 237, 251) : Color.FromArgb(250, 220, 220); + _modeLabel.ForeColor = _dryRunCheckBox.Checked ? BrandColor : DangerColor; + } + + private void StatusGridCellFormatting(object sender, DataGridViewCellFormattingEventArgs e) + { + if (e.RowIndex < 0 || e.ColumnIndex != 3 || e.Value == null) return; + var value = Convert.ToString(e.Value); + if (value.IndexOf("Gestartet", StringComparison.OrdinalIgnoreCase) >= 0 || value.IndexOf("Aktiviert", StringComparison.OrdinalIgnoreCase) >= 0 || value == "OK") + _statusGrid.Rows[e.RowIndex].DefaultCellStyle.ForeColor = SuccessColor; + else if (value.IndexOf("Gestoppt", StringComparison.OrdinalIgnoreCase) >= 0 || value.IndexOf("Deaktiviert", StringComparison.OrdinalIgnoreCase) >= 0) + _statusGrid.Rows[e.RowIndex].DefaultCellStyle.ForeColor = Color.DimGray; + else if (value.IndexOf("→", StringComparison.Ordinal) >= 0) + _statusGrid.Rows[e.RowIndex].DefaultCellStyle.ForeColor = DangerColor; + } + + private void MainFormClosing(object sender, FormClosingEventArgs e) + { + if (!_busy) return; + e.Cancel = true; + MessageBox.Show(this, "Ein Vorgang läuft noch. Bitte warten Sie bis zum Abschluss.", "Vorgang läuft", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + private void InvokeIfRequired(Action action) { - if (IsDisposed) - { - return; - } - - if (InvokeRequired) - { - BeginInvoke(action); - } - else - { - action(); - } + if (IsDisposed || Disposing) return; + if (InvokeRequired) BeginInvoke(action); else action(); } - private static Label Label(string text) + private static Label FieldLabel(string text) { - return new Label { Text = text, Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleRight, Margin = new Padding(0, 5, 4, 5) }; + return new Label { Text = text, Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleRight, Margin = new Padding(0, 6, 8, 6), ForeColor = Color.FromArgb(55, 65, 75) }; } - private static Button ActionButton(string text, EventHandler handler) + private static Button WorkflowButton(string text, Color color, EventHandler handler) { - var button = new Button { Text = text, Width = 112, Height = 34, Margin = new Padding(0, 0, 8, 0) }; + var button = new Button + { + Text = text, + Dock = DockStyle.Fill, + Margin = new Padding(4, 2, 4, 4), + FlatStyle = FlatStyle.Flat, + BackColor = color, + ForeColor = Color.White, + Font = new Font("Segoe UI", 9F, FontStyle.Bold), + Cursor = Cursors.Hand + }; + button.FlatAppearance.BorderSize = 0; + button.Click += handler; + return button; + } + + private static Button SmallButton(string text, EventHandler handler) + { + var button = new Button { Text = text, Dock = DockStyle.Fill, Margin = new Padding(5, 6, 5, 6), FlatStyle = FlatStyle.System }; button.Click += handler; return button; } @@ -489,41 +655,7 @@ namespace BizTalkPlatformManagementTool.Ui private static void ConfigureInput(Control control) { control.Dock = DockStyle.Fill; - control.Margin = new Padding(4, 6, 8, 6); - } - - private void UpdateEnvironmentStatus(BizTalkSnapshot snapshot) - { - if (snapshot == null || snapshot.HostInstances == null || snapshot.HostInstances.Count == 0) - { - _environmentStatusLabel.Text = "Environment: Unknown"; - _environmentStatusLabel.BackColor = SystemColors.Control; - _environmentStatusLabel.ForeColor = SystemColors.ControlText; - return; - } - - var total = snapshot.HostInstances.Count; - var started = snapshot.HostInstances.Count(x => x.RawState == ArtifactStates.HostStarted); - var pending = snapshot.HostInstances.Count(x => x.RawState == ArtifactStates.HostStartPending || x.RawState == ArtifactStates.HostStopPending); - - if (started == total) - { - _environmentStatusLabel.Text = "Environment: Started (" + started + "/" + total + ")"; - _environmentStatusLabel.BackColor = Color.FromArgb(230, 245, 233); - _environmentStatusLabel.ForeColor = Color.DarkGreen; - } - else if (started == 0 && pending == 0) - { - _environmentStatusLabel.Text = "Environment: Stopped (0/" + total + ")"; - _environmentStatusLabel.BackColor = Color.FromArgb(252, 232, 230); - _environmentStatusLabel.ForeColor = Color.DarkRed; - } - else - { - _environmentStatusLabel.Text = "Environment: Partial (" + started + "/" + total + ")"; - _environmentStatusLabel.BackColor = Color.FromArgb(255, 244, 214); - _environmentStatusLabel.ForeColor = Color.DarkOrange; - } + control.Margin = new Padding(4, 7, 8, 7); } private static void ConfigureGrid(DataGridView grid) @@ -531,32 +663,42 @@ namespace BizTalkPlatformManagementTool.Ui grid.Dock = DockStyle.Fill; grid.AllowUserToAddRows = false; grid.AllowUserToDeleteRows = false; + grid.AllowUserToResizeRows = false; grid.ReadOnly = true; grid.RowHeadersVisible = false; grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect; - grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells; - grid.BackgroundColor = SystemColors.Window; + grid.MultiSelect = false; + grid.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.AllCellsExceptHeaders; + grid.BackgroundColor = Color.White; + grid.BorderStyle = BorderStyle.None; + grid.CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal; + grid.GridColor = Color.FromArgb(228, 232, 238); + grid.EnableHeadersVisualStyles = false; + grid.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(235, 239, 244); + grid.ColumnHeadersDefaultCellStyle.ForeColor = BrandColor; + grid.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI", 9F, FontStyle.Bold); + grid.ColumnHeadersHeight = 34; + grid.RowTemplate.Height = 30; + grid.DefaultCellStyle.Padding = new Padding(4, 3, 4, 3); + grid.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(248, 250, 252); + } + + private static void AddGridColumn(DataGridView grid, string name, string title, int width, bool fill) + { + var column = new DataGridViewTextBoxColumn { Name = name, HeaderText = title, Width = width, SortMode = DataGridViewColumnSortMode.Automatic }; + if (fill) column.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; + grid.Columns.Add(column); } private static string FormatException(Exception ex) { - if (ex == null) - { - return "Operation failed."; - } - + if (ex == null) return "Vorgang fehlgeschlagen."; var message = ex.Message; - var inner = ex.InnerException; - while (inner != null) + for (var inner = ex.InnerException; inner != null; inner = inner.InnerException) { if (!string.IsNullOrWhiteSpace(inner.Message) && message.IndexOf(inner.Message, StringComparison.OrdinalIgnoreCase) < 0) - { - message += " Inner error: " + inner.Message; - } - - inner = inner.InnerException; + message += " Ursache: " + inner.Message; } - return message; } } diff --git a/src/BizTalkPlatformManagementTool/app.manifest b/src/BizTalkPlatformManagementTool/app.manifest new file mode 100644 index 0000000..c1c8fdd --- /dev/null +++ b/src/BizTalkPlatformManagementTool/app.manifest @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/tests/BizTalkPlatformManagementTool.Tests/BizTalkPlatformManagementTool.Tests.csproj b/tests/BizTalkPlatformManagementTool.Tests/BizTalkPlatformManagementTool.Tests.csproj new file mode 100644 index 0000000..cd63a0e --- /dev/null +++ b/tests/BizTalkPlatformManagementTool.Tests/BizTalkPlatformManagementTool.Tests.csproj @@ -0,0 +1,44 @@ + + + + + Debug + AnyCPU + {B2AD340B-859F-413B-8811-7FBA4BD2BC02} + Exe + BizTalkPlatformManagementTool.Tests + BizTalkPlatformManagementTool.Tests + v4.6.1 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + 4 + + + pdbonly + true + bin\Release\ + TRACE + 4 + + + + + + + + + + + {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A} + BizTalkPlatformManagementTool + + + + diff --git a/tests/BizTalkPlatformManagementTool.Tests/Program.cs b/tests/BizTalkPlatformManagementTool.Tests/Program.cs new file mode 100644 index 0000000..c96f24e --- /dev/null +++ b/tests/BizTalkPlatformManagementTool.Tests/Program.cs @@ -0,0 +1,136 @@ +using System; +using System.IO; +using System.Linq; +using BizTalkPlatformManagementTool.Models; +using BizTalkPlatformManagementTool.Services; + +namespace BizTalkPlatformManagementTool.Tests +{ + internal static class Program + { + private static int _failed; + + private static int Main() + { + Run("RestoreStartsOnlyPreviouslyActiveArtifacts", RestoreStartsOnlyPreviouslyActiveArtifacts); + Run("ShutdownStopsOnlyPreviouslyActiveArtifacts", ShutdownStopsOnlyPreviouslyActiveArtifacts); + Run("SnapshotComparisonUsesCompositeIdentity", SnapshotComparisonUsesCompositeIdentity); + Run("SnapshotServerValidationIsSafe", SnapshotServerValidationIsSafe); + Run("JsonStoreReplacesAtomically", JsonStoreReplacesAtomically); + Console.WriteLine(_failed == 0 ? "PASS: 5 tests" : "FAIL: " + _failed + " test(s)"); + return _failed == 0 ? 0 : 1; + } + + private static void RestoreStartsOnlyPreviouslyActiveArtifacts() + { + var snapshot = FullSnapshot(); + var plan = new BizTalkOperationService(null).CreateRestorePlan(snapshot, "BIZ01"); + AssertEqual(4, plan.Steps.Count(x => x.Execute), "restore executable step count"); + Assert(plan.Steps.Any(x => x.Kind == "HostInstance" && x.Name == "HostStarted"), "started host missing"); + Assert(plan.Steps.Any(x => x.Kind == "SendPort" && x.Name == "SendStarted"), "started send port missing"); + Assert(plan.Steps.Any(x => x.Kind == "Orchestration" && x.Name == "OrchStarted"), "started orchestration missing"); + AssertEqual("AssemblyName", plan.Steps.Single(x => x.Kind == "Orchestration").QualifierProperty, "orchestration qualifier property"); + var receive = plan.Steps.Single(x => x.Kind == "ReceiveLocation"); + AssertEqual("ReceivePortName", receive.QualifierProperty, "receive qualifier property"); + AssertEqual("RP1", receive.QualifierValue, "receive qualifier value"); + Assert(!plan.Steps.Any(x => x.Name == "SendStopped" || x.Name == "SendBound" || x.Name == "ReceiveDisabled"), "inactive artifacts must remain untouched"); + } + + private static void ShutdownStopsOnlyPreviouslyActiveArtifacts() + { + var plan = new BizTalkOperationService(null).CreateShutdownPlan(FullSnapshot(), "BIZ01"); + AssertEqual(4, plan.Steps.Count(x => x.Execute), "shutdown executable step count"); + Assert(!plan.Steps.Any(x => x.Name == "SendStopped" || x.Name == "SendBound" || x.Name == "ReceiveDisabled"), "inactive artifacts must not be stopped"); + } + + private static void SnapshotComparisonUsesCompositeIdentity() + { + var before = EmptySnapshot("BIZ01"); + var after = EmptySnapshot("BIZ01"); + before.Applications.Add(App("AppA", ArtifactStates.SendPortStarted)); + before.Applications.Add(App("AppB", ArtifactStates.SendPortStopped)); + after.Applications.Add(App("AppA", ArtifactStates.SendPortStopped)); + after.Applications.Add(App("AppB", ArtifactStates.SendPortStopped)); + var diff = SnapshotComparer.Compare(before, after); + AssertEqual(1, diff.ArtifactDifferences.Count, "composite send port comparison"); + AssertEqual("AppA", diff.ArtifactDifferences[0].Application, "changed application"); + } + + private static void SnapshotServerValidationIsSafe() + { + BizTalkOperationService.ValidateSnapshot(EmptySnapshot("biz01.domain.local"), "BIZ01"); + AssertThrows(() => BizTalkOperationService.ValidateSnapshot(EmptySnapshot("BIZ02"), "BIZ01"), "foreign server must be rejected"); + } + + private static void JsonStoreReplacesAtomically() + { + var directory = Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool.Tests." + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + try + { + var path = Path.Combine(directory, "state.json"); + JsonFileStore.Save(path, EmptySnapshot("BIZ01")); + JsonFileStore.Save(path, EmptySnapshot("BIZ02")); + var loaded = JsonFileStore.Load(path); + AssertEqual("BIZ02", loaded.Server, "atomic replacement payload"); + AssertEqual(0, Directory.GetFiles(directory, "*.tmp.*").Length, "temporary files"); + } + finally + { + Directory.Delete(directory, true); + } + } + + private static BizTalkSnapshot FullSnapshot() + { + var snapshot = EmptySnapshot("BIZ01"); + var app = new ApplicationSnapshot { Application = "Orders" }; + app.ReceiveLocations.Add(new ReceiveLocationState { Application = "Orders", Name = "ReceiveEnabled", ReceivePortName = "RP1", Enabled = true }); + app.ReceiveLocations.Add(new ReceiveLocationState { Application = "Orders", Name = "ReceiveDisabled", ReceivePortName = "RP1", Enabled = false }); + app.SendPorts.Add(new SendPortState { Application = "Orders", Name = "SendStarted", Status = ArtifactStates.SendPortStarted }); + app.SendPorts.Add(new SendPortState { Application = "Orders", Name = "SendStopped", Status = ArtifactStates.SendPortStopped }); + app.SendPorts.Add(new SendPortState { Application = "Orders", Name = "SendBound", Status = ArtifactStates.SendPortBound }); + app.Orchestrations.Add(new OrchestrationState { Application = "Orders", Name = "OrchStarted", OrchestrationStatus = ArtifactStates.OrchestrationStarted, AssemblyName = "Orders.Processes" }); + app.Orchestrations.Add(new OrchestrationState { Application = "Orders", Name = "OrchStopped", OrchestrationStatus = ArtifactStates.OrchestrationStopped }); + snapshot.Applications.Add(app); + snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HostStarted", Server = "BIZ01", RawState = ArtifactStates.HostStarted }); + snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HostStopped", Server = "BIZ01", RawState = ArtifactStates.HostStopped }); + return snapshot; + } + + private static BizTalkSnapshot EmptySnapshot(string server) + { + return new BizTalkSnapshot { Server = server, CreatedAt = DateTime.Now.ToString("s"), ToolVersion = BizTalkOperationService.Version }; + } + + private static ApplicationSnapshot App(string name, int sendStatus) + { + var app = new ApplicationSnapshot { Application = name }; + app.SendPorts.Add(new SendPortState { Application = name, Name = "SharedName", Status = sendStatus }); + return app; + } + + private static void Run(string name, Action test) + { + try { test(); Console.WriteLine("PASS: " + name); } + catch (Exception ex) { _failed++; Console.WriteLine("FAIL: " + name + " - " + ex.Message); } + } + + private static void Assert(bool condition, string message) + { + if (!condition) throw new InvalidOperationException(message); + } + + private static void AssertEqual(T expected, T actual, string label) + { + if (!object.Equals(expected, actual)) throw new InvalidOperationException(label + ": expected=" + expected + ", actual=" + actual); + } + + private static void AssertThrows(Action action, string label) where T : Exception + { + try { action(); } + catch (T) { return; } + throw new InvalidOperationException(label); + } + } +}