Add transactional installer and harden runtime operations
This commit is contained in:
@@ -18,6 +18,7 @@ obj/
|
||||
*.png
|
||||
work/
|
||||
release/*
|
||||
artifacts/
|
||||
|
||||
# PowerShell specifics
|
||||
*.ps1.orig
|
||||
|
||||
@@ -4,6 +4,12 @@ VisualStudioVersion = 15.0.28307.168
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool", "src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj", "{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Setup", "src\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj", "{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Packager", "src\BizTalkPlatformManagementTool.Packager\BizTalkPlatformManagementTool.Packager.csproj", "{74A5D422-0BA5-4559-BD81-C89C071A8FE4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Tests", "tests\BizTalkPlatformManagementTool.Tests\BizTalkPlatformManagementTool.Tests.csproj", "{318F4307-F62C-47C9-9B90-F0C9BF2F812A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -14,6 +20,18 @@ Global
|
||||
{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{74A5D422-0BA5-4559-BD81-C89C071A8FE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{74A5D422-0BA5-4559-BD81-C89C071A8FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{74A5D422-0BA5-4559-BD81-C89C071A8FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{74A5D422-0BA5-4559-BD81-C89C071A8FE4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{318F4307-F62C-47C9-9B90-F0C9BF2F812A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{318F4307-F62C-47C9-9B90-F0C9BF2F812A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{318F4307-F62C-47C9-9B90-F0C9BF2F812A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{318F4307-F62C-47C9-9B90-F0C9BF2F812A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## [2.1.0] - 2026-08-11
|
||||
### Added
|
||||
- Transactional Windows setup/updater with SHA-256 payload manifest, isolated staging, pre/post activation self-tests, rollback, shortcuts and uninstall registration.
|
||||
- Certutil-compatible installer ZIP as Base64 TXT plus separate ZIP SHA-256 file.
|
||||
- WMI-free application self-test and regression test project covering persistence, diffs, restore safety, CSV hardening and installer rollback.
|
||||
- UAC manifest and single-instance protection for the management application.
|
||||
|
||||
### Changed
|
||||
- Snapshot and plan JSON writes now replace files atomically and keep BOM-tolerant reads.
|
||||
- Snapshot diffs identify artifacts by application and name.
|
||||
- Restore planning rejects snapshots from a different target server.
|
||||
- Real shutdown/restore requires confirmation of the fully prepared and saved plan.
|
||||
- Runtime logs are written to `%ProgramData%\BizTalkPlatformManagementTool\Logs` with executable-directory fallback.
|
||||
- WMI query objects and WMI method output objects are disposed deterministically.
|
||||
- CSV exports neutralize formula-like cell prefixes.
|
||||
|
||||
## [Unreleased] - 2026-04-27
|
||||
### Changed
|
||||
- Projekt auf `BizTalkPlatformManagementTool` umbenannt.
|
||||
|
||||
+52
-5
@@ -1,6 +1,6 @@
|
||||
# BizTalk Platform Management Tool Dokumentation
|
||||
|
||||
**Stand:** 2026-04-27
|
||||
**Stand:** 2026-08-11
|
||||
**Implementierung:** C# WinForms, .NET Framework 4.6.1
|
||||
**Archivierte PowerShell-Version:** `archive/powershell/BizTalkPlatformManagementTool.ps1`
|
||||
|
||||
@@ -21,6 +21,10 @@ Das BizTalk Platform Management Tool unterstützt kontrollierte Wartungsfenster
|
||||
- UI: `src/BizTalkPlatformManagementTool/Ui/MainForm.cs`
|
||||
- WMI-Zugriff: `src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs`
|
||||
- Operationslogik: `src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs`
|
||||
- Snapshot-Validierung: `src/BizTalkPlatformManagementTool/Services/SnapshotValidator.cs`
|
||||
- Installer: `src/BizTalkPlatformManagementTool.Setup`
|
||||
- Release-Paketierung: `src/BizTalkPlatformManagementTool.Packager`
|
||||
- Regressionstests: `tests/BizTalkPlatformManagementTool.Tests`
|
||||
- PowerShell-Archiv: `archive/powershell/BizTalkPlatformManagementTool.ps1`
|
||||
|
||||
## UI Workflow
|
||||
@@ -42,9 +46,9 @@ Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des
|
||||
|
||||
- Dry-run ist standardmäßig aktiviert.
|
||||
- Beim Start wird geprüft, ob die Anwendung mit Administratorrechten läuft. Ohne erhöhte Rechte wird eine Fehlermeldung angezeigt und die Anwendung beendet.
|
||||
- Echte Shutdown-/Restore-Aktionen verlangen bei deaktiviertem Dry-run eine zusätzliche Bestätigung.
|
||||
- Echte Shutdown-/Restore-Aktionen verlangen erst nach Erzeugung und Speicherung des frischen Plans eine zusätzliche Bestätigung mit Zielserver, Plandatei und exakter Zahl ausführbarer Schritte.
|
||||
- Jede Operation schreibt Einträge in das sichtbare Operation Log.
|
||||
- Zusätzlich wird neben der EXE eine tägliche Logdatei `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben.
|
||||
- Zusätzlich wird unter `%ProgramData%\BizTalkPlatformManagementTool\Logs` eine tägliche Logdatei `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben. Nur wenn ProgramData nicht verfügbar ist, wird auf das EXE-Verzeichnis zurückgefallen.
|
||||
- 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.
|
||||
@@ -52,6 +56,9 @@ Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des
|
||||
- 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.
|
||||
- Restore-Pläne werden abgelehnt, wenn Snapshot-Server und ausgewählter Zielserver nicht übereinstimmen; Kurzname und FQDN desselben Hosts gelten als identisch.
|
||||
- Pro Windows-Sitzung ist nur eine Toolinstanz zulässig; das Fenster kann während einer aktiven WMI-Operation nicht geschlossen werden.
|
||||
- Die EXE enthält zusätzlich einen WMI-freien `--self-test`, den der Installer vor und nach der Aktivierung ausführt.
|
||||
|
||||
## Shutdown-Reihenfolge
|
||||
|
||||
@@ -81,13 +88,53 @@ Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des
|
||||
- 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`
|
||||
- Laufzeitlogs: `%ProgramData%\BizTalkPlatformManagementTool\Logs\BizTalkPlatformManagementTool-yyyy-MM-dd.log`
|
||||
- Installerlogs: `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs\setup-*.log`
|
||||
|
||||
## Fehleranalyse
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
Snapshot- und Plan-JSON-Dateien werden als UTF-8 ohne BOM über eine temporäre Datei im Zielverzeichnis und anschließenden atomaren Austausch geschrieben. Beim Laden werden vorhandene Dateien mit UTF-8-BOM oder durch Encoding-Konvertierung sichtbar gewordenem BOM-Marker toleriert. Deserialisierte Snapshots werden normalisiert und auf leere Namen, Duplikate und fehlende Strukturen geprüft.
|
||||
|
||||
Diffs verwenden den zusammengesetzten Schlüssel aus Anwendung und Artefaktname. Gleichnamige Artefakte in verschiedenen BizTalk-Anwendungen überschreiben sich daher nicht mehr. CSV-Werte mit Präfix `=`, `+`, `-`, `@` oder Tab werden mit einem Apostroph neutralisiert, damit Tabellenkalkulationen sie nicht als Formel ausführen.
|
||||
|
||||
WMI-Abfrageobjekte sowie Rückgabeobjekte von WMI-Methoden werden deterministisch freigegeben. Polling wartet am Timeout-Ende nur noch für die tatsächlich verbleibende Zeit.
|
||||
|
||||
## Installer- und Updatearchitektur
|
||||
|
||||
Das Releasepaket enthält `Setup.exe`, `application.manifest`, den Payload-Ordner `application` und die Installationsanleitung. Vor jeder Änderung prüft der Installer, dass jede Payload-Datei vollständig im Manifest enthalten ist und Länge sowie SHA-256 entsprechen. Nicht deklarierte Zusatzdateien führen zum Abbruch.
|
||||
|
||||
Die Aktivierung ist transaktional aufgebaut:
|
||||
|
||||
1. Payload validieren.
|
||||
2. Eindeutiges Staging-Verzeichnis neben dem Installationsziel erstellen.
|
||||
3. Kopierte Dateien nochmals per SHA-256 und die Staging-EXE per `--self-test` prüfen.
|
||||
4. Laufende Toolinstanz ausschließen.
|
||||
5. Bestehendes Verzeichnis in ein eindeutiges Backup verschieben.
|
||||
6. Staging auf demselben Volume als produktives Verzeichnis aktivieren.
|
||||
7. Aktivierte EXE erneut per `--self-test` prüfen.
|
||||
8. Erst danach Startmenü, optionale Desktop-Verknüpfung und Windows-Uninstall-Eintrag schreiben.
|
||||
9. Bei einem Fehler die neue Version entfernen und das Backup einschließlich Windows-Integration wiederherstellen.
|
||||
|
||||
Das äußere ZIP erhält zusätzlich eine SHA-256-Datei und eine Certutil-kompatible Base64-TXT-Datei. Diese äußere Prüfsumme erkennt Übertragungsfehler; sie ist keine digitale Herausgebersignatur. Details und Befehle stehen in `Installation.md`.
|
||||
|
||||
## Automatisierte Verifikation
|
||||
|
||||
`tests/BizTalkPlatformManagementTool.Tests` prüft derzeit:
|
||||
|
||||
- Atomare JSON-Aktualisierung und BOM-Kompatibilität.
|
||||
- Anwendungsbezogene Diff-Identität bei gleichnamigen Artefakten.
|
||||
- Restore-Servergrenze und sichere Restore-Reihenfolge.
|
||||
- Neutralisierung formelartiger CSV-Werte.
|
||||
- Erkennung manipulierter Payload-Dateien.
|
||||
- Ablehnung nicht deklarierter Dateien und aus dem Payload-Verzeichnis ausbrechender Manifestpfade.
|
||||
- Staging-Fehler ohne Mutation einer bestehenden Installation.
|
||||
- Erfolgreiche Staging-Aktivierung.
|
||||
- Wiederherstellung der Vorversion, wenn der Self-Test nach Aktivierung fehlschlägt.
|
||||
- Deinstallation durch atomare Umbenennung des Programmverzeichnisses vor der bestmöglichen Bereinigung.
|
||||
|
||||
Der portable Build, die Tests, der Anwendungsselftest und die Paketkonsistenz sind lokal unter Mono prüfbar. Die endgültige Freigabe erfordert zusätzlich einen Windows-Test von UAC, Registry, Verknüpfungen und Setup-Rollback sowie einen repräsentativen BizTalk-2020-Test von Diagnose, Dry-run, Shutdown und Restore.
|
||||
|
||||
## Status Mapping
|
||||
|
||||
|
||||
+68
-26
@@ -1,40 +1,82 @@
|
||||
# Installation
|
||||
# Installation und Update
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Windows Server 2019/2022 oder ein Windows-Administrationshost
|
||||
- Microsoft BizTalk Server 2020 oder BizTalk Administration Tools
|
||||
- .NET Framework 4.6.1 Runtime
|
||||
- Für Builds: Visual Studio mit .NET Framework 4.6.1 Developer Pack
|
||||
- Zugriff auf den WMI-Namespace `root\MicrosoftBizTalkServer`
|
||||
- Ausreichende Rechte zum Lesen und Ändern von BizTalk-Artefakten
|
||||
- Lokale Administratorrechte und Start der EXE mit **Als Administrator ausführen**
|
||||
- Schreibrechte im Verzeichnis der EXE für die tägliche Logdatei
|
||||
- Lokale Administratorrechte; `Setup.exe` und die Anwendung fordern diese per UAC-Manifest an
|
||||
- Zugriff auf `root\MicrosoftBizTalkServer` mit den erforderlichen BizTalk-Rechten
|
||||
|
||||
## Build
|
||||
## Übergabe als TXT
|
||||
|
||||
1. Repository öffnen.
|
||||
2. `BizTalkPlatformManagementTool.sln` in Visual Studio öffnen.
|
||||
3. Konfiguration `Release|Any CPU` auswählen.
|
||||
4. Solution bauen.
|
||||
5. Das Ergebnis liegt unter `src\BizTalkPlatformManagementTool\bin\Release\`.
|
||||
Das Release erzeugt folgende Dateien unter `artifacts`:
|
||||
|
||||
## Deployment
|
||||
- `BizTalkPlatformManagementTool-Setup.zip.b64.txt`: Certutil-kompatible Base64-Übertragung
|
||||
- `BizTalkPlatformManagementTool-Setup.zip.sha256.txt`: SHA-256 des ZIP-Archivs
|
||||
- `BizTalkPlatformManagementTool-Setup.zip`: direkt entpackbares Installationspaket
|
||||
|
||||
1. Den Release-Ordner auf einen BizTalk-Server oder einen Administrationshost kopieren.
|
||||
2. Sicherstellen, dass der ausführende Benutzer WMI-Zugriff auf `root\MicrosoftBizTalkServer` hat.
|
||||
3. `BizTalkPlatformManagementTool.exe` mit **Als Administrator ausführen** starten.
|
||||
4. Als Ausgabeverzeichnis einen Ordner wählen, in dem Plan-, Snapshot- und Report-Dateien abgelegt werden dürfen.
|
||||
5. Prüfen, dass im EXE-Verzeichnis `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben werden kann. Logs werden für maximal fünf Tage vorgehalten.
|
||||
Auf dem Zielsystem wird die TXT-Datei so rekonstruiert und geprüft:
|
||||
|
||||
## Erster Funktionstest
|
||||
```bat
|
||||
certutil -decode BizTalkPlatformManagementTool-Setup.zip.b64.txt BizTalkPlatformManagementTool-Setup.zip
|
||||
certutil -hashfile BizTalkPlatformManagementTool-Setup.zip SHA256
|
||||
type BizTalkPlatformManagementTool-Setup.zip.sha256.txt
|
||||
```
|
||||
|
||||
1. Anwendung mit **Als Administrator ausführen** starten.
|
||||
2. Zielserver eintragen oder den vorgeschlagenen lokalen Server verwenden.
|
||||
3. **Dry run** aktiviert lassen.
|
||||
4. **Diagnose** ausführen.
|
||||
5. **Snapshot Before** ausführen und prüfen, ob `before.json` sowie CSV/HTML-Reports erzeugt wurden.
|
||||
Der Hash aus `certutil` muss exakt dem Wert in der SHA-256-Datei entsprechen. Danach das ZIP in einen neuen Ordner entpacken und `Setup.exe` starten. Ein Code-Signing-Zertifikat ist derzeit nicht Bestandteil des Repositories; deshalb schützt SHA-256 gegen Übertragungsfehler, ersetzt aber keine Signaturprüfung der Herausgeberidentität.
|
||||
|
||||
## Produktive Nutzung
|
||||
## Neuinstallation
|
||||
|
||||
Vor produktiven Änderungen immer zuerst einen Dry-run ausführen und die erzeugten `shutdown-plan.json` beziehungsweise `restore-plan.json` prüfen. Dry-run erst deaktivieren, wenn der Plan fachlich und technisch korrekt ist.
|
||||
1. ZIP vollständig entpacken; `Setup.exe`, `application.manifest` und der Ordner `application` müssen nebeneinander liegen.
|
||||
2. `Setup.exe` starten und die UAC-Abfrage bestätigen.
|
||||
3. Optional die Desktop-Verknüpfung abwählen.
|
||||
4. **Installieren** wählen.
|
||||
5. Den Abschluss und den Pfad des Diagnoselogs prüfen.
|
||||
6. Die Anwendung starten, **Dry run** aktiviert lassen und zuerst **Diagnose** ausführen.
|
||||
|
||||
Installationsziele:
|
||||
|
||||
- Programm: `%ProgramFiles%\BizTalkPlatformManagementTool`
|
||||
- Laufzeitlogs: `%ProgramData%\BizTalkPlatformManagementTool\Logs`
|
||||
- Installerlogs und Uninstaller: `%ProgramData%\BizTalkPlatformManagementTool`
|
||||
- Startmenü: `BizTalk Platform Management Tool`
|
||||
|
||||
## Update und Rollback
|
||||
|
||||
Der Installer verändert eine bestehende Installation erst nach erfolgreicher Paketprüfung:
|
||||
|
||||
1. Jede Payload-Datei wird gegen Länge und SHA-256 im `application.manifest` geprüft; unbekannte Zusatzdateien werden abgelehnt.
|
||||
2. Die neue Version wird in ein eindeutiges Staging-Verzeichnis kopiert und dort mit `--self-test` geprüft.
|
||||
3. Eine laufende Toolinstanz blockiert das Update.
|
||||
4. Die bestehende Installation wird in ein Backup-Verzeichnis verschoben.
|
||||
5. Das validierte Staging wird auf demselben Volume aktiviert.
|
||||
6. Die aktivierte EXE führt den Self-Test erneut aus.
|
||||
7. Erst danach werden Verknüpfungen und Windows-Uninstall-Eintrag aktualisiert.
|
||||
|
||||
Schlägt ein Schritt nach Beginn der Umschaltung fehl, entfernt das Setup die neue Version und stellt das Backup wieder her. Staging und Backup werden anschließend bestmöglich bereinigt. Das genaue Phasenprotokoll steht unter `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs`.
|
||||
|
||||
## Deinstallation
|
||||
|
||||
Die Deinstallation ist über **Apps & Features / Programme und Features** oder über den Setup-Button **Deinstallieren** möglich. Vorher muss die Anwendung geschlossen sein. Das Programmverzeichnis wird zuerst atomar aus dem aktiven Pfad in ein eindeutiges Quarantäneverzeichnis verschoben; erst danach werden Verknüpfungen und Uninstall-Eintrag entfernt und die Dateien bestmöglich gelöscht. Scheitert die Windows-Integration, werden Programmverzeichnis, Registrywerte, Verknüpfungen und vorheriger Uninstaller wiederhergestellt. Installerlogs und der supportfähige Setup-Ordner bleiben bewusst zur Fehleranalyse unter `%ProgramData%\BizTalkPlatformManagementTool` erhalten.
|
||||
|
||||
## Build, Test und Paketierung
|
||||
|
||||
In einer Visual-Studio-Developer-Eingabeaufforderung mit .NET Framework 4.6.1 Developer Pack:
|
||||
|
||||
```bat
|
||||
scripts\test-release.cmd
|
||||
scripts\package-release.cmd
|
||||
```
|
||||
|
||||
`test-release.cmd` baut alle vier Projekte und führt die Regressionstests aus. `package-release.cmd` baut und testet erneut, erzeugt Paket, ZIP, Base64-TXT und SHA-256-Datei und validiert dabei das interne Payload-Manifest.
|
||||
|
||||
Unter Mono kann der portable Anteil lokal geprüft werden:
|
||||
|
||||
```sh
|
||||
msbuild BizTalkPlatformManagementTool.sln /p:Configuration=Release /p:Platform="Any CPU" /m:1
|
||||
mono tests/BizTalkPlatformManagementTool.Tests/bin/Release/BizTalkPlatformManagementTool.Tests.exe
|
||||
mono src/BizTalkPlatformManagementTool/bin/Release/BizTalkPlatformManagementTool.exe --self-test
|
||||
```
|
||||
|
||||
Mono ersetzt nicht die abschließende Prüfung von UAC, Registry, Verknüpfungen und BizTalk-WMI auf einem repräsentativen Windows-/BizTalk-System.
|
||||
|
||||
@@ -21,10 +21,12 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
|
||||
- Dry-run mode enabled by default
|
||||
- WMI access through `root\MicrosoftBizTalkServer`
|
||||
- Startup check for administrator rights
|
||||
- Detailed operation logging in the GUI and daily rolling log files next to the executable
|
||||
- Detailed operation logging in the GUI and daily rolling log files under ProgramData
|
||||
- Environment status indicator based on host instance state
|
||||
- Clear and Close actions in the main toolbar
|
||||
- No compile-time dependency on BizTalk ExplorerOM assemblies
|
||||
- Transactional Windows installer/updater with SHA-256 payload validation and rollback
|
||||
- WMI-free runtime self-test plus automated regression test executable
|
||||
|
||||
## Safe Usage
|
||||
|
||||
@@ -39,7 +41,9 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
The application requests administrator rights through its UAC manifest and checks them again during startup. Only one GUI instance can run per Windows session.
|
||||
|
||||
Before a real shutdown or restore, the exact fresh plan is saved and a second dialog shows its executable step count, target server and plan path. Restore is rejected when the snapshot server does not match the selected target (short name and FQDN of the same host are accepted).
|
||||
|
||||
## Operation Order
|
||||
|
||||
@@ -64,9 +68,9 @@ Orchestrations that were `Bound` are deliberately left unchanged during restore
|
||||
- `shutdown-after.json`, `restore-after.json`
|
||||
- `diff.json`, `diff.csv`, `diff.html`
|
||||
- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html`
|
||||
- Runtime logs next to the executable: `BizTalkPlatformManagementTool-yyyy-MM-dd.log`
|
||||
- Runtime logs under `%ProgramData%\BizTalkPlatformManagementTool\Logs`
|
||||
|
||||
Log files are retained for the current day plus the previous four days. Older `BizTalkPlatformManagementTool-*.log` files are removed on startup.
|
||||
Log files are retained for the current day plus the previous four days. Older `BizTalkPlatformManagementTool-*.log` files are removed on startup. If ProgramData is unexpectedly unavailable, logging falls back to the executable directory.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -74,14 +78,19 @@ The Operation Log shows the WMI class, key property, key value and method for re
|
||||
|
||||
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.
|
||||
|
||||
JSON snapshots and plans are written through a same-directory temporary file and atomic replacement. Snapshot comparison keys artifacts by application plus name, preventing collisions between equal artifact names in different applications. CSV fields that could be interpreted as spreadsheet formulas are neutralized.
|
||||
|
||||
## Build
|
||||
|
||||
Open `BizTalkPlatformManagementTool.sln` in Visual Studio on Windows with the .NET Framework 4.6.1 Developer Pack installed, then build the `Release|Any CPU` configuration.
|
||||
|
||||
The app targets .NET Framework 4.6.1 for compatibility with customer environments that do not have newer .NET Framework developer packs installed.
|
||||
|
||||
Use `scripts\test-release.cmd` for the build and regression suite and `scripts\package-release.cmd` for the tested installer ZIP, Certutil-compatible Base64 TXT and SHA-256 file. See [Installation](Installation.md) for decoding and update/rollback details.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Installation](Installation.md)
|
||||
- [Dokumentation](Dokumentation.md)
|
||||
- [Installer stability analysis](docs/Installer-Stabilitaetsanalyse-2026-08-11.md)
|
||||
- [References](REFERENCES.md)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Installer-Stabilitätsanalyse vom 11.08.2026
|
||||
|
||||
## Ausgangslage
|
||||
|
||||
Vor Version 2.1.0 enthielt das Repository keinen Installer für die C#-Anwendung. Die Datei `release/BizTalkPlatformManagementTool.ps1` ist eine ältere PowerShell-Implementierung des Tools und kein Installationsprogramm. Frühere `release/*.zip.txt` waren Base64-Quellpakete ohne Update-, Abnahme- oder Rollbacklogik.
|
||||
|
||||
## Implementierte Sicherheitsgrenzen
|
||||
|
||||
- Vollständiges internes Payload-Manifest mit Dateipfad, Länge und SHA-256.
|
||||
- Ablehnung fehlender, veränderter, doppelter, zusätzlicher oder aus dem Payload-Verzeichnis ausbrechender Pfade.
|
||||
- Keine Mutation vor vollständig bestandenem Manifest- und Staging-Self-Test.
|
||||
- Update nur bei geschlossener produktiver Toolinstanz.
|
||||
- Staging und Backup als eindeutige Geschwister des Installationsverzeichnisses auf demselben Volume.
|
||||
- Zweiter Self-Test nach Aktivierung und vor Windows-Registrierung.
|
||||
- Automatisches Datei- und Registrierungsrollback bei Fehlern.
|
||||
- Dauerhaftes phasenbezogenes Installerlog unter ProgramData.
|
||||
- Deinstallation über Windows-Uninstall-Eintrag; Diagnoseprotokolle bleiben erhalten.
|
||||
- Äußere ZIP-Prüfsumme und Certutil-kompatible Base64-TXT für kontrollierte Übertragung.
|
||||
|
||||
## Lokal verifiziert
|
||||
|
||||
- Release-Build aller Projekte mit Mono MSBuild.
|
||||
- Elf Regressionstests einschließlich manipulierter/zusätzlicher/ausbrechender Payload-Pfade, Staging-Abbruch ohne Mutation, erzwungenem Fehler des zweiten Self-Tests mit Wiederherstellung der Vorversion und Deinstallation über ein Quarantäneverzeichnis.
|
||||
- WMI-freier Self-Test der produktiven EXE.
|
||||
- Erstellung des Installationsordners, ZIPs, Base64-TXTs und der SHA-256-Datei.
|
||||
- Rückdekodierung der Base64-TXT und Bytevergleich mit dem ZIP.
|
||||
- Erneute Prüfung des internen Manifests nach der Paketierung.
|
||||
|
||||
## Noch auf Windows/BizTalk zu validieren
|
||||
|
||||
Die lokale Linux-/Mono-Verifikation kann folgende Windows-spezifische Punkte nicht abschließend beweisen:
|
||||
|
||||
1. UAC-Anforderung beider EXE-Dateien auf Windows Server 2019/2022.
|
||||
2. Startmenü- und optionale Desktop-Verknüpfung über Windows Script Host.
|
||||
3. 64-Bit-Uninstall-Eintrag und Aufruf über Apps & Features.
|
||||
4. Updateblockade bei laufender installierter GUI.
|
||||
5. Reales Rollback bei Dateisperren, Virenscannerzugriff oder Registryfehlern.
|
||||
6. Diagnose und Laufzeitoperationen gegen `root\MicrosoftBizTalkServer` auf BizTalk Server 2020.
|
||||
|
||||
Bis diese Punkte repräsentativ geprüft sind, ist der Installer lokal automatisiert gehärtet, aber noch nicht als vollständig produktionsvalidiert auf Windows/BizTalk zu bezeichnen.
|
||||
@@ -0,0 +1,9 @@
|
||||
@echo off
|
||||
setlocal
|
||||
where msbuild.exe >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo MSBuild.exe was not found in PATH. Run this from a Visual Studio Developer Command Prompt.
|
||||
exit /b 1
|
||||
)
|
||||
msbuild "%~dp0..\BizTalkPlatformManagementTool.sln" /p:Configuration=Release /p:Platform="Any CPU" /m:1 /v:minimal
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
setlocal
|
||||
call "%~dp0test-release.cmd"
|
||||
if errorlevel 1 exit /b 1
|
||||
"%~dp0..\src\BizTalkPlatformManagementTool.Packager\bin\Release\BizTalkPlatformManagementTool.Packager.exe" "%~dp0.." Release
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
setlocal
|
||||
call "%~dp0build-release.cmd"
|
||||
if errorlevel 1 exit /b 1
|
||||
"%~dp0..\tests\BizTalkPlatformManagementTool.Tests\bin\Release\BizTalkPlatformManagementTool.Tests.exe"
|
||||
exit /b %ERRORLEVEL%
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?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>{74A5D422-0BA5-4559-BD81-C89C071A8FE4}</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.Setup\BizTalkPlatformManagementTool.Setup.csproj"><Project>{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}</Project><Name>BizTalkPlatformManagementTool.Setup</Name></ProjectReference></ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using BizTalkPlatformManagementTool.Setup;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Packager
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (args.Length != 2) throw new ArgumentException("Usage: BizTalkPlatformManagementTool.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 application = Path.Combine(package, "application");
|
||||
var zip = Path.Combine(artifacts, "BizTalkPlatformManagementTool-Setup.zip");
|
||||
|
||||
if (Directory.Exists(package)) Directory.Delete(package, true);
|
||||
Directory.CreateDirectory(application);
|
||||
Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool.Setup", "bin", configuration, "BizTalkPlatformManagementTool.Setup.exe"), Path.Combine(package, "Setup.exe"));
|
||||
Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool", "bin", configuration, "BizTalkPlatformManagementTool.exe"), Path.Combine(application, "BizTalkPlatformManagementTool.exe"));
|
||||
Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool", "bin", configuration, "BizTalkPlatformManagementTool.exe.config"), Path.Combine(application, "BizTalkPlatformManagementTool.exe.config"));
|
||||
Copy(Path.Combine(root, "Installation.md"), Path.Combine(package, "INSTALLATION.md"));
|
||||
PackageManifest.Write(application, Path.Combine(package, "application.manifest"));
|
||||
PackageManifest.ValidateAndRead(application, Path.Combine(package, "application.manifest"));
|
||||
|
||||
if (File.Exists(zip)) File.Delete(zip);
|
||||
ZipFile.CreateFromDirectory(package, zip, CompressionLevel.Optimal, false);
|
||||
WriteBase64(zip, zip + ".b64.txt");
|
||||
File.WriteAllText(zip + ".sha256.txt", PackageManifest.Sha256(zip) + " " + Path.GetFileName(zip) + Environment.NewLine, new UTF8Encoding(false));
|
||||
Console.WriteLine("SETUP_PACKAGE=" + package);
|
||||
Console.WriteLine("SETUP_ZIP=" + zip);
|
||||
Console.WriteLine("SETUP_BASE64=" + zip + ".b64.txt");
|
||||
Console.WriteLine("SETUP_SHA256=" + zip + ".sha256.txt");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine("Packaging failed: " + ex);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Copy(string source, string target)
|
||||
{
|
||||
if (!File.Exists(source)) throw new FileNotFoundException("Required package file missing: " + source, source);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(target));
|
||||
File.Copy(source, target, true);
|
||||
}
|
||||
|
||||
private static void WriteBase64(string source, string target)
|
||||
{
|
||||
var encoded = Convert.ToBase64String(File.ReadAllBytes(source));
|
||||
var builder = new StringBuilder(encoded.Length + encoded.Length / 64 + 2);
|
||||
for (var offset = 0; offset < encoded.Length; offset += 64)
|
||||
{
|
||||
builder.Append(encoded, offset, Math.Min(64, encoded.Length - offset));
|
||||
builder.Append('\n');
|
||||
}
|
||||
File.WriteAllText(target, builder.ToString(), new UTF8Encoding(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>BizTalkPlatformManagementTool.Setup</RootNamespace>
|
||||
<AssemblyName>BizTalkPlatformManagementTool.Setup</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</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="Microsoft.CSharp" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Security" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="InstallerEngine.cs" />
|
||||
<Compile Include="MainForm.cs" />
|
||||
<Compile Include="PackageManifest.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="SetupOperationLog.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup><None Include="app.manifest" /></ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,403 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
internal sealed class InstallerEngine
|
||||
{
|
||||
internal const string ApplicationExeName = "BizTalkPlatformManagementTool.exe";
|
||||
private const string ProductName = "BizTalk Platform Management Tool";
|
||||
private const string ProductVersion = "2.1.0";
|
||||
private const string UninstallKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\BizTalkPlatformManagementTool";
|
||||
private readonly string packageDirectory;
|
||||
private readonly string installDirectory;
|
||||
private readonly string dataDirectory;
|
||||
private readonly bool registerWindowsIntegration;
|
||||
private readonly Func<string, bool> selfTestRunner;
|
||||
|
||||
private sealed class WindowsIntegrationSnapshot
|
||||
{
|
||||
public bool RegistryKeyExisted { get; set; }
|
||||
public Dictionary<string, Tuple<object, RegistryValueKind>> RegistryValues { get; set; }
|
||||
public byte[] DesktopShortcut { get; set; }
|
||||
public byte[] StartMenuShortcut { get; set; }
|
||||
public byte[] Uninstaller { get; set; }
|
||||
}
|
||||
|
||||
public InstallerEngine(string packageDirectory)
|
||||
: this(
|
||||
packageDirectory,
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "BizTalkPlatformManagementTool"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "BizTalkPlatformManagementTool"),
|
||||
true,
|
||||
RunApplicationSelfTest)
|
||||
{
|
||||
}
|
||||
|
||||
internal InstallerEngine(string packageDirectory, string installDirectory, string dataDirectory, bool registerWindowsIntegration, Func<string, bool> selfTestRunner)
|
||||
{
|
||||
this.packageDirectory = Path.GetFullPath(packageDirectory);
|
||||
this.installDirectory = Path.GetFullPath(installDirectory);
|
||||
this.dataDirectory = Path.GetFullPath(dataDirectory);
|
||||
this.registerWindowsIntegration = registerWindowsIntegration;
|
||||
this.selfTestRunner = selfTestRunner ?? throw new ArgumentNullException("selfTestRunner");
|
||||
}
|
||||
|
||||
/// <summary>Gets the fixed machine-wide application installation directory.</summary>
|
||||
public string InstallDirectory { get { return installDirectory; } }
|
||||
/// <summary>Gets whether this setup copy contains a complete install/update payload.</summary>
|
||||
public bool HasInstallPayload { get { return Directory.Exists(Path.Combine(packageDirectory, "application")) && File.Exists(Path.Combine(packageDirectory, "application.manifest")); } }
|
||||
/// <summary>Gets whether the application executable is present at the install target.</summary>
|
||||
public bool IsInstalled { get { return File.Exists(Path.Combine(installDirectory, ApplicationExeName)); } }
|
||||
|
||||
/// <summary>Validates, stages and transactionally installs or updates the application.</summary>
|
||||
public void Install(bool createDesktopShortcut, Action<string> report)
|
||||
{
|
||||
report = report ?? delegate { };
|
||||
Directory.CreateDirectory(dataDirectory);
|
||||
var log = SetupOperationLog.Create(dataDirectory, "install-update");
|
||||
Action<string> write = message => { log.Write("INFO", message); report(message); };
|
||||
write("Diagnoselog: " + (log.FilePath.Length == 0 ? "nicht verfuegbar" : log.FilePath));
|
||||
|
||||
var sourceApplication = Path.Combine(packageDirectory, "application");
|
||||
var manifestPath = Path.Combine(packageDirectory, "application.manifest");
|
||||
var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N");
|
||||
var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N");
|
||||
var hadExistingInstallation = Directory.Exists(installDirectory);
|
||||
var integrationSnapshot = registerWindowsIntegration ? CaptureWindowsIntegration() : null;
|
||||
var backupCreated = false;
|
||||
var activated = false;
|
||||
|
||||
try
|
||||
{
|
||||
write("Phase 1/6: Paketmanifest und SHA-256 pruefen.");
|
||||
var files = PackageManifest.ValidateAndRead(sourceApplication, manifestPath);
|
||||
RequirePayload(files, ApplicationExeName);
|
||||
RequirePayload(files, ApplicationExeName + ".config");
|
||||
EnsureApplicationNotRunning();
|
||||
|
||||
write("Phase 2/6: Update in isoliertes Staging kopieren.");
|
||||
CopyPayload(sourceApplication, stagingDirectory, files);
|
||||
var stagedExe = Path.Combine(stagingDirectory, ApplicationExeName);
|
||||
if (!selfTestRunner(stagedExe)) throw new InvalidOperationException("Der WMI-freie Self-Test der Staging-Version ist fehlgeschlagen.");
|
||||
write("Staging-Self-Test erfolgreich.");
|
||||
|
||||
write("Phase 3/6: Vorhandene Version sichern und Staging atomar aktivieren.");
|
||||
if (hadExistingInstallation)
|
||||
{
|
||||
Directory.Move(installDirectory, backupDirectory);
|
||||
backupCreated = true;
|
||||
}
|
||||
Directory.Move(stagingDirectory, installDirectory);
|
||||
activated = true;
|
||||
|
||||
write("Phase 4/6: Aktivierte Version erneut pruefen.");
|
||||
var targetExe = Path.Combine(installDirectory, ApplicationExeName);
|
||||
if (!selfTestRunner(targetExe)) throw new InvalidOperationException("Der Self-Test der aktivierten Version ist fehlgeschlagen.");
|
||||
File.WriteAllText(
|
||||
Path.Combine(installDirectory, "install-state.txt"),
|
||||
"ProductVersion=" + ProductVersion + Environment.NewLine
|
||||
+ "InstalledAt=" + DateTimeOffset.Now.ToString("o", CultureInfo.InvariantCulture) + Environment.NewLine
|
||||
+ "ManifestSha256=" + PackageManifest.Sha256(manifestPath) + Environment.NewLine,
|
||||
new UTF8Encoding(false));
|
||||
|
||||
write("Phase 5/6: Windows-Integration registrieren.");
|
||||
if (registerWindowsIntegration)
|
||||
{
|
||||
RegisterWindowsIntegration(targetExe, createDesktopShortcut);
|
||||
}
|
||||
|
||||
write("Phase 6/6: Backup bereinigen.");
|
||||
TryDeleteDirectory(backupDirectory, write);
|
||||
write("Installation/Update erfolgreich abgeschlossen: " + installDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Write("ERROR", ex.ToString());
|
||||
var rollbackErrors = new List<string>();
|
||||
try
|
||||
{
|
||||
if (activated && Directory.Exists(installDirectory)) Directory.Delete(installDirectory, true);
|
||||
if (backupCreated && Directory.Exists(backupDirectory)) Directory.Move(backupDirectory, installDirectory);
|
||||
write(backupCreated ? "Rollback: vorherige Programmversion wiederhergestellt." : "Rollback: unvollstaendige Neuinstallation entfernt.");
|
||||
}
|
||||
catch (Exception rollbackException)
|
||||
{
|
||||
rollbackErrors.Add(rollbackException.Message);
|
||||
log.Write("ERROR", "Rollback files: " + rollbackException);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (registerWindowsIntegration)
|
||||
{
|
||||
RestoreWindowsIntegration(integrationSnapshot);
|
||||
}
|
||||
}
|
||||
catch (Exception rollbackException)
|
||||
{
|
||||
rollbackErrors.Add(rollbackException.Message);
|
||||
log.Write("ERROR", "Rollback registration: " + rollbackException);
|
||||
}
|
||||
|
||||
TryDeleteDirectory(stagingDirectory, write);
|
||||
TryDeleteDirectory(backupDirectory, write);
|
||||
var suffix = rollbackErrors.Count == 0 ? " Rollback erfolgreich." : " Rollback-Fehler: " + string.Join(" | ", rollbackErrors);
|
||||
throw new InvalidOperationException("Installation/Update fehlgeschlagen." + suffix + " Ursache: " + ex.Message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Removes the active program directory and registered Windows integration.</summary>
|
||||
public void Uninstall(Action<string> report)
|
||||
{
|
||||
report = report ?? delegate { };
|
||||
Directory.CreateDirectory(dataDirectory);
|
||||
var log = SetupOperationLog.Create(dataDirectory, "uninstall");
|
||||
Action<string> write = message => { log.Write("INFO", message); report(message); };
|
||||
var integrationSnapshot = registerWindowsIntegration ? CaptureWindowsIntegration() : null;
|
||||
var removalDirectory = installDirectory + ".removed." + Guid.NewGuid().ToString("N");
|
||||
var filesMoved = false;
|
||||
try
|
||||
{
|
||||
EnsureApplicationNotRunning();
|
||||
if (Directory.Exists(installDirectory))
|
||||
{
|
||||
Directory.Move(installDirectory, removalDirectory);
|
||||
filesMoved = true;
|
||||
}
|
||||
if (registerWindowsIntegration) RemoveWindowsIntegration();
|
||||
TryDeleteDirectory(removalDirectory, write);
|
||||
write("Deinstallation erfolgreich. Installer-Logs bleiben erhalten: " + Path.Combine(dataDirectory, "InstallerLogs"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Write("ERROR", ex.ToString());
|
||||
if (filesMoved && !Directory.Exists(installDirectory) && Directory.Exists(removalDirectory))
|
||||
{
|
||||
try { Directory.Move(removalDirectory, installDirectory); }
|
||||
catch (Exception rollbackException) { log.Write("ERROR", "Uninstall rollback files: " + rollbackException); }
|
||||
}
|
||||
if (registerWindowsIntegration)
|
||||
{
|
||||
try { RestoreWindowsIntegration(integrationSnapshot); }
|
||||
catch (Exception rollbackException) { log.Write("ERROR", "Uninstall rollback registration: " + rollbackException); }
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterWindowsIntegration(string targetExe, bool createDesktopShortcut)
|
||||
{
|
||||
var programsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName);
|
||||
Directory.CreateDirectory(programsDirectory);
|
||||
CreateShortcut(Path.Combine(programsDirectory, ProductName + ".lnk"), targetExe, installDirectory, ProductName);
|
||||
|
||||
if (createDesktopShortcut) CreateShortcut(DesktopShortcutPath, targetExe, installDirectory, ProductName);
|
||||
else if (File.Exists(DesktopShortcutPath)) File.Delete(DesktopShortcutPath);
|
||||
|
||||
var setupDirectory = Path.Combine(dataDirectory, "Setup");
|
||||
Directory.CreateDirectory(setupDirectory);
|
||||
var uninstaller = Path.Combine(setupDirectory, "Uninstall.exe");
|
||||
File.Copy(Assembly.GetExecutingAssembly().Location, uninstaller, true);
|
||||
|
||||
using (var key = Registry.LocalMachine.CreateSubKey(UninstallKeyPath))
|
||||
{
|
||||
if (key == null) throw new InvalidOperationException("Windows uninstall registry key could not be created.");
|
||||
key.SetValue("DisplayName", ProductName);
|
||||
key.SetValue("DisplayVersion", ProductVersion);
|
||||
key.SetValue("Publisher", "BEW");
|
||||
key.SetValue("InstallLocation", installDirectory);
|
||||
key.SetValue("DisplayIcon", targetExe);
|
||||
key.SetValue("UninstallString", "\"" + uninstaller + "\" --uninstall");
|
||||
key.SetValue("NoModify", 1, RegistryValueKind.DWord);
|
||||
key.SetValue("NoRepair", 1, RegistryValueKind.DWord);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveWindowsIntegration()
|
||||
{
|
||||
TryDeleteFile(DesktopShortcutPath);
|
||||
var programsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName);
|
||||
TryDeleteFile(Path.Combine(programsDirectory, ProductName + ".lnk"));
|
||||
if (Directory.Exists(programsDirectory) && Directory.GetFileSystemEntries(programsDirectory).Length == 0) Directory.Delete(programsDirectory);
|
||||
Registry.LocalMachine.DeleteSubKeyTree(UninstallKeyPath, false);
|
||||
}
|
||||
|
||||
private static void CreateShortcut(string shortcutPath, string targetPath, string workingDirectory, string description)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(shortcutPath));
|
||||
var shellType = Type.GetTypeFromProgID("WScript.Shell");
|
||||
if (shellType == null) throw new InvalidOperationException("Windows Script Host is unavailable; shortcut creation failed.");
|
||||
object shell = null;
|
||||
object shortcut = null;
|
||||
try
|
||||
{
|
||||
shell = Activator.CreateInstance(shellType);
|
||||
dynamic dynamicShell = shell;
|
||||
shortcut = dynamicShell.CreateShortcut(shortcutPath);
|
||||
dynamic dynamicShortcut = shortcut;
|
||||
dynamicShortcut.TargetPath = targetPath;
|
||||
dynamicShortcut.WorkingDirectory = workingDirectory;
|
||||
dynamicShortcut.Description = description;
|
||||
dynamicShortcut.IconLocation = targetPath + ",0";
|
||||
dynamicShortcut.Save();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (shortcut != null && Marshal.IsComObject(shortcut)) Marshal.FinalReleaseComObject(shortcut);
|
||||
if (shell != null && Marshal.IsComObject(shell)) Marshal.FinalReleaseComObject(shell);
|
||||
}
|
||||
}
|
||||
|
||||
private WindowsIntegrationSnapshot CaptureWindowsIntegration()
|
||||
{
|
||||
var snapshot = new WindowsIntegrationSnapshot
|
||||
{
|
||||
RegistryValues = new Dictionary<string, Tuple<object, RegistryValueKind>>(StringComparer.OrdinalIgnoreCase),
|
||||
DesktopShortcut = ReadFileOrNull(DesktopShortcutPath),
|
||||
StartMenuShortcut = ReadFileOrNull(StartMenuShortcutPath),
|
||||
Uninstaller = ReadFileOrNull(UninstallerPath)
|
||||
};
|
||||
using (var key = Registry.LocalMachine.OpenSubKey(UninstallKeyPath, false))
|
||||
{
|
||||
snapshot.RegistryKeyExisted = key != null;
|
||||
if (key != null)
|
||||
{
|
||||
foreach (var name in key.GetValueNames())
|
||||
{
|
||||
snapshot.RegistryValues[name] = Tuple.Create(key.GetValue(name, null, RegistryValueOptions.DoNotExpandEnvironmentNames), key.GetValueKind(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private void RestoreWindowsIntegration(WindowsIntegrationSnapshot snapshot)
|
||||
{
|
||||
if (snapshot == null) return;
|
||||
Registry.LocalMachine.DeleteSubKeyTree(UninstallKeyPath, false);
|
||||
if (snapshot.RegistryKeyExisted)
|
||||
{
|
||||
using (var key = Registry.LocalMachine.CreateSubKey(UninstallKeyPath))
|
||||
{
|
||||
if (key == null) throw new InvalidOperationException("Previous uninstall registry key could not be restored.");
|
||||
foreach (var pair in snapshot.RegistryValues)
|
||||
{
|
||||
key.SetValue(pair.Key, pair.Value.Item1, pair.Value.Item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
RestoreFile(DesktopShortcutPath, snapshot.DesktopShortcut);
|
||||
RestoreFile(StartMenuShortcutPath, snapshot.StartMenuShortcut);
|
||||
RestoreFile(UninstallerPath, snapshot.Uninstaller);
|
||||
}
|
||||
|
||||
private static byte[] ReadFileOrNull(string path)
|
||||
{
|
||||
return File.Exists(path) ? File.ReadAllBytes(path) : null;
|
||||
}
|
||||
|
||||
private static void RestoreFile(string path, byte[] content)
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
TryDeleteFile(path);
|
||||
return;
|
||||
}
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
File.WriteAllBytes(path, content);
|
||||
}
|
||||
|
||||
private void EnsureApplicationNotRunning()
|
||||
{
|
||||
var target = Path.Combine(installDirectory, ApplicationExeName);
|
||||
if (!File.Exists(target)) return;
|
||||
foreach (var process in Process.GetProcessesByName(Path.GetFileNameWithoutExtension(ApplicationExeName)))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.Equals(Path.GetFullPath(process.MainModule.FileName), Path.GetFullPath(target), StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException(ProductName + " is still running. Close it before installation or removal.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool RunApplicationSelfTest(string executable)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo(executable, "--self-test")
|
||||
{
|
||||
WorkingDirectory = Path.GetDirectoryName(executable),
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
using (var process = Process.Start(startInfo))
|
||||
{
|
||||
if (process == null) return false;
|
||||
if (!process.WaitForExit(60000))
|
||||
{
|
||||
try { process.Kill(); } catch { }
|
||||
return false;
|
||||
}
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyPayload(string sourceRoot, string targetRoot, IEnumerable<PackageFile> files)
|
||||
{
|
||||
Directory.CreateDirectory(targetRoot);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var source = PackageManifest.ResolveContainedPath(sourceRoot, file.RelativePath);
|
||||
var target = PackageManifest.ResolveContainedPath(targetRoot, file.RelativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(target));
|
||||
File.Copy(source, target, false);
|
||||
if (!string.Equals(PackageManifest.Sha256(target), file.Sha256, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("Copied payload failed SHA-256 verification: " + file.RelativePath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RequirePayload(IEnumerable<PackageFile> files, string relativePath)
|
||||
{
|
||||
if (!files.Any(x => string.Equals(x.RelativePath, relativePath, StringComparison.OrdinalIgnoreCase)))
|
||||
throw new InvalidDataException("Required payload file is missing from the manifest: " + relativePath);
|
||||
}
|
||||
|
||||
private static void TryDeleteDirectory(string path, Action<string> report)
|
||||
{
|
||||
try { if (Directory.Exists(path)) Directory.Delete(path, true); }
|
||||
catch (Exception ex) { report("WARNUNG: Verzeichnis konnte nicht bereinigt werden: " + path + " - " + ex.Message); }
|
||||
}
|
||||
|
||||
private static void TryDeleteFile(string path)
|
||||
{
|
||||
try { if (File.Exists(path)) File.Delete(path); } catch { }
|
||||
}
|
||||
|
||||
private static string DesktopShortcutPath
|
||||
{
|
||||
get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), ProductName + ".lnk"); }
|
||||
}
|
||||
|
||||
private static string StartMenuShortcutPath
|
||||
{
|
||||
get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName, ProductName + ".lnk"); }
|
||||
}
|
||||
|
||||
private string UninstallerPath
|
||||
{
|
||||
get { return Path.Combine(dataDirectory, "Setup", "Uninstall.exe"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
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 bool uninstallMode;
|
||||
private readonly TextBox output = new TextBox();
|
||||
private readonly CheckBox desktopShortcut = new CheckBox();
|
||||
private readonly Button installButton = new Button();
|
||||
private readonly Button uninstallButton = new Button();
|
||||
private bool busy;
|
||||
|
||||
public MainForm(InstallerEngine engine, bool uninstallMode)
|
||||
{
|
||||
this.engine = engine;
|
||||
this.uninstallMode = uninstallMode;
|
||||
Text = "BizTalk Platform Management Tool Setup";
|
||||
Width = 780;
|
||||
Height = 520;
|
||||
MinimumSize = new Size(680, 420);
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
BuildUi();
|
||||
FormClosing += OnFormClosing;
|
||||
}
|
||||
|
||||
private void BuildUi()
|
||||
{
|
||||
var root = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(16), RowCount = 5, ColumnCount = 1 };
|
||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
|
||||
root.Controls.Add(new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
|
||||
Text = "BizTalk Platform Management Tool 2.1.0"
|
||||
});
|
||||
root.Controls.Add(new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Padding = new Padding(0, 8, 0, 8),
|
||||
Text = "Transaktionaler Installer mit SHA-256-Pruefung, Staging-Self-Test und automatischem Rollback.\r\nZiel: " + engine.InstallDirectory
|
||||
});
|
||||
|
||||
desktopShortcut.Text = "Desktop-Verknuepfung fuer alle Benutzer erstellen";
|
||||
desktopShortcut.Checked = true;
|
||||
desktopShortcut.AutoSize = true;
|
||||
desktopShortcut.Enabled = !uninstallMode;
|
||||
root.Controls.Add(desktopShortcut);
|
||||
|
||||
output.Multiline = true;
|
||||
output.ReadOnly = true;
|
||||
output.ScrollBars = ScrollBars.Vertical;
|
||||
output.Dock = DockStyle.Fill;
|
||||
output.Font = new Font(FontFamily.GenericMonospace, 9);
|
||||
root.Controls.Add(output);
|
||||
|
||||
var buttons = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft };
|
||||
var closeButton = new Button { Text = "Schliessen", AutoSize = true };
|
||||
closeButton.Click += (sender, args) => Close();
|
||||
installButton.Text = engine.IsInstalled ? "Update installieren" : "Installieren";
|
||||
installButton.AutoSize = true;
|
||||
installButton.Enabled = engine.HasInstallPayload && !uninstallMode;
|
||||
installButton.Click += (sender, args) => Run(false);
|
||||
uninstallButton.Text = "Deinstallieren";
|
||||
uninstallButton.AutoSize = true;
|
||||
uninstallButton.Enabled = engine.IsInstalled;
|
||||
uninstallButton.Click += (sender, args) => Run(true);
|
||||
buttons.Controls.Add(closeButton);
|
||||
buttons.Controls.Add(uninstallButton);
|
||||
buttons.Controls.Add(installButton);
|
||||
root.Controls.Add(buttons);
|
||||
Controls.Add(root);
|
||||
|
||||
if (uninstallMode) Append("Deinstallationsmodus. Installer-Logs bleiben zu Diagnosezwecken unter ProgramData erhalten.");
|
||||
else if (!engine.HasInstallPayload) Append("Kein Installationspayload neben Setup.exe gefunden. Dieser Aufruf erlaubt nur die Deinstallation.");
|
||||
}
|
||||
|
||||
private void Run(bool uninstall)
|
||||
{
|
||||
if (uninstall && MessageBox.Show(this, "BizTalk Platform Management Tool wirklich deinstallieren?", "Deinstallation bestaetigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
var createDesktopShortcut = desktopShortcut.Checked;
|
||||
SetBusy(true);
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (uninstall) engine.Uninstall(Append);
|
||||
else engine.Install(createDesktopShortcut, Append);
|
||||
Append(uninstall ? "FERTIG: Deinstallation erfolgreich." : "FERTIG: Installation/Update erfolgreich.");
|
||||
Invoke(new Action(() =>
|
||||
{
|
||||
installButton.Text = engine.IsInstalled ? "Update installieren" : "Installieren";
|
||||
uninstallButton.Enabled = engine.IsInstalled;
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Append("FEHLER: " + ex);
|
||||
Invoke(new Action(() => MessageBox.Show(this, ex.Message, "Setup fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetBusy(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void Append(string message)
|
||||
{
|
||||
if (IsDisposed || Disposing) return;
|
||||
if (InvokeRequired)
|
||||
{
|
||||
try { BeginInvoke(new Action<string>(Append), message); } catch (InvalidOperationException) { }
|
||||
return;
|
||||
}
|
||||
output.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message + Environment.NewLine);
|
||||
}
|
||||
|
||||
private void SetBusy(bool value)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
try { BeginInvoke(new Action<bool>(SetBusy), value); } catch (InvalidOperationException) { }
|
||||
return;
|
||||
}
|
||||
busy = value;
|
||||
installButton.Enabled = !value && engine.HasInstallPayload && !uninstallMode;
|
||||
uninstallButton.Enabled = !value && engine.IsInstalled;
|
||||
desktopShortcut.Enabled = !value && !uninstallMode;
|
||||
UseWaitCursor = value;
|
||||
}
|
||||
|
||||
private void OnFormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
if (!busy) return;
|
||||
e.Cancel = true;
|
||||
MessageBox.Show(this, "Das Setup arbeitet noch. Bitte warten Sie bis zum Abschluss.", "Setup laeuft", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
public sealed class PackageFile
|
||||
{
|
||||
/// <summary>Gets or sets the normalized payload-relative path.</summary>
|
||||
public string RelativePath { get; set; }
|
||||
/// <summary>Gets or sets the declared file length.</summary>
|
||||
public long Length { get; set; }
|
||||
/// <summary>Gets or sets the lowercase SHA-256 digest.</summary>
|
||||
public string Sha256 { get; set; }
|
||||
}
|
||||
|
||||
public static class PackageManifest
|
||||
{
|
||||
/// <summary>Reads and cryptographically validates a complete application payload manifest.</summary>
|
||||
public static IList<PackageFile> ValidateAndRead(string applicationDirectory, string manifestPath)
|
||||
{
|
||||
if (!Directory.Exists(applicationDirectory)) throw new DirectoryNotFoundException("Application payload missing: " + applicationDirectory);
|
||||
if (!File.Exists(manifestPath)) throw new FileNotFoundException("Package manifest missing: " + manifestPath, manifestPath);
|
||||
|
||||
var files = new List<PackageFile>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var rawLine in File.ReadAllLines(manifestPath, Encoding.UTF8))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
if (line.Length == 0 || line.StartsWith("#", StringComparison.Ordinal)) continue;
|
||||
var parts = line.Split(new[] { '|' }, 3);
|
||||
long length;
|
||||
if (parts.Length != 3 || parts[0].Length != 64 || !long.TryParse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out length))
|
||||
throw new InvalidDataException("Invalid package manifest line: " + rawLine);
|
||||
|
||||
var relative = NormalizeRelativePath(parts[2]);
|
||||
if (!seen.Add(relative)) throw new InvalidDataException("Duplicate package manifest path: " + relative);
|
||||
var fullPath = ResolveContainedPath(applicationDirectory, relative);
|
||||
if (!File.Exists(fullPath)) throw new FileNotFoundException("Manifest payload file missing: " + relative, fullPath);
|
||||
var info = new FileInfo(fullPath);
|
||||
if (info.Length != length) throw new InvalidDataException("Payload size mismatch: " + relative);
|
||||
var actualHash = Sha256(fullPath);
|
||||
if (!string.Equals(actualHash, parts[0], StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("Payload SHA-256 mismatch: " + relative);
|
||||
files.Add(new PackageFile { RelativePath = relative, Length = length, Sha256 = actualHash });
|
||||
}
|
||||
|
||||
if (files.Count == 0) throw new InvalidDataException("The package manifest does not contain payload files.");
|
||||
var actualFiles = Directory.GetFiles(applicationDirectory, "*", SearchOption.AllDirectories)
|
||||
.Select(x => NormalizeRelativePath(x.Substring(Path.GetFullPath(applicationDirectory).TrimEnd(Path.DirectorySeparatorChar).Length + 1)))
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
var declaredFiles = files.Select(x => x.RelativePath).OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
if (!actualFiles.SequenceEqual(declaredFiles, StringComparer.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("The application payload contains files not covered by the package manifest.");
|
||||
return files;
|
||||
}
|
||||
|
||||
/// <summary>Creates a deterministic manifest covering every application payload file.</summary>
|
||||
public static void Write(string applicationDirectory, string manifestPath)
|
||||
{
|
||||
var root = Path.GetFullPath(applicationDirectory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
var lines = Directory.GetFiles(applicationDirectory, "*", SearchOption.AllDirectories)
|
||||
.Select(path => new FileInfo(path))
|
||||
.OrderBy(info => info.FullName, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(info => Sha256(info.FullName) + "|" + info.Length.ToString(CultureInfo.InvariantCulture) + "|" + NormalizeRelativePath(info.FullName.Substring(root.Length)))
|
||||
.ToArray();
|
||||
File.WriteAllLines(manifestPath, lines, new UTF8Encoding(false));
|
||||
}
|
||||
|
||||
/// <summary>Calculates the lowercase SHA-256 digest of a file.</summary>
|
||||
public static string Sha256(string path)
|
||||
{
|
||||
using (var stream = File.OpenRead(path))
|
||||
using (var algorithm = SHA256.Create())
|
||||
{
|
||||
var hash = algorithm.ComputeHash(stream);
|
||||
var builder = new StringBuilder(hash.Length * 2);
|
||||
foreach (var value in hash) builder.Append(value.ToString("x2", CultureInfo.InvariantCulture));
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Resolves a relative payload path and rejects directory traversal.</summary>
|
||||
public static string ResolveContainedPath(string root, string relative)
|
||||
{
|
||||
var normalizedRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
var result = Path.GetFullPath(Path.Combine(normalizedRoot, NormalizeRelativePath(relative).Replace('/', Path.DirectorySeparatorChar)));
|
||||
if (!result.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("Package path escapes the payload root: " + relative);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string NormalizeRelativePath(string path)
|
||||
{
|
||||
path = (path ?? string.Empty).Replace('\\', '/').Trim();
|
||||
if (path.Length == 0 || path.StartsWith("/", StringComparison.Ordinal) || path.Contains("../") || path == ".." || Path.IsPathRooted(path))
|
||||
throw new InvalidDataException("Unsafe package path: " + path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
var uninstallMode = args != null && args.Length == 1 && string.Equals(args[0], "--uninstall", StringComparison.OrdinalIgnoreCase);
|
||||
Application.Run(new MainForm(new InstallerEngine(AppDomain.CurrentDomain.BaseDirectory), uninstallMode));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("BizTalk Platform Management Tool Setup")]
|
||||
[assembly: AssemblyDescription("Transactional installer and updater for BizTalk Platform Management Tool")]
|
||||
[assembly: AssemblyCompany("BEW")]
|
||||
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
|
||||
[assembly: AssemblyVersion("2.1.0.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.0.0")]
|
||||
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
internal sealed class SetupOperationLog
|
||||
{
|
||||
private readonly object sync = new object();
|
||||
|
||||
private SetupOperationLog(string filePath)
|
||||
{
|
||||
FilePath = filePath;
|
||||
}
|
||||
|
||||
public string FilePath { get; private set; }
|
||||
|
||||
public static SetupOperationLog Create(string dataDirectory, string operation)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.Combine(dataDirectory, "InstallerLogs");
|
||||
Directory.CreateDirectory(directory);
|
||||
var path = Path.Combine(directory, "setup-" + DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) + "-" + operation + ".log");
|
||||
return new SetupOperationLog(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new SetupOperationLog(string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(string level, string message)
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath)) return;
|
||||
var line = "[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) + "][" + level + "] " + (message ?? string.Empty) + Environment.NewLine;
|
||||
try
|
||||
{
|
||||
lock (sync) File.AppendAllText(FilePath, line, new UTF8Encoding(false));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Setup logging must not hide the actual installation result.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="2.1.0.0" name="BizTalkPlatformManagementTool.Setup" />
|
||||
<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="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /></application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
@@ -12,6 +12,7 @@
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
@@ -45,7 +46,9 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="RuntimeSelfTest.cs" />
|
||||
<Compile Include="Models\ArtifactStates.cs" />
|
||||
<Compile Include="Models\BizTalkSnapshot.cs" />
|
||||
<Compile Include="Models\DiffModels.cs" />
|
||||
@@ -56,12 +59,14 @@
|
||||
<Compile Include="Services\JsonFileStore.cs" />
|
||||
<Compile Include="Services\OperationLogger.cs" />
|
||||
<Compile Include="Services\SnapshotComparer.cs" />
|
||||
<Compile Include="Services\SnapshotValidator.cs" />
|
||||
<Compile Include="Services\SnapshotStore.cs" />
|
||||
<Compile Include="Services\BizTalkOperationService.cs" />
|
||||
<Compile Include="Ui\MainForm.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
<None Include="app.manifest" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
|
||||
@@ -1,26 +1,81 @@
|
||||
namespace BizTalkPlatformManagementTool.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains BizTalk WMI state constants and display helpers used by snapshots,
|
||||
/// plans and reports.
|
||||
/// </summary>
|
||||
public static class ArtifactStates
|
||||
{
|
||||
/// <summary>
|
||||
/// WMI status value for a bound send port.
|
||||
/// </summary>
|
||||
public const int SendPortBound = 1;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a stopped send port.
|
||||
/// </summary>
|
||||
public const int SendPortStopped = 2;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a started send port.
|
||||
/// </summary>
|
||||
public const int SendPortStarted = 3;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for an unbound orchestration.
|
||||
/// </summary>
|
||||
public const int OrchestrationUnbound = 1;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a bound orchestration.
|
||||
/// </summary>
|
||||
public const int OrchestrationBound = 2;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a stopped orchestration.
|
||||
/// </summary>
|
||||
public const int OrchestrationStopped = 3;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a started orchestration.
|
||||
/// </summary>
|
||||
public const int OrchestrationStarted = 4;
|
||||
|
||||
/// <summary>
|
||||
/// WMI service state value for a stopped host instance.
|
||||
/// </summary>
|
||||
public const int HostStopped = 1;
|
||||
|
||||
/// <summary>
|
||||
/// WMI service state value for a host instance that is starting.
|
||||
/// </summary>
|
||||
public const int HostStartPending = 2;
|
||||
|
||||
/// <summary>
|
||||
/// WMI service state value for a host instance that is stopping.
|
||||
/// </summary>
|
||||
public const int HostStopPending = 3;
|
||||
|
||||
/// <summary>
|
||||
/// WMI service state value for a started host instance.
|
||||
/// </summary>
|
||||
public const int HostStarted = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a receive location enabled flag into the text used in reports.
|
||||
/// </summary>
|
||||
/// <param name="enabled">True when the receive location is enabled.</param>
|
||||
/// <returns>A display value for the receive location state.</returns>
|
||||
public static string FormatReceiveLocation(bool enabled)
|
||||
{
|
||||
return enabled ? "Enabled" : "Disabled";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an MSBTS_SendPort.Status value into a display string.
|
||||
/// </summary>
|
||||
/// <param name="status">The raw WMI send port status value.</param>
|
||||
/// <returns>A known status name or an Unknown value with the raw code.</returns>
|
||||
public static string FormatSendPort(int status)
|
||||
{
|
||||
switch (status)
|
||||
@@ -32,6 +87,11 @@ namespace BizTalkPlatformManagementTool.Models
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an MSBTS_Orchestration.OrchestrationStatus value into a display string.
|
||||
/// </summary>
|
||||
/// <param name="status">The raw WMI orchestration status value.</param>
|
||||
/// <returns>A known status name or an Unknown value with the raw code.</returns>
|
||||
public static string FormatOrchestration(int status)
|
||||
{
|
||||
switch (status)
|
||||
@@ -44,6 +104,11 @@ namespace BizTalkPlatformManagementTool.Models
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an MSBTS_HostInstance.ServiceState value into a display string.
|
||||
/// </summary>
|
||||
/// <param name="state">The raw WMI host instance service state value.</param>
|
||||
/// <returns>A known service state name or an Unknown value with the raw code.</returns>
|
||||
public static string FormatHostInstance(int state)
|
||||
{
|
||||
switch (state)
|
||||
|
||||
@@ -3,34 +3,62 @@ using System.Runtime.Serialization;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents one captured BizTalk runtime state including application artifacts
|
||||
/// and host instances.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class BizTalkSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new snapshot with empty application and host instance collections.
|
||||
/// </summary>
|
||||
public BizTalkSnapshot()
|
||||
{
|
||||
Applications = new List<ApplicationSnapshot>();
|
||||
HostInstances = new List<HostInstanceState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tool version that created the snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string ToolVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local timestamp when the snapshot was created.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk server name used for the WMI connection.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk application snapshots captured from WMI.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public List<ApplicationSnapshot> Applications { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the host instances captured from the BizTalk group.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public List<HostInstanceState> HostInstances { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Groups the captured artifact states for one BizTalk application.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class ApplicationSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an application snapshot with empty artifact collections.
|
||||
/// </summary>
|
||||
public ApplicationSnapshot()
|
||||
{
|
||||
ReceiveLocations = new List<ReceiveLocationState>();
|
||||
@@ -38,88 +66,169 @@ namespace BizTalkPlatformManagementTool.Models
|
||||
Orchestrations = new List<OrchestrationState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk application name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive locations that belong to the application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public List<ReceiveLocationState> ReceiveLocations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the send ports that belong to the application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public List<SendPortState> SendPorts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the orchestrations that belong to the application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public List<OrchestrationState> Orchestrations { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the relevant WMI state for a BizTalk receive location.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class ReceiveLocationState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive location name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parent receive port name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string ReceivePortName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the receive location is enabled.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive adapter name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string AdapterName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive location transport address.
|
||||
/// </summary>
|
||||
[DataMember(Order = 6)]
|
||||
public string Address { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the relevant WMI state for a BizTalk send port.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class SendPortState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the send port name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw MSBTS_SendPort.Status value.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the primary transport adapter type.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string PrimaryTransportType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the primary transport address.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string PrimaryTransportAddress { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the relevant WMI state for a BizTalk orchestration.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class OrchestrationState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the orchestration name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw MSBTS_Orchestration.OrchestrationStatus value.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public int OrchestrationStatus { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the relevant WMI state for a BizTalk host instance.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class HostInstanceState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the host instance name used as the WMI key.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string InstanceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk host name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string HostName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server on which the host instance runs.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw MSBTS_HostInstance.ServiceState value.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public int RawState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted host instance state.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string StateText { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,56 +3,104 @@ using System.Runtime.Serialization;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains all differences detected between two BizTalk snapshots.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class SnapshotDiff
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new diff with empty artifact and host instance collections.
|
||||
/// </summary>
|
||||
public SnapshotDiff()
|
||||
{
|
||||
ArtifactDifferences = new List<ArtifactDiffEntry>();
|
||||
HostInstanceDifferences = new List<HostInstanceDiffEntry>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets state differences for receive locations, send ports and orchestrations.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public List<ArtifactDiffEntry> ArtifactDifferences { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets state differences for host instances.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public List<HostInstanceDiffEntry> HostInstanceDifferences { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a before/after state change for one BizTalk application artifact.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class ArtifactDiffEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the artifact category, such as SendPort or ReceiveLocation.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string ArtifactType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the artifact name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted state in the before snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string Before { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted state in the after snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string After { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a before/after state change for one BizTalk host instance.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class HostInstanceDiffEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the host instance name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string InstanceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk host name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string HostName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server on which the host instance runs.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted state in the before snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string Before { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted state in the after snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string After { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,92 +3,211 @@ using System.Runtime.Serialization;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the supported operation plan modes.
|
||||
/// </summary>
|
||||
public enum OperationMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Plan mode for stopping BizTalk runtime artifacts before maintenance.
|
||||
/// </summary>
|
||||
Shutdown,
|
||||
|
||||
/// <summary>
|
||||
/// Plan mode for returning BizTalk runtime artifacts to a captured state.
|
||||
/// </summary>
|
||||
Restore
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the artifact categories that can appear in an operation plan.
|
||||
/// </summary>
|
||||
public enum OperationStepKind
|
||||
{
|
||||
/// <summary>
|
||||
/// A receive location step.
|
||||
/// </summary>
|
||||
ReceiveLocation,
|
||||
|
||||
/// <summary>
|
||||
/// A send port step.
|
||||
/// </summary>
|
||||
SendPort,
|
||||
|
||||
/// <summary>
|
||||
/// An orchestration step.
|
||||
/// </summary>
|
||||
Orchestration,
|
||||
|
||||
/// <summary>
|
||||
/// A host instance step.
|
||||
/// </summary>
|
||||
HostInstance,
|
||||
|
||||
/// <summary>
|
||||
/// An informational step that is intentionally not executed.
|
||||
/// </summary>
|
||||
Note
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the ordered shutdown or restore plan written before any
|
||||
/// runtime-changing operation is executed.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class OperationPlan
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new operation plan with an empty step collection.
|
||||
/// </summary>
|
||||
public OperationPlan()
|
||||
{
|
||||
Steps = new List<OperationStep>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the plan mode as a serialized string.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local timestamp when the plan was created.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target server for server-scoped plan steps.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ordered operation steps.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public List<OperationStep> Steps { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes one executable or informational step in a shutdown or restore plan.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class OperationStep
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the artifact kind for this step.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Kind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application, when applicable.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the artifact or host instance name displayed to the user.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server associated with the step, when server scoped.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the human-readable action description.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WMI class used to resolve the runtime object.
|
||||
/// </summary>
|
||||
[DataMember(Order = 6)]
|
||||
public string WmiClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WMI key property used to locate the runtime object.
|
||||
/// </summary>
|
||||
[DataMember(Order = 7)]
|
||||
public string KeyProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WMI key value used to locate the runtime object.
|
||||
/// </summary>
|
||||
[DataMember(Order = 8)]
|
||||
public string KeyValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WMI method or service pseudo-method to execute.
|
||||
/// </summary>
|
||||
[DataMember(Order = 9)]
|
||||
public string MethodName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the numeric arguments passed to the WMI method.
|
||||
/// </summary>
|
||||
[DataMember(Order = 10)]
|
||||
public int[] Arguments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw WMI state expected after the method completes.
|
||||
/// </summary>
|
||||
[DataMember(Order = 11)]
|
||||
public int? TargetState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the step should be executed.
|
||||
/// </summary>
|
||||
[DataMember(Order = 12)]
|
||||
public bool Execute { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an operator-facing warning for skipped or risky steps.
|
||||
/// </summary>
|
||||
[DataMember(Order = 13)]
|
||||
public string Warning { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains user-selected runtime options for snapshot, shutdown and restore actions.
|
||||
/// </summary>
|
||||
public sealed class OperationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk server used for WMI operations.
|
||||
/// </summary>
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the directory where snapshots, plans, reports and diffs are written.
|
||||
/// </summary>
|
||||
public string OutputDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state file used as restore input.
|
||||
/// </summary>
|
||||
public string StateFile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether operations should only be logged.
|
||||
/// </summary>
|
||||
public bool DryRun { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of seconds to wait for a target runtime state.
|
||||
/// </summary>
|
||||
public int WaitTimeoutSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of seconds between WMI polling attempts.
|
||||
/// </summary>
|
||||
public int PollIntervalSeconds { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
using System;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using BizTalkPlatformManagementTool.Ui;
|
||||
|
||||
namespace BizTalkPlatformManagementTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the WinForms application entry point and startup guard checks.
|
||||
/// </summary>
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts the application after verifying that BizTalk WMI operations can run elevated.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
if (args != null && args.Length == 1 && string.Equals(args[0], "--self-test", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return RuntimeSelfTest.Run();
|
||||
}
|
||||
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
@@ -22,12 +34,32 @@ namespace BizTalkPlatformManagementTool
|
||||
"Administrator Rights Required",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool createdNew;
|
||||
using (var mutex = new Mutex(true, @"Local\BizTalkPlatformManagementTool", out createdNew))
|
||||
{
|
||||
if (!createdNew)
|
||||
{
|
||||
MessageBox.Show(
|
||||
"BizTalk Platform Management Tool is already running in this Windows session.",
|
||||
"BizTalk Platform Management Tool",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
return 2;
|
||||
}
|
||||
|
||||
Application.Run(new MainForm());
|
||||
GC.KeepAlive(mutex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the current Windows identity is a local administrator.
|
||||
/// </summary>
|
||||
/// <returns>True when the process is elevated as administrator; otherwise false.</returns>
|
||||
private static bool IsRunningAsAdministrator()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("BizTalk Platform Management Tool")]
|
||||
[assembly: AssemblyDescription("Controlled BizTalk Server maintenance snapshots, plans and runtime operations")]
|
||||
[assembly: AssemblyCompany("BEW")]
|
||||
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2026")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
|
||||
[assembly: AssemblyVersion("2.1.0.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.0.0")]
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using BizTalkPlatformManagementTool.Models;
|
||||
using BizTalkPlatformManagementTool.Services;
|
||||
|
||||
namespace BizTalkPlatformManagementTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a WMI-free smoke test used by the installer before and after activation.
|
||||
/// </summary>
|
||||
internal static class RuntimeSelfTest
|
||||
{
|
||||
public static int Run()
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool.SelfTest." + Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
var before = SampleSnapshot(ArtifactStates.SendPortStarted);
|
||||
var after = SampleSnapshot(ArtifactStates.SendPortStopped);
|
||||
var snapshotPath = Path.Combine(directory, "snapshot.json");
|
||||
JsonFileStore.Save(snapshotPath, before);
|
||||
var loaded = JsonFileStore.Load<BizTalkSnapshot>(snapshotPath);
|
||||
SnapshotValidator.Validate(loaded);
|
||||
|
||||
var diff = SnapshotComparer.Compare(loaded, after);
|
||||
if (diff.ArtifactDifferences.Count != 1)
|
||||
{
|
||||
throw new InvalidOperationException("Snapshot diff self-test returned an unexpected result.");
|
||||
}
|
||||
|
||||
SnapshotStore.SaveSnapshotSet(Path.Combine(directory, "before.json"), loaded);
|
||||
SnapshotStore.SaveDiffSet(Path.Combine(directory, "diff.json"), diff);
|
||||
Console.WriteLine("SELF_TEST_OK version=" + BizTalkOperationService.Version);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine("SELF_TEST_FAILED " + ex);
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(directory))
|
||||
{
|
||||
Directory.Delete(directory, true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// The self-test result is more important than temporary cleanup.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BizTalkSnapshot SampleSnapshot(int sendPortState)
|
||||
{
|
||||
var snapshot = new BizTalkSnapshot
|
||||
{
|
||||
ToolVersion = BizTalkOperationService.Version,
|
||||
CreatedAt = DateTimeOffset.Now.ToString("o"),
|
||||
Server = Environment.MachineName
|
||||
};
|
||||
var app = new ApplicationSnapshot { Application = "SelfTest" };
|
||||
app.SendPorts.Add(new SendPortState
|
||||
{
|
||||
Application = app.Application,
|
||||
Name = "SelfTest.SendPort",
|
||||
Status = sendPortState
|
||||
});
|
||||
snapshot.Applications.Add(app);
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,27 +7,60 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Coordinates BizTalk snapshot, plan, shutdown, restore and persistence operations.
|
||||
/// </summary>
|
||||
public sealed class BizTalkOperationService
|
||||
{
|
||||
public const string Version = "2.0.0-net461";
|
||||
/// <summary>
|
||||
/// Current tool version written into generated snapshots.
|
||||
/// </summary>
|
||||
public const string Version = "2.1.0-net461";
|
||||
|
||||
/// <summary>
|
||||
/// Fallback application name used when WMI does not expose an application property.
|
||||
/// </summary>
|
||||
private const string UnknownApplication = "(Unknown Application)";
|
||||
|
||||
/// <summary>
|
||||
/// Logger used for all operator-facing operation messages.
|
||||
/// </summary>
|
||||
private readonly OperationLogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new operation service.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger used for operator-visible progress and diagnostics.</param>
|
||||
public BizTalkOperationService(OperationLogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the BizTalk WMI namespace is reachable and readable.
|
||||
/// </summary>
|
||||
/// <param name="server">The BizTalk server or management host to query.</param>
|
||||
public void Diagnose(string server)
|
||||
{
|
||||
using (var client = CreateClient(server))
|
||||
{
|
||||
var ports = client.Query("MSBTS_SendPort");
|
||||
try
|
||||
{
|
||||
_logger.Success("WMI/CIM diagnostic succeeded. Send ports visible: " + ports.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisposeAll(ports);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the current BizTalk runtime state from WMI.
|
||||
/// </summary>
|
||||
/// <param name="server">The BizTalk server or management host to query.</param>
|
||||
/// <returns>A complete snapshot of supported BizTalk artifacts and host instances.</returns>
|
||||
public BizTalkSnapshot CreateSnapshot(string server)
|
||||
{
|
||||
using (var client = CreateClient(server))
|
||||
@@ -35,12 +68,15 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
var snapshot = new BizTalkSnapshot
|
||||
{
|
||||
ToolVersion = Version,
|
||||
CreatedAt = DateTime.Now.ToString("s"),
|
||||
CreatedAt = DateTimeOffset.Now.ToString("o"),
|
||||
Server = client.Server
|
||||
};
|
||||
|
||||
var apps = new Dictionary<string, ApplicationSnapshot>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var item in client.Query("MSBTS_ReceiveLocation"))
|
||||
var receiveLocations = client.Query("MSBTS_ReceiveLocation");
|
||||
try
|
||||
{
|
||||
foreach (var item in receiveLocations)
|
||||
{
|
||||
var appName = GetApplicationName(item);
|
||||
var app = GetApplication(apps, appName);
|
||||
@@ -54,8 +90,16 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
Address = BizTalkWmiClient.SafeGetString(item, "InboundTransportURL", string.Empty)
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisposeAll(receiveLocations);
|
||||
}
|
||||
|
||||
foreach (var item in client.Query("MSBTS_SendPort"))
|
||||
var sendPorts = client.Query("MSBTS_SendPort");
|
||||
try
|
||||
{
|
||||
foreach (var item in sendPorts)
|
||||
{
|
||||
var appName = GetApplicationName(item);
|
||||
var app = GetApplication(apps, appName);
|
||||
@@ -68,8 +112,16 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
PrimaryTransportAddress = BizTalkWmiClient.SafeGetString(item, "PTAddress", string.Empty)
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisposeAll(sendPorts);
|
||||
}
|
||||
|
||||
foreach (var item in client.Query("MSBTS_Orchestration"))
|
||||
var orchestrations = client.Query("MSBTS_Orchestration");
|
||||
try
|
||||
{
|
||||
foreach (var item in orchestrations)
|
||||
{
|
||||
var appName = GetApplicationName(item);
|
||||
var app = GetApplication(apps, appName);
|
||||
@@ -80,8 +132,16 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
OrchestrationStatus = BizTalkWmiClient.SafeGetInt32(item, "OrchestrationStatus", 0)
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisposeAll(orchestrations);
|
||||
}
|
||||
|
||||
foreach (var item in client.Query("MSBTS_HostInstance"))
|
||||
var hostInstances = client.Query("MSBTS_HostInstance");
|
||||
try
|
||||
{
|
||||
foreach (var item in hostInstances)
|
||||
{
|
||||
var state = BizTalkWmiClient.SafeGetInt32(item, "ServiceState", 0);
|
||||
snapshot.HostInstances.Add(new HostInstanceState
|
||||
@@ -93,6 +153,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
StateText = ArtifactStates.FormatHostInstance(state)
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisposeAll(hostInstances);
|
||||
}
|
||||
|
||||
foreach (var app in apps.Values.OrderBy(a => a.Application))
|
||||
{
|
||||
@@ -103,13 +168,21 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
snapshot.HostInstances = snapshot.HostInstances.OrderBy(x => x.Server).ThenBy(x => x.InstanceName).ToList();
|
||||
|
||||
SnapshotValidator.Validate(snapshot);
|
||||
_logger.Success("Snapshot created. Applications: " + snapshot.Applications.Count + ", host instances: " + snapshot.HostInstances.Count);
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ordered shutdown plan from a snapshot.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The runtime state used as the source for the plan.</param>
|
||||
/// <param name="server">The selected server on which host instance steps may execute.</param>
|
||||
/// <returns>An ordered shutdown plan.</returns>
|
||||
public OperationPlan CreateShutdownPlan(BizTalkSnapshot snapshot, string server)
|
||||
{
|
||||
SnapshotValidator.EnsureServerMatches(snapshot, server);
|
||||
var plan = NewPlan(OperationMode.Shutdown, server);
|
||||
|
||||
foreach (var app in snapshot.Applications)
|
||||
@@ -144,8 +217,15 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return plan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ordered restore plan from a previously captured snapshot.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The state that should be restored.</param>
|
||||
/// <param name="server">The selected server on which host instance steps may execute.</param>
|
||||
/// <returns>An ordered restore plan.</returns>
|
||||
public OperationPlan CreateRestorePlan(BizTalkSnapshot snapshot, string server)
|
||||
{
|
||||
SnapshotValidator.EnsureServerMatches(snapshot, server);
|
||||
var plan = NewPlan(OperationMode.Restore, server);
|
||||
|
||||
foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted))
|
||||
@@ -214,8 +294,21 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return plan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes an operation plan or logs each step when dry-run mode is enabled.
|
||||
/// </summary>
|
||||
/// <param name="plan">The ordered plan to execute.</param>
|
||||
/// <param name="options">The runtime options controlling server, dry-run and wait behavior.</param>
|
||||
public void ExecutePlan(OperationPlan plan, OperationOptions options)
|
||||
{
|
||||
if (plan == null || options == null)
|
||||
{
|
||||
throw new ArgumentNullException(plan == null ? "plan" : "options");
|
||||
}
|
||||
if (!SnapshotValidator.ServerNamesEqual(plan.Server, options.Server))
|
||||
{
|
||||
throw new InvalidOperationException("The operation plan targets server '" + plan.Server + "' but execution was requested for '" + options.Server + "'.");
|
||||
}
|
||||
using (var client = CreateClient(options.Server))
|
||||
{
|
||||
foreach (var step in plan.Steps)
|
||||
@@ -253,6 +346,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a snapshot and its report sidecars.
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">The directory where output files are written.</param>
|
||||
/// <param name="fileName">The primary JSON file name.</param>
|
||||
/// <param name="snapshot">The snapshot to persist.</param>
|
||||
/// <returns>The primary JSON file path.</returns>
|
||||
public string SaveSnapshot(string outputDirectory, string fileName, BizTalkSnapshot snapshot)
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
@@ -262,6 +362,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves an operation plan as JSON.
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">The directory where output files are written.</param>
|
||||
/// <param name="fileName">The plan JSON file name.</param>
|
||||
/// <param name="plan">The plan to persist.</param>
|
||||
/// <returns>The saved plan file path.</returns>
|
||||
public string SavePlan(string outputDirectory, string fileName, OperationPlan plan)
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
@@ -271,6 +378,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a diff and its report sidecars.
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">The directory where output files are written.</param>
|
||||
/// <param name="fileName">The primary diff JSON file name.</param>
|
||||
/// <param name="diff">The diff to persist.</param>
|
||||
/// <returns>The primary diff JSON file path.</returns>
|
||||
public string SaveDiff(string outputDirectory, string fileName, SnapshotDiff diff)
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
@@ -280,6 +394,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes one concrete WMI operation step and waits for its target state.
|
||||
/// </summary>
|
||||
/// <param name="client">The connected WMI client.</param>
|
||||
/// <param name="instance">The resolved WMI object for the step.</param>
|
||||
/// <param name="step">The operation step to execute.</param>
|
||||
/// <param name="options">The runtime options controlling wait behavior.</param>
|
||||
private void ExecuteStep(BizTalkWmiClient client, ManagementObject instance, OperationStep step, OperationOptions options)
|
||||
{
|
||||
if (string.Equals(step.MethodName, "StopOrEnlist", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -318,6 +439,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
WaitForTarget(client, step, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits until a WMI object reaches the target state described by a plan step.
|
||||
/// </summary>
|
||||
/// <param name="client">The connected WMI client.</param>
|
||||
/// <param name="step">The step whose target state should be verified.</param>
|
||||
/// <param name="options">The runtime options controlling timeout and polling interval.</param>
|
||||
private void WaitForTarget(BizTalkWmiClient client, OperationStep step, OperationOptions options)
|
||||
{
|
||||
if (!step.TargetState.HasValue)
|
||||
@@ -344,6 +471,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and connects a WMI client for a server.
|
||||
/// </summary>
|
||||
/// <param name="server">The BizTalk server or management host to connect to.</param>
|
||||
/// <returns>A connected WMI client.</returns>
|
||||
private BizTalkWmiClient CreateClient(string server)
|
||||
{
|
||||
var client = new BizTalkWmiClient(server, _logger);
|
||||
@@ -351,16 +483,37 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new empty operation plan with common metadata.
|
||||
/// </summary>
|
||||
/// <param name="mode">The operation mode represented by the plan.</param>
|
||||
/// <param name="server">The target server stored in the plan metadata.</param>
|
||||
/// <returns>A new operation plan.</returns>
|
||||
private static OperationPlan NewPlan(OperationMode mode, string server)
|
||||
{
|
||||
return new OperationPlan
|
||||
{
|
||||
Mode = mode.ToString(),
|
||||
CreatedAt = DateTime.Now.ToString("s"),
|
||||
CreatedAt = DateTimeOffset.Now.ToString("o"),
|
||||
Server = server
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates one executable operation step.
|
||||
/// </summary>
|
||||
/// <param name="kind">The artifact kind displayed and serialized for the step.</param>
|
||||
/// <param name="application">The owning BizTalk application, when applicable.</param>
|
||||
/// <param name="name">The artifact or host instance name.</param>
|
||||
/// <param name="server">The server associated with the step, when applicable.</param>
|
||||
/// <param name="action">The human-readable action text.</param>
|
||||
/// <param name="wmiClass">The WMI class used to resolve the target object.</param>
|
||||
/// <param name="keyProperty">The WMI key property used for lookup.</param>
|
||||
/// <param name="keyValue">The WMI key value used for lookup.</param>
|
||||
/// <param name="methodName">The WMI method or pseudo-method to execute.</param>
|
||||
/// <param name="arguments">Optional numeric WMI method arguments.</param>
|
||||
/// <param name="targetState">Optional raw WMI state expected after execution.</param>
|
||||
/// <returns>A configured operation step.</returns>
|
||||
private static OperationStep Step(string kind, string application, string name, string server, string action, string wmiClass, string keyProperty, string keyValue, string methodName, int[] arguments, int? targetState)
|
||||
{
|
||||
return new OperationStep
|
||||
@@ -380,6 +533,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts integer method arguments into the object array required by WMI.
|
||||
/// </summary>
|
||||
/// <param name="values">The integer values from the operation step.</param>
|
||||
/// <returns>An object array suitable for ManagementObject.InvokeMethod.</returns>
|
||||
private static object[] ToObjects(int[] values)
|
||||
{
|
||||
if (values == null || values.Length == 0)
|
||||
@@ -395,6 +553,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an operator-facing description for one plan step.
|
||||
/// </summary>
|
||||
/// <param name="step">The step to describe.</param>
|
||||
/// <returns>A compact step description for logs and errors.</returns>
|
||||
private static string DescribeStep(OperationStep step)
|
||||
{
|
||||
if (step == null)
|
||||
@@ -412,6 +575,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
+ "]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates an application snapshot bucket in a dictionary.
|
||||
/// </summary>
|
||||
/// <param name="apps">The application snapshot dictionary keyed by application name.</param>
|
||||
/// <param name="appName">The application name to resolve.</param>
|
||||
/// <returns>The existing or newly created application snapshot.</returns>
|
||||
private static ApplicationSnapshot GetApplication(Dictionary<string, ApplicationSnapshot> apps, string appName)
|
||||
{
|
||||
ApplicationSnapshot app;
|
||||
@@ -423,6 +592,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the application name from the available WMI properties.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object being mapped into a snapshot item.</param>
|
||||
/// <returns>The discovered application name or a stable fallback.</returns>
|
||||
private static string GetApplicationName(ManagementObject item)
|
||||
{
|
||||
var names = new[] { "ApplicationName", "Application", "BizTalkApplication" };
|
||||
@@ -436,5 +610,20 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
return UnknownApplication;
|
||||
}
|
||||
|
||||
private static void DisposeAll(IEnumerable<ManagementObject> items)
|
||||
{
|
||||
if (items == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
item.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,24 +6,53 @@ using System.Threading;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides the WMI access layer for BizTalk Server objects in root\MicrosoftBizTalkServer.
|
||||
/// </summary>
|
||||
public sealed class BizTalkWmiClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// BizTalk WMI namespace used for all platform management queries.
|
||||
/// </summary>
|
||||
private const string NamespacePath = "root\\MicrosoftBizTalkServer";
|
||||
|
||||
/// <summary>
|
||||
/// Target server for the WMI connection.
|
||||
/// </summary>
|
||||
private readonly string _server;
|
||||
|
||||
/// <summary>
|
||||
/// Logger used for WMI diagnostics and operation traces.
|
||||
/// </summary>
|
||||
private readonly OperationLogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Connected WMI management scope, created lazily or by Connect.
|
||||
/// </summary>
|
||||
private ManagementScope _scope;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new WMI client for the specified server.
|
||||
/// </summary>
|
||||
/// <param name="server">The target server name, or an empty value to use the local machine.</param>
|
||||
/// <param name="logger">The operation logger used for diagnostics and trace output.</param>
|
||||
public BizTalkWmiClient(string server, OperationLogger logger)
|
||||
{
|
||||
_server = string.IsNullOrWhiteSpace(server) ? Environment.MachineName : server.Trim();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the normalized target server name used by this client.
|
||||
/// </summary>
|
||||
public string Server
|
||||
{
|
||||
get { return _server; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the BizTalk WMI namespace on the target server.
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
var path = "\\\\" + _server + "\\" + NamespacePath;
|
||||
@@ -33,11 +62,22 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
_logger.Success("WMI connection established.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a broad WMI query for all instances of the requested BizTalk class.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <returns>The matching WMI objects. The caller owns the returned objects.</returns>
|
||||
public List<ManagementObject> Query(string className)
|
||||
{
|
||||
return Query(className, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a broad WMI query and optionally logs the query text.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <param name="logQuery">True to write the query to the operation log.</param>
|
||||
/// <returns>The matching WMI objects. The caller owns the returned objects.</returns>
|
||||
private List<ManagementObject> Query(string className, bool logQuery)
|
||||
{
|
||||
EnsureConnected();
|
||||
@@ -63,17 +103,37 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
catch (ManagementException ex)
|
||||
{
|
||||
foreach (var item in result)
|
||||
{
|
||||
item.Dispose();
|
||||
}
|
||||
throw new InvalidOperationException("WMI query failed. Query: " + queryText + ". WMI error: " + ex.Message, ex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds one WMI object by comparing a property value client-side.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <param name="propertyName">The property used as the lookup key.</param>
|
||||
/// <param name="value">The expected property value.</param>
|
||||
/// <returns>The matching object, or null when no object matches.</returns>
|
||||
public ManagementObject FindByProperty(string className, string propertyName, string value)
|
||||
{
|
||||
return FindByProperty(className, propertyName, value, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds one WMI object using SELECT * plus a client-side filter so special
|
||||
/// characters in BizTalk names cannot break a WQL WHERE clause.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <param name="propertyName">The property used as the lookup key.</param>
|
||||
/// <param name="value">The expected property value.</param>
|
||||
/// <param name="logLookup">True to write the lookup details to the operation log.</param>
|
||||
/// <returns>The matching object, or null when no object matches.</returns>
|
||||
private ManagementObject FindByProperty(string className, string propertyName, string value, bool logLookup)
|
||||
{
|
||||
EnsureConnected();
|
||||
@@ -118,6 +178,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a WMI method and validates that the method returned success.
|
||||
/// </summary>
|
||||
/// <param name="instance">The WMI object on which the method should be called.</param>
|
||||
/// <param name="methodName">The method name to invoke.</param>
|
||||
/// <param name="arguments">Optional method arguments.</param>
|
||||
/// <returns>The WMI return code.</returns>
|
||||
public uint InvokeMethod(ManagementObject instance, string methodName, params object[] arguments)
|
||||
{
|
||||
if (instance == null)
|
||||
@@ -138,6 +205,9 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
throw new InvalidOperationException("WMI method failed. Class: " + instance.Path.ClassName + ", method: " + methodToCall + ", object: " + SafeObjectName(instance) + ". WMI error: " + ex.Message, ex);
|
||||
}
|
||||
|
||||
var output = result as ManagementBaseObject;
|
||||
try
|
||||
{
|
||||
var returnCode = ExtractReturnCode(result);
|
||||
if (returnCode != 0)
|
||||
{
|
||||
@@ -146,7 +216,25 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
|
||||
return returnCode;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (output != null)
|
||||
{
|
||||
output.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polls one WMI object until it reaches the expected state or times out.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <param name="keyProperty">The WMI key property used to find the object.</param>
|
||||
/// <param name="keyValue">The WMI key value used to find the object.</param>
|
||||
/// <param name="isReached">Predicate that returns true when the state is reached.</param>
|
||||
/// <param name="description">Human-readable state description used in logs and errors.</param>
|
||||
/// <param name="timeoutSeconds">Maximum number of seconds to wait.</param>
|
||||
/// <param name="pollIntervalSeconds">Number of seconds between polling attempts.</param>
|
||||
public void WaitForState(string className, string keyProperty, string keyValue, Func<ManagementObject, bool> isReached, string description, int timeoutSeconds, int pollIntervalSeconds)
|
||||
{
|
||||
var deadline = DateTime.UtcNow.AddSeconds(Math.Max(1, timeoutSeconds));
|
||||
@@ -163,12 +251,24 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
Thread.Sleep(TimeSpan.FromSeconds(delay));
|
||||
var remaining = deadline - DateTime.UtcNow;
|
||||
if (remaining <= TimeSpan.Zero)
|
||||
{
|
||||
break;
|
||||
}
|
||||
Thread.Sleep(remaining < TimeSpan.FromSeconds(delay) ? remaining : TimeSpan.FromSeconds(delay));
|
||||
}
|
||||
|
||||
throw new TimeoutException("Timeout while waiting for " + description + " [" + className + "." + keyProperty + "=" + keyValue + "]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a WMI property as a string without failing on missing or invalid properties.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object or output parameter object.</param>
|
||||
/// <param name="propertyName">The property to read.</param>
|
||||
/// <param name="fallback">The value returned when the property cannot be read.</param>
|
||||
/// <returns>The property value or the fallback value.</returns>
|
||||
public static string SafeGetString(ManagementBaseObject item, string propertyName, string fallback)
|
||||
{
|
||||
try
|
||||
@@ -187,6 +287,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a WMI property as an integer without failing on missing or invalid properties.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object or output parameter object.</param>
|
||||
/// <param name="propertyName">The property to read.</param>
|
||||
/// <param name="fallback">The value returned when the property cannot be read.</param>
|
||||
/// <returns>The property value or the fallback value.</returns>
|
||||
public static int SafeGetInt32(ManagementBaseObject item, string propertyName, int fallback)
|
||||
{
|
||||
try
|
||||
@@ -205,6 +312,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a WMI property as a Boolean without failing on missing or invalid properties.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object or output parameter object.</param>
|
||||
/// <param name="propertyName">The property to read.</param>
|
||||
/// <param name="fallback">The value returned when the property cannot be read.</param>
|
||||
/// <returns>The property value or the fallback value.</returns>
|
||||
public static bool SafeGetBoolean(ManagementBaseObject item, string propertyName, bool fallback)
|
||||
{
|
||||
try
|
||||
@@ -223,6 +337,9 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the management scope is connected before a WMI operation runs.
|
||||
/// </summary>
|
||||
private void EnsureConnected()
|
||||
{
|
||||
if (_scope == null || !_scope.IsConnected)
|
||||
@@ -231,6 +348,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a WMI object exposes a property, using case-insensitive comparison.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object to inspect.</param>
|
||||
/// <param name="propertyName">The property name to find.</param>
|
||||
/// <returns>True when the property exists; otherwise false.</returns>
|
||||
private static bool HasProperty(ManagementBaseObject item, string propertyName)
|
||||
{
|
||||
foreach (PropertyData property in item.Properties)
|
||||
@@ -244,6 +367,14 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a WMI object matches a requested key value.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object to inspect.</param>
|
||||
/// <param name="className">The WMI class name of the object.</param>
|
||||
/// <param name="propertyName">The requested key property.</param>
|
||||
/// <param name="expectedValue">The expected key value.</param>
|
||||
/// <returns>True when the object matches the requested value.</returns>
|
||||
private static bool MatchesProperty(ManagementBaseObject item, string className, string propertyName, string expectedValue)
|
||||
{
|
||||
foreach (var candidate in CandidatePropertyNames(className, propertyName))
|
||||
@@ -263,6 +394,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the primary and compatibility property names for a WMI lookup.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name being queried.</param>
|
||||
/// <param name="propertyName">The requested key property.</param>
|
||||
/// <returns>The candidate property names to inspect.</returns>
|
||||
private static IEnumerable<string> CandidatePropertyNames(string className, string propertyName)
|
||||
{
|
||||
yield return propertyName;
|
||||
@@ -274,6 +411,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a readable object name for diagnostics without letting WMI metadata failures escape.
|
||||
/// </summary>
|
||||
/// <param name="instance">The WMI object being described.</param>
|
||||
/// <returns>A relative WMI path or fallback object name.</returns>
|
||||
private static string SafeObjectName(ManagementObject instance)
|
||||
{
|
||||
try
|
||||
@@ -286,6 +428,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the exact method casing exposed by the WMI class.
|
||||
/// </summary>
|
||||
/// <param name="instance">The WMI object whose class should be inspected.</param>
|
||||
/// <param name="requestedName">The requested method name.</param>
|
||||
/// <returns>The method name as exposed by WMI.</returns>
|
||||
private static string ResolveMethodName(ManagementObject instance, string requestedName)
|
||||
{
|
||||
using (var managementClass = new ManagementClass(instance.Scope, new ManagementPath(instance.Path.ClassName), null))
|
||||
@@ -302,6 +450,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
throw new MissingMethodException(instance.Path.ClassName, requestedName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the WMI return code from either a scalar return value or output parameters.
|
||||
/// </summary>
|
||||
/// <param name="result">The object returned by ManagementObject.InvokeMethod.</param>
|
||||
/// <returns>The numeric WMI return code.</returns>
|
||||
private static uint ExtractReturnCode(object result)
|
||||
{
|
||||
if (result == null)
|
||||
@@ -318,8 +471,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return Convert.ToUInt32(result, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases resources owned by this client.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_scope = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,16 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes snapshot and diff data to CSV files for review outside the GUI.
|
||||
/// </summary>
|
||||
public static class CsvWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes application artifact states from a snapshot to a CSV file.
|
||||
/// </summary>
|
||||
/// <param name="path">The target CSV file path.</param>
|
||||
/// <param name="snapshot">The snapshot whose artifact states should be exported.</param>
|
||||
public static void WriteSnapshotArtifacts(string path, BizTalkSnapshot snapshot)
|
||||
{
|
||||
var lines = new List<string> { "Application,Type,Name,Status" };
|
||||
@@ -28,6 +36,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
File.WriteAllLines(path, lines, Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes host instance states from a snapshot to a CSV file.
|
||||
/// </summary>
|
||||
/// <param name="path">The target CSV file path.</param>
|
||||
/// <param name="snapshot">The snapshot whose host instance states should be exported.</param>
|
||||
public static void WriteSnapshotHosts(string path, BizTalkSnapshot snapshot)
|
||||
{
|
||||
var lines = new List<string> { "InstanceName,HostName,Server,State" };
|
||||
@@ -38,6 +51,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
File.WriteAllLines(path, lines, Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes snapshot differences to a CSV file.
|
||||
/// </summary>
|
||||
/// <param name="path">The target CSV file path.</param>
|
||||
/// <param name="diff">The diff model to export.</param>
|
||||
public static void WriteDiff(string path, SnapshotDiff diff)
|
||||
{
|
||||
var lines = new List<string> { "Scope,Application,Type,Name,Server,Before,After" };
|
||||
@@ -52,6 +70,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
File.WriteAllLines(path, lines, Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds one CSV row from already ordered field values.
|
||||
/// </summary>
|
||||
/// <param name="values">The values that should be escaped and joined.</param>
|
||||
/// <returns>A single CSV row.</returns>
|
||||
private static string Row(params string[] values)
|
||||
{
|
||||
var escaped = new string[values.Length];
|
||||
@@ -62,9 +85,18 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return string.Join(",", escaped);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes a CSV field when it contains separators, quotes or line breaks.
|
||||
/// </summary>
|
||||
/// <param name="value">The raw field value.</param>
|
||||
/// <returns>The CSV-safe field value.</returns>
|
||||
private static string Escape(string value)
|
||||
{
|
||||
value = value ?? string.Empty;
|
||||
if (value.Length > 0 && (value[0] == '=' || value[0] == '+' || value[0] == '-' || value[0] == '@' || value[0] == '\t'))
|
||||
{
|
||||
value = "'" + value;
|
||||
}
|
||||
if (value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) < 0)
|
||||
{
|
||||
return value;
|
||||
|
||||
@@ -4,8 +4,16 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes HTML reports for BizTalk snapshots and snapshot differences.
|
||||
/// </summary>
|
||||
public static class HtmlReportWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes a complete HTML snapshot report.
|
||||
/// </summary>
|
||||
/// <param name="path">The target HTML file path.</param>
|
||||
/// <param name="snapshot">The snapshot to render.</param>
|
||||
public static void WriteSnapshot(string path, BizTalkSnapshot snapshot)
|
||||
{
|
||||
var html = new StringBuilder();
|
||||
@@ -50,6 +58,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
System.IO.File.WriteAllText(path, html.ToString(), Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a complete HTML diff report.
|
||||
/// </summary>
|
||||
/// <param name="path">The target HTML file path.</param>
|
||||
/// <param name="diff">The diff model to render.</param>
|
||||
public static void WriteDiff(string path, SnapshotDiff diff)
|
||||
{
|
||||
var html = new StringBuilder();
|
||||
@@ -74,12 +87,26 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
System.IO.File.WriteAllText(path, html.ToString(), Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one artifact status row to a report table.
|
||||
/// </summary>
|
||||
/// <param name="html">The report builder receiving the row markup.</param>
|
||||
/// <param name="name">The artifact name.</param>
|
||||
/// <param name="status">The formatted artifact status.</param>
|
||||
/// <param name="ok">True when the row should use the positive status style.</param>
|
||||
/// <param name="detail1">The first detail column value.</param>
|
||||
/// <param name="detail2">The second detail column value.</param>
|
||||
private static void StatusRow(StringBuilder html, string name, string status, bool ok, string detail1, string detail2)
|
||||
{
|
||||
html.Append("<tr><td>").Append(Encode(name)).Append("</td><td class='").Append(ok ? "ok" : "bad").Append("'>").Append(Encode(status))
|
||||
.Append("</td><td>").Append(Encode(detail1)).Append("</td><td>").Append(Encode(detail2)).Append("</td></tr>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the common document header, style block and title.
|
||||
/// </summary>
|
||||
/// <param name="html">The report builder receiving the header markup.</param>
|
||||
/// <param name="title">The document title and main heading.</param>
|
||||
private static void Header(StringBuilder html, string title)
|
||||
{
|
||||
html.Append("<!doctype html><html><head><meta charset='utf-8'><title>").Append(Encode(title)).Append("</title><style>")
|
||||
@@ -87,11 +114,20 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
.Append("</style></head><body><h1>").Append(Encode(title)).Append("</h1>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the common HTML document footer.
|
||||
/// </summary>
|
||||
/// <param name="html">The report builder receiving the footer markup.</param>
|
||||
private static void Footer(StringBuilder html)
|
||||
{
|
||||
html.Append("</body></html>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTML-encodes report values and treats null values as empty text.
|
||||
/// </summary>
|
||||
/// <param name="value">The raw value to encode.</param>
|
||||
/// <returns>An HTML-safe value.</returns>
|
||||
private static string Encode(string value)
|
||||
{
|
||||
return WebUtility.HtmlEncode(value ?? string.Empty);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Json;
|
||||
@@ -5,13 +6,32 @@ using System.Text;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Persists DataContract models as JSON with repository-defined encoding rules.
|
||||
/// </summary>
|
||||
public static class JsonFileStore
|
||||
{
|
||||
/// <summary>
|
||||
/// UTF-8 encoding instance that writes JSON without a byte order mark.
|
||||
/// </summary>
|
||||
private static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a value to a UTF-8 JSON file without a byte order mark.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The model type to serialize.</typeparam>
|
||||
/// <param name="path">The target JSON file path.</param>
|
||||
/// <param name="value">The value to serialize.</param>
|
||||
public static void Save<T>(string path, T value)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path)));
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentException("A JSON target path is required.", "path");
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
Directory.CreateDirectory(directory);
|
||||
var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
|
||||
{
|
||||
UseSimpleDictionaryFormat = true
|
||||
@@ -21,10 +41,16 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
serializer.WriteObject(stream, value);
|
||||
var json = Utf8NoBom.GetString(stream.ToArray());
|
||||
File.WriteAllText(path, json, Utf8NoBom);
|
||||
WriteAtomically(fullPath, json);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a JSON file into the requested DataContract model type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The model type to deserialize.</typeparam>
|
||||
/// <param name="path">The JSON file path to load.</param>
|
||||
/// <returns>The deserialized model.</returns>
|
||||
public static T Load<T>(string path)
|
||||
{
|
||||
var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
|
||||
@@ -46,6 +72,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes byte order mark variants that may exist in previously written files.
|
||||
/// </summary>
|
||||
/// <param name="json">The raw JSON text read from disk.</param>
|
||||
/// <returns>The JSON text without a leading BOM marker.</returns>
|
||||
private static string NormalizeJson(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
@@ -66,6 +97,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return json;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a short diagnostic message for a JSON deserialization failure.
|
||||
/// </summary>
|
||||
/// <param name="json">The normalized JSON text that failed to deserialize.</param>
|
||||
/// <returns>A diagnostic suffix describing the beginning of the JSON content.</returns>
|
||||
private static string DescribeJsonStart(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
@@ -82,5 +118,75 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
|
||||
return "The file starts with valid JSON syntax but could not be deserialized into the expected model.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a file through a same-directory temporary file so an interrupted
|
||||
/// save cannot leave a truncated snapshot or operation plan behind.
|
||||
/// </summary>
|
||||
private static void WriteAtomically(string path, string content)
|
||||
{
|
||||
var temporaryPath = path + ".tmp." + Guid.NewGuid().ToString("N");
|
||||
var backupPath = path + ".bak." + Guid.NewGuid().ToString("N");
|
||||
try
|
||||
{
|
||||
File.WriteAllText(temporaryPath, content, Utf8NoBom);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
File.Move(temporaryPath, path);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.Replace(temporaryPath, path, backupPath, true);
|
||||
TryDelete(backupPath);
|
||||
}
|
||||
catch (PlatformNotSupportedException)
|
||||
{
|
||||
ReplaceWithRenameFallback(path, temporaryPath, backupPath);
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
ReplaceWithRenameFallback(path, temporaryPath, backupPath);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(temporaryPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReplaceWithRenameFallback(string path, string temporaryPath, string backupPath)
|
||||
{
|
||||
File.Move(path, backupPath);
|
||||
try
|
||||
{
|
||||
File.Move(temporaryPath, path);
|
||||
TryDelete(backupPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (!File.Exists(path) && File.Exists(backupPath))
|
||||
{
|
||||
File.Move(backupPath, path);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDelete(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Temporary cleanup is best-effort and must not hide the save result.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,39 +5,107 @@ using System.Threading;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the severity used for visible and file-based operation log entries.
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Informational progress or diagnostic entry.
|
||||
/// </summary>
|
||||
Info,
|
||||
|
||||
/// <summary>
|
||||
/// Non-fatal condition that needs operator attention.
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// Failed operation or exception entry.
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// Successful operation entry.
|
||||
/// </summary>
|
||||
Success
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one operation log entry displayed in the GUI and written to disk.
|
||||
/// </summary>
|
||||
public sealed class LogEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the local time when the entry was created.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the entry severity.
|
||||
/// </summary>
|
||||
public LogLevel Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the operator-facing message.
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes operation log entries to a daily rolling file and an optional UI sink.
|
||||
/// </summary>
|
||||
public sealed class OperationLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Prefix used for daily log files written to the resolved log directory.
|
||||
/// </summary>
|
||||
private const string LogFilePrefix = "BizTalkPlatformManagementTool-";
|
||||
|
||||
/// <summary>
|
||||
/// File extension used for operation log files.
|
||||
/// </summary>
|
||||
private const string LogFileExtension = ".log";
|
||||
|
||||
/// <summary>
|
||||
/// Number of daily log files retained, including the current day.
|
||||
/// </summary>
|
||||
private const int RetentionDays = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide lock that serializes log file appends.
|
||||
/// </summary>
|
||||
private static readonly object FileLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide flag that ensures log cleanup runs only once.
|
||||
/// </summary>
|
||||
private static int _cleanupDone;
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback for forwarding entries to the UI.
|
||||
/// </summary>
|
||||
private readonly Action<LogEntry> _sink;
|
||||
|
||||
/// <summary>
|
||||
/// Directory where daily log files are written.
|
||||
/// </summary>
|
||||
private readonly string _logDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new logger that writes beside the executable.
|
||||
/// </summary>
|
||||
/// <param name="sink">Optional callback that receives entries for display.</param>
|
||||
public OperationLogger(Action<LogEntry> sink)
|
||||
{
|
||||
_sink = sink;
|
||||
_logDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
_logDirectory = ResolveLogDirectory();
|
||||
CleanupOldLogs();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the daily log file for the current date.
|
||||
/// </summary>
|
||||
public string LogFilePath
|
||||
{
|
||||
get
|
||||
@@ -46,26 +114,47 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an informational entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public void Info(string message)
|
||||
{
|
||||
Write(LogLevel.Info, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a warning entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public void Warning(string message)
|
||||
{
|
||||
Write(LogLevel.Warning, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an error entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public void Error(string message)
|
||||
{
|
||||
Write(LogLevel.Error, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a success entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public void Success(string message)
|
||||
{
|
||||
Write(LogLevel.Success, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a log entry and sends it to both output targets.
|
||||
/// </summary>
|
||||
/// <param name="level">The entry severity.</param>
|
||||
/// <param name="message">The message to log.</param>
|
||||
private void Write(LogLevel level, string message)
|
||||
{
|
||||
var entry = new LogEntry
|
||||
@@ -85,6 +174,10 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
_sink(entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one entry to the current daily log file.
|
||||
/// </summary>
|
||||
/// <param name="entry">The entry to write.</param>
|
||||
private void WriteToFile(LogEntry entry)
|
||||
{
|
||||
try
|
||||
@@ -108,6 +201,9 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes log files older than the configured retention window.
|
||||
/// </summary>
|
||||
private void CleanupOldLogs()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _cleanupDone, 1) == 1)
|
||||
@@ -132,5 +228,20 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
// Log retention cleanup is best-effort.
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveLogDirectory()
|
||||
{
|
||||
var commonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||
var preferred = Path.Combine(commonData, "BizTalkPlatformManagementTool", "Logs");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(preferred);
|
||||
return preferred;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return AppDomain.CurrentDomain.BaseDirectory;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,26 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Compares two BizTalk snapshots and returns the state differences that matter
|
||||
/// after a maintenance window.
|
||||
/// </summary>
|
||||
public static class SnapshotComparer
|
||||
{
|
||||
/// <summary>
|
||||
/// Compares all supported artifact and host instance states.
|
||||
/// </summary>
|
||||
/// <param name="before">The snapshot captured before maintenance.</param>
|
||||
/// <param name="after">The snapshot captured after maintenance.</param>
|
||||
/// <returns>A diff containing changed, new and missing artifacts.</returns>
|
||||
public static SnapshotDiff Compare(BizTalkSnapshot before, BizTalkSnapshot after)
|
||||
{
|
||||
SnapshotValidator.Validate(before);
|
||||
SnapshotValidator.Validate(after);
|
||||
if (!SnapshotValidator.ServerNamesEqual(before.Server, after.Server))
|
||||
{
|
||||
throw new InvalidOperationException("Snapshots from different servers cannot be compared: '" + before.Server + "' and '" + after.Server + "'.");
|
||||
}
|
||||
var diff = new SnapshotDiff();
|
||||
|
||||
CompareReceiveLocations(diff, FlattenReceiveLocations(before), FlattenReceiveLocations(after));
|
||||
@@ -18,6 +34,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return diff;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds receive location differences to the shared diff model.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entries.</param>
|
||||
/// <param name="before">Receive locations keyed by name from the before snapshot.</param>
|
||||
/// <param name="after">Receive locations keyed by name from the after snapshot.</param>
|
||||
private static void CompareReceiveLocations(SnapshotDiff diff, Dictionary<string, ReceiveLocationState> before, Dictionary<string, ReceiveLocationState> after)
|
||||
{
|
||||
foreach (var pair in after)
|
||||
@@ -42,6 +64,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds send port differences to the shared diff model.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entries.</param>
|
||||
/// <param name="before">Send ports keyed by name from the before snapshot.</param>
|
||||
/// <param name="after">Send ports keyed by name from the after snapshot.</param>
|
||||
private static void CompareSendPorts(SnapshotDiff diff, Dictionary<string, SendPortState> before, Dictionary<string, SendPortState> after)
|
||||
{
|
||||
foreach (var pair in after)
|
||||
@@ -66,6 +94,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds orchestration differences to the shared diff model.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entries.</param>
|
||||
/// <param name="before">Orchestrations keyed by name from the before snapshot.</param>
|
||||
/// <param name="after">Orchestrations keyed by name from the after snapshot.</param>
|
||||
private static void CompareOrchestrations(SnapshotDiff diff, Dictionary<string, OrchestrationState> before, Dictionary<string, OrchestrationState> after)
|
||||
{
|
||||
foreach (var pair in after)
|
||||
@@ -90,6 +124,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds host instance differences to the shared diff model.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entries.</param>
|
||||
/// <param name="before">Host instances from the before snapshot.</param>
|
||||
/// <param name="after">Host instances from the after snapshot.</param>
|
||||
private static void CompareHostInstances(SnapshotDiff diff, List<HostInstanceState> before, List<HostInstanceState> after)
|
||||
{
|
||||
var beforeMap = new Dictionary<string, HostInstanceState>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -97,11 +137,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
|
||||
foreach (var item in before)
|
||||
{
|
||||
beforeMap[item.InstanceName ?? string.Empty] = item;
|
||||
beforeMap[SnapshotValidator.ArtifactKey(item.Server, item.InstanceName)] = item;
|
||||
}
|
||||
foreach (var item in after)
|
||||
{
|
||||
afterMap[item.InstanceName ?? string.Empty] = item;
|
||||
afterMap[SnapshotValidator.ArtifactKey(item.Server, item.InstanceName)] = item;
|
||||
}
|
||||
|
||||
foreach (var pair in afterMap)
|
||||
@@ -126,6 +166,15 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds one artifact diff entry.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entry.</param>
|
||||
/// <param name="application">The BizTalk application name.</param>
|
||||
/// <param name="type">The artifact type.</param>
|
||||
/// <param name="name">The artifact name.</param>
|
||||
/// <param name="before">The formatted before state.</param>
|
||||
/// <param name="after">The formatted after state.</param>
|
||||
private static void AddArtifact(SnapshotDiff diff, string application, string type, string name, string before, string after)
|
||||
{
|
||||
diff.ArtifactDifferences.Add(new ArtifactDiffEntry
|
||||
@@ -138,6 +187,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flattens receive locations from all applications into a case-insensitive name map.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The snapshot to flatten.</param>
|
||||
/// <returns>A map keyed by receive location name.</returns>
|
||||
private static Dictionary<string, ReceiveLocationState> FlattenReceiveLocations(BizTalkSnapshot snapshot)
|
||||
{
|
||||
var map = new Dictionary<string, ReceiveLocationState>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -145,12 +199,17 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
foreach (var item in app.ReceiveLocations)
|
||||
{
|
||||
map[item.Name ?? string.Empty] = item;
|
||||
map[SnapshotValidator.ArtifactKey(app.Application, item.Name)] = item;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flattens send ports from all applications into a case-insensitive name map.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The snapshot to flatten.</param>
|
||||
/// <returns>A map keyed by send port name.</returns>
|
||||
private static Dictionary<string, SendPortState> FlattenSendPorts(BizTalkSnapshot snapshot)
|
||||
{
|
||||
var map = new Dictionary<string, SendPortState>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -158,12 +217,17 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
foreach (var item in app.SendPorts)
|
||||
{
|
||||
map[item.Name ?? string.Empty] = item;
|
||||
map[SnapshotValidator.ArtifactKey(app.Application, item.Name)] = item;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flattens orchestrations from all applications into a case-insensitive name map.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The snapshot to flatten.</param>
|
||||
/// <returns>A map keyed by orchestration name.</returns>
|
||||
private static Dictionary<string, OrchestrationState> FlattenOrchestrations(BizTalkSnapshot snapshot)
|
||||
{
|
||||
var map = new Dictionary<string, OrchestrationState>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -171,7 +235,7 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
foreach (var item in app.Orchestrations)
|
||||
{
|
||||
map[item.Name ?? string.Empty] = item;
|
||||
map[SnapshotValidator.ArtifactKey(app.Application, item.Name)] = item;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
|
||||
@@ -3,8 +3,16 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves snapshot and diff models together with their sidecar report formats.
|
||||
/// </summary>
|
||||
public static class SnapshotStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves a snapshot as JSON and creates CSV, host CSV and HTML sidecars.
|
||||
/// </summary>
|
||||
/// <param name="jsonPath">The primary JSON output path.</param>
|
||||
/// <param name="snapshot">The snapshot to persist.</param>
|
||||
public static void SaveSnapshotSet(string jsonPath, BizTalkSnapshot snapshot)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath)));
|
||||
@@ -14,6 +22,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
HtmlReportWriter.WriteSnapshot(jsonPath + ".html", snapshot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a diff as JSON and creates CSV and HTML sidecars.
|
||||
/// </summary>
|
||||
/// <param name="jsonPath">The primary JSON output path.</param>
|
||||
/// <param name="diff">The diff model to persist.</param>
|
||||
public static void SaveDiffSet(string jsonPath, SnapshotDiff diff)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath)));
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Normalizes deserialized legacy snapshots and rejects ambiguous or unsafe input.
|
||||
/// </summary>
|
||||
public static class SnapshotValidator
|
||||
{
|
||||
/// <summary>Normalizes optional collections and rejects missing or duplicate artifact identities.</summary>
|
||||
public static void Validate(BizTalkSnapshot snapshot)
|
||||
{
|
||||
if (snapshot == null)
|
||||
{
|
||||
throw new InvalidOperationException("The snapshot is empty.");
|
||||
}
|
||||
|
||||
snapshot.Applications = snapshot.Applications ?? new List<ApplicationSnapshot>();
|
||||
snapshot.HostInstances = snapshot.HostInstances ?? new List<HostInstanceState>();
|
||||
|
||||
var receiveLocations = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var sendPorts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var orchestrations = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var app in snapshot.Applications)
|
||||
{
|
||||
if (app == null || string.IsNullOrWhiteSpace(app.Application))
|
||||
{
|
||||
throw new InvalidOperationException("The snapshot contains an application without a name.");
|
||||
}
|
||||
|
||||
app.ReceiveLocations = app.ReceiveLocations ?? new List<ReceiveLocationState>();
|
||||
app.SendPorts = app.SendPorts ?? new List<SendPortState>();
|
||||
app.Orchestrations = app.Orchestrations ?? new List<OrchestrationState>();
|
||||
|
||||
ValidateArtifacts(app.Application, "receive location", app.ReceiveLocations, x => x == null ? null : x.Name, receiveLocations);
|
||||
ValidateArtifacts(app.Application, "send port", app.SendPorts, x => x == null ? null : x.Name, sendPorts);
|
||||
ValidateArtifacts(app.Application, "orchestration", app.Orchestrations, x => x == null ? null : x.Name, orchestrations);
|
||||
}
|
||||
|
||||
var hostInstances = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var host in snapshot.HostInstances)
|
||||
{
|
||||
if (host == null || string.IsNullOrWhiteSpace(host.InstanceName))
|
||||
{
|
||||
throw new InvalidOperationException("The snapshot contains a host instance without an instance name.");
|
||||
}
|
||||
if (!hostInstances.Add(host.InstanceName))
|
||||
{
|
||||
throw new InvalidOperationException("The snapshot contains the host instance more than once: " + host.InstanceName);
|
||||
}
|
||||
host.StateText = ArtifactStates.FormatHostInstance(host.RawState);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Validates a snapshot and ensures it belongs to the requested operation server.</summary>
|
||||
public static void EnsureServerMatches(BizTalkSnapshot snapshot, string targetServer)
|
||||
{
|
||||
Validate(snapshot);
|
||||
if (string.IsNullOrWhiteSpace(snapshot.Server) || string.IsNullOrWhiteSpace(targetServer))
|
||||
{
|
||||
throw new InvalidOperationException("Snapshot server and target server must both be specified before a restore plan can be created.");
|
||||
}
|
||||
if (!ServerNamesEqual(snapshot.Server, targetServer))
|
||||
{
|
||||
throw new InvalidOperationException("The snapshot belongs to server '" + snapshot.Server + "' but the selected restore target is '" + targetServer + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Compares server names while accepting short-name/FQDN variants of the same host.</summary>
|
||||
public static bool ServerNamesEqual(string left, string right)
|
||||
{
|
||||
var normalizedLeft = NormalizeServer(left);
|
||||
var normalizedRight = NormalizeServer(right);
|
||||
if (string.Equals(normalizedLeft, normalizedRight, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return string.Equals(ShortName(normalizedLeft), ShortName(normalizedRight), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Builds the collision-safe identity used for application artifacts.</summary>
|
||||
public static string ArtifactKey(string application, string name)
|
||||
{
|
||||
return (application ?? string.Empty).Trim() + "\u001f" + (name ?? string.Empty).Trim();
|
||||
}
|
||||
|
||||
private static void ValidateArtifacts<T>(string application, string type, IEnumerable<T> values, Func<T, string> getName, HashSet<string> keys)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var name = getName(value);
|
||||
if (value == null || string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new InvalidOperationException("Application '" + application + "' contains a " + type + " without a name.");
|
||||
}
|
||||
var key = ArtifactKey(application, name);
|
||||
if (!keys.Add(key))
|
||||
{
|
||||
throw new InvalidOperationException("Application '" + application + "' contains the " + type + " more than once: " + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeServer(string value)
|
||||
{
|
||||
value = (value ?? string.Empty).Trim().TrimStart('\\');
|
||||
if (value == "." || string.Equals(value, "localhost", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Environment.MachineName;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string ShortName(string value)
|
||||
{
|
||||
var index = value.IndexOf('.');
|
||||
return index < 0 ? value : value.Substring(0, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,31 +9,125 @@ using BizTalkPlatformManagementTool.Services;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Ui
|
||||
{
|
||||
/// <summary>
|
||||
/// Main WinForms surface for diagnosing, snapshotting, comparing, shutting down
|
||||
/// and restoring a BizTalk platform state.
|
||||
/// </summary>
|
||||
public sealed class MainForm : Form
|
||||
{
|
||||
/// <summary>
|
||||
/// Text input for the BizTalk server or management host.
|
||||
/// </summary>
|
||||
private readonly TextBox _serverTextBox = new TextBox();
|
||||
|
||||
/// <summary>
|
||||
/// Text input for the directory that receives snapshots, plans and reports.
|
||||
/// </summary>
|
||||
private readonly TextBox _outputTextBox = new TextBox();
|
||||
|
||||
/// <summary>
|
||||
/// Text input for the restore state file name or path.
|
||||
/// </summary>
|
||||
private readonly TextBox _stateFileTextBox = new TextBox();
|
||||
|
||||
/// <summary>
|
||||
/// Numeric input for the maximum wait time of runtime state changes.
|
||||
/// </summary>
|
||||
private readonly NumericUpDown _timeoutInput = new NumericUpDown();
|
||||
|
||||
/// <summary>
|
||||
/// Numeric input for the WMI polling interval.
|
||||
/// </summary>
|
||||
private readonly NumericUpDown _pollInput = new NumericUpDown();
|
||||
|
||||
/// <summary>
|
||||
/// Checkbox that keeps shutdown and restore operations in dry-run mode.
|
||||
/// </summary>
|
||||
private readonly CheckBox _dryRunCheckBox = new CheckBox();
|
||||
|
||||
/// <summary>
|
||||
/// Grid used to show snapshots, diffs and operation plans.
|
||||
/// </summary>
|
||||
private readonly DataGridView _statusGrid = new DataGridView();
|
||||
|
||||
/// <summary>
|
||||
/// Grid used to show operation log entries.
|
||||
/// </summary>
|
||||
private readonly DataGridView _logGrid = new DataGridView();
|
||||
|
||||
/// <summary>
|
||||
/// Status strip at the bottom of the form.
|
||||
/// </summary>
|
||||
private readonly StatusStrip _statusStrip = new StatusStrip();
|
||||
|
||||
/// <summary>
|
||||
/// Text label inside the status strip.
|
||||
/// </summary>
|
||||
private readonly ToolStripStatusLabel _statusLabel = new ToolStripStatusLabel();
|
||||
|
||||
/// <summary>
|
||||
/// Header indicator derived from the latest host instance snapshot.
|
||||
/// </summary>
|
||||
private readonly Label _environmentStatusLabel = new Label();
|
||||
|
||||
/// <summary>
|
||||
/// Logger that writes to disk and mirrors entries into the UI log grid.
|
||||
/// </summary>
|
||||
private readonly OperationLogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Service that performs all BizTalk runtime operations.
|
||||
/// </summary>
|
||||
private readonly BizTalkOperationService _service;
|
||||
|
||||
/// <summary>
|
||||
/// Button that validates WMI access.
|
||||
/// </summary>
|
||||
private Button _diagnoseButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that creates the before snapshot.
|
||||
/// </summary>
|
||||
private Button _beforeButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that creates the after snapshot.
|
||||
/// </summary>
|
||||
private Button _afterButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that compares before and after snapshots.
|
||||
/// </summary>
|
||||
private Button _compareButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that creates and executes the shutdown plan.
|
||||
/// </summary>
|
||||
private Button _shutdownButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that creates and executes the restore plan.
|
||||
/// </summary>
|
||||
private Button _restoreButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that clears visible grids and status.
|
||||
/// </summary>
|
||||
private Button _clearButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that closes the application.
|
||||
/// </summary>
|
||||
private Button _closeButton;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a background operation is still active.
|
||||
/// </summary>
|
||||
private bool _isBusy;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the form, operation services and visual controls.
|
||||
/// </summary>
|
||||
public MainForm()
|
||||
{
|
||||
Text = "BizTalk Platform Management Tool";
|
||||
@@ -45,9 +139,13 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
_logger = new OperationLogger(AppendLog);
|
||||
_service = new BizTalkOperationService(_logger);
|
||||
BuildUi();
|
||||
FormClosing += MainFormClosing;
|
||||
_logger.Info("Log file: " + _logger.LogFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the root form layout and adds settings, action, result and status areas.
|
||||
/// </summary>
|
||||
private void BuildUi()
|
||||
{
|
||||
var root = new TableLayoutPanel
|
||||
@@ -72,6 +170,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
root.Controls.Add(_statusStrip, 0, 3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the settings panel with server, output, state file and timing inputs.
|
||||
/// </summary>
|
||||
/// <returns>The configured settings panel.</returns>
|
||||
private Control BuildSettingsPanel()
|
||||
{
|
||||
var panel = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 10, RowCount = 2 };
|
||||
@@ -140,6 +242,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
return panel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the toolbar-like action panel.
|
||||
/// </summary>
|
||||
/// <returns>The configured action panel.</returns>
|
||||
private Control BuildActionPanel()
|
||||
{
|
||||
var panel = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.LeftToRight, Padding = new Padding(0, 8, 0, 0), WrapContents = false };
|
||||
@@ -163,6 +269,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
return panel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the tab control containing status/results and operation logs.
|
||||
/// </summary>
|
||||
/// <returns>The configured tab control.</returns>
|
||||
private Control BuildTabs()
|
||||
{
|
||||
var tabs = new TabControl { Dock = DockStyle.Fill };
|
||||
@@ -189,12 +299,22 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
return tabs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Diagnose button click.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void DiagnoseClick(object sender, EventArgs e)
|
||||
{
|
||||
var server = _serverTextBox.Text.Trim();
|
||||
RunAsync("Diagnosing WMI access...", () => _service.Diagnose(server));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Snapshot Before button click.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void BeforeClick(object sender, EventArgs e)
|
||||
{
|
||||
var options = GetOptions();
|
||||
@@ -206,6 +326,11 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Snapshot After button click.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void AfterClick(object sender, EventArgs e)
|
||||
{
|
||||
var options = GetOptions();
|
||||
@@ -217,6 +342,11 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Compare button click.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void CompareClick(object sender, EventArgs e)
|
||||
{
|
||||
var options = GetOptions();
|
||||
@@ -230,21 +360,26 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Shutdown button click and runs the guarded shutdown workflow.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void ShutdownClick(object sender, EventArgs e)
|
||||
{
|
||||
if (!ConfirmDangerousAction("Shutdown"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var options = GetOptions();
|
||||
RunAsync("Preparing shutdown...", () =>
|
||||
{
|
||||
var snapshot = _service.CreateSnapshot(options.Server);
|
||||
_service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot);
|
||||
var plan = _service.CreateShutdownPlan(snapshot, options.Server);
|
||||
_service.SavePlan(options.OutputDirectory, "shutdown-plan.json", plan);
|
||||
var planPath = _service.SavePlan(options.OutputDirectory, "shutdown-plan.json", plan);
|
||||
ShowPlan(plan);
|
||||
if (!options.DryRun && !ConfirmPreparedPlan("Shutdown", plan, options.Server, planPath))
|
||||
{
|
||||
_logger.Warning("Shutdown cancelled after plan review. No runtime state was changed.");
|
||||
return;
|
||||
}
|
||||
_service.ExecutePlan(plan, options);
|
||||
if (!options.DryRun)
|
||||
{
|
||||
@@ -255,20 +390,25 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Restore button click and runs the guarded restore workflow.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void RestoreClick(object sender, EventArgs e)
|
||||
{
|
||||
if (!ConfirmDangerousAction("Restore"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var options = GetOptions();
|
||||
RunAsync("Preparing restore...", () =>
|
||||
{
|
||||
var snapshot = JsonFileStore.Load<BizTalkSnapshot>(ResolveStateFile(options));
|
||||
var plan = _service.CreateRestorePlan(snapshot, options.Server);
|
||||
_service.SavePlan(options.OutputDirectory, "restore-plan.json", plan);
|
||||
var planPath = _service.SavePlan(options.OutputDirectory, "restore-plan.json", plan);
|
||||
ShowPlan(plan);
|
||||
if (!options.DryRun && !ConfirmPreparedPlan("Restore", plan, options.Server, planPath))
|
||||
{
|
||||
_logger.Warning("Restore cancelled after plan review. No runtime state was changed.");
|
||||
return;
|
||||
}
|
||||
_service.ExecutePlan(plan, options);
|
||||
if (!options.DryRun)
|
||||
{
|
||||
@@ -279,6 +419,11 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Clear button click by removing visible state without deleting files.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void ClearClick(object sender, EventArgs e)
|
||||
{
|
||||
_statusGrid.Rows.Clear();
|
||||
@@ -287,11 +432,21 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
_statusLabel.Text = "Ready.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Close button click.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void CloseClick(object sender, EventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs long-running BizTalk work on a background task and keeps the UI responsive.
|
||||
/// </summary>
|
||||
/// <param name="status">The status text shown while the work is running.</param>
|
||||
/// <param name="work">The work to execute on the background task.</param>
|
||||
private void RunAsync(string status, Action work)
|
||||
{
|
||||
SetBusy(true, status);
|
||||
@@ -311,6 +466,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads and normalizes the current UI options.
|
||||
/// </summary>
|
||||
/// <returns>The runtime options selected by the user.</returns>
|
||||
private OperationOptions GetOptions()
|
||||
{
|
||||
var server = string.IsNullOrWhiteSpace(_serverTextBox.Text) ? Environment.MachineName : _serverTextBox.Text.Trim();
|
||||
@@ -330,27 +489,52 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a restore state file against the output directory when it is relative.
|
||||
/// </summary>
|
||||
/// <param name="options">The options containing the state file and output directory.</param>
|
||||
/// <returns>The absolute or output-relative state file path.</returns>
|
||||
private string ResolveStateFile(OperationOptions options)
|
||||
{
|
||||
return Path.IsPathRooted(options.StateFile) ? options.StateFile : Path.Combine(options.OutputDirectory, options.StateFile);
|
||||
}
|
||||
|
||||
private bool ConfirmDangerousAction(string actionName)
|
||||
/// <summary>
|
||||
/// Confirms a fully prepared runtime-changing plan immediately before execution.
|
||||
/// </summary>
|
||||
/// <param name="actionName">The action name displayed in the confirmation dialog.</param>
|
||||
/// <returns>True when the action may continue; otherwise false.</returns>
|
||||
private bool ConfirmPreparedPlan(string actionName, OperationPlan plan, string server, string planPath)
|
||||
{
|
||||
if (_dryRunCheckBox.Checked)
|
||||
var confirmed = false;
|
||||
Action showConfirmation = () =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var executableSteps = plan.Steps.Count(x => x.Execute);
|
||||
var result = MessageBox.Show(
|
||||
actionName + " will change the BizTalk runtime state on server '" + _serverTextBox.Text + "'. Continue?",
|
||||
"Confirm BizTalk Runtime Change",
|
||||
actionName + " will execute " + executableSteps + " step(s) on server '" + server + "'.\n\n"
|
||||
+ "The exact plan was saved to:\n" + planPath + "\n\nContinue now?",
|
||||
"Confirm Prepared BizTalk Plan",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Warning,
|
||||
MessageBoxDefaultButton.Button2);
|
||||
return result == DialogResult.Yes;
|
||||
confirmed = result == DialogResult.Yes;
|
||||
};
|
||||
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(showConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
showConfirmation();
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a snapshot in the status grid and updates the environment indicator.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The snapshot to display.</param>
|
||||
private void ShowSnapshot(BizTalkSnapshot snapshot)
|
||||
{
|
||||
InvokeIfRequired(() =>
|
||||
@@ -379,6 +563,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays snapshot differences in the status grid.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff to display.</param>
|
||||
private void ShowDiff(SnapshotDiff diff)
|
||||
{
|
||||
InvokeIfRequired(() =>
|
||||
@@ -395,6 +583,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays an operation plan in the status grid before or during execution.
|
||||
/// </summary>
|
||||
/// <param name="plan">The plan to display.</param>
|
||||
private void ShowPlan(OperationPlan plan)
|
||||
{
|
||||
InvokeIfRequired(() =>
|
||||
@@ -407,6 +599,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one operation log entry to the log grid.
|
||||
/// </summary>
|
||||
/// <param name="entry">The log entry to display.</param>
|
||||
private void AppendLog(LogEntry entry)
|
||||
{
|
||||
InvokeIfRequired(() =>
|
||||
@@ -429,10 +625,16 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables action buttons and updates the status strip.
|
||||
/// </summary>
|
||||
/// <param name="busy">True while a background operation is running.</param>
|
||||
/// <param name="status">The status text to display.</param>
|
||||
private void SetBusy(bool busy, string status)
|
||||
{
|
||||
InvokeIfRequired(() =>
|
||||
{
|
||||
_isBusy = busy;
|
||||
_diagnoseButton.Enabled = !busy;
|
||||
_beforeButton.Enabled = !busy;
|
||||
_afterButton.Enabled = !busy;
|
||||
@@ -445,6 +647,11 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the output directory Browse button click.
|
||||
/// </summary>
|
||||
/// <param name="sender">The control that raised the event.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void BrowseButtonClick(object sender, EventArgs e)
|
||||
{
|
||||
using (var dialog = new FolderBrowserDialog())
|
||||
@@ -457,28 +664,69 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a UI update on the UI thread when required.
|
||||
/// </summary>
|
||||
/// <param name="action">The UI action to execute.</param>
|
||||
private void InvokeIfRequired(Action action)
|
||||
{
|
||||
if (IsDisposed)
|
||||
if (IsDisposed || Disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (InvokeRequired)
|
||||
{
|
||||
try
|
||||
{
|
||||
BeginInvoke(action);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// The form was closed between the state check and BeginInvoke.
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prevents the form from being disposed while a maintenance operation is active.
|
||||
/// </summary>
|
||||
private void MainFormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
if (!_isBusy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.Cancel = true;
|
||||
MessageBox.Show(
|
||||
this,
|
||||
"A BizTalk operation is still running. Wait for it to finish before closing the tool.",
|
||||
"Operation in progress",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a right-aligned label for form inputs.
|
||||
/// </summary>
|
||||
/// <param name="text">The label text.</param>
|
||||
/// <returns>The configured label control.</returns>
|
||||
private static Label Label(string text)
|
||||
{
|
||||
return new Label { Text = text, Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleRight, Margin = new Padding(0, 5, 4, 5) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a standard action button and attaches its click handler.
|
||||
/// </summary>
|
||||
/// <param name="text">The button text.</param>
|
||||
/// <param name="handler">The click event handler.</param>
|
||||
/// <returns>The configured button.</returns>
|
||||
private static Button ActionButton(string text, EventHandler handler)
|
||||
{
|
||||
var button = new Button { Text = text, Width = 112, Height = 34, Margin = new Padding(0, 0, 8, 0) };
|
||||
@@ -486,12 +734,20 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
return button;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies common docking and spacing to an input control.
|
||||
/// </summary>
|
||||
/// <param name="control">The control to configure.</param>
|
||||
private static void ConfigureInput(Control control)
|
||||
{
|
||||
control.Dock = DockStyle.Fill;
|
||||
control.Margin = new Padding(4, 6, 8, 6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the environment indicator from host instance states in the latest snapshot.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The latest snapshot, or null when the state is unknown.</param>
|
||||
private void UpdateEnvironmentStatus(BizTalkSnapshot snapshot)
|
||||
{
|
||||
if (snapshot == null || snapshot.HostInstances == null || snapshot.HostInstances.Count == 0)
|
||||
@@ -526,6 +782,10 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies standard read-only display settings to a grid.
|
||||
/// </summary>
|
||||
/// <param name="grid">The grid to configure.</param>
|
||||
private static void ConfigureGrid(DataGridView grid)
|
||||
{
|
||||
grid.Dock = DockStyle.Fill;
|
||||
@@ -538,6 +798,11 @@ namespace BizTalkPlatformManagementTool.Ui
|
||||
grid.BackgroundColor = SystemColors.Window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats an exception chain into a concise message for the operation log.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception to format.</param>
|
||||
/// <returns>A message containing the top-level and unique inner exception messages.</returns>
|
||||
private static string FormatException(Exception ex)
|
||||
{
|
||||
if (ex == null)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="2.1.0.0" name="BizTalkPlatformManagementTool" />
|
||||
<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="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?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>{318F4307-F62C-47C9-9B90-F0C9BF2F812A}</ProjectGuid><OutputType>Exe</OutputType><RootNamespace>BizTalkPlatformManagementTool.Tests</RootNamespace><AssemblyName>BizTalkPlatformManagementTool.Tests</AssemblyName><TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion><FileAlignment>512</FileAlignment><Deterministic>true</Deterministic></PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "><DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel></PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "><DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel></PropertyGroup>
|
||||
<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>
|
||||
<ProjectReference Include="..\..\src\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj"><Project>{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}</Project><Name>BizTalkPlatformManagementTool.Setup</Name></ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,229 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using BizTalkPlatformManagementTool.Models;
|
||||
using BizTalkPlatformManagementTool.Services;
|
||||
using BizTalkPlatformManagementTool.Setup;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Tests
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private static int failures;
|
||||
|
||||
private static int Main()
|
||||
{
|
||||
Run("JsonRoundTripIsBomTolerantAndAtomic", JsonRoundTripIsBomTolerantAndAtomic);
|
||||
Run("DiffUsesApplicationAndNameIdentity", DiffUsesApplicationAndNameIdentity);
|
||||
Run("RestoreRejectsDifferentServer", RestoreRejectsDifferentServer);
|
||||
Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder);
|
||||
Run("CsvNeutralizesFormulaValues", CsvNeutralizesFormulaValues);
|
||||
Run("PackageManifestRejectsTampering", PackageManifestRejectsTampering);
|
||||
Run("PackageManifestRejectsUndeclaredAndTraversalFiles", PackageManifestRejectsUndeclaredAndTraversalFiles);
|
||||
Run("InstallerActivatesValidatedPayload", InstallerActivatesValidatedPayload);
|
||||
Run("InstallerDoesNotMutateOnStagingFailure", InstallerDoesNotMutateOnStagingFailure);
|
||||
Run("InstallerRollsBackFailedActivatedSelfTest", InstallerRollsBackFailedActivatedSelfTest);
|
||||
Run("InstallerUninstallRemovesProgramDirectory", InstallerUninstallRemovesProgramDirectory);
|
||||
Console.WriteLine(failures == 0 ? "ALL TESTS PASSED" : failures + " TEST(S) FAILED");
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
private static void Run(string name, Action test)
|
||||
{
|
||||
try { test(); Console.WriteLine("PASS " + name); }
|
||||
catch (Exception ex) { failures++; Console.Error.WriteLine("FAIL " + name + ": " + ex); }
|
||||
}
|
||||
|
||||
private static void JsonRoundTripIsBomTolerantAndAtomic()
|
||||
{
|
||||
InTemp(directory =>
|
||||
{
|
||||
var path = Path.Combine(directory, "snapshot.json");
|
||||
var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted);
|
||||
JsonFileStore.Save(path, snapshot);
|
||||
JsonFileStore.Save(path, snapshot);
|
||||
var original = File.ReadAllBytes(path);
|
||||
var withBom = new byte[original.Length + 3];
|
||||
withBom[0] = 0xef; withBom[1] = 0xbb; withBom[2] = 0xbf;
|
||||
Buffer.BlockCopy(original, 0, withBom, 3, original.Length);
|
||||
File.WriteAllBytes(path, withBom);
|
||||
Assert(JsonFileStore.Load<BizTalkSnapshot>(path).Applications.Count == 1, "BOM JSON did not load");
|
||||
Assert(Directory.GetFiles(directory, "*.tmp.*").Length == 0, "temporary files remained");
|
||||
Assert(Directory.GetFiles(directory, "*.bak.*").Length == 0, "backup files remained");
|
||||
});
|
||||
}
|
||||
|
||||
private static void DiffUsesApplicationAndNameIdentity()
|
||||
{
|
||||
var before = Snapshot("APP-A", "SHARED", ArtifactStates.SendPortStarted);
|
||||
before.Applications.Add(Snapshot("APP-B", "SHARED", ArtifactStates.SendPortStarted).Applications[0]);
|
||||
var after = Snapshot("APP-A", "SHARED", ArtifactStates.SendPortStopped);
|
||||
after.Applications.Add(Snapshot("APP-B", "SHARED", ArtifactStates.SendPortStarted).Applications[0]);
|
||||
var diff = SnapshotComparer.Compare(before, after);
|
||||
Assert(diff.ArtifactDifferences.Count == 1, "expected one application-scoped difference");
|
||||
Assert(diff.ArtifactDifferences[0].Application == "APP-A", "wrong application was compared");
|
||||
}
|
||||
|
||||
private static void RestoreRejectsDifferentServer()
|
||||
{
|
||||
var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted);
|
||||
snapshot.Server = "BIZTALK-A.example.local";
|
||||
SnapshotValidator.EnsureServerMatches(snapshot, "BIZTALK-A");
|
||||
Expect<InvalidOperationException>(() => SnapshotValidator.EnsureServerMatches(snapshot, "BIZTALK-B"));
|
||||
}
|
||||
|
||||
private static void RestorePlanUsesSafeOrder()
|
||||
{
|
||||
var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted);
|
||||
snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HOST:SERVER", HostName = "HOST", Server = snapshot.Server, RawState = ArtifactStates.HostStarted });
|
||||
snapshot.Applications[0].Orchestrations.Add(new OrchestrationState { Application = "APP", Name = "ORCH", OrchestrationStatus = ArtifactStates.OrchestrationBound });
|
||||
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RL", Enabled = true });
|
||||
var plan = new BizTalkOperationService(null).CreateRestorePlan(snapshot, snapshot.Server);
|
||||
Assert(plan.Steps.First().Kind == "HostInstance", "host instance must start first");
|
||||
Assert(plan.Steps.Last().Kind == "ReceiveLocation", "receive location must be restored last");
|
||||
var bound = plan.Steps.Single(x => x.Name == "ORCH");
|
||||
Assert(!bound.Execute && bound.Kind == "Note", "bound orchestration must remain unchanged");
|
||||
}
|
||||
|
||||
private static void CsvNeutralizesFormulaValues()
|
||||
{
|
||||
InTemp(directory =>
|
||||
{
|
||||
var path = Path.Combine(directory, "diff.csv");
|
||||
var diff = new SnapshotDiff();
|
||||
diff.ArtifactDifferences.Add(new ArtifactDiffEntry { Application = "=cmd|' /C calc'!A0", ArtifactType = "SendPort", Name = "PORT", Before = "Started", After = "Stopped" });
|
||||
CsvWriter.WriteDiff(path, diff);
|
||||
Assert(File.ReadAllText(path).Contains("'=cmd"), "formula-like CSV field was not neutralized");
|
||||
});
|
||||
}
|
||||
|
||||
private static void PackageManifestRejectsTampering()
|
||||
{
|
||||
InTemp(directory =>
|
||||
{
|
||||
var app = Path.Combine(directory, "application"); Directory.CreateDirectory(app);
|
||||
File.WriteAllText(Path.Combine(app, InstallerEngine.ApplicationExeName), "payload");
|
||||
File.WriteAllText(Path.Combine(app, InstallerEngine.ApplicationExeName + ".config"), "config");
|
||||
var manifest = Path.Combine(directory, "application.manifest");
|
||||
PackageManifest.Write(app, manifest);
|
||||
PackageManifest.ValidateAndRead(app, manifest);
|
||||
File.AppendAllText(Path.Combine(app, InstallerEngine.ApplicationExeName), "tampered");
|
||||
Expect<InvalidDataException>(() => PackageManifest.ValidateAndRead(app, manifest));
|
||||
});
|
||||
}
|
||||
|
||||
private static void InstallerActivatesValidatedPayload()
|
||||
{
|
||||
InTemp(directory =>
|
||||
{
|
||||
var package = CreatePackage(directory, "new");
|
||||
var install = Path.Combine(directory, "install");
|
||||
var data = Path.Combine(directory, "data");
|
||||
var engine = new InstallerEngine(package, install, data, false, path => File.Exists(path));
|
||||
engine.Install(false, null);
|
||||
Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "new", "new payload not activated");
|
||||
Assert(File.Exists(Path.Combine(install, "install-state.txt")), "install state missing");
|
||||
});
|
||||
}
|
||||
|
||||
private static void PackageManifestRejectsUndeclaredAndTraversalFiles()
|
||||
{
|
||||
InTemp(directory =>
|
||||
{
|
||||
var app = Path.Combine(directory, "application"); Directory.CreateDirectory(app);
|
||||
var exe = Path.Combine(app, InstallerEngine.ApplicationExeName);
|
||||
File.WriteAllText(exe, "payload");
|
||||
File.WriteAllText(Path.Combine(app, InstallerEngine.ApplicationExeName + ".config"), "config");
|
||||
var manifest = Path.Combine(directory, "application.manifest");
|
||||
PackageManifest.Write(app, manifest);
|
||||
File.WriteAllText(Path.Combine(app, "undeclared.dll"), "extra");
|
||||
Expect<InvalidDataException>(() => PackageManifest.ValidateAndRead(app, manifest));
|
||||
File.Delete(Path.Combine(app, "undeclared.dll"));
|
||||
File.WriteAllText(manifest, PackageManifest.Sha256(exe) + "|" + new FileInfo(exe).Length + "|../escape.exe" + Environment.NewLine);
|
||||
Expect<InvalidDataException>(() => PackageManifest.ValidateAndRead(app, manifest));
|
||||
});
|
||||
}
|
||||
|
||||
private static void InstallerDoesNotMutateOnStagingFailure()
|
||||
{
|
||||
InTemp(directory =>
|
||||
{
|
||||
var package = CreatePackage(directory, "new");
|
||||
var install = Path.Combine(directory, "install"); Directory.CreateDirectory(install);
|
||||
File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName), "old");
|
||||
File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName + ".config"), "old-config");
|
||||
var engine = new InstallerEngine(package, install, Path.Combine(directory, "data"), false, path => false);
|
||||
Expect<InvalidOperationException>(() => engine.Install(false, null));
|
||||
Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "old", "staging failure modified the installed payload");
|
||||
Assert(!Directory.GetDirectories(directory, "install.backup.*").Any(), "backup was created before staging passed");
|
||||
});
|
||||
}
|
||||
|
||||
private static void InstallerRollsBackFailedActivatedSelfTest()
|
||||
{
|
||||
InTemp(directory =>
|
||||
{
|
||||
var package = CreatePackage(directory, "new");
|
||||
var install = Path.Combine(directory, "install"); Directory.CreateDirectory(install);
|
||||
File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName), "old");
|
||||
File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName + ".config"), "old-config");
|
||||
var calls = 0;
|
||||
var engine = new InstallerEngine(package, install, Path.Combine(directory, "data"), false, path => ++calls == 1);
|
||||
Expect<InvalidOperationException>(() => engine.Install(false, null));
|
||||
Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "old", "previous payload was not restored");
|
||||
Assert(!Directory.GetDirectories(directory, "install.staging.*").Any(), "staging directory remained");
|
||||
Assert(!Directory.GetDirectories(directory, "install.backup.*").Any(), "backup directory remained");
|
||||
});
|
||||
}
|
||||
|
||||
private static void InstallerUninstallRemovesProgramDirectory()
|
||||
{
|
||||
InTemp(directory =>
|
||||
{
|
||||
var package = CreatePackage(directory, "new");
|
||||
var install = Path.Combine(directory, "install"); Directory.CreateDirectory(install);
|
||||
File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName), "installed");
|
||||
var engine = new InstallerEngine(package, install, Path.Combine(directory, "data"), false, path => true);
|
||||
engine.Uninstall(null);
|
||||
Assert(!Directory.Exists(install), "program directory still exists after uninstall");
|
||||
Assert(!Directory.GetDirectories(directory, "install.removed.*").Any(), "uninstall quarantine directory remained");
|
||||
});
|
||||
}
|
||||
|
||||
private static BizTalkSnapshot Snapshot(string application, string port, int state)
|
||||
{
|
||||
var result = new BizTalkSnapshot { ToolVersion = "test", CreatedAt = DateTimeOffset.Now.ToString("o"), Server = Environment.MachineName };
|
||||
var app = new ApplicationSnapshot { Application = application };
|
||||
app.SendPorts.Add(new SendPortState { Application = application, Name = port, Status = state });
|
||||
result.Applications.Add(app);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string CreatePackage(string root, string payload)
|
||||
{
|
||||
var package = Path.Combine(root, "package");
|
||||
var app = Path.Combine(package, "application"); Directory.CreateDirectory(app);
|
||||
File.WriteAllText(Path.Combine(app, InstallerEngine.ApplicationExeName), payload);
|
||||
File.WriteAllText(Path.Combine(app, InstallerEngine.ApplicationExeName + ".config"), "config");
|
||||
PackageManifest.Write(app, Path.Combine(package, "application.manifest"));
|
||||
return package;
|
||||
}
|
||||
|
||||
private static void InTemp(Action<string> action)
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool.Tests." + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(directory);
|
||||
try { action(directory); }
|
||||
finally { if (Directory.Exists(directory)) Directory.Delete(directory, true); }
|
||||
}
|
||||
|
||||
private static void Assert(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); }
|
||||
private static void Expect<T>(Action action) where T : Exception
|
||||
{
|
||||
try { action(); }
|
||||
catch (T) { return; }
|
||||
throw new InvalidOperationException("Expected exception " + typeof(T).Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user