Compare commits

...
1 Commits
Author SHA1 Message Date
admin 0cb3fda151 Harden BizTalk maintenance workflow and add installer 2026-08-06 18:46:28 +02:00
32 changed files with 1782 additions and 565 deletions
+15
View File
@@ -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
-1
View File
@@ -1,2 +1 @@
* text=auto eol=crlf * text=auto eol=crlf
+1
View File
@@ -10,6 +10,7 @@ bin/
obj/ obj/
# Logs / Outputs # Logs / Outputs
artifacts/
*.log *.log
*.json *.json
*.html *.html
+18
View File
@@ -4,6 +4,12 @@ VisualStudioVersion = 15.0.28307.168
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool", "src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj", "{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool", "src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj", "{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
+37 -9
View File
@@ -1,27 +1,55 @@
# Changelog # 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 ### 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. - Projekt auf `BizTalkPlatformManagementTool` umbenannt.
- Solution, Projektdatei, Namespace, Assemblyname und Standard-Ausgabepfad angepasst. - Solution, Projektdatei, Namespace, Assemblyname und Standard-Ausgabepfad angepasst.
- Dokumentation aktualisiert und `Installation.md` ergänzt. - Dokumentation aktualisiert und `Installation.md` ergänzt.
## [1.3.0] - 2026-02-02 ## [1.3.0] - 2026-02-02
### Added ### Added
- HTML Report mit farblicher Kennzeichnung (grün/rot)
- Farbige Statusausgabe auf STDOUT - HTML-Report mit farblicher Kennzeichnung
- RemoteServer Parameter (-Server) und Ausgabeverzeichnis (-OutDir) - farbige Statusausgabe
- Verbesserte DiffErkennung (neu/entfernt) - Remote-Server-Parameter und Ausgabeverzeichnis
- verbesserte Diff-Erkennung für neue und entfernte Artefakte
## [1.2.0] - 2026-02-02 ## [1.2.0] - 2026-02-02
### Added ### Added
- Remotefähige SnapshotFunktion, Logging (Konsole + Datei)
- Remote-fähige Snapshot-Funktion und Logging
## [1.1.0] - 2026-02-02 ## [1.1.0] - 2026-02-02
### Added ### Added
- Textuelle DiffAusgabe auf STDOUT + HTML Report
- textuelle Diff-Ausgabe und HTML-Report
## [1.0.0] - 2026-02-02 ## [1.0.0] - 2026-02-02
### Added ### Added
- Grundfunktion: Snapshot & Diff (Receive Locations, Send Ports, Orchestrations)
- Snapshot und Diff für Receive Locations, Send Ports und Orchestrierungen
+133 -79
View File
@@ -1,105 +1,159 @@
# BizTalk Platform Management Tool Dokumentation # Technische Dokumentation
**Stand:** 2026-04-27 **Stand:** 2026-08-06
**Implementierung:** C# WinForms, .NET Framework 4.6.1
**Archivierte PowerShell-Version:** `archive/powershell/BizTalkPlatformManagementTool.ps1`
## 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. ## Ziel und Sicherheitsprinzip
- 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.
## 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` - Nur Artefakte, die im Snapshot aktiv waren, werden heruntergefahren.
- Projekt: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj` - Nur Artefakte, die im Snapshot aktiv waren, werden wieder gestartet.
- UI: `src/BizTalkPlatformManagementTool/Ui/MainForm.cs` - Zuvor gestoppte, gebundene oder deaktivierte Artefakte werden beim Restore nicht verändert.
- WMI-Zugriff: `src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs` - Ein vorhandener Vorher-Snapshot wird durch **Herunterfahren** nicht ersetzt.
- Operationslogik: `src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs` - Snapshot und Zielserver müssen übereinstimmen; Kurzname und FQDN desselben Servers werden akzeptiert.
- PowerShell-Archiv: `archive/powershell/BizTalkPlatformManagementTool.ps1` - 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. Die Oberfläche führt von links nach rechts durch sechs Schritte:
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 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. Der Dry-run ist standardmäßig aktiviert. Bei echten Änderungen zeigt die
- Beim Start wird geprüft, ob die Anwendung mit Administratorrechten läuft. Ohne erhöhte Rechte wird eine Fehlermeldung angezeigt und die Anwendung beendet. Statusleiste einen roten Hinweis und jede Änderung verlangt eine zusätzliche
- Echte Shutdown-/Restore-Aktionen verlangen bei deaktiviertem Dry-run eine zusätzliche Bestätigung. Bestätigung mit Zielserver und verwendeter Zustandsdatei. Während einer
- Jede Operation schreibt Einträge in das sichtbare Operation Log. Operation sind Eingaben gesperrt; das Fenster kann nicht versehentlich
- Zusätzlich wird neben der EXE eine tägliche Logdatei `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben. geschlossen werden.
- 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.
## Shutdown-Reihenfolge ## Erfasster Zustand
1. Receive Locations deaktivieren, die im Snapshot aktiviert waren. Über `root\MicrosoftBizTalkServer` werden gelesen:
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.
## 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. Transportadressen werden bewusst nicht persistiert. Sie sind für Start/Stop
2. Send Ports wiederherstellen. nicht erforderlich und können Benutzerinformationen, Kennwörter oder andere
3. Orchestrations wiederherstellen, soweit dies sicher möglich ist. sensible Bestandteile enthalten.
4. Receive Locations zuletzt wiederherstellen.
## Restore-Grenzen ## Zustandswerte
- Send Ports werden auf `Started`, `Stopped` oder `Bound` zurückgesetzt. - Send Port: `1=Bound`, `2=Stopped`, `3=Started`
- Orchestrations mit Status `Started`, `Stopped` oder `Unbound` werden direkt behandelt. - Orchestrierung: `1=Unbound`, `2=Bound`, `3=Stopped`, `4=Started`
- Orchestrations mit Status `Bound` bleiben unverändert und werden zur manuellen Prüfung markiert, weil ein blindes Unenlist zu `Unbound` führen kann. - Hostinstanz: `1=Stopped`, `2=StartPending`, `3=StopPending`, `4=Running`
- Receive Locations werden zuletzt aktiviert, damit eingehender Verkehr erst nach Host Instances und abhängigen Artefakten wieder aufgenommen wird. - Receive Location: `Enabled = !IsDisabled`
## Ausgabedateien Weitere Host-Servicezustände werden als unbekannt angezeigt und nicht
automatisch als gestartet behandelt.
- Snapshots: `before.json`, `after.json` ## Shutdown-Plan
- 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`
## 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 <class>`-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` Gestoppte oder gebundene Send Ports, gestoppte/gebundene/ungebundene
- `MSBTS_Orchestration.OrchestrationStatus`: `1=Unbound`, `2=Bound`, `3=Stopped`, `4=Started` Orchestrierungen, deaktivierte Receive Locations und gestoppte Hostinstanzen
- `MSBTS_ReceiveLocation`: `Enabled = !IsDisabled` erzeugen keine Restore-Aktion.
- `MSBTS_HostInstance.ServiceState`: `1=Stopped`, `2=StartPending`, `3=StopPending`, `4=Started`
## Anforderungen ## Eindeutige WMI-Auflösung
- Windows Server oder Administrationshost mit .NET Framework 4.6.1 WMI-Abfragen werden ohne dynamisch zusammengesetzte WQL-`WHERE`-Klauseln
- BizTalk Server 2020 oder installierte BizTalk Administration Tools ausgeführt. Die Filterung erfolgt im Prozess und behandelt Sonderzeichen in
- Berechtigungen auf den WMI-Namespace `root\MicrosoftBizTalkServer` Namen sicher. Zusätzlich werden verwendet:
- Lokale Administratorrechte und Start der Anwendung mit **Als Administrator ausführen**
- Visual Studio mit .NET Framework 4.6.1 Developer Pack für Builds - 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.
+49 -30
View File
@@ -1,40 +1,59 @@
# Installation # Installation und Update
## Voraussetzungen ## Voraussetzungen
- Windows Server 2019/2022 oder ein Windows-Administrationshost - Windows Server 2019/2022 oder Administrationshost
- Microsoft BizTalk Server 2020 oder BizTalk Administration Tools - Microsoft BizTalk Server 2020 oder installierte BizTalk Administration Tools
- .NET Framework 4.6.1 Runtime - .NET Framework 4.6.1 oder neuer
- Für Builds: Visual Studio mit .NET Framework 4.6.1 Developer Pack - lokales Administratorkonto
- Zugriff auf den WMI-Namespace `root\MicrosoftBizTalkServer` - Mitgliedschaft in der Gruppe **BizTalk Server Administrators**
- Ausreichende Rechte zum Lesen und Ändern von BizTalk-Artefakten - WMI-Zugriff auf `root\MicrosoftBizTalkServer`
- Lokale Administratorrechte und Start der EXE mit **Als Administrator ausführen**
- Schreibrechte im Verzeichnis der EXE für die tägliche Logdatei
## Build ## Installation
1. Repository öffnen. 1. `BizTalkPlatformManagementTool-Setup.zip` vollständig entpacken.
2. `BizTalkPlatformManagementTool.sln` in Visual Studio öffnen. 2. `Setup.exe` starten; die UAC-Anforderung bestätigen.
3. Konfiguration `Release|Any CPU` auswählen. 3. Optional die systemweite Desktop-Verknüpfung auswählen.
4. Solution bauen. 4. **Installieren / aktualisieren** wählen.
5. Das Ergebnis liegt unter `src\BizTalkPlatformManagementTool\bin\Release\`.
## 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. ## Sicheres Update
2. Sicherstellen, dass der ausführende Benutzer WMI-Zugriff auf `root\MicrosoftBizTalkServer` hat.
3. `BizTalkPlatformManagementTool.exe` mit **Als Administrator ausführen** starten. Der Installer validiert das Paket, kopiert die neue Version zunächst in ein
4. Als Ausgabeverzeichnis einen Ordner wählen, in dem Plan-, Snapshot- und Report-Dateien abgelegt werden dürfen. Staging-Verzeichnis und aktiviert sie erst danach. Eine vorhandene Installation
5. Prüfen, dass im EXE-Verzeichnis `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben werden kann. Logs werden für maximal fünf Tage vorgehalten. 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 ## Erster Funktionstest
1. Anwendung mit **Als Administrator ausführen** starten. 1. Tool über Startmenü oder Desktop-Verknüpfung starten.
2. Zielserver eintragen oder den vorgeschlagenen lokalen Server verwenden. 2. Dry-run aktiviert lassen.
3. **Dry run** aktiviert lassen. 3. **Verbindung prüfen** ausführen.
4. **Diagnose** ausführen. 4. **Zustand erfassen** ausführen.
5. **Snapshot Before** ausführen und prüfen, ob `before.json` sowie CSV/HTML-Reports erzeugt wurden. 5. JSON-, CSV- und HTML-Dateien im Arbeitsordner prüfen.
6. **Herunterfahren** im Dry-run ausführen und den Plan kontrollieren.
## 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.
+81 -63
View File
@@ -1,87 +1,105 @@
# BizTalk Platform Management Tool # 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 - übersichtliche deutschsprachige WinForms-Oberfläche mit klarer Sechs-Schritt-Führung
- Target framework: .NET Framework 4.6.1 - Dry-run standardmäßig aktiv
- Solution: `BizTalkPlatformManagementTool.sln` - Vorher-Snapshot als verbindliche Wiederherstellungsgrundlage
- Project: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj` - vorhandener Vorher-Snapshot wird beim Herunterfahren nicht überschrieben
- Primary namespace and assembly: `BizTalkPlatformManagementTool` - servergebundene Validierung verhindert die Verwendung einer Zustandsdatei für den falschen BizTalk-Server
- Legacy PowerShell archive: `archive/powershell/BizTalkPlatformManagementTool.ps1` - 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 1. Tool als Administrator starten.
- Diff between `before.json` and `after.json` 2. Zielserver und Arbeitsordner prüfen.
- Controlled shutdown from the current runtime state 3. **Verbindung prüfen** ausführen.
- Controlled restore from `before.json` 4. Mit aktiviertem Dry-run **Zustand erfassen**.
- Host instance handling for the selected BizTalk server 5. **Herunterfahren** im Dry-run ausführen und `shutdown-plan.json` prüfen.
- Dry-run mode enabled by default 6. Dry-run deaktivieren und **Herunterfahren** bestätigen.
- WMI access through `root\MicrosoftBizTalkServer` 7. Server manuell patchen.
- Startup check for administrator rights 8. Dieselbe Vorher-Zustandsdatei auswählen.
- Detailed operation logging in the GUI and daily rolling log files next to the executable 9. **Wiederherstellen** zunächst im Dry-run prüfen und danach real ausführen.
- Environment status indicator based on host instance state 10. **Nachher erfassen** und **Vergleichen**.
- Clear and Close actions in the main toolbar
- No compile-time dependency on BizTalk ExplorerOM assemblies
## 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. ## Reihenfolge
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**.
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: 1. zuvor gestartete Hostinstanzen des ausgewählten Servers starten
- Disable receive locations that were enabled. 2. zuvor gestartete Send Ports starten
- Stop orchestrations that were started. 3. zuvor gestartete Orchestrierungen starten
- Stop send ports that were started. 4. zuvor aktivierte Receive Locations zuletzt aktivieren
- Stop host instances that were started on the selected server.
Restore: ## Dateien
- Start host instances that were previously started.
- Restore send ports to Started, Stopped or Bound.
- Restore orchestrations where safe.
- Restore receive locations last.
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` Zu jedem Snapshot entstehen CSV- und HTML-Begleitberichte. Vollständige
- `shutdown-plan.json`, `restore-plan.json` Transportadressen werden nicht gespeichert, weil sie Zugangsdaten oder andere
- `shutdown-after.json`, `restore-after.json` sensible URI-Bestandteile enthalten können und für die Wiederherstellung nicht
- `diff.json`, `diff.csv`, `diff.html` benötigt werden.
- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html`
- Runtime logs next to the executable: `BizTalkPlatformManagementTool-yyyy-MM-dd.log`
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 <class>` 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. ## Dokumentation
The app targets .NET Framework 4.6.1 for compatibility with customer environments that do not have newer .NET Framework developer packs installed.
## Documentation
- [Installation](Installation.md) - [Installation](Installation.md)
- [Dokumentation](Dokumentation.md) - [Technische Dokumentation](Dokumentation.md)
- [References](REFERENCES.md) - [Microsoft-Referenzen](REFERENCES.md)
+3
View File
@@ -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.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_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 (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.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_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 (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.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.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
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>BizTalkPlatformManagementTool.Packager</RootNamespace>
<AssemblyName>BizTalkPlatformManagementTool.Packager</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" /><Reference Include="System.Core" /><Reference Include="System.IO.Compression" /><Reference Include="System.IO.Compression.FileSystem" /><Reference Include="System.Security" />
</ItemGroup>
<ItemGroup><Compile Include="Program.cs" /></ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj"><Project>{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}</Project><Name>BizTalkPlatformManagementTool</Name><ReferenceOutputAssembly>false</ReferenceOutputAssembly></ProjectReference>
<ProjectReference Include="..\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj"><Project>{38E61630-11AB-4F8D-B421-F8A88831C4BD}</Project><Name>BizTalkPlatformManagementTool.Setup</Name><ReferenceOutputAssembly>false</ReferenceOutputAssembly></ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="CreateInstallPackage" AfterTargets="Build" Condition=" '$(Configuration)' == 'Release' ">
<Exec Command="&quot;$(TargetPath)&quot; &quot;$(MSBuildProjectDirectory)\..\..&quot; &quot;$(Configuration)&quot;" />
</Target>
</Project>
@@ -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 <repository-root> <configuration>");
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);
}
}
}
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{38E61630-11AB-4F8D-B421-F8A88831C4BD}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>BizTalkPlatformManagementTool.Setup</RootNamespace>
<AssemblyName>BizTalkPlatformManagementTool.Setup</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<ApplicationManifest>app.manifest</ApplicationManifest>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" /><Reference Include="System.Core" /><Reference Include="System.Drawing" /><Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
<Compile Include="InstallerEngine.cs" /><Compile Include="MainForm.cs" /><Compile Include="Program.cs" /><None Include="app.manifest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj"><Project>{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}</Project><Name>BizTalkPlatformManagementTool</Name><ReferenceOutputAssembly>false</ReferenceOutputAssembly></ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
@@ -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<string> 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<string>();
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<string> 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 { }
}
}
}
@@ -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<string>(Report), message); return; }
_status.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message + Environment.NewLine);
}
private void SetBusy(bool busy)
{
if (InvokeRequired) { BeginInvoke(new Action<bool>(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);
}
}
}
@@ -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)));
}
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="BizTalkPlatformManagementTool.Setup" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
@@ -12,6 +12,7 @@
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic> <Deterministic>true</Deterministic>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget> <PlatformTarget>AnyCPU</PlatformTarget>
@@ -46,11 +47,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Program.cs" /> <Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Models\ArtifactStates.cs" /> <Compile Include="Models\ArtifactStates.cs" />
<Compile Include="Models\BizTalkSnapshot.cs" /> <Compile Include="Models\BizTalkSnapshot.cs" />
<Compile Include="Models\DiffModels.cs" /> <Compile Include="Models\DiffModels.cs" />
<Compile Include="Models\OperationModels.cs" /> <Compile Include="Models\OperationModels.cs" />
<Compile Include="Services\BizTalkWmiClient.cs" /> <Compile Include="Services\BizTalkWmiClient.cs" />
<Compile Include="Services\AppPaths.cs" />
<Compile Include="Services\CsvWriter.cs" /> <Compile Include="Services\CsvWriter.cs" />
<Compile Include="Services\HtmlReportWriter.cs" /> <Compile Include="Services\HtmlReportWriter.cs" />
<Compile Include="Services\JsonFileStore.cs" /> <Compile Include="Services\JsonFileStore.cs" />
@@ -62,6 +65,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="App.config" /> <None Include="App.config" />
<None Include="app.manifest" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>
@@ -18,17 +18,17 @@ namespace BizTalkPlatformManagementTool.Models
public static string FormatReceiveLocation(bool enabled) public static string FormatReceiveLocation(bool enabled)
{ {
return enabled ? "Enabled" : "Disabled"; return enabled ? "Aktiviert" : "Deaktiviert";
} }
public static string FormatSendPort(int status) public static string FormatSendPort(int status)
{ {
switch (status) switch (status)
{ {
case SendPortStarted: return "Started"; case SendPortStarted: return "Gestartet";
case SendPortStopped: return "Stopped"; case SendPortStopped: return "Gestoppt";
case SendPortBound: return "Bound"; case SendPortBound: return "Gebunden";
default: return "Unknown (" + status + ")"; default: return "Unbekannt (" + status + ")";
} }
} }
@@ -36,11 +36,11 @@ namespace BizTalkPlatformManagementTool.Models
{ {
switch (status) switch (status)
{ {
case OrchestrationStarted: return "Started"; case OrchestrationStarted: return "Gestartet";
case OrchestrationStopped: return "Stopped"; case OrchestrationStopped: return "Gestoppt";
case OrchestrationBound: return "Bound"; case OrchestrationBound: return "Gebunden";
case OrchestrationUnbound: return "Unbound"; case OrchestrationUnbound: return "Nicht gebunden";
default: return "Unknown (" + status + ")"; default: return "Unbekannt (" + status + ")";
} }
} }
@@ -48,11 +48,11 @@ namespace BizTalkPlatformManagementTool.Models
{ {
switch (state) switch (state)
{ {
case HostStarted: return "Started"; case HostStarted: return "Gestartet";
case HostStartPending: return "StartPending"; case HostStartPending: return "Wird gestartet";
case HostStopPending: return "StopPending"; case HostStopPending: return "Wird gestoppt";
case HostStopped: return "Stopped"; case HostStopped: return "Gestoppt";
default: return "Unknown (" + state + ")"; default: return "Unbekannt (" + state + ")";
} }
} }
} }
@@ -103,6 +103,18 @@ namespace BizTalkPlatformManagementTool.Models
[DataMember(Order = 3)] [DataMember(Order = 3)]
public int OrchestrationStatus { get; set; } 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] [DataContract]
@@ -80,6 +80,12 @@ namespace BizTalkPlatformManagementTool.Models
[DataMember(Order = 13)] [DataMember(Order = 13)]
public string Warning { get; set; } 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 public sealed class OperationOptions
+9 -4
View File
@@ -12,14 +12,19 @@ namespace BizTalkPlatformManagementTool
{ {
Application.EnableVisualStyles(); Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false); 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()) if (!IsRunningAsAdministrator())
{ {
MessageBox.Show( MessageBox.Show(
"BizTalk Platform Management Tool must be started with administrator rights.\r\n\r\n" + "Das BizTalk Platform Management Tool benötigt Administratorrechte.\r\n\r\n" +
"Please close the application and start it again with 'Run as administrator'. " + "Bitte starten Sie die Anwendung über Als Administrator ausführen erneut. " +
"Without elevated rights, BizTalk WMI operations in root\\MicrosoftBizTalkServer will fail.", "Ohne erhöhte Rechte sind BizTalk-WMI-Operationen nicht zuverlässig möglich.",
"Administrator Rights Required", "Administratorrechte erforderlich",
MessageBoxButtons.OK, MessageBoxButtons.OK,
MessageBoxIcon.Error); MessageBoxIcon.Error);
return; return;
@@ -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")]
@@ -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"); }
}
}
}
@@ -9,8 +9,8 @@ namespace BizTalkPlatformManagementTool.Services
{ {
public sealed class BizTalkOperationService public sealed class BizTalkOperationService
{ {
public const string Version = "2.0.0-net461"; public const string Version = "2.1.0-net461";
private const string UnknownApplication = "(Unknown Application)"; private const string UnknownApplication = "(Unbekannte Anwendung)";
private readonly OperationLogger _logger; private readonly OperationLogger _logger;
@@ -24,7 +24,14 @@ namespace BizTalkPlatformManagementTool.Services
using (var client = CreateClient(server)) using (var client = CreateClient(server))
{ {
var ports = client.Query("MSBTS_SendPort"); 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<string, ApplicationSnapshot>(StringComparer.OrdinalIgnoreCase); var apps = new Dictionary<string, ApplicationSnapshot>(StringComparer.OrdinalIgnoreCase);
foreach (var item in client.Query("MSBTS_ReceiveLocation")) foreach (var item in client.Query("MSBTS_ReceiveLocation"))
{ {
var appName = GetApplicationName(item); using (item)
var app = GetApplication(apps, appName);
app.ReceiveLocations.Add(new ReceiveLocationState
{ {
Application = app.Application, var appName = GetApplicationName(item);
Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty), var app = GetApplication(apps, appName);
ReceivePortName = BizTalkWmiClient.SafeGetString(item, "ReceivePortName", string.Empty), app.ReceiveLocations.Add(new ReceiveLocationState
Enabled = !BizTalkWmiClient.SafeGetBoolean(item, "IsDisabled", true), {
AdapterName = BizTalkWmiClient.SafeGetString(item, "AdapterName", string.Empty), Application = app.Application,
Address = BizTalkWmiClient.SafeGetString(item, "InboundTransportURL", string.Empty) 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")) foreach (var item in client.Query("MSBTS_SendPort"))
{ {
var appName = GetApplicationName(item); using (item)
var app = GetApplication(apps, appName);
app.SendPorts.Add(new SendPortState
{ {
Application = app.Application, var appName = GetApplicationName(item);
Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty), var app = GetApplication(apps, appName);
Status = BizTalkWmiClient.SafeGetInt32(item, "Status", 0), app.SendPorts.Add(new SendPortState
PrimaryTransportType = BizTalkWmiClient.SafeGetString(item, "PTTransportType", string.Empty), {
PrimaryTransportAddress = BizTalkWmiClient.SafeGetString(item, "PTAddress", string.Empty) 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")) foreach (var item in client.Query("MSBTS_Orchestration"))
{ {
var appName = GetApplicationName(item); using (item)
var app = GetApplication(apps, appName);
app.Orchestrations.Add(new OrchestrationState
{ {
Application = app.Application, var appName = GetApplicationName(item);
Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty), var app = GetApplication(apps, appName);
OrchestrationStatus = BizTalkWmiClient.SafeGetInt32(item, "OrchestrationStatus", 0) 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")) foreach (var item in client.Query("MSBTS_HostInstance"))
{ {
var state = BizTalkWmiClient.SafeGetInt32(item, "ServiceState", 0); using (item)
snapshot.HostInstances.Add(new HostInstanceState
{ {
InstanceName = BizTalkWmiClient.SafeGetString(item, "InstanceName", BizTalkWmiClient.SafeGetString(item, "Name", string.Empty)), var state = BizTalkWmiClient.SafeGetInt32(item, "ServiceState", 0);
HostName = BizTalkWmiClient.SafeGetString(item, "HostName", string.Empty), snapshot.HostInstances.Add(new HostInstanceState
Server = BizTalkWmiClient.SafeGetString(item, "RunningServer", string.Empty), {
RawState = state, InstanceName = BizTalkWmiClient.SafeGetString(item, "InstanceName", BizTalkWmiClient.SafeGetString(item, "Name", string.Empty)),
StateText = ArtifactStates.FormatHostInstance(state) 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)) 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(); 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; return snapshot;
} }
} }
@@ -116,27 +141,32 @@ namespace BizTalkPlatformManagementTool.Services
{ {
foreach (var item in app.ReceiveLocations.Where(x => x.Enabled)) 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)) 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)) 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)) 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); var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Hostinstanz stoppen", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Stop", null, ArtifactStates.HostStopped);
if (!string.IsNullOrEmpty(item.Server) && !string.Equals(item.Server, server, StringComparison.OrdinalIgnoreCase)) SetQualifier(step, "RunningServer", item.Server);
if (!string.IsNullOrEmpty(item.Server) && !SameServer(item.Server, server))
{ {
step.Execute = false; 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); plan.Steps.Add(step);
} }
@@ -150,64 +180,37 @@ namespace BizTalkPlatformManagementTool.Services
foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted)) foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted))
{ {
var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Start host instance", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Start", null, ArtifactStates.HostStarted); var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Hostinstanz starten", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Start", null, ArtifactStates.HostStarted);
if (!string.IsNullOrEmpty(item.Server) && !string.Equals(item.Server, server, StringComparison.OrdinalIgnoreCase)) SetQualifier(step, "RunningServer", item.Server);
if (!string.IsNullOrEmpty(item.Server) && !SameServer(item.Server, server))
{ {
step.Execute = false; 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); plan.Steps.Add(step);
} }
foreach (var app in snapshot.Applications) 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, "Send Port starten", "MSBTS_SendPort", "Name", item.Name, "Start", null, 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));
}
} }
foreach (var item in app.Orchestrations) foreach (var item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted))
{ {
if (item.OrchestrationStatus == ArtifactStates.OrchestrationStarted) 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("Orchestration", app.Application, item.Name, null, "Start orchestration", "MSBTS_Orchestration", "Name", item.Name, "Start", new[] { 1, 1, 1 }, ArtifactStates.OrchestrationStarted)); plan.Steps.Add(step);
}
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));
}
} }
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 try
{ {
_logger.Info("Executing step: " + DescribeStep(step)); _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) if (instance == null)
{ {
@@ -262,6 +265,31 @@ namespace BizTalkPlatformManagementTool.Services
return path; 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) public string SavePlan(string outputDirectory, string fileName, OperationPlan plan)
{ {
Directory.CreateDirectory(outputDirectory); Directory.CreateDirectory(outputDirectory);
@@ -282,6 +310,12 @@ namespace BizTalkPlatformManagementTool.Services
private void ExecuteStep(BizTalkWmiClient client, ManagementObject instance, OperationStep step, OperationOptions options) 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)) if (string.Equals(step.MethodName, "StopOrEnlist", StringComparison.OrdinalIgnoreCase))
{ {
var status = BizTalkWmiClient.SafeGetInt32(instance, "Status", 0); var status = BizTalkWmiClient.SafeGetInt32(instance, "Status", 0);
@@ -318,6 +352,33 @@ namespace BizTalkPlatformManagementTool.Services
WaitForTarget(client, step, options); 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) private void WaitForTarget(BizTalkWmiClient client, OperationStep step, OperationOptions options)
{ {
if (!step.TargetState.HasValue) if (!step.TargetState.HasValue)
@@ -325,22 +386,22 @@ namespace BizTalkPlatformManagementTool.Services
if (step.Kind == "ReceiveLocation") if (step.Kind == "ReceiveLocation")
{ {
var shouldBeEnabled = string.Equals(step.MethodName, "Enable", StringComparison.OrdinalIgnoreCase); 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; return;
} }
if (step.Kind == "SendPort") 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") 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") 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) private static object[] ToObjects(int[] values)
{ {
if (values == null || values.Length == 0) if (values == null || values.Length == 0)
@@ -71,10 +71,15 @@ namespace BizTalkPlatformManagementTool.Services
public ManagementObject FindByProperty(string className, string propertyName, string value) 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(); EnsureConnected();
@@ -84,6 +89,14 @@ namespace BizTalkPlatformManagementTool.Services
"{0} = \"{1}\"", "{0} = \"{1}\"",
propertyName, propertyName,
value ?? string.Empty); value ?? string.Empty);
if (!string.IsNullOrWhiteSpace(qualifierProperty))
{
filter += string.Format(
CultureInfo.InvariantCulture,
" AND {0} = \"{1}\"",
qualifierProperty,
qualifierValue ?? string.Empty);
}
if (logLookup) if (logLookup)
{ {
_logger.Info("Resolving WMI object: " + queryText + " with client-side filter " + filter); _logger.Info("Resolving WMI object: " + queryText + " with client-side filter " + filter);
@@ -91,26 +104,37 @@ namespace BizTalkPlatformManagementTool.Services
try try
{ {
ManagementObject match = null;
var items = Query(className, false); var items = Query(className, false);
var matches = new List<ManagementObject>();
foreach (var item in items) 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; matches.Add(item);
break;
} }
} }
foreach (var item in items) foreach (var item in items)
{ {
if (!object.ReferenceEquals(item, match)) if (!matches.Contains(item))
{ {
item.Dispose(); 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) catch (ManagementException ex)
{ {
@@ -147,14 +171,14 @@ namespace BizTalkPlatformManagementTool.Services
return returnCode; return returnCode;
} }
public void WaitForState(string className, string keyProperty, string keyValue, Func<ManagementObject, bool> isReached, string description, int timeoutSeconds, int pollIntervalSeconds) public void WaitForState(string className, string keyProperty, string keyValue, string qualifierProperty, string qualifierValue, Func<ManagementObject, bool> isReached, string description, int timeoutSeconds, int pollIntervalSeconds)
{ {
var deadline = DateTime.UtcNow.AddSeconds(Math.Max(1, timeoutSeconds)); var deadline = DateTime.UtcNow.AddSeconds(Math.Max(1, timeoutSeconds));
var delay = Math.Max(1, pollIntervalSeconds); var delay = Math.Max(1, pollIntervalSeconds);
while (DateTime.UtcNow <= deadline) 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)) if (current != null && isReached(current))
{ {
@@ -7,6 +7,7 @@ namespace BizTalkPlatformManagementTool.Services
{ {
public static class JsonFileStore public static class JsonFileStore
{ {
private const long MaximumJsonBytes = 16L * 1024L * 1024L;
private static readonly Encoding Utf8NoBom = new UTF8Encoding(false); private static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
public static void Save<T>(string path, T value) public static void Save<T>(string path, T value)
@@ -17,11 +18,41 @@ namespace BizTalkPlatformManagementTool.Services
UseSimpleDictionaryFormat = true 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); using (var stream = new MemoryStream())
var json = Utf8NoBom.GetString(stream.ToArray()); {
File.WriteAllText(path, json, Utf8NoBom); 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 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)); var json = NormalizeJson(File.ReadAllText(path, Encoding.UTF8));
using (var stream = new MemoryStream(Utf8NoBom.GetBytes(json))) using (var stream = new MemoryStream(Utf8NoBom.GetBytes(json)))
{ {
@@ -41,7 +82,7 @@ namespace BizTalkPlatformManagementTool.Services
} }
catch (SerializationException ex) 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)) 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 trimmed = json.TrimStart();
var first = trimmed.Length == 0 ? '\0' : trimmed[0]; var first = trimmed.Length == 0 ? '\0' : trimmed[0];
if (first != '{' && first != '[') 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.";
} }
} }
} }
@@ -34,7 +34,7 @@ namespace BizTalkPlatformManagementTool.Services
public OperationLogger(Action<LogEntry> sink) public OperationLogger(Action<LogEntry> sink)
{ {
_sink = sink; _sink = sink;
_logDirectory = AppDomain.CurrentDomain.BaseDirectory; _logDirectory = ResolveLogDirectory();
CleanupOldLogs(); CleanupOldLogs();
} }
@@ -132,5 +132,30 @@ namespace BizTalkPlatformManagementTool.Services
// Log retention cleanup is best-effort. // 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;
}
} }
} }
@@ -8,6 +8,8 @@ namespace BizTalkPlatformManagementTool.Services
{ {
public static SnapshotDiff Compare(BizTalkSnapshot before, BizTalkSnapshot after) 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(); var diff = new SnapshotDiff();
CompareReceiveLocations(diff, FlattenReceiveLocations(before), FlattenReceiveLocations(after)); CompareReceiveLocations(diff, FlattenReceiveLocations(before), FlattenReceiveLocations(after));
@@ -97,11 +99,11 @@ namespace BizTalkPlatformManagementTool.Services
foreach (var item in before) foreach (var item in before)
{ {
beforeMap[item.InstanceName ?? string.Empty] = item; beforeMap[Key(item.Server, item.InstanceName)] = item;
} }
foreach (var item in after) foreach (var item in after)
{ {
afterMap[item.InstanceName ?? string.Empty] = item; afterMap[Key(item.Server, item.InstanceName)] = item;
} }
foreach (var pair in afterMap) foreach (var pair in afterMap)
@@ -145,7 +147,7 @@ namespace BizTalkPlatformManagementTool.Services
{ {
foreach (var item in app.ReceiveLocations) foreach (var item in app.ReceiveLocations)
{ {
map[item.Name ?? string.Empty] = item; map[Key(item.Application, item.ReceivePortName, item.Name)] = item;
} }
} }
return map; return map;
@@ -158,7 +160,7 @@ namespace BizTalkPlatformManagementTool.Services
{ {
foreach (var item in app.SendPorts) foreach (var item in app.SendPorts)
{ {
map[item.Name ?? string.Empty] = item; map[Key(item.Application, item.Name)] = item;
} }
} }
return map; return map;
@@ -171,10 +173,15 @@ namespace BizTalkPlatformManagementTool.Services
{ {
foreach (var item in app.Orchestrations) 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; return map;
} }
private static string Key(params string[] parts)
{
return string.Join("|", Array.ConvertAll(parts, value => (value ?? string.Empty).Trim().ToUpperInvariant()));
}
} }
} }
+390 -248
View File
@@ -11,6 +11,11 @@ namespace BizTalkPlatformManagementTool.Ui
{ {
public sealed class MainForm : Form 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 _serverTextBox = new TextBox();
private readonly TextBox _outputTextBox = new TextBox(); private readonly TextBox _outputTextBox = new TextBox();
private readonly TextBox _stateFileTextBox = new TextBox(); private readonly TextBox _stateFileTextBox = new TextBox();
@@ -19,33 +24,39 @@ namespace BizTalkPlatformManagementTool.Ui
private readonly CheckBox _dryRunCheckBox = new CheckBox(); private readonly CheckBox _dryRunCheckBox = new CheckBox();
private readonly DataGridView _statusGrid = new DataGridView(); private readonly DataGridView _statusGrid = new DataGridView();
private readonly DataGridView _logGrid = new DataGridView(); private readonly DataGridView _logGrid = new DataGridView();
private readonly StatusStrip _statusStrip = new StatusStrip();
private readonly ToolStripStatusLabel _statusLabel = new ToolStripStatusLabel(); private readonly ToolStripStatusLabel _statusLabel = new ToolStripStatusLabel();
private readonly ToolStripStatusLabel _modeLabel = new ToolStripStatusLabel();
private readonly Label _environmentStatusLabel = new Label(); private readonly Label _environmentStatusLabel = new Label();
private readonly ProgressBar _progressBar = new ProgressBar();
private readonly ToolTip _toolTip = new ToolTip();
private readonly OperationLogger _logger; private readonly OperationLogger _logger;
private readonly BizTalkOperationService _service; private readonly BizTalkOperationService _service;
private Button _diagnoseButton; private Button _diagnoseButton;
private Button _beforeButton; private Button _beforeButton;
private Button _afterButton;
private Button _compareButton;
private Button _shutdownButton; private Button _shutdownButton;
private Button _restoreButton; private Button _restoreButton;
private Button _afterButton;
private Button _compareButton;
private Button _clearButton; private Button _clearButton;
private Button _closeButton; private Button _closeButton;
private bool _busy;
public MainForm() public MainForm()
{ {
Text = "BizTalk Platform Management Tool"; Text = "BizTalk Platform Management Tool";
Width = 1180; ClientSize = new Size(1240, 800);
Height = 760; MinimumSize = new Size(1040, 700);
MinimumSize = new Size(980, 640);
StartPosition = FormStartPosition.CenterScreen; StartPosition = FormStartPosition.CenterScreen;
AutoScaleMode = AutoScaleMode.Dpi;
Font = new Font("Segoe UI", 9F);
BackColor = Color.FromArgb(245, 247, 250);
_logger = new OperationLogger(AppendLog); _logger = new OperationLogger(AppendLog);
_service = new BizTalkOperationService(_logger); _service = new BizTalkOperationService(_logger);
BuildUi(); BuildUi();
_logger.Info("Log file: " + _logger.LogFilePath); FormClosing += MainFormClosing;
_logger.Info("Protokolldatei: " + _logger.LogFilePath);
} }
private void BuildUi() private void BuildUi()
@@ -54,134 +65,215 @@ namespace BizTalkPlatformManagementTool.Ui
{ {
Dock = DockStyle.Fill, Dock = DockStyle.Fill,
ColumnCount = 1, ColumnCount = 1,
RowCount = 4, RowCount = 5,
Padding = new Padding(10) 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, 92));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 54));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); 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); Controls.Add(root);
root.Controls.Add(BuildSettingsPanel(), 0, 0); root.Controls.Add(BuildHeader(), 0, 0);
root.Controls.Add(BuildActionPanel(), 0, 1); root.Controls.Add(BuildSettingsPanel(), 0, 1);
root.Controls.Add(BuildTabs(), 0, 2); root.Controls.Add(BuildWorkflowPanel(), 0, 2);
root.Controls.Add(BuildTabs(), 0, 3);
root.Controls.Add(BuildStatusStrip(), 0, 4);
}
_statusStrip.Items.Add(_statusLabel); private Control BuildHeader()
_statusLabel.Text = "Ready. Dry run is enabled."; {
root.Controls.Add(_statusStrip, 0, 3); 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() private Control BuildSettingsPanel()
{ {
var panel = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 10, RowCount = 2 }; var group = new GroupBox
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 64)); {
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 30)); Text = "Ziel und Wartungsdateien",
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 64)); Dock = DockStyle.Fill,
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 45)); Margin = new Padding(18, 14, 18, 6),
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 96)); Padding = new Padding(12, 10, 12, 10),
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 70)); BackColor = Color.White
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 76)); };
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 72)); var panel = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 6, RowCount = 3 };
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 76)); panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 118));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 220)); panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 48));
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 40)); panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 106));
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 40)); 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; _serverTextBox.Text = Environment.MachineName;
ConfigureInput(_serverTextBox); ConfigureInput(_serverTextBox);
_outputTextBox.Text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "BizTalkPlatformManagementTool"); _outputTextBox.Text = AppPaths.WorkspaceDirectory;
ConfigureInput(_outputTextBox); ConfigureInput(_outputTextBox);
_stateFileTextBox.Text = "before.json"; _stateFileTextBox.Text = "Wartungsstatus-" + DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".json";
ConfigureInput(_stateFileTextBox); ConfigureInput(_stateFileTextBox);
_timeoutInput.Minimum = 30; _timeoutInput.Minimum = 30;
_timeoutInput.Maximum = 3600; _timeoutInput.Maximum = 3600;
_timeoutInput.Value = 300; _timeoutInput.Value = 300;
ConfigureInput(_timeoutInput); ConfigureInput(_timeoutInput);
_pollInput.Minimum = 1; _pollInput.Minimum = 1;
_pollInput.Maximum = 60; _pollInput.Maximum = 60;
_pollInput.Value = 5; _pollInput.Value = 5;
ConfigureInput(_pollInput); ConfigureInput(_pollInput);
_dryRunCheckBox.Text = "Dry run"; _dryRunCheckBox.Text = "Sicherheitsmodus: nur planen (Dry-run)";
_dryRunCheckBox.Checked = true; _dryRunCheckBox.Checked = true;
_dryRunCheckBox.Dock = DockStyle.Fill; _dryRunCheckBox.Dock = DockStyle.Fill;
_dryRunCheckBox.Margin = new Padding(8, 7, 8, 7); _dryRunCheckBox.Margin = new Padding(5, 8, 8, 6);
_dryRunCheckBox.TextAlign = ContentAlignment.MiddleLeft; _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) }; var browseOutput = SmallButton("Ordner …", BrowseOutputClick);
browseButton.Click += BrowseButtonClick; var browseState = SmallButton("Datei …", BrowseStateClick);
_environmentStatusLabel.Dock = DockStyle.Fill; panel.Controls.Add(FieldLabel("BizTalk-Server"), 0, 0);
_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(_serverTextBox, 1, 0); panel.Controls.Add(_serverTextBox, 1, 0);
panel.Controls.Add(Label("Output"), 2, 0); panel.Controls.Add(FieldLabel("Timeout (Sek.)"), 3, 0);
panel.Controls.Add(_outputTextBox, 3, 0); panel.Controls.Add(_timeoutInput, 4, 0);
panel.Controls.Add(browseButton, 4, 0);
panel.Controls.Add(Label("Timeout"), 5, 0);
panel.Controls.Add(_timeoutInput, 6, 0);
panel.Controls.Add(Label("State"), 0, 1); panel.Controls.Add(FieldLabel("Arbeitsordner"), 0, 1);
panel.Controls.Add(_stateFileTextBox, 1, 1); panel.Controls.Add(_outputTextBox, 1, 1);
panel.SetColumnSpan(_stateFileTextBox, 2); panel.Controls.Add(browseOutput, 2, 1);
panel.Controls.Add(_dryRunCheckBox, 3, 1); panel.Controls.Add(FieldLabel("Prüfintervall (Sek.)"), 3, 1);
panel.Controls.Add(Label("Poll sec."), 7, 1); panel.Controls.Add(_pollInput, 4, 1);
panel.Controls.Add(_pollInput, 8, 1);
panel.Controls.Add(_environmentStatusLabel, 9, 0);
panel.SetRowSpan(_environmentStatusLabel, 2);
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 }; var outer = new TableLayoutPanel
_diagnoseButton = ActionButton("Diagnose", DiagnoseClick); {
_beforeButton = ActionButton("Snapshot Before", BeforeClick); Dock = DockStyle.Fill,
_afterButton = ActionButton("Snapshot After", AfterClick); ColumnCount = 2,
_compareButton = ActionButton("Compare", CompareClick); RowCount = 1,
_shutdownButton = ActionButton("Shutdown", ShutdownClick); Margin = new Padding(18, 4, 18, 8),
_restoreButton = ActionButton("Restore", RestoreClick); Padding = new Padding(18, 4, 18, 8)
_clearButton = ActionButton("Clear", ClearClick); };
_closeButton = ActionButton("Close", CloseClick); outer.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
outer.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 190));
panel.Controls.Add(_diagnoseButton); var workflow = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 6, RowCount = 2 };
panel.Controls.Add(_beforeButton); for (var i = 0; i < 6; i++) workflow.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 16.666F));
panel.Controls.Add(_afterButton); workflow.RowStyles.Add(new RowStyle(SizeType.Absolute, 22));
panel.Controls.Add(_compareButton); workflow.RowStyles.Add(new RowStyle(SizeType.Absolute, 48));
panel.Controls.Add(_shutdownButton);
panel.Controls.Add(_restoreButton); var captions = new[] { "Vorprüfung", "Vor Wartung", "Wartung", "Nach Wartung", "Kontrolle", "Abschluss" };
panel.Controls.Add(_clearButton); foreach (var caption in captions.Select((value, index) => new { value, index }))
panel.Controls.Add(_closeButton); {
return panel; 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() private Control BuildTabs()
{ {
var tabs = new TabControl { Dock = DockStyle.Fill }; var tabs = new TabControl { Dock = DockStyle.Fill, Margin = new Padding(18, 0, 18, 8), Padding = new Point(14, 5) };
var statusPage = new TabPage("Status / Results"); var statusPage = new TabPage("Status und Ausführungsplan") { Padding = new Padding(4) };
var logPage = new TabPage("Operation Log"); var logPage = new TabPage("Aktivitätsprotokoll") { Padding = new Padding(4) };
ConfigureGrid(_statusGrid); ConfigureGrid(_statusGrid);
_statusGrid.Columns.Add("Scope", "Scope"); AddGridColumn(_statusGrid, "Scope", "Bereich", 130, false);
_statusGrid.Columns.Add("Application", "Application"); AddGridColumn(_statusGrid, "Application", "Anwendung", 190, false);
_statusGrid.Columns.Add("Name", "Name"); AddGridColumn(_statusGrid, "Name", "Name", 250, true);
_statusGrid.Columns.Add("Status", "Status"); AddGridColumn(_statusGrid, "Status", "Status / Aktion", 180, false);
_statusGrid.Columns.Add("Details", "Details"); AddGridColumn(_statusGrid, "Details", "Details", 300, true);
_statusGrid.CellFormatting += StatusGridCellFormatting;
statusPage.Controls.Add(_statusGrid); statusPage.Controls.Add(_statusGrid);
ConfigureGrid(_logGrid); ConfigureGrid(_logGrid);
_logGrid.Columns.Add("Time", "Time"); AddGridColumn(_logGrid, "Time", "Zeit", 72, false);
_logGrid.Columns.Add("Level", "Level"); AddGridColumn(_logGrid, "Level", "Stufe", 85, false);
_logGrid.Columns.Add("Message", "Message"); AddGridColumn(_logGrid, "Message", "Meldung", 600, true);
_logGrid.Columns[2].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
logPage.Controls.Add(_logGrid); logPage.Controls.Add(_logGrid);
tabs.TabPages.Add(statusPage); tabs.TabPages.Add(statusPage);
@@ -189,19 +281,38 @@ namespace BizTalkPlatformManagementTool.Ui
return tabs; 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) private void DiagnoseClick(object sender, EventArgs e)
{ {
var server = _serverTextBox.Text.Trim(); var server = GetOptions().Server;
RunAsync("Diagnosing WMI access...", () => _service.Diagnose(server)); RunAsync("WMI-Zugriff wird geprüft …", () => _service.Diagnose(server));
} }
private void BeforeClick(object sender, EventArgs e) private void BeforeClick(object sender, EventArgs e)
{ {
var options = GetOptions(); 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); var snapshot = _service.CreateSnapshot(options.Server);
_service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot); _service.SaveSnapshot(path, snapshot);
ShowSnapshot(snapshot); ShowSnapshot(snapshot);
}); });
} }
@@ -209,7 +320,7 @@ namespace BizTalkPlatformManagementTool.Ui
private void AfterClick(object sender, EventArgs e) private void AfterClick(object sender, EventArgs e)
{ {
var options = GetOptions(); var options = GetOptions();
RunAsync("Creating after snapshot...", () => RunAsync("Nachher-Zustand wird erfasst …", () =>
{ {
var snapshot = _service.CreateSnapshot(options.Server); var snapshot = _service.CreateSnapshot(options.Server);
_service.SaveSnapshot(options.OutputDirectory, "after.json", snapshot); _service.SaveSnapshot(options.OutputDirectory, "after.json", snapshot);
@@ -220,10 +331,10 @@ namespace BizTalkPlatformManagementTool.Ui
private void CompareClick(object sender, EventArgs e) private void CompareClick(object sender, EventArgs e)
{ {
var options = GetOptions(); var options = GetOptions();
RunAsync("Comparing before and after snapshots...", () => RunAsync("Vorher- und Nachher-Zustand werden verglichen …", () =>
{ {
var before = JsonFileStore.Load<BizTalkSnapshot>(Path.Combine(options.OutputDirectory, "before.json")); var before = LoadAndValidateSnapshot(ResolveStateFile(options), options.Server);
var after = JsonFileStore.Load<BizTalkSnapshot>(Path.Combine(options.OutputDirectory, "after.json")); var after = LoadAndValidateSnapshot(Path.Combine(options.OutputDirectory, "after.json"), options.Server);
var diff = SnapshotComparer.Compare(before, after); var diff = SnapshotComparer.Compare(before, after);
_service.SaveDiff(options.OutputDirectory, "diff.json", diff); _service.SaveDiff(options.OutputDirectory, "diff.json", diff);
ShowDiff(diff); ShowDiff(diff);
@@ -232,16 +343,23 @@ namespace BizTalkPlatformManagementTool.Ui
private void ShutdownClick(object sender, EventArgs e) private void ShutdownClick(object sender, EventArgs e)
{ {
if (!ConfirmDangerousAction("Shutdown"))
{
return;
}
var options = GetOptions(); var options = GetOptions();
RunAsync("Preparing shutdown...", () => if (!ConfirmDangerousAction("herunterfahren", options)) return;
RunAsync("Herunterfahren wird vorbereitet …", () =>
{ {
var snapshot = _service.CreateSnapshot(options.Server); var statePath = ResolveStateFile(options);
_service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot); 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); var plan = _service.CreateShutdownPlan(snapshot, options.Server);
_service.SavePlan(options.OutputDirectory, "shutdown-plan.json", plan); _service.SavePlan(options.OutputDirectory, "shutdown-plan.json", plan);
ShowPlan(plan); ShowPlan(plan);
@@ -257,15 +375,11 @@ namespace BizTalkPlatformManagementTool.Ui
private void RestoreClick(object sender, EventArgs e) private void RestoreClick(object sender, EventArgs e)
{ {
if (!ConfirmDangerousAction("Restore"))
{
return;
}
var options = GetOptions(); var options = GetOptions();
RunAsync("Preparing restore...", () => if (!ConfirmDangerousAction("wiederherstellen", options)) return;
RunAsync("Wiederherstellung wird vorbereitet …", () =>
{ {
var snapshot = JsonFileStore.Load<BizTalkSnapshot>(ResolveStateFile(options)); var snapshot = LoadAndValidateSnapshot(ResolveStateFile(options), options.Server);
var plan = _service.CreateRestorePlan(snapshot, options.Server); var plan = _service.CreateRestorePlan(snapshot, options.Server);
_service.SavePlan(options.OutputDirectory, "restore-plan.json", plan); _service.SavePlan(options.OutputDirectory, "restore-plan.json", plan);
ShowPlan(plan); ShowPlan(plan);
@@ -284,7 +398,7 @@ namespace BizTalkPlatformManagementTool.Ui
_statusGrid.Rows.Clear(); _statusGrid.Rows.Clear();
_logGrid.Rows.Clear(); _logGrid.Rows.Clear();
UpdateEnvironmentStatus(null); UpdateEnvironmentStatus(null);
_statusLabel.Text = "Ready."; _statusLabel.Text = "Bereit.";
} }
private void CloseClick(object sender, EventArgs e) private void CloseClick(object sender, EventArgs e)
@@ -300,13 +414,14 @@ namespace BizTalkPlatformManagementTool.Ui
try try
{ {
work(); work();
_logger.Success("Operation completed."); _logger.Success("Vorgang abgeschlossen.");
SetBusy(false, "Ready."); SetBusy(false, "Bereit.");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(FormatException(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() private OperationOptions GetOptions()
{ {
var server = string.IsNullOrWhiteSpace(_serverTextBox.Text) ? Environment.MachineName : _serverTextBox.Text.Trim(); var server = string.IsNullOrWhiteSpace(_serverTextBox.Text) ? Environment.MachineName : _serverTextBox.Text.Trim();
var output = string.IsNullOrWhiteSpace(_outputTextBox.Text) var output = string.IsNullOrWhiteSpace(_outputTextBox.Text) ? AppPaths.WorkspaceDirectory : _outputTextBox.Text.Trim();
? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "BizTalkPlatformManagementTool") var stateFile = string.IsNullOrWhiteSpace(_stateFileTextBox.Text) ? "Wartungsstatus.json" : _stateFileTextBox.Text.Trim();
: _outputTextBox.Text.Trim();
var stateFile = string.IsNullOrWhiteSpace(_stateFileTextBox.Text) ? "before.json" : _stateFileTextBox.Text.Trim();
return new OperationOptions return new OperationOptions
{ {
Server = server, 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<BizTalkSnapshot>(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); 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) if (options.DryRun) return true;
{ var message = "Die BizTalk-Umgebung auf '" + options.Server + "' wird jetzt tatsächlich " + verb + ".\r\n\r\n"
return true; + "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( private bool ConfirmOverwrite(string path)
actionName + " will change the BizTalk runtime state on server '" + _serverTextBox.Text + "'. Continue?", {
"Confirm BizTalk Runtime Change", 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, MessageBoxButtons.YesNo,
MessageBoxIcon.Warning, MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2); MessageBoxDefaultButton.Button2) == DialogResult.Yes;
return result == DialogResult.Yes;
} }
private void ShowSnapshot(BizTalkSnapshot snapshot) private void ShowSnapshot(BizTalkSnapshot snapshot)
@@ -359,22 +480,14 @@ namespace BizTalkPlatformManagementTool.Ui
foreach (var app in snapshot.Applications) foreach (var app in snapshot.Applications)
{ {
foreach (var item in app.ReceiveLocations) foreach (var item in app.ReceiveLocations)
{ _statusGrid.Rows.Add("Receive Location", app.Application, item.Name, ArtifactStates.FormatReceiveLocation(item.Enabled), item.AdapterName);
_statusGrid.Rows.Add("ReceiveLocation", app.Application, item.Name, ArtifactStates.FormatReceiveLocation(item.Enabled), item.AdapterName + " " + item.Address);
}
foreach (var item in app.SendPorts) foreach (var item in app.SendPorts)
{ _statusGrid.Rows.Add("Send Port", app.Application, item.Name, ArtifactStates.FormatSendPort(item.Status), item.PrimaryTransportType);
_statusGrid.Rows.Add("SendPort", app.Application, item.Name, ArtifactStates.FormatSendPort(item.Status), item.PrimaryTransportType + " " + item.PrimaryTransportAddress);
}
foreach (var item in app.Orchestrations) foreach (var item in app.Orchestrations)
{ _statusGrid.Rows.Add("Orchestrierung", app.Application, item.Name, ArtifactStates.FormatOrchestration(item.OrchestrationStatus), item.AssemblyName);
_statusGrid.Rows.Add("Orchestration", app.Application, item.Name, ArtifactStates.FormatOrchestration(item.OrchestrationStatus), string.Empty);
}
} }
foreach (var item in snapshot.HostInstances) foreach (var item in snapshot.HostInstances)
{ _statusGrid.Rows.Add("Hostinstanz", string.Empty, item.InstanceName, item.StateText, item.HostName + " auf " + item.Server);
_statusGrid.Rows.Add("HostInstance", string.Empty, item.InstanceName, item.StateText, item.HostName + " on " + item.Server);
}
UpdateEnvironmentStatus(snapshot); UpdateEnvironmentStatus(snapshot);
}); });
} }
@@ -385,13 +498,11 @@ namespace BizTalkPlatformManagementTool.Ui
{ {
_statusGrid.Rows.Clear(); _statusGrid.Rows.Clear();
foreach (var item in diff.ArtifactDifferences) foreach (var item in diff.ArtifactDifferences)
{ _statusGrid.Rows.Add("Abweichung", item.Application, item.Name, item.Before + " → " + item.After, item.ArtifactType);
_statusGrid.Rows.Add("Diff", item.Application, item.Name, item.Before + " -> " + item.After, item.ArtifactType);
}
foreach (var item in diff.HostInstanceDifferences) foreach (var item in diff.HostInstanceDifferences)
{ _statusGrid.Rows.Add("Host-Abweichung", string.Empty, item.InstanceName, item.Before + " → " + item.After, item.Server);
_statusGrid.Rows.Add("HostDiff", 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(); _statusGrid.Rows.Clear();
foreach (var step in plan.Steps) foreach (var step in plan.Steps)
{ _statusGrid.Rows.Add(step.Kind, step.Application, step.Name, step.Execute ? step.Action : "Übersprungen", step.Warning ?? step.MethodName);
_statusGrid.Rows.Add(step.Kind, step.Application, step.Name, step.Execute ? step.Action : "Skipped", 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 index = _logGrid.Rows.Add(entry.Timestamp.ToString("HH:mm:ss"), entry.Level.ToString(), entry.Message);
var row = _logGrid.Rows[index]; var row = _logGrid.Rows[index];
if (entry.Level == LogLevel.Error) if (entry.Level == LogLevel.Error) row.DefaultCellStyle.ForeColor = DangerColor;
{ else if (entry.Level == LogLevel.Warning) row.DefaultCellStyle.ForeColor = Color.DarkOrange;
row.DefaultCellStyle.ForeColor = Color.DarkRed; else if (entry.Level == LogLevel.Success) row.DefaultCellStyle.ForeColor = SuccessColor;
} if (_logGrid.Rows.Count > 0) _logGrid.FirstDisplayedScrollingRowIndex = _logGrid.Rows.Count - 1;
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);
}); });
} }
@@ -433,55 +533,121 @@ namespace BizTalkPlatformManagementTool.Ui
{ {
InvokeIfRequired(() => InvokeIfRequired(() =>
{ {
_diagnoseButton.Enabled = !busy; _busy = busy;
_beforeButton.Enabled = !busy; foreach (var button in new[] { _diagnoseButton, _beforeButton, _shutdownButton, _restoreButton, _afterButton, _compareButton, _clearButton, _closeButton })
_afterButton.Enabled = !busy; button.Enabled = !busy;
_compareButton.Enabled = !busy; _serverTextBox.Enabled = !busy;
_shutdownButton.Enabled = !busy; _outputTextBox.Enabled = !busy;
_restoreButton.Enabled = !busy; _stateFileTextBox.Enabled = !busy;
_clearButton.Enabled = !busy; _dryRunCheckBox.Enabled = !busy;
_closeButton.Enabled = !busy; _progressBar.Visible = busy;
_statusLabel.Text = status; _statusLabel.Text = status;
}); });
} }
private void BrowseButtonClick(object sender, EventArgs e) private void BrowseOutputClick(object sender, EventArgs e)
{ {
using (var dialog = new FolderBrowserDialog()) using (var dialog = new FolderBrowserDialog())
{ {
dialog.SelectedPath = Directory.Exists(_outputTextBox.Text) ? _outputTextBox.Text : Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); dialog.Description = "Arbeitsordner für Snapshots, Pläne und Berichte wählen";
if (dialog.ShowDialog(this) == DialogResult.OK) dialog.SelectedPath = Directory.Exists(_outputTextBox.Text) ? _outputTextBox.Text : AppPaths.WorkspaceDirectory;
{ if (dialog.ShowDialog(this) == DialogResult.OK) _outputTextBox.Text = dialog.SelectedPath;
_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) private void InvokeIfRequired(Action action)
{ {
if (IsDisposed) if (IsDisposed || Disposing) return;
{ if (InvokeRequired) BeginInvoke(action); else action();
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; button.Click += handler;
return button; return button;
} }
@@ -489,41 +655,7 @@ namespace BizTalkPlatformManagementTool.Ui
private static void ConfigureInput(Control control) private static void ConfigureInput(Control control)
{ {
control.Dock = DockStyle.Fill; control.Dock = DockStyle.Fill;
control.Margin = new Padding(4, 6, 8, 6); control.Margin = new Padding(4, 7, 8, 7);
}
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;
}
} }
private static void ConfigureGrid(DataGridView grid) private static void ConfigureGrid(DataGridView grid)
@@ -531,32 +663,42 @@ namespace BizTalkPlatformManagementTool.Ui
grid.Dock = DockStyle.Fill; grid.Dock = DockStyle.Fill;
grid.AllowUserToAddRows = false; grid.AllowUserToAddRows = false;
grid.AllowUserToDeleteRows = false; grid.AllowUserToDeleteRows = false;
grid.AllowUserToResizeRows = false;
grid.ReadOnly = true; grid.ReadOnly = true;
grid.RowHeadersVisible = false; grid.RowHeadersVisible = false;
grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect; grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells; grid.MultiSelect = false;
grid.BackgroundColor = SystemColors.Window; 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) private static string FormatException(Exception ex)
{ {
if (ex == null) if (ex == null) return "Vorgang fehlgeschlagen.";
{
return "Operation failed.";
}
var message = ex.Message; var message = ex.Message;
var inner = ex.InnerException; for (var inner = ex.InnerException; inner != null; inner = inner.InnerException)
while (inner != null)
{ {
if (!string.IsNullOrWhiteSpace(inner.Message) && message.IndexOf(inner.Message, StringComparison.OrdinalIgnoreCase) < 0) if (!string.IsNullOrWhiteSpace(inner.Message) && message.IndexOf(inner.Message, StringComparison.OrdinalIgnoreCase) < 0)
{ message += " Ursache: " + inner.Message;
message += " Inner error: " + inner.Message;
}
inner = inner.InnerException;
} }
return message; return message;
} }
} }
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="BizTalkPlatformManagementTool.app" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{4f476546-937d-4f22-9fc8-4b416b2f8f9f}" />
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{B2AD340B-859F-413B-8811-7FBA4BD2BC02}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>BizTalkPlatformManagementTool.Tests</RootNamespace>
<AssemblyName>BizTalkPlatformManagementTool.Tests</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj">
<Project>{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}</Project>
<Name>BizTalkPlatformManagementTool</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
@@ -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<InvalidDataException>(() => 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<BizTalkSnapshot>(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>(T expected, T actual, string label)
{
if (!object.Equals(expected, actual)) throw new InvalidOperationException(label + ": expected=" + expected + ", actual=" + actual);
}
private static void AssertThrows<T>(Action action, string label) where T : Exception
{
try { action(); }
catch (T) { return; }
throw new InvalidOperationException(label);
}
}
}