Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cb3fda151 |
@@ -0,0 +1,15 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = crlf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{md,json}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
* text=auto eol=crlf
|
* text=auto eol=crlf
|
||||||
|
|||||||
+1
-1
@@ -10,6 +10,7 @@ bin/
|
|||||||
obj/
|
obj/
|
||||||
|
|
||||||
# Logs / Outputs
|
# Logs / Outputs
|
||||||
|
artifacts/
|
||||||
*.log
|
*.log
|
||||||
*.json
|
*.json
|
||||||
*.html
|
*.html
|
||||||
@@ -18,7 +19,6 @@ obj/
|
|||||||
*.png
|
*.png
|
||||||
work/
|
work/
|
||||||
release/*
|
release/*
|
||||||
artifacts/
|
|
||||||
|
|
||||||
# PowerShell specifics
|
# PowerShell specifics
|
||||||
*.ps1.orig
|
*.ps1.orig
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ VisualStudioVersion = 15.0.28307.168
|
|||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool", "src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj", "{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool", "src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj", "{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Setup", "src\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj", "{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Tests", "tests\BizTalkPlatformManagementTool.Tests\BizTalkPlatformManagementTool.Tests.csproj", "{B2AD340B-859F-413B-8811-7FBA4BD2BC02}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Packager", "src\BizTalkPlatformManagementTool.Packager\BizTalkPlatformManagementTool.Packager.csproj", "{74A5D422-0BA5-4559-BD81-C89C071A8FE4}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Setup", "src\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj", "{38E61630-11AB-4F8D-B421-F8A88831C4BD}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Tests", "tests\BizTalkPlatformManagementTool.Tests\BizTalkPlatformManagementTool.Tests.csproj", "{318F4307-F62C-47C9-9B90-F0C9BF2F812A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Packager", "src\BizTalkPlatformManagementTool.Packager\BizTalkPlatformManagementTool.Packager.csproj", "{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -20,18 +20,18 @@ Global
|
|||||||
{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{B2AD340B-859F-413B-8811-7FBA4BD2BC02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B2AD340B-859F-413B-8811-7FBA4BD2BC02}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B2AD340B-859F-413B-8811-7FBA4BD2BC02}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B2AD340B-859F-413B-8811-7FBA4BD2BC02}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{74A5D422-0BA5-4559-BD81-C89C071A8FE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{38E61630-11AB-4F8D-B421-F8A88831C4BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{74A5D422-0BA5-4559-BD81-C89C071A8FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{38E61630-11AB-4F8D-B421-F8A88831C4BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{74A5D422-0BA5-4559-BD81-C89C071A8FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{38E61630-11AB-4F8D-B421-F8A88831C4BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{74A5D422-0BA5-4559-BD81-C89C071A8FE4}.Release|Any CPU.Build.0 = Release|Any CPU
|
{38E61630-11AB-4F8D-B421-F8A88831C4BD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{318F4307-F62C-47C9-9B90-F0C9BF2F812A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{318F4307-F62C-47C9-9B90-F0C9BF2F812A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{318F4307-F62C-47C9-9B90-F0C9BF2F812A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{318F4307-F62C-47C9-9B90-F0C9BF2F812A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
+31
-41
@@ -1,65 +1,55 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [2.1.2] - 2026-08-11
|
## [Unreleased] - 2026-08-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Complete XML documentation for types and methods across application, setup, packager and regression projects, including parameters, generic type parameters and return values.
|
|
||||||
- Focused German inline comments for non-obvious BizTalk ordering, WMI compatibility, persistence, security and installer transaction decisions.
|
- Überarbeitete deutschsprachige GUI mit einheitlichem Workflow und DPI-Skalierung.
|
||||||
- XML documentation output in every Release project configuration for compiler-side validation.
|
- .NET-Installer mit sicherem Update, Rollback und Deinstallation.
|
||||||
|
- Release-Packager für ZIP, Base64-TXT und SHA-256.
|
||||||
|
- Regressionstestprojekt für Plan-, Vergleichs-, Validierungs- und Dateilogik.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- README, technical documentation and installation/build guidance now describe the source-documentation standard and generated XML developer artifacts.
|
|
||||||
|
|
||||||
## [2.1.1] - 2026-08-11
|
- Restore startet ausschließlich Artefakte, die im Vorher-Snapshot aktiv waren.
|
||||||
### Added
|
- Vorhandene Vorher-Snapshots werden beim Herunterfahren wiederverwendet.
|
||||||
- Stable setup/uninstall phase codes, environment and file metadata, complete self-test output, exception chains with HRESULT/stacktrace, and per-step rollback diagnostics.
|
- WMI-Auflösung nutzt zusätzliche Qualifier und bricht bei Mehrdeutigkeit ab.
|
||||||
- Installer-log fallback from ProgramData to Temp plus a setup button for opening diagnostic logs.
|
- Snapshot-Vergleich verwendet zusammengesetzte Artefaktidentitäten.
|
||||||
- Post-write verification of shortcuts, uninstall executable and essential uninstall-registry values.
|
- JSON-Dateien werden atomar und größenbegrenzt geschrieben.
|
||||||
- Regression coverage for pre-mutation rollback wording, diagnostic context, log fallback and UI-report isolation.
|
- Logs und Wartungsdateien liegen unter `%ProgramData%` und überleben Updates.
|
||||||
|
- Transport-URIs werden nicht mehr in neuen Snapshots persistiert.
|
||||||
|
|
||||||
|
## [2.0.0] - 2026-04-27
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Pre-mutation failures now explicitly report that no rollback was required; completed rollbacks are only reported as successful when every restoration step succeeded.
|
|
||||||
- Self-test timeout handling is bounded after termination is requested and records kill/capture status.
|
|
||||||
- Technical installer acceptance is explicitly separated from BizTalk WMI/environment validation.
|
|
||||||
- Setup and uninstall preserve undeletable backup/quarantine data and record a successful completion with a cleanup warning.
|
|
||||||
|
|
||||||
## [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.
|
- Projekt auf `BizTalkPlatformManagementTool` umbenannt.
|
||||||
- Solution, Projektdatei, Namespace, Assemblyname und Standard-Ausgabepfad angepasst.
|
- Solution, Projektdatei, Namespace, Assemblyname und Standard-Ausgabepfad angepasst.
|
||||||
- Dokumentation aktualisiert und `Installation.md` ergänzt.
|
- Dokumentation aktualisiert und `Installation.md` ergänzt.
|
||||||
|
|
||||||
## [1.3.0] - 2026-02-02
|
## [1.3.0] - 2026-02-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- HTML Report mit farblicher Kennzeichnung (grün/rot)
|
|
||||||
- Farbige Statusausgabe auf STDOUT
|
- HTML-Report mit farblicher Kennzeichnung
|
||||||
- Remote‑Server Parameter (-Server) und Ausgabeverzeichnis (-OutDir)
|
- farbige Statusausgabe
|
||||||
- Verbesserte Diff‑Erkennung (neu/entfernt)
|
- Remote-Server-Parameter und Ausgabeverzeichnis
|
||||||
|
- verbesserte Diff-Erkennung für neue und entfernte Artefakte
|
||||||
|
|
||||||
## [1.2.0] - 2026-02-02
|
## [1.2.0] - 2026-02-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Remote‑fähige Snapshot‑Funktion, Logging (Konsole + Datei)
|
|
||||||
|
- Remote-fähige Snapshot-Funktion und Logging
|
||||||
|
|
||||||
## [1.1.0] - 2026-02-02
|
## [1.1.0] - 2026-02-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Textuelle Diff‑Ausgabe auf STDOUT + HTML Report
|
|
||||||
|
- textuelle Diff-Ausgabe und HTML-Report
|
||||||
|
|
||||||
## [1.0.0] - 2026-02-02
|
## [1.0.0] - 2026-02-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Grundfunktion: Snapshot & Diff (Receive Locations, Send Ports, Orchestrations)
|
|
||||||
|
- Snapshot und Diff für Receive Locations, Send Ports und Orchestrierungen
|
||||||
|
|||||||
+117
-136
@@ -1,178 +1,159 @@
|
|||||||
# BizTalk Platform Management Tool Dokumentation
|
# Technische Dokumentation
|
||||||
|
|
||||||
**Stand:** 2026-08-11
|
**Stand:** 2026-08-06
|
||||||
**Implementierung:** C# WinForms, .NET Framework 4.6.1
|
|
||||||
**Archivierte PowerShell-Version:** `archive/powershell/BizTalkPlatformManagementTool.ps1`
|
|
||||||
|
|
||||||
## Zweck
|
**Version:** 2.1.0
|
||||||
|
|
||||||
Das BizTalk Platform Management Tool unterstützt kontrollierte Wartungsfenster für Microsoft BizTalk Server 2020:
|
**Technik:** C# WinForms, .NET Framework 4.6.1
|
||||||
|
|
||||||
- Exakten Laufzeitstatus vor einer Wartung erfassen.
|
## Ziel und Sicherheitsprinzip
|
||||||
- Receive Locations, Orchestrations, Send Ports und Host Instances in definierter Reihenfolge herunterfahren.
|
|
||||||
- Den Laufzeitstatus aus einer gespeicherten `before.json` wiederherstellen.
|
|
||||||
- Den Status nach der Wartung erfassen und beide Snapshots vergleichen.
|
|
||||||
- Plan- und Ergebnisdateien für Review, Audit und Nachvollziehbarkeit schreiben.
|
|
||||||
|
|
||||||
## Projektstruktur
|
Das Tool unterstützt geplante BizTalk-Wartungen, ohne den fachlich gewünschten
|
||||||
|
Ausgangszustand zu verlieren. Der Vorher-Snapshot ist die einzige Quelle für
|
||||||
|
den Restore. Das bedeutet insbesondere:
|
||||||
|
|
||||||
- Solution: `BizTalkPlatformManagementTool.sln`
|
- Nur Artefakte, die im Snapshot aktiv waren, werden heruntergefahren.
|
||||||
- Projekt: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj`
|
- Nur Artefakte, die im Snapshot aktiv waren, werden wieder gestartet.
|
||||||
- UI: `src/BizTalkPlatformManagementTool/Ui/MainForm.cs`
|
- Zuvor gestoppte, gebundene oder deaktivierte Artefakte werden beim Restore nicht verändert.
|
||||||
- WMI-Zugriff: `src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs`
|
- Ein vorhandener Vorher-Snapshot wird durch **Herunterfahren** nicht ersetzt.
|
||||||
- Operationslogik: `src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs`
|
- Snapshot und Zielserver müssen übereinstimmen; Kurzname und FQDN desselben Servers werden akzeptiert.
|
||||||
- Snapshot-Validierung: `src/BizTalkPlatformManagementTool/Services/SnapshotValidator.cs`
|
- Bei mehrdeutiger WMI-Auflösung wird abgebrochen, statt möglicherweise das falsche Objekt zu verändern.
|
||||||
- Installer: `src/BizTalkPlatformManagementTool.Setup`
|
|
||||||
- Release-Paketierung: `src/BizTalkPlatformManagementTool.Packager`
|
|
||||||
- Regressionstests: `tests/BizTalkPlatformManagementTool.Tests`
|
|
||||||
- PowerShell-Archiv: `archive/powershell/BizTalkPlatformManagementTool.ps1`
|
|
||||||
|
|
||||||
## Code-Dokumentationsstandard
|
## Oberfläche
|
||||||
|
|
||||||
Die vollständige C#-Codebasis in Anwendung, Installer, Packager und Regressionstests ist auf Typ- und Methodenebene mit XML-Dokumentationskommentaren versehen. Methoden dokumentieren ihre Parameter mit `<param>`, generische Typen mit `<typeparam>` und Rückgabewerte mit `<returns>`, soweit jeweils vorhanden. Die Release-Konfiguration jedes Projekts erzeugt zusätzlich eine XML-Dokumentationsdatei im jeweiligen `bin\Release`-Verzeichnis. Dadurch prüft der Compiler Syntax und Referenzen der öffentlichen Dokumentation bei jedem Release-Build.
|
Die Oberfläche führt von links nach rechts durch sechs Schritte:
|
||||||
|
|
||||||
Deutsche Inline-Kommentare stehen gezielt an Stellen, deren Zweck nicht allein aus dem Code hervorgeht. Dazu zählen insbesondere:
|
1. **Verbindung prüfen**
|
||||||
|
2. **Zustand erfassen**
|
||||||
|
3. **Herunterfahren**
|
||||||
|
4. **Wiederherstellen**
|
||||||
|
5. **Nachher erfassen**
|
||||||
|
6. **Vergleichen**
|
||||||
|
|
||||||
- sichere Shutdown-/Restore-Reihenfolge und Schutz gebundener Orchestrierungen,
|
Kopfbereich, Status-Badge, einheitliche Eingabefelder, gleich große
|
||||||
- WMI-Auflösung über breite Abfrage mit clientseitigem Filter,
|
Workflow-Schaltflächen, farblich getrennte destruktive und wiederherstellende
|
||||||
- atomare JSON-Ersetzung auf demselben Volume,
|
Aktionen sowie responsive Ergebnis- und Logtabellen sorgen für eine klare
|
||||||
- Neutralisierung formelartiger CSV-Werte,
|
Bedienung auch bei kleineren Bildschirmgrößen und DPI-Skalierung.
|
||||||
- Persistenz eines Operationsplans vor der Benutzerbestätigung,
|
|
||||||
- Staging-, Aktivierungs-, Quarantäne- und Rollbackgrenzen des Installers,
|
|
||||||
- begrenzte Self-Test-Prozess- und Streambehandlung.
|
|
||||||
|
|
||||||
Selbsterklärende Zuweisungen und reine UI-Konstruktion werden nicht zeilenweise kommentiert. Kommentare sollen die fachliche Begründung, Sicherheitsgrenze oder Plattformbesonderheit festhalten und nicht lediglich den unmittelbar sichtbaren Code wiederholen.
|
Der Dry-run ist standardmäßig aktiviert. Bei echten Änderungen zeigt die
|
||||||
|
Statusleiste einen roten Hinweis und jede Änderung verlangt eine zusätzliche
|
||||||
|
Bestätigung mit Zielserver und verwendeter Zustandsdatei. Während einer
|
||||||
|
Operation sind Eingaben gesperrt; das Fenster kann nicht versehentlich
|
||||||
|
geschlossen werden.
|
||||||
|
|
||||||
## UI Workflow
|
## Erfasster Zustand
|
||||||
|
|
||||||
1. Anwendung mit Administratorrechten starten.
|
Über `root\MicrosoftBizTalkServer` werden gelesen:
|
||||||
2. BizTalk-Servernamen eintragen.
|
|
||||||
3. Ausgabeverzeichnis wählen.
|
|
||||||
4. **Dry run** aktiviert lassen.
|
|
||||||
5. Mit **Diagnose** den Zugriff auf `root\MicrosoftBizTalkServer` prüfen.
|
|
||||||
6. Mit **Snapshot Before** die Datei `before.json` erzeugen.
|
|
||||||
7. Mit **Shutdown** den Plan `shutdown-plan.json` erzeugen und prüfen.
|
|
||||||
8. **Dry run** erst deaktivieren, wenn der Plan korrekt ist.
|
|
||||||
9. Nach der Wartung mit **Restore** aus `before.json` wiederherstellen.
|
|
||||||
10. Mit **Snapshot After** und **Compare** die Umgebung validieren.
|
|
||||||
|
|
||||||
Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des letzten Snapshots und zeigt `Started`, `Stopped`, `Partial` oder `Unknown`. **Clear** leert die sichtbaren Ergebnis- und Log-Grids, loescht aber keine Dateien. **Close** beendet die Anwendung.
|
| WMI-Klasse | Relevanter Zustand |
|
||||||
|
| --- | --- |
|
||||||
|
| `MSBTS_ReceiveLocation` | `IsDisabled`, Name, Receive Port, Adapter |
|
||||||
|
| `MSBTS_SendPort` | `Status`, Name, Adaptertyp |
|
||||||
|
| `MSBTS_Orchestration` | `OrchestrationStatus`, Name und Assembly-Identität |
|
||||||
|
| `MSBTS_HostInstance` | `ServiceState`, Hostname und ausführender Server |
|
||||||
|
|
||||||
## Sicherheitsdesign
|
Transportadressen werden bewusst nicht persistiert. Sie sind für Start/Stop
|
||||||
|
nicht erforderlich und können Benutzerinformationen, Kennwörter oder andere
|
||||||
|
sensible Bestandteile enthalten.
|
||||||
|
|
||||||
- Dry-run ist standardmäßig aktiviert.
|
## Zustandswerte
|
||||||
- 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 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 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.
|
|
||||||
- WMI-Methodenrückgaben werden geprüft.
|
|
||||||
- Wartezeiten nutzen konfigurierbare Timeout- und Polling-Werte.
|
|
||||||
- Host Instances auf anderen Servern werden übersprungen und als Warnung protokolliert.
|
|
||||||
- Echte Shutdown-/Restore-Schritte protokollieren WMI-Klasse, Schlüssel, Zielobjekt und Methode, damit Fehler wie WMI-Query- oder Methodenfehler eindeutig zugeordnet werden können.
|
|
||||||
- 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
|
- Send Port: `1=Bound`, `2=Stopped`, `3=Started`
|
||||||
|
- Orchestrierung: `1=Unbound`, `2=Bound`, `3=Stopped`, `4=Started`
|
||||||
|
- Hostinstanz: `1=Stopped`, `2=StartPending`, `3=StopPending`, `4=Running`
|
||||||
|
- Receive Location: `Enabled = !IsDisabled`
|
||||||
|
|
||||||
1. Receive Locations deaktivieren, die im Snapshot aktiviert waren.
|
Weitere Host-Servicezustände werden als unbekannt angezeigt und nicht
|
||||||
2. Orchestrations stoppen, die gestartet waren.
|
automatisch als gestartet behandelt.
|
||||||
3. Send Ports stoppen, die gestartet waren.
|
|
||||||
4. Host Instances stoppen, die auf dem ausgewählten Server gestartet waren.
|
|
||||||
|
|
||||||
## Restore-Reihenfolge
|
## Shutdown-Plan
|
||||||
|
|
||||||
1. Host Instances starten, die auf dem ausgewählten Server zuvor gestartet waren.
|
1. Alle im Snapshot aktivierten Receive Locations deaktivieren.
|
||||||
2. Send Ports wiederherstellen.
|
2. Alle im Snapshot gestarteten Orchestrierungen kontrolliert stoppen.
|
||||||
3. Orchestrations wiederherstellen, soweit dies sicher möglich ist.
|
3. Alle im Snapshot gestarteten Send Ports stoppen.
|
||||||
4. Receive Locations zuletzt wiederherstellen.
|
4. Alle im Snapshot gestarteten Hostinstanzen des ausgewählten Servers stoppen.
|
||||||
|
|
||||||
## Restore-Grenzen
|
Hostinstanzen anderer BizTalk-Knoten erscheinen als übersprungene Hinweise.
|
||||||
|
Damit kann derselbe Gruppen-Snapshot nachvollziehbar bleiben, ohne remote
|
||||||
|
Hostinstanzen versehentlich vom falschen Knoten aus zu verändern.
|
||||||
|
|
||||||
- Send Ports werden auf `Started`, `Stopped` oder `Bound` zurückgesetzt.
|
## Restore-Plan
|
||||||
- Orchestrations mit Status `Started`, `Stopped` oder `Unbound` werden direkt behandelt.
|
|
||||||
- Orchestrations mit Status `Bound` bleiben unverändert und werden zur manuellen Prüfung markiert, weil ein blindes Unenlist zu `Unbound` führen kann.
|
|
||||||
- Receive Locations werden zuletzt aktiviert, damit eingehender Verkehr erst nach Host Instances und abhängigen Artefakten wieder aufgenommen wird.
|
|
||||||
|
|
||||||
## Ausgabedateien
|
1. Zuvor gestartete Hostinstanzen starten.
|
||||||
|
2. Zuvor gestartete Send Ports starten.
|
||||||
|
3. Zuvor gestartete Orchestrierungen starten.
|
||||||
|
4. Zuvor aktivierte Receive Locations zuletzt aktivieren.
|
||||||
|
|
||||||
- Snapshots: `before.json`, `after.json`
|
Gestoppte oder gebundene Send Ports, gestoppte/gebundene/ungebundene
|
||||||
- Operationspläne: `shutdown-plan.json`, `restore-plan.json`
|
Orchestrierungen, deaktivierte Receive Locations und gestoppte Hostinstanzen
|
||||||
- Nachher-Snapshots: `shutdown-after.json`, `restore-after.json`
|
erzeugen keine Restore-Aktion.
|
||||||
- Diff: `diff.json`, `diff.csv`, `diff.html`
|
|
||||||
- Snapshot-Reports: `*.csv`, `*.hosts.csv`, `*.html`
|
|
||||||
- Laufzeitlogs: `%ProgramData%\BizTalkPlatformManagementTool\Logs\BizTalkPlatformManagementTool-yyyy-MM-dd.log`
|
|
||||||
- Installerlogs: `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs\setup-*.log`
|
|
||||||
|
|
||||||
## Fehleranalyse
|
## Eindeutige WMI-Auflösung
|
||||||
|
|
||||||
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.
|
WMI-Abfragen werden ohne dynamisch zusammengesetzte WQL-`WHERE`-Klauseln
|
||||||
|
ausgeführt. Die Filterung erfolgt im Prozess und behandelt Sonderzeichen in
|
||||||
|
Namen sicher. Zusätzlich werden verwendet:
|
||||||
|
|
||||||
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.
|
- Receive Location: `Name` plus `ReceivePortName`
|
||||||
|
- Orchestrierung: `Name` plus `AssemblyName`
|
||||||
|
- Hostinstanz: `InstanceName` plus `RunningServer`
|
||||||
|
|
||||||
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.
|
Die vollständige Assembly-Identität einer Orchestrierung wird im Snapshot und
|
||||||
|
beim Vergleich berücksichtigt. Liefert WMI trotz der Qualifier mehrere
|
||||||
|
Treffer, wird die Operation mit einer eindeutigen Fehlermeldung abgebrochen.
|
||||||
|
Jeder Schritt prüft vor dem Methodenaufruf den aktuellen Zustand. Bereits
|
||||||
|
erreichte Zielzustände werden protokolliert und übersprungen; dadurch können
|
||||||
|
teilweise ausgeführte Pläne nach einer Fehlerkorrektur sicher erneut gestartet
|
||||||
|
werden.
|
||||||
|
|
||||||
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.
|
## Dateisicherheit
|
||||||
|
|
||||||
## Installer- und Updatearchitektur
|
JSON-Dateien werden zunächst vollständig in eine temporäre Datei geschrieben,
|
||||||
|
auf den Datenträger gespült und anschließend atomar ersetzt. Leere Dateien und
|
||||||
|
Dateien über 16 MiB werden verworfen. UTF-8 mit und ohne BOM sowie der sichtbare
|
||||||
|
BOM-Marker älterer Konvertierungen werden beim Lesen toleriert.
|
||||||
|
|
||||||
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.
|
Vergleiche verwenden zusammengesetzte Identitäten. Gleichnamige Artefakte in
|
||||||
|
unterschiedlichen Anwendungen, Receive Ports oder Assembly-Versionen werden
|
||||||
|
nicht miteinander vermischt.
|
||||||
|
|
||||||
Die Aktivierung ist transaktional aufgebaut:
|
## Verzeichnisse
|
||||||
|
|
||||||
1. Payload validieren.
|
```text
|
||||||
2. Eindeutiges Staging-Verzeichnis neben dem Installationsziel erstellen.
|
%ProgramFiles%\BizTalkPlatformManagementTool\
|
||||||
3. Kopierte Dateien nochmals per SHA-256 und die Staging-EXE per `--self-test` prüfen.
|
BizTalkPlatformManagementTool.exe
|
||||||
4. Laufende Toolinstanz ausschließen.
|
BizTalkPlatformManagementTool.exe.config
|
||||||
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. Windows-Integration durch Rücklesen der Verknüpfungen und der wesentlichen Registrywerte verifizieren.
|
|
||||||
10. Bei einem Fehler nach der ersten Mutation die neue Version entfernen und das Backup einschließlich Windows-Integration wiederherstellen.
|
|
||||||
|
|
||||||
Fehler vor der ersten Mutation werden als **Kein Rollback erforderlich** ausgewiesen. Ein Rollback wird nur als erfolgreich bezeichnet, wenn seine einzelnen Schritte ohne Fehler abgeschlossen wurden; ein verbliebenes Backup ohne aktives Programmverzeichnis verletzt eine zusätzlich geprüfte Invariante und wird als unvollständiger Rollback gemeldet. Bereinigungsprobleme an einem nicht mehr benötigten Backup oder Deinstallations-Quarantäneverzeichnis werden als erfolgreiche Operation mit Warnung protokolliert und zerstören die wiederherstellbaren Dateien nicht blind.
|
%ProgramData%\BizTalkPlatformManagementTool\
|
||||||
|
Wartungen\
|
||||||
|
Logs\
|
||||||
|
```
|
||||||
|
|
||||||
Das dauerhafte Setup-Log enthält pro Lauf einen Kontextkopf mit Version, Betriebssystem, 32-/64-Bit-Kontext, CLR, Rechner, Identität und Elevation. Hinzu kommen Pfade, freier Speicher, Dateiversion/-größe/-SHA-256, vollständige Self-Test-Ausgaben mit Exitcode, Exception-Ketten mit HRESULT und Stacktrace sowie jeder Rollbackschritt. Stabile `SETUP-*`- und `UNINSTALL-*`-Fehlercodes ermöglichen die Zuordnung unabhängig vom lokalisierten Meldungstext. Die Aufbewahrung beträgt 90 Tage. Wenn ProgramData für die Diagnose nicht verfügbar ist, wird ein Temp-Fallback verwendet und sichtbar gemeldet; die Schaltfläche **Diagnoselogs öffnen** öffnet den regulären Ablageort.
|
Die Trennung verhindert, dass ein Update Zustandsdateien überschreibt, und
|
||||||
|
ermöglicht normale Logrotation außerhalb des geschützten Programmverzeichnisses.
|
||||||
|
Logs werden für den aktuellen Tag und vier vorherige Tage aufbewahrt.
|
||||||
|
|
||||||
Der Self-Test ist eine technische Installationsabnahme ohne BizTalk-WMI. Ein fachlicher Umgebungszustand wie fehlender WMI-Zugriff oder `Unknown` führt deshalb nicht zum Installer-Rollback. Nach erfolgreicher Installation bleiben **Diagnose** und ein Dry-run gegen das Zielsystem verpflichtende Abnahmeschritte.
|
## Installer und Rollback
|
||||||
|
|
||||||
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`.
|
Der .NET-Installer benötigt keine PowerShell. Er validiert die Assembly, baut
|
||||||
|
ein Staging-Verzeichnis auf, verschiebt eine bestehende Installation in ein
|
||||||
|
Backup und aktiviert anschließend die neue Version. Bei einem Fehler wird das
|
||||||
|
Backup zurückverschoben. Startmenü- und optionale Desktop-Verknüpfung werden
|
||||||
|
systemweit angelegt.
|
||||||
|
|
||||||
## Automatisierte Verifikation
|
Der Release-Packager erzeugt den Installationsordner, ein ZIP, eine
|
||||||
|
Base64-Transportdatei und eine SHA-256-Datei.
|
||||||
|
|
||||||
`tests/BizTalkPlatformManagementTool.Tests` prüft derzeit:
|
## Tests
|
||||||
|
|
||||||
- Atomare JSON-Aktualisierung und BOM-Kompatibilität.
|
Die Regressionstests prüfen:
|
||||||
- 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.
|
|
||||||
- Eindeutige Fehlerphase ohne irreführende Rollbackmeldung bei einem Staging-Fehler.
|
|
||||||
- Vollständiger Diagnosekontext mit Exception-Kette und Temp-Fallback für das Setup-Log.
|
|
||||||
- Unabhängigkeit der Installation von Fehlern der UI-Fortschrittsanzeige.
|
|
||||||
|
|
||||||
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.
|
- Restore enthält ausschließlich zuvor aktive Artefakte.
|
||||||
|
- Shutdown enthält ausschließlich zuvor aktive Artefakte.
|
||||||
|
- Receive Locations erhalten ihren zusätzlichen WMI-Qualifier.
|
||||||
|
- Vergleiche trennen gleichnamige Artefakte verschiedener Anwendungen.
|
||||||
|
- fremde Server-Snapshots werden abgewiesen.
|
||||||
|
- atomarer JSON-Ersatz hinterlässt keine temporären Dateien.
|
||||||
|
|
||||||
## Status Mapping
|
Ein echter WMI-Integrationstest muss auf einem BizTalk-2020-System im Dry-run
|
||||||
|
und anschließend in einem freigegebenen Wartungsfenster erfolgen.
|
||||||
- `MSBTS_SendPort.Status`: `1=Bound`, `2=Stopped`, `3=Started`
|
|
||||||
- `MSBTS_Orchestration.OrchestrationStatus`: `1=Unbound`, `2=Bound`, `3=Stopped`, `4=Started`
|
|
||||||
- `MSBTS_ReceiveLocation`: `Enabled = !IsDisabled`
|
|
||||||
- `MSBTS_HostInstance.ServiceState`: `1=Stopped`, `2=StartPending`, `3=StopPending`, `4=Started`
|
|
||||||
|
|
||||||
## Anforderungen
|
|
||||||
|
|
||||||
- Windows Server oder Administrationshost mit .NET Framework 4.6.1
|
|
||||||
- BizTalk Server 2020 oder installierte BizTalk Administration Tools
|
|
||||||
- Berechtigungen auf den WMI-Namespace `root\MicrosoftBizTalkServer`
|
|
||||||
- Lokale Administratorrechte und Start der Anwendung mit **Als Administrator ausführen**
|
|
||||||
- Visual Studio mit .NET Framework 4.6.1 Developer Pack für Builds
|
|
||||||
|
|||||||
+39
-91
@@ -2,110 +2,58 @@
|
|||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- Windows Server 2019/2022 oder ein Windows-Administrationshost
|
- Windows Server 2019/2022 oder Administrationshost
|
||||||
- Microsoft BizTalk Server 2020 oder BizTalk Administration Tools
|
- Microsoft BizTalk Server 2020 oder installierte BizTalk Administration Tools
|
||||||
- .NET Framework 4.6.1 Runtime
|
- .NET Framework 4.6.1 oder neuer
|
||||||
- Lokale Administratorrechte; `Setup.exe` und die Anwendung fordern diese per UAC-Manifest an
|
- lokales Administratorkonto
|
||||||
- Zugriff auf `root\MicrosoftBizTalkServer` mit den erforderlichen BizTalk-Rechten
|
- Mitgliedschaft in der Gruppe **BizTalk Server Administrators**
|
||||||
|
- WMI-Zugriff auf `root\MicrosoftBizTalkServer`
|
||||||
|
|
||||||
## Übergabe als TXT
|
## Installation
|
||||||
|
|
||||||
Das Release erzeugt folgende Dateien unter `artifacts`:
|
1. `BizTalkPlatformManagementTool-Setup.zip` vollständig entpacken.
|
||||||
|
2. `Setup.exe` starten; die UAC-Anforderung bestätigen.
|
||||||
|
3. Optional die systemweite Desktop-Verknüpfung auswählen.
|
||||||
|
4. **Installieren / aktualisieren** wählen.
|
||||||
|
|
||||||
- `BizTalkPlatformManagementTool-Setup.zip.b64.txt`: Certutil-kompatible Base64-Übertragung
|
Installiert wird nach `%ProgramFiles%\BizTalkPlatformManagementTool`.
|
||||||
- `BizTalkPlatformManagementTool-Setup.zip.sha256.txt`: SHA-256 des ZIP-Archivs
|
Persistente Wartungszustände und Logs liegen getrennt unter
|
||||||
- `BizTalkPlatformManagementTool-Setup.zip`: direkt entpackbares Installationspaket
|
`%ProgramData%\BizTalkPlatformManagementTool`.
|
||||||
|
|
||||||
Auf dem Zielsystem wird die TXT-Datei so rekonstruiert und geprüft:
|
## Sicheres Update
|
||||||
|
|
||||||
```bat
|
Der Installer validiert das Paket, kopiert die neue Version zunächst in ein
|
||||||
certutil -decode BizTalkPlatformManagementTool-Setup.zip.b64.txt BizTalkPlatformManagementTool-Setup.zip
|
Staging-Verzeichnis und aktiviert sie erst danach. Eine vorhandene Installation
|
||||||
certutil -hashfile BizTalkPlatformManagementTool-Setup.zip SHA256
|
wird vorübergehend umbenannt. Schlägt das Update fehl, wird die vorherige
|
||||||
type BizTalkPlatformManagementTool-Setup.zip.sha256.txt
|
Programmversion wiederhergestellt. Wartungszustände und Logs unter
|
||||||
```
|
`%ProgramData%` bleiben unberührt.
|
||||||
|
|
||||||
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.
|
Vor dem Update muss das Management Tool geschlossen sein. Laufende
|
||||||
|
Wartungsoperationen dürfen nicht durch ein Update unterbrochen werden.
|
||||||
## Neuinstallation
|
|
||||||
|
|
||||||
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: `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs`
|
|
||||||
- Uninstaller: `%ProgramData%\BizTalkPlatformManagementTool\Setup\Uninstall.exe`
|
|
||||||
- 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. Tritt der Fehler bereits bei Paketprüfung oder Staging auf, meldet das Setup ausdrücklich **Kein Rollback erforderlich**, weil die aktive Installation unverändert blieb. Staging und Backup werden anschließend bestmöglich bereinigt. Kann ein nicht mehr benötigtes Backup wegen einer Dateisperre nicht gelöscht werden, bleibt die erfolgreiche Installation aktiv und das Log kennzeichnet den Abschluss als `success_with_backup_cleanup_warning`.
|
|
||||||
|
|
||||||
## Installer-Diagnose
|
|
||||||
|
|
||||||
Jeder Installations-, Update- und Deinstallationslauf erzeugt ein eigenes UTF-8-Log. Über **Diagnoselogs öffnen** kann der Ordner direkt aus dem Setup geöffnet werden. Die Logs werden 90 Tage aufbewahrt. Ist ProgramData nicht beschreibbar, versucht das Setup ein Fallback unter `%TEMP%\BizTalkPlatformManagementTool\InstallerLogs` und zeigt dessen Pfad an. Kann auch dort kein Log erzeugt werden, bleibt der eigentliche Fehler in der Setup-Meldung sichtbar.
|
|
||||||
|
|
||||||
Die Abschlussmeldung enthält einen stabilen Fehlercode und die betroffene Phase:
|
|
||||||
|
|
||||||
| Fehlercode | Bedeutung |
|
|
||||||
|---|---|
|
|
||||||
| `SETUP-INITIALIZATION` | Daten-/Logverzeichnis oder Ausgangszustand konnte nicht vorbereitet werden |
|
|
||||||
| `SETUP-PACKAGE-VALIDATION` | Manifest, Payload, SHA-256 oder Prüfung auf laufende Anwendung fehlgeschlagen |
|
|
||||||
| `SETUP-STAGING-VALIDATION` | Kopie ins Staging oder erster Self-Test fehlgeschlagen; aktive Installation unverändert |
|
|
||||||
| `SETUP-ACTIVATION` | Sichern oder Aktivieren der Programmdateien fehlgeschlagen |
|
|
||||||
| `SETUP-ACTIVATED-SELFTEST` | Zweiter Self-Test nach der Aktivierung fehlgeschlagen |
|
|
||||||
| `SETUP-WINDOWS-INTEGRATION` | Verknüpfung, Uninstaller oder Registry-Registrierung/Verifikation fehlgeschlagen |
|
|
||||||
| `UNINSTALL-*` | Die entsprechende Initialisierungs-, Prozess-, Quarantäne- oder Integrationsphase der Deinstallation fehlgeschlagen |
|
|
||||||
|
|
||||||
Das Log enthält Betriebssystem, Prozess-/OS-Bitness, .NET-Version, Rechner, Windows-Identität und Elevation sowie Installationspfade, freien Speicher, Dateigrößen, Versionen und SHA-256. Für Self-Tests werden Laufzeit, Exitcode dezimal und hexadezimal sowie vollständiges `stdout` und `stderr` geschrieben. Bei Fehlern folgen Exception-Kette, HRESULT und Stacktrace; jeder Rollbackschritt und sein Ergebnis werden separat protokolliert. Passwörter oder Verbindungsdaten werden vom Installer nicht verarbeitet oder geloggt.
|
|
||||||
|
|
||||||
Für eine Supportanalyse bitte sichern:
|
|
||||||
|
|
||||||
1. Vollständige Fehlermeldung einschließlich Fehlercode und Phase.
|
|
||||||
2. Das jüngste `setup-*.log` aus dem vom Setup angezeigten Pfad.
|
|
||||||
3. Ob Neuinstallation, Update oder Deinstallation ausgeführt wurde und ob Virenscanner/Endpoint Protection eine Datei blockiert hat.
|
|
||||||
|
|
||||||
Ein erfolgreicher Installer-Self-Test bestätigt Paket, Programmstart und lokale Kernfunktionen. Der fachliche BizTalk-Zustand ist bewusst kein Rollbackkriterium: WMI-Erreichbarkeit, Berechtigungen und Umgebungszustand danach separat über **Diagnose** und einen Dry-run prüfen. So wird beispielsweise ein erreichbarer Installer nicht wegen eines fachlichen `Unknown`-Zustands zurückgerollt.
|
|
||||||
|
|
||||||
## Deinstallation
|
## 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.
|
Im Setup kann die Anwendung wieder deinstalliert werden. Standardmäßig bleiben
|
||||||
|
Wartungszustände und Logs erhalten. Die Option kann deaktiviert werden, wenn
|
||||||
|
auch diese Daten bewusst entfernt werden sollen.
|
||||||
|
|
||||||
## Build, Test und Paketierung
|
## Transport-TXT zurückwandeln
|
||||||
|
|
||||||
In einer Visual-Studio-Developer-Eingabeaufforderung mit .NET Framework 4.6.1 Developer Pack:
|
Die Datei `BizTalkPlatformManagementTool-Setup.zip.b64.txt` kann auf dem
|
||||||
|
Zielsystem beispielsweise mit `certutil.exe` zurückgewandelt werden:
|
||||||
|
|
||||||
```bat
|
```cmd
|
||||||
scripts\test-release.cmd
|
certutil.exe -decode BizTalkPlatformManagementTool-Setup.zip.b64.txt BizTalkPlatformManagementTool-Setup.zip
|
||||||
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.
|
Anschließend die SHA-256-Prüfsumme mit
|
||||||
|
`BizTalkPlatformManagementTool-Setup.zip.sha256.txt` vergleichen.
|
||||||
|
|
||||||
Die Release-Konfiguration erzeugt außerdem pro Assembly eine XML-Dokumentationsdatei im jeweiligen `bin\Release`-Verzeichnis. Damit werden XML-Kommentare und `cref`-Referenzen während des Builds compilerseitig geprüft; diese Entwicklerartefakte sind für den Betrieb nicht erforderlich und deshalb nicht Bestandteil der Installer-Payload.
|
## Erster Funktionstest
|
||||||
|
|
||||||
Unter Mono kann der portable Anteil lokal geprüft werden:
|
1. Tool über Startmenü oder Desktop-Verknüpfung starten.
|
||||||
|
2. Dry-run aktiviert lassen.
|
||||||
```sh
|
3. **Verbindung prüfen** ausführen.
|
||||||
msbuild BizTalkPlatformManagementTool.sln /p:Configuration=Release /p:Platform="Any CPU" /m:1
|
4. **Zustand erfassen** ausführen.
|
||||||
mono tests/BizTalkPlatformManagementTool.Tests/bin/Release/BizTalkPlatformManagementTool.Tests.exe
|
5. JSON-, CSV- und HTML-Dateien im Arbeitsordner prüfen.
|
||||||
mono src/BizTalkPlatformManagementTool/bin/Release/BizTalkPlatformManagementTool.exe --self-test
|
6. **Herunterfahren** im Dry-run ausführen und den Plan kontrollieren.
|
||||||
```
|
|
||||||
|
|
||||||
Mono ersetzt nicht die abschließende Prüfung von UAC, Registry, Verknüpfungen und BizTalk-WMI auf einem repräsentativen Windows-/BizTalk-System.
|
|
||||||
|
|||||||
@@ -1,105 +1,105 @@
|
|||||||
# BizTalk Platform Management Tool
|
# BizTalk Platform Management Tool
|
||||||
|
|
||||||
WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations during maintenance windows. The tool captures runtime snapshots, creates shutdown and restore plans, executes those plans in a safe order, and writes diff reports for validation.
|
Grafisches .NET-Werkzeug für kontrollierte Wartungsfenster einer Microsoft
|
||||||
|
BizTalk-Server-2020-Umgebung. Es erfasst den Laufzeitzustand, fährt ausschließlich
|
||||||
|
die zuvor aktiven Artefakte in sicherer Reihenfolge herunter und startet nach
|
||||||
|
dem Patchen ausschließlich diese Artefakte wieder.
|
||||||
|
|
||||||
## Current State
|
## Wichtigste Eigenschaften
|
||||||
|
|
||||||
- Application: C# WinForms
|
- übersichtliche deutschsprachige WinForms-Oberfläche mit klarer Sechs-Schritt-Führung
|
||||||
- Target framework: .NET Framework 4.6.1
|
- Dry-run standardmäßig aktiv
|
||||||
- Solution: `BizTalkPlatformManagementTool.sln`
|
- Vorher-Snapshot als verbindliche Wiederherstellungsgrundlage
|
||||||
- Project: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj`
|
- vorhandener Vorher-Snapshot wird beim Herunterfahren nicht überschrieben
|
||||||
- Primary namespace and assembly: `BizTalkPlatformManagementTool`
|
- servergebundene Validierung verhindert die Verwendung einer Zustandsdatei für den falschen BizTalk-Server
|
||||||
- Legacy PowerShell archive: `archive/powershell/BizTalkPlatformManagementTool.ps1`
|
- Receive Locations, Send Ports, Orchestrierungen und Hostinstanzen
|
||||||
|
- kontrollierte Reihenfolge mit Zustandsprüfung und konfigurierbarem Timeout
|
||||||
|
- nur vorher aktive Artefakte werden heruntergefahren und wieder gestartet
|
||||||
|
- WMI-Objekte werden eindeutig aufgelöst; mehrdeutige Treffer führen sicher zum Abbruch
|
||||||
|
- wiederholbare Ausführung: bereits erreichte Zielzustände werden übersprungen
|
||||||
|
- atomar geschriebene JSON-Dateien mit Größenbegrenzung
|
||||||
|
- keine Transport-URIs, Benutzernamen oder Kennwörter im Snapshot
|
||||||
|
- CSV-/HTML-Berichte und Vorher-/Nachher-Vergleich
|
||||||
|
- tägliche Logs unter `%ProgramData%\BizTalkPlatformManagementTool\Logs`
|
||||||
|
- eigenständiger .NET-Installer für Installation, Update und Deinstallation
|
||||||
|
- ZIP-, SHA-256- und Base64-TXT-Paket werden beim Release-Build erzeugt
|
||||||
|
|
||||||
## Features
|
## Sicherer Wartungsablauf
|
||||||
|
|
||||||
- Snapshot before and after downtime as JSON, CSV and HTML
|
1. Tool als Administrator starten.
|
||||||
- Diff between `before.json` and `after.json`
|
2. Zielserver und Arbeitsordner prüfen.
|
||||||
- Controlled shutdown from the current runtime state
|
3. **Verbindung prüfen** ausführen.
|
||||||
- Controlled restore from `before.json`
|
4. Mit aktiviertem Dry-run **Zustand erfassen**.
|
||||||
- Host instance handling for the selected BizTalk server
|
5. **Herunterfahren** im Dry-run ausführen und `shutdown-plan.json` prüfen.
|
||||||
- Dry-run mode enabled by default
|
6. Dry-run deaktivieren und **Herunterfahren** bestätigen.
|
||||||
- WMI access through `root\MicrosoftBizTalkServer`
|
7. Server manuell patchen.
|
||||||
- Startup check for administrator rights
|
8. Dieselbe Vorher-Zustandsdatei auswählen.
|
||||||
- Detailed operation logging in the GUI and daily rolling log files under ProgramData
|
9. **Wiederherstellen** zunächst im Dry-run prüfen und danach real ausführen.
|
||||||
- Environment status indicator based on host instance state
|
10. **Nachher erfassen** und **Vergleichen**.
|
||||||
- Clear and Close actions in the main toolbar
|
|
||||||
- No compile-time dependency on BizTalk ExplorerOM assemblies
|
|
||||||
- Transactional Windows installer/updater with SHA-256 payload validation and rollback
|
|
||||||
- Installer diagnostics with stable phase codes, complete child-process output, exception chains and a ProgramData-to-Temp log fallback
|
|
||||||
- WMI-free runtime self-test plus automated regression test executable
|
|
||||||
|
|
||||||
## Safe Usage
|
Die Zustandsdatei ist sicherheitskritisch: Sie definiert exakt, welche
|
||||||
|
Artefakte wieder gestartet werden. Bewusst gestoppte, gebundene oder
|
||||||
|
deaktivierte Artefakte bleiben beim Restore unangetastet.
|
||||||
|
|
||||||
1. Start the app with **Run as administrator** on a BizTalk server or a management host with WMI access.
|
## Reihenfolge
|
||||||
2. Keep **Dry run** enabled.
|
|
||||||
3. Click **Diagnose** to verify WMI access.
|
|
||||||
4. Click **Snapshot Before**.
|
|
||||||
5. Click **Shutdown** and review `shutdown-plan.json`.
|
|
||||||
6. Disable **Dry run** only when the plan is correct.
|
|
||||||
7. After maintenance, click **Restore** using the saved `before.json`.
|
|
||||||
8. Click **Snapshot After** and **Compare**.
|
|
||||||
|
|
||||||
The environment indicator shows `Started`, `Stopped`, `Partial` or `Unknown` from the most recent snapshot. `Clear` removes the visible status and operation log grids; it does not delete files.
|
Herunterfahren:
|
||||||
|
|
||||||
The application requests administrator rights through its UAC manifest and checks them again during startup. Only one GUI instance can run per Windows session.
|
1. zuvor aktivierte Receive Locations deaktivieren
|
||||||
|
2. zuvor gestartete Orchestrierungen stoppen
|
||||||
|
3. zuvor gestartete Send Ports stoppen
|
||||||
|
4. zuvor gestartete Hostinstanzen des ausgewählten Servers stoppen
|
||||||
|
|
||||||
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).
|
Wiederherstellen:
|
||||||
|
|
||||||
## Operation Order
|
1. zuvor gestartete Hostinstanzen des ausgewählten Servers starten
|
||||||
|
2. zuvor gestartete Send Ports starten
|
||||||
|
3. zuvor gestartete Orchestrierungen starten
|
||||||
|
4. zuvor aktivierte Receive Locations zuletzt aktivieren
|
||||||
|
|
||||||
Shutdown:
|
## Dateien
|
||||||
- Disable receive locations that were enabled.
|
|
||||||
- Stop orchestrations that were started.
|
|
||||||
- Stop send ports that were started.
|
|
||||||
- Stop host instances that were started on the selected server.
|
|
||||||
|
|
||||||
Restore:
|
Standard-Arbeitsbereich:
|
||||||
- Start host instances that were previously started.
|
|
||||||
- Restore send ports to Started, Stopped or Bound.
|
|
||||||
- Restore orchestrations where safe.
|
|
||||||
- Restore receive locations last.
|
|
||||||
|
|
||||||
Orchestrations that were `Bound` are deliberately left unchanged during restore to avoid accidentally making them `Unbound`.
|
```text
|
||||||
|
%ProgramData%\BizTalkPlatformManagementTool\
|
||||||
|
Wartungen\
|
||||||
|
Wartungsstatus-YYYYMMDD-HHmmss.json
|
||||||
|
shutdown-plan.json
|
||||||
|
shutdown-after.json
|
||||||
|
restore-plan.json
|
||||||
|
restore-after.json
|
||||||
|
after.json
|
||||||
|
diff.json / diff.csv / diff.html
|
||||||
|
Logs\
|
||||||
|
BizTalkPlatformManagementTool-YYYY-MM-DD.log
|
||||||
|
```
|
||||||
|
|
||||||
## Outputs
|
Zu jedem Snapshot entstehen CSV- und HTML-Begleitberichte. Vollständige
|
||||||
|
Transportadressen werden nicht gespeichert, weil sie Zugangsdaten oder andere
|
||||||
|
sensible URI-Bestandteile enthalten können und für die Wiederherstellung nicht
|
||||||
|
benötigt werden.
|
||||||
|
|
||||||
- `before.json`, `after.json`
|
## Build und Tests
|
||||||
- `shutdown-plan.json`, `restore-plan.json`
|
|
||||||
- `shutdown-after.json`, `restore-after.json`
|
|
||||||
- `diff.json`, `diff.csv`, `diff.html`
|
|
||||||
- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html`
|
|
||||||
- Runtime logs 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. If ProgramData is unexpectedly unavailable, logging falls back to the executable directory.
|
Voraussetzung ist das .NET Framework 4.6.1 Developer Pack:
|
||||||
|
|
||||||
## Troubleshooting
|
```cmd
|
||||||
|
msbuild BizTalkPlatformManagementTool.sln /t:Rebuild /p:Configuration=Release /p:Platform="Any CPU"
|
||||||
|
tests\BizTalkPlatformManagementTool.Tests\bin\Release\BizTalkPlatformManagementTool.Tests.exe
|
||||||
|
```
|
||||||
|
|
||||||
The Operation Log shows the WMI class, key property, key value and method for real shutdown and restore steps. WMI objects are resolved with a broad `SELECT * FROM <class>` query and a client-side key filter so names containing special characters do not break the WMI query parser.
|
Der Release-Build erzeugt automatisch:
|
||||||
|
|
||||||
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.
|
```text
|
||||||
|
artifacts\BizTalkPlatformManagementTool-Setup\
|
||||||
|
artifacts\BizTalkPlatformManagementTool-Setup.zip
|
||||||
|
artifacts\BizTalkPlatformManagementTool-Setup.zip.b64.txt
|
||||||
|
artifacts\BizTalkPlatformManagementTool-Setup.zip.sha256.txt
|
||||||
|
```
|
||||||
|
|
||||||
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.
|
## Dokumentation
|
||||||
|
|
||||||
Installer, update and uninstall diagnostics are retained for 90 days under `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs`. The setup UI can open that directory directly. If the primary log cannot be created, setup reports and uses a `%TEMP%\BizTalkPlatformManagementTool\InstallerLogs` fallback. See [Installation](Installation.md#installer-diagnose) for phase codes and the support checklist.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
## Source Documentation
|
|
||||||
|
|
||||||
All C# types and methods in the application, setup, packager and regression project use XML documentation comments. Method contracts include `param`, `typeparam` and `returns` elements where applicable. Release builds generate one XML documentation file per assembly, so malformed or missing public documentation becomes visible during compilation.
|
|
||||||
|
|
||||||
Targeted German inline comments explain non-obvious operational decisions such as WMI client-side filtering, shutdown/restore order, atomic file replacement, CSV formula neutralization and installer transaction boundaries. Trivial statements are intentionally not paraphrased in comments; the comments record the reason or safety constraint behind the code.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- [Installation](Installation.md)
|
- [Installation](Installation.md)
|
||||||
- [Dokumentation](Dokumentation.md)
|
- [Technische Dokumentation](Dokumentation.md)
|
||||||
- [Installer stability analysis](docs/Installer-Stabilitaetsanalyse-2026-08-11.md)
|
- [Microsoft-Referenzen](REFERENCES.md)
|
||||||
- [References](REFERENCES.md)
|
|
||||||
|
|||||||
@@ -12,8 +12,11 @@
|
|||||||
- MSBTS_Orchestration.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-stop-method-wmi
|
- MSBTS_Orchestration.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-stop-method-wmi
|
||||||
- MSBTS_Orchestration.Unenlist (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-unenlist-method-wmi
|
- MSBTS_Orchestration.Unenlist (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-unenlist-method-wmi
|
||||||
- MSBTS_ReceiveLocation (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi
|
- MSBTS_ReceiveLocation (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi
|
||||||
|
- MSBTS_ReceiveLocation.Name / zusammengesetzter Schlüssel: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-name-property-wmi
|
||||||
- MSBTS_ReceiveLocation.Enable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-enable-method-wmi
|
- MSBTS_ReceiveLocation.Enable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-enable-method-wmi
|
||||||
- MSBTS_ReceiveLocation.Disable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-disable-method-wmi
|
- MSBTS_ReceiveLocation.Disable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-disable-method-wmi
|
||||||
- MSBTS_HostInstance (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-wmi
|
- MSBTS_HostInstance (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-wmi
|
||||||
- MSBTS_HostInstance.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-start-method-wmi
|
- MSBTS_HostInstance.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-start-method-wmi
|
||||||
- MSBTS_HostInstance.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-stop-method-wmi
|
- MSBTS_HostInstance.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-stop-method-wmi
|
||||||
|
- MSBTS_HostInstance.ServiceState (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-servicestate-property-wmi
|
||||||
|
- MSBTS_Orchestration.AssemblyName / zusammengesetzter Schlüssel: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-assemblyname-property-wmi
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
# 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.
|
|
||||||
- Temp-Fallback, falls das primäre Installerlog nicht angelegt werden kann.
|
|
||||||
- Stabile Fehlercodes, vollständige Self-Test-Ausgaben, Exception-Ketten, HRESULTs und einzeln protokollierte Rollbackschritte.
|
|
||||||
- Explizite Unterscheidung zwischen Fehlern vor der ersten Mutation und tatsächlich ausgeführtem Rollback.
|
|
||||||
- Rücklesende Verifikation von Verknüpfungen, Uninstaller und zentralen Registrywerten.
|
|
||||||
- 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.
|
|
||||||
- Vierzehn Regressionstests einschließlich manipulierter/zusätzlicher/ausbrechender Payload-Pfade, Staging-Abbruch ohne Mutation, erzwungenem Fehler des zweiten Self-Tests mit Wiederherstellung der Vorversion, Deinstallation über ein Quarantäneverzeichnis, Diagnosekontext, Log-Fallback und Fehlerisolierung der UI-Ausgabe.
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
## Zweite Prüfung nach den Deployment-Erfahrungen des Schwesterprojekts
|
|
||||||
|
|
||||||
Die erneute Prüfung orientierte sich an den dokumentierten Schwierigkeiten aus `biztalk-checkmk-pulse`. Zwei Erkenntnisse wurden bewusst übertragen:
|
|
||||||
|
|
||||||
1. Ein Fehler vor der ersten Änderung darf nicht als erfolgreicher Rollback beschrieben werden. Der Installer meldet daher eindeutig, dass kein Rollback erforderlich war und protokolliert `mutation_started=false`.
|
|
||||||
2. Technische Installationsfähigkeit und fachlicher Laufzeitzustand sind getrennte Abnahmen. Der WMI-freie Self-Test entscheidet über Aktivierung/Rollback; BizTalk-WMI, Berechtigungen und ein möglicher `Unknown`-Zustand werden erst danach mit **Diagnose** und Dry-run bewertet.
|
|
||||||
|
|
||||||
Zusätzlich wurden die Informationen erfasst, die bei einem schwer reproduzierbaren Windows-Deployment typischerweise fehlen: genaue Setup-/Dateiversionen und Hashes, vollständiges `stdout`/`stderr`, dezimaler und hexadezimaler Exitcode, Timeout-/Kill-Ergebnis, Identität/Elevation, Exception-Kette/HRESULT/Stacktrace und jeder Rollbackschritt. Der Self-Test wartet nach einem Timeout höchstens weitere fünf Sekunden auf Prozessende und Ausgabekanäle; ein nicht beendbarer Prozess kann das Setup somit nicht unbegrenzt blockieren.
|
|
||||||
|
|
||||||
## 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. ProgramData-Fehler und Temp-Log-Fallback unter realen Windows-ACLs.
|
|
||||||
7. 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.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
@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%
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
@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%
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
@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%
|
|
||||||
+25
-8
@@ -2,15 +2,32 @@
|
|||||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration><Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
<ProjectGuid>{74A5D422-0BA5-4559-BD81-C89C071A8FE4}</ProjectGuid><OutputType>Exe</OutputType>
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
<RootNamespace>BizTalkPlatformManagementTool.Packager</RootNamespace><AssemblyName>BizTalkPlatformManagementTool.Packager</AssemblyName>
|
<ProjectGuid>{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}</ProjectGuid>
|
||||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion><FileAlignment>512</FileAlignment><Deterministic>true</Deterministic>
|
<OutputType>Exe</OutputType>
|
||||||
|
<RootNamespace>BizTalkPlatformManagementTool.Packager</RootNamespace>
|
||||||
|
<AssemblyName>BizTalkPlatformManagementTool.Packager</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "><DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel></PropertyGroup>
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "><DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel><DocumentationFile>bin\Release\BizTalkPlatformManagementTool.Packager.xml</DocumentationFile></PropertyGroup>
|
<DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel>
|
||||||
<ItemGroup><Reference Include="System" /><Reference Include="System.Core" /><Reference Include="System.IO.Compression" /><Reference Include="System.IO.Compression.FileSystem" /><Reference Include="System.Security" /></ItemGroup>
|
</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><Compile Include="Program.cs" /></ItemGroup>
|
||||||
<ItemGroup><ProjectReference Include="..\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj"><Project>{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}</Project><Name>BizTalkPlatformManagementTool.Setup</Name></ProjectReference></ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj"><Project>{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}</Project><Name>BizTalkPlatformManagementTool</Name><ReferenceOutputAssembly>false</ReferenceOutputAssembly></ProjectReference>
|
||||||
|
<ProjectReference Include="..\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj"><Project>{38E61630-11AB-4F8D-B421-F8A88831C4BD}</Project><Name>BizTalkPlatformManagementTool.Setup</Name><ReferenceOutputAssembly>false</ReferenceOutputAssembly></ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
<Target Name="CreateInstallPackage" AfterTargets="Build" Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<Exec Command=""$(TargetPath)" "$(MSBuildProjectDirectory)\..\.." "$(Configuration)"" />
|
||||||
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,81 +1,92 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using BizTalkPlatformManagementTool.Setup;
|
|
||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Packager
|
namespace BizTalkPlatformManagementTool.Packager
|
||||||
{
|
{
|
||||||
/// <summary>Erzeugt aus den Release-Binärdateien das übertragbare Setup-Paket.</summary>
|
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Erstellt Payload, Manifest, ZIP, Certutil-kompatible Base64-TXT und SHA-256-Datei.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="args">Repository-Wurzel und Build-Konfiguration.</param>
|
|
||||||
/// <returns>Null bei erfolgreicher Paketierung, andernfalls eins.</returns>
|
|
||||||
private static int Main(string[] args)
|
private static int Main(string[] args)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (args.Length != 2) throw new ArgumentException("Usage: BizTalkPlatformManagementTool.Packager.exe <repository-root> <configuration>");
|
if (args.Length != 2) throw new ArgumentException("Usage: Packager.exe <repository-root> <configuration>");
|
||||||
var root = Path.GetFullPath(args[0]);
|
var root = Path.GetFullPath(args[0]);
|
||||||
var configuration = args[1];
|
var configuration = args[1];
|
||||||
var artifacts = Path.Combine(root, "artifacts");
|
var artifacts = Path.Combine(root, "artifacts");
|
||||||
var package = Path.Combine(artifacts, "BizTalkPlatformManagementTool-Setup");
|
var package = Path.Combine(artifacts, "BizTalkPlatformManagementTool-Setup");
|
||||||
var application = Path.Combine(package, "application");
|
var zip = package + ".zip";
|
||||||
var zip = Path.Combine(artifacts, "BizTalkPlatformManagementTool-Setup.zip");
|
var base64 = zip + ".b64.txt";
|
||||||
|
var checksum = zip + ".sha256.txt";
|
||||||
|
RecreateDirectory(package);
|
||||||
|
Directory.CreateDirectory(Path.Combine(package, "application"));
|
||||||
|
Directory.CreateDirectory(Path.Combine(package, "documentation"));
|
||||||
|
|
||||||
if (Directory.Exists(package)) Directory.Delete(package, true);
|
var applicationOutput = Path.Combine(root, "src", "BizTalkPlatformManagementTool", "bin", configuration);
|
||||||
Directory.CreateDirectory(application);
|
var setupOutput = Path.Combine(root, "src", "BizTalkPlatformManagementTool.Setup", "bin", configuration);
|
||||||
Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool.Setup", "bin", configuration, "BizTalkPlatformManagementTool.Setup.exe"), Path.Combine(package, "Setup.exe"));
|
CopyRequired(Path.Combine(applicationOutput, "BizTalkPlatformManagementTool.exe"), Path.Combine(package, "application", "BizTalkPlatformManagementTool.exe"));
|
||||||
Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool", "bin", configuration, "BizTalkPlatformManagementTool.exe"), Path.Combine(application, "BizTalkPlatformManagementTool.exe"));
|
CopyRequired(Path.Combine(applicationOutput, "BizTalkPlatformManagementTool.exe.config"), Path.Combine(package, "application", "BizTalkPlatformManagementTool.exe.config"));
|
||||||
Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool", "bin", configuration, "BizTalkPlatformManagementTool.exe.config"), Path.Combine(application, "BizTalkPlatformManagementTool.exe.config"));
|
CopyRequired(Path.Combine(setupOutput, "BizTalkPlatformManagementTool.Setup.exe"), Path.Combine(package, "Setup.exe"));
|
||||||
Copy(Path.Combine(root, "Installation.md"), Path.Combine(package, "INSTALLATION.md"));
|
CopyRequired(Path.Combine(root, "README.md"), Path.Combine(package, "documentation", "README.md"));
|
||||||
PackageManifest.Write(application, Path.Combine(package, "application.manifest"));
|
CopyRequired(Path.Combine(root, "Dokumentation.md"), Path.Combine(package, "documentation", "Dokumentation.md"));
|
||||||
// Das frisch erzeugte Manifest wird vor dem äußeren ZIP sofort gegen die Payload geprüft.
|
CopyRequired(Path.Combine(root, "Installation.md"), Path.Combine(package, "documentation", "Installation.md"));
|
||||||
PackageManifest.ValidateAndRead(application, Path.Combine(package, "application.manifest"));
|
|
||||||
|
|
||||||
if (File.Exists(zip)) File.Delete(zip);
|
DeleteIfExists(zip);
|
||||||
|
DeleteIfExists(base64);
|
||||||
|
DeleteIfExists(checksum);
|
||||||
ZipFile.CreateFromDirectory(package, zip, CompressionLevel.Optimal, false);
|
ZipFile.CreateFromDirectory(package, zip, CompressionLevel.Optimal, false);
|
||||||
WriteBase64(zip, zip + ".b64.txt");
|
WriteBase64(zip, base64);
|
||||||
File.WriteAllText(zip + ".sha256.txt", PackageManifest.Sha256(zip) + " " + Path.GetFileName(zip) + Environment.NewLine, new UTF8Encoding(false));
|
var hash = ComputeSha256(zip);
|
||||||
Console.WriteLine("SETUP_PACKAGE=" + package);
|
File.WriteAllText(checksum, hash + " " + Path.GetFileName(zip) + "\r\n", new UTF8Encoding(false));
|
||||||
Console.WriteLine("SETUP_ZIP=" + zip);
|
Console.WriteLine("Installationsordner: " + package);
|
||||||
Console.WriteLine("SETUP_BASE64=" + zip + ".b64.txt");
|
Console.WriteLine("Installations-ZIP: " + zip);
|
||||||
Console.WriteLine("SETUP_SHA256=" + zip + ".sha256.txt");
|
Console.WriteLine("Transport-TXT: " + base64);
|
||||||
|
Console.WriteLine("SHA-256: " + hash);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("Packaging failed: " + ex);
|
Console.Error.WriteLine("Packaging fehlgeschlagen: " + ex.Message);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Kopiert eine erforderliche Release-Datei und legt ihr Zielverzeichnis an.</summary>
|
|
||||||
/// <param name="source">Der vorhandene Quelldateipfad.</param>
|
|
||||||
/// <param name="target">Der Zieldateipfad innerhalb des Pakets.</param>
|
|
||||||
private static void Copy(string source, string target)
|
|
||||||
{
|
|
||||||
if (!File.Exists(source)) throw new FileNotFoundException("Required package file missing: " + source, source);
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(target));
|
|
||||||
File.Copy(source, target, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Schreibt eine Datei als Certutil-kompatible Base64-TXT mit 64 Zeichen pro Zeile.</summary>
|
|
||||||
/// <param name="source">Die binäre Quelldatei.</param>
|
|
||||||
/// <param name="target">Die zu erzeugende Textdatei.</param>
|
|
||||||
private static void WriteBase64(string source, string target)
|
private static void WriteBase64(string source, string target)
|
||||||
{
|
{
|
||||||
var encoded = Convert.ToBase64String(File.ReadAllBytes(source));
|
var encoded = Convert.ToBase64String(File.ReadAllBytes(source));
|
||||||
var builder = new StringBuilder(encoded.Length + encoded.Length / 64 + 2);
|
using (var writer = new StreamWriter(target, false, new UTF8Encoding(false)))
|
||||||
for (var offset = 0; offset < encoded.Length; offset += 64)
|
|
||||||
{
|
{
|
||||||
builder.Append(encoded, offset, Math.Min(64, encoded.Length - offset));
|
for (var offset = 0; offset < encoded.Length; offset += 120)
|
||||||
builder.Append('\n');
|
writer.WriteLine(encoded.Substring(offset, Math.Min(120, encoded.Length - offset)));
|
||||||
}
|
}
|
||||||
File.WriteAllText(target, builder.ToString(), new UTF8Encoding(false));
|
}
|
||||||
|
|
||||||
|
private static string ComputeSha256(string path)
|
||||||
|
{
|
||||||
|
using (var algorithm = SHA256.Create())
|
||||||
|
using (var stream = File.OpenRead(path))
|
||||||
|
{
|
||||||
|
return BitConverter.ToString(algorithm.ComputeHash(stream)).Replace("-", string.Empty).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyRequired(string source, string destination)
|
||||||
|
{
|
||||||
|
if (!File.Exists(source)) throw new FileNotFoundException("Datei fehlt: " + source, source);
|
||||||
|
File.Copy(source, destination, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RecreateDirectory(string path)
|
||||||
|
{
|
||||||
|
if (Directory.Exists(path)) Directory.Delete(path, true);
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DeleteIfExists(string path)
|
||||||
|
{
|
||||||
|
if (File.Exists(path)) File.Delete(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-23
@@ -4,42 +4,29 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
<ProjectGuid>{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}</ProjectGuid>
|
<ProjectGuid>{38E61630-11AB-4F8D-B421-F8A88831C4BD}</ProjectGuid>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<RootNamespace>BizTalkPlatformManagementTool.Setup</RootNamespace>
|
<RootNamespace>BizTalkPlatformManagementTool.Setup</RootNamespace>
|
||||||
<AssemblyName>BizTalkPlatformManagementTool.Setup</AssemblyName>
|
<AssemblyName>BizTalkPlatformManagementTool.Setup</AssemblyName>
|
||||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||||
<FileAlignment>512</FileAlignment>
|
|
||||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
|
||||||
<Deterministic>true</Deterministic>
|
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<Prefer32Bit>false</Prefer32Bit>
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
<DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize>
|
<DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel>
|
||||||
<OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
<DebugType>pdbonly</DebugType><Optimize>true</Optimize>
|
<DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel>
|
||||||
<OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel>
|
|
||||||
<DocumentationFile>bin\Release\BizTalkPlatformManagementTool.Setup.xml</DocumentationFile>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.CSharp" />
|
<Reference Include="System" /><Reference Include="System.Core" /><Reference Include="System.Drawing" /><Reference Include="System.Windows.Forms" />
|
||||||
<Reference Include="System" />
|
|
||||||
<Reference Include="System.Core" />
|
|
||||||
<Reference Include="System.Drawing" />
|
|
||||||
<Reference Include="System.Security" />
|
|
||||||
<Reference Include="System.Windows.Forms" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="InstallerEngine.cs" /><Compile Include="MainForm.cs" /><Compile Include="Program.cs" /><None Include="app.manifest" />
|
||||||
<Compile Include="InstallerEngine.cs" />
|
</ItemGroup>
|
||||||
<Compile Include="MainForm.cs" />
|
<ItemGroup>
|
||||||
<Compile Include="PackageManifest.cs" />
|
<ProjectReference Include="..\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj"><Project>{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}</Project><Name>BizTalkPlatformManagementTool</Name><ReferenceOutputAssembly>false</ReferenceOutputAssembly></ProjectReference>
|
||||||
<Compile Include="Program.cs" />
|
|
||||||
<Compile Include="SetupOperationLog.cs" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup><None Include="app.manifest" /></ItemGroup>
|
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,899 +1,146 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Setup
|
namespace BizTalkPlatformManagementTool.Setup
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Führt Installation, Update und Deinstallation mit Staging, Validierung und Rollback aus.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class InstallerEngine
|
internal sealed class InstallerEngine
|
||||||
{
|
{
|
||||||
/// <summary>Dateiname der installierten Hauptanwendung.</summary>
|
private const string ProductName = "BizTalkPlatformManagementTool";
|
||||||
internal const string ApplicationExeName = "BizTalkPlatformManagementTool.exe";
|
private readonly string _packageDirectory;
|
||||||
|
private readonly string _installDirectory;
|
||||||
|
private readonly string _runtimeDirectory;
|
||||||
|
|
||||||
/// <summary>Anzeigename für Verknüpfungen und Windows-Uninstall-Eintrag.</summary>
|
|
||||||
private const string ProductName = "BizTalk Platform Management Tool";
|
|
||||||
|
|
||||||
/// <summary>Aktuelle Produktversion des Installers und Uninstall-Eintrags.</summary>
|
|
||||||
private const string ProductVersion = "2.1.2";
|
|
||||||
|
|
||||||
/// <summary>Maschinenweiter Registrypfad des Windows-Uninstall-Eintrags.</summary>
|
|
||||||
private const string UninstallKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\BizTalkPlatformManagementTool";
|
|
||||||
|
|
||||||
/// <summary>Verzeichnis der entpackten Setup-Dateien.</summary>
|
|
||||||
private readonly string packageDirectory;
|
|
||||||
|
|
||||||
/// <summary>Aktives maschinenweites Programmverzeichnis.</summary>
|
|
||||||
private readonly string installDirectory;
|
|
||||||
|
|
||||||
/// <summary>Dauerhaftes Datenverzeichnis unter ProgramData.</summary>
|
|
||||||
private readonly string dataDirectory;
|
|
||||||
|
|
||||||
/// <summary>Legt fest, ob Registry, Verknüpfungen und Uninstaller verwaltet werden.</summary>
|
|
||||||
private readonly bool registerWindowsIntegration;
|
|
||||||
|
|
||||||
/// <summary>Optional injizierte Self-Test-Funktion für portable Regressionstests.</summary>
|
|
||||||
private readonly Func<string, bool> selfTestRunner;
|
|
||||||
|
|
||||||
/// <summary>Zuletzt tatsächlich verwendetes primäres oder temporäres Diagnoseverzeichnis.</summary>
|
|
||||||
private string lastInstallerLogDirectory;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enthält den vor einer Mutation gesicherten Zustand der Windows-Integration.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class WindowsIntegrationSnapshot
|
|
||||||
{
|
|
||||||
/// <summary>Ruft ab oder legt fest, ob der Uninstall-Schlüssel vorher existierte.</summary>
|
|
||||||
public bool RegistryKeyExisted { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Ruft die vorherigen Registrywerte einschließlich ihres Typs ab oder legt sie fest.</summary>
|
|
||||||
public Dictionary<string, Tuple<object, RegistryValueKind>> RegistryValues { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Ruft den vorherigen Inhalt der Desktop-Verknüpfung ab oder legt ihn fest.</summary>
|
|
||||||
public byte[] DesktopShortcut { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Ruft den vorherigen Inhalt der Startmenü-Verknüpfung ab oder legt ihn fest.</summary>
|
|
||||||
public byte[] StartMenuShortcut { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Ruft den vorherigen Inhalt der Uninstaller-Datei ab oder legt ihn fest.</summary>
|
|
||||||
public byte[] Uninstaller { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialisiert den Installer mit den maschinenweiten Standardzielpfaden.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="packageDirectory">Das Verzeichnis mit Setup-Payload und Manifest.</param>
|
|
||||||
public InstallerEngine(string packageDirectory)
|
public InstallerEngine(string packageDirectory)
|
||||||
: this(
|
|
||||||
packageDirectory,
|
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "BizTalkPlatformManagementTool"),
|
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "BizTalkPlatformManagementTool"),
|
|
||||||
true,
|
|
||||||
null)
|
|
||||||
{
|
{
|
||||||
|
_packageDirectory = Path.GetFullPath(packageDirectory);
|
||||||
|
_installDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), ProductName);
|
||||||
|
_runtimeDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), ProductName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public bool IsInstalled
|
||||||
/// Initialisiert den Installer mit expliziten Pfaden und austauschbarer Self-Test-Ausführung.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="packageDirectory">Das Verzeichnis mit Setup-Payload und Manifest.</param>
|
|
||||||
/// <param name="installDirectory">Das aktive Programmverzeichnis.</param>
|
|
||||||
/// <param name="dataDirectory">Das dauerhafte Daten- und Diagnoseverzeichnis.</param>
|
|
||||||
/// <param name="registerWindowsIntegration"><c>true</c>, wenn Registry und Verknüpfungen verwaltet werden sollen.</param>
|
|
||||||
/// <param name="selfTestRunner">Optionale Testfunktion für Regressionstests; <c>null</c> startet die reale EXE.</param>
|
|
||||||
internal InstallerEngine(string packageDirectory, string installDirectory, string dataDirectory, bool registerWindowsIntegration, Func<string, bool> selfTestRunner)
|
|
||||||
{
|
{
|
||||||
this.packageDirectory = Path.GetFullPath(packageDirectory);
|
get { return File.Exists(Path.Combine(_installDirectory, "BizTalkPlatformManagementTool.exe")); }
|
||||||
this.installDirectory = Path.GetFullPath(installDirectory);
|
|
||||||
this.dataDirectory = Path.GetFullPath(dataDirectory);
|
|
||||||
this.registerWindowsIntegration = registerWindowsIntegration;
|
|
||||||
this.selfTestRunner = selfTestRunner;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Ruft das feste maschinenweite Programmverzeichnis ab.</summary>
|
|
||||||
public string InstallDirectory { get { return installDirectory; } }
|
|
||||||
/// <summary>Ruft das zuletzt verwendete beziehungsweise reguläre Installer-Logverzeichnis ab.</summary>
|
|
||||||
public string InstallerLogDirectory
|
|
||||||
{
|
|
||||||
get { return lastInstallerLogDirectory ?? Path.Combine(dataDirectory, "InstallerLogs"); }
|
|
||||||
}
|
|
||||||
/// <summary>Ruft ab, ob diese Setup-Kopie Payload und Manifest für Installation oder Update enthält.</summary>
|
|
||||||
public bool HasInstallPayload { get { return Directory.Exists(Path.Combine(packageDirectory, "application")) && File.Exists(Path.Combine(packageDirectory, "application.manifest")); } }
|
|
||||||
/// <summary>Ruft ab, ob die Anwendungs-EXE im Installationsziel vorhanden ist.</summary>
|
|
||||||
public bool IsInstalled { get { return File.Exists(Path.Combine(installDirectory, ApplicationExeName)); } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validiert und staged die Payload und installiert oder aktualisiert die Anwendung transaktional.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="createDesktopShortcut"><c>true</c>, wenn eine Desktop-Verknüpfung angelegt werden soll.</param>
|
|
||||||
/// <param name="report">Optionale Fortschrittsausgabe für die Setup-Oberfläche.</param>
|
|
||||||
public void Install(bool createDesktopShortcut, Action<string> report)
|
public void Install(bool createDesktopShortcut, Action<string> report)
|
||||||
{
|
{
|
||||||
var uiReport = report ?? delegate { };
|
report = report ?? delegate { };
|
||||||
var log = SetupOperationLog.Create(dataDirectory, "install-update");
|
var source = Path.Combine(_packageDirectory, "application");
|
||||||
Action<string> write = message => ReportSafely(log, uiReport, "INFO", message);
|
var sourceExe = Path.Combine(source, "BizTalkPlatformManagementTool.exe");
|
||||||
if (!string.IsNullOrEmpty(log.CreationError))
|
RequireFile(sourceExe);
|
||||||
write((log.IsFallback ? "WARNUNG: Primaeres Diagnoselog nicht verfuegbar; Temp-Fallback wird verwendet: " : "WARNUNG: Diagnoselog konnte nicht angelegt werden: ") + log.CreationError);
|
RequireFile(sourceExe + ".config");
|
||||||
if (log.FilePath.Length != 0)
|
var assembly = AssemblyName.GetAssemblyName(sourceExe);
|
||||||
{
|
report("Paket geprüft: Version " + assembly.Version + ".");
|
||||||
lastInstallerLogDirectory = Path.GetDirectoryName(log.FilePath);
|
|
||||||
write("Diagnoselog: " + log.FilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
var sourceApplication = Path.Combine(packageDirectory, "application");
|
var staging = _installDirectory + ".staging." + Guid.NewGuid().ToString("N");
|
||||||
var manifestPath = Path.Combine(packageDirectory, "application.manifest");
|
var backup = _installDirectory + ".backup." + Guid.NewGuid().ToString("N");
|
||||||
// Staging und Backup sind Geschwister des Zielverzeichnisses. Dadurch bleiben
|
var hadInstallation = Directory.Exists(_installDirectory);
|
||||||
// die späteren Directory.Move-Operationen auf demselben Volume atomar.
|
|
||||||
var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N");
|
|
||||||
var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N");
|
|
||||||
var hadExistingInstallation = Directory.Exists(installDirectory);
|
|
||||||
WindowsIntegrationSnapshot integrationSnapshot = null;
|
|
||||||
var backupCreated = false;
|
var backupCreated = false;
|
||||||
var activated = false;
|
var activated = false;
|
||||||
var integrationMutationStarted = false;
|
|
||||||
var phaseCode = "SETUP-INITIALIZATION";
|
|
||||||
var phase = "Initialisierung";
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(dataDirectory);
|
CopyDirectory(source, staging);
|
||||||
log.Write(
|
AssemblyName.GetAssemblyName(Path.Combine(staging, "BizTalkPlatformManagementTool.exe"));
|
||||||
"INFO",
|
report("Neue Version im Staging-Verzeichnis validiert.");
|
||||||
"event=setup_context package_directory=\"" + packageDirectory
|
|
||||||
+ "\" source_application=\"" + sourceApplication
|
|
||||||
+ "\" manifest=\"" + manifestPath
|
|
||||||
+ "\" install_directory=\"" + installDirectory
|
|
||||||
+ "\" data_directory=\"" + dataDirectory
|
|
||||||
+ "\" staging_directory=\"" + stagingDirectory
|
|
||||||
+ "\" backup_directory=\"" + backupDirectory
|
|
||||||
+ "\" existing_installation=" + hadExistingInstallation
|
|
||||||
+ " desktop_shortcut_requested=" + createDesktopShortcut
|
|
||||||
+ " windows_integration=" + registerWindowsIntegration);
|
|
||||||
LogDriveSpace(log, installDirectory);
|
|
||||||
log.WriteFileDetails("setup_executable", Assembly.GetExecutingAssembly().Location);
|
|
||||||
log.WriteFileDetails("existing_application", Path.Combine(installDirectory, ApplicationExeName));
|
|
||||||
// Der vollständige Integrationszustand wird vor der ersten Mutation gesichert.
|
|
||||||
integrationSnapshot = registerWindowsIntegration ? CaptureWindowsIntegration() : null;
|
|
||||||
if (integrationSnapshot != null)
|
|
||||||
log.Write("INFO", "event=integration_snapshot registry_key_existed=" + integrationSnapshot.RegistryKeyExisted
|
|
||||||
+ " registry_value_count=" + integrationSnapshot.RegistryValues.Count
|
|
||||||
+ " desktop_shortcut=" + (integrationSnapshot.DesktopShortcut != null)
|
|
||||||
+ " start_menu_shortcut=" + (integrationSnapshot.StartMenuShortcut != null)
|
|
||||||
+ " uninstaller=" + (integrationSnapshot.Uninstaller != null));
|
|
||||||
|
|
||||||
phaseCode = "SETUP-PACKAGE-VALIDATION";
|
if (hadInstallation)
|
||||||
phase = "Paketmanifest und SHA-256 pruefen";
|
|
||||||
write("Phase 1/6: " + phase + ".");
|
|
||||||
var files = PackageManifest.ValidateAndRead(sourceApplication, manifestPath);
|
|
||||||
RequirePayload(files, ApplicationExeName);
|
|
||||||
RequirePayload(files, ApplicationExeName + ".config");
|
|
||||||
log.WriteFileDetails("package_manifest", manifestPath);
|
|
||||||
foreach (var file in files)
|
|
||||||
log.Write("INFO", "event=payload_validated relative_path=\"" + file.RelativePath + "\" size_bytes=" + file.Length + " sha256=" + file.Sha256);
|
|
||||||
EnsureApplicationNotRunning(log);
|
|
||||||
|
|
||||||
phaseCode = "SETUP-STAGING-VALIDATION";
|
|
||||||
phase = "Update in isoliertes Staging kopieren und pruefen";
|
|
||||||
write("Phase 2/6: " + phase + ".");
|
|
||||||
CopyPayload(sourceApplication, stagingDirectory, files);
|
|
||||||
var stagedExe = Path.Combine(stagingDirectory, ApplicationExeName);
|
|
||||||
RunAndValidateSelfTest(stagedExe, "staging", log);
|
|
||||||
write("Staging-Self-Test erfolgreich.");
|
|
||||||
|
|
||||||
phaseCode = "SETUP-ACTIVATION";
|
|
||||||
phase = "Vorhandene Version sichern und Staging atomar aktivieren";
|
|
||||||
write("Phase 3/6: " + phase + ". Ab hier beginnt die Systemaenderung.");
|
|
||||||
// Erst nach Manifestprüfung und bestandenem Staging-Self-Test wird die aktive Version verändert.
|
|
||||||
if (hadExistingInstallation)
|
|
||||||
{
|
{
|
||||||
log.Write("INFO", "event=directory_move role=backup source=\"" + installDirectory + "\" target=\"" + backupDirectory + "\"");
|
Directory.Move(_installDirectory, backup);
|
||||||
Directory.Move(installDirectory, backupDirectory);
|
|
||||||
backupCreated = true;
|
backupCreated = true;
|
||||||
log.Write("INFO", "event=directory_move_complete role=backup");
|
|
||||||
}
|
}
|
||||||
log.Write("INFO", "event=directory_move role=activate source=\"" + stagingDirectory + "\" target=\"" + installDirectory + "\"");
|
|
||||||
Directory.Move(stagingDirectory, installDirectory);
|
Directory.Move(staging, _installDirectory);
|
||||||
activated = true;
|
activated = true;
|
||||||
log.Write("INFO", "event=directory_move_complete role=activate");
|
Directory.CreateDirectory(Path.Combine(_runtimeDirectory, "Wartungen"));
|
||||||
|
Directory.CreateDirectory(Path.Combine(_runtimeDirectory, "Logs"));
|
||||||
phaseCode = "SETUP-ACTIVATED-SELFTEST";
|
CreateShortcuts(createDesktopShortcut);
|
||||||
phase = "Aktivierte Version erneut pruefen";
|
report(hadInstallation ? "Programmdateien sicher aktualisiert." : "Programmdateien installiert.");
|
||||||
write("Phase 4/6: " + phase + ".");
|
report("Arbeitsdaten bleiben unter " + _runtimeDirectory + ".");
|
||||||
var targetExe = Path.Combine(installDirectory, ApplicationExeName);
|
TryDeleteDirectory(backup);
|
||||||
RunAndValidateSelfTest(targetExe, "activated", log);
|
|
||||||
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));
|
|
||||||
|
|
||||||
phaseCode = "SETUP-WINDOWS-INTEGRATION";
|
|
||||||
phase = "Windows-Integration registrieren und verifizieren";
|
|
||||||
write("Phase 5/6: " + phase + ".");
|
|
||||||
if (registerWindowsIntegration)
|
|
||||||
{
|
|
||||||
integrationMutationStarted = true;
|
|
||||||
RegisterWindowsIntegration(targetExe, createDesktopShortcut);
|
|
||||||
ValidateWindowsIntegration(targetExe, createDesktopShortcut, log);
|
|
||||||
}
|
|
||||||
|
|
||||||
phaseCode = "SETUP-CLEANUP";
|
|
||||||
phase = "Backup bereinigen";
|
|
||||||
write("Phase 6/6: " + phase + ".");
|
|
||||||
var backupCleanupSucceeded = TryDeleteDirectory(backupDirectory, write);
|
|
||||||
log.Write(
|
|
||||||
backupCleanupSucceeded ? "INFO" : "WARN",
|
|
||||||
"event=setup_completed result=" + (backupCleanupSucceeded ? "success" : "success_with_backup_cleanup_warning")
|
|
||||||
+ " backup_directory_preserved=" + Directory.Exists(backupDirectory)
|
|
||||||
+ " install_directory=\"" + installDirectory + "\"");
|
|
||||||
write("Installation/Update erfolgreich abgeschlossen: " + installDirectory);
|
|
||||||
write("Technische Installationsabnahme erfolgreich. BizTalk-WMI wurde bewusst nicht als Installerkriterium verwendet; bitte anschliessend in der Anwendung 'Diagnose' und einen Dry-run ausfuehren.");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
log.WriteException(phaseCode, phase, ex);
|
|
||||||
var rollbackErrors = new List<string>();
|
var rollbackErrors = new List<string>();
|
||||||
var mutationStarted = backupCreated || activated || integrationMutationStarted;
|
|
||||||
var fileRollbackSucceeded = true;
|
|
||||||
if (!mutationStarted)
|
|
||||||
{
|
|
||||||
write("Kein Rollback erforderlich: Der Fehler trat vor der ersten Systemaenderung auf.");
|
|
||||||
log.Write("ERROR", "event=rollback_summary result=not_required mutation_started=false");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Dateisystem und Windows-Integration werden unabhängig behandelt, damit
|
|
||||||
// ein Fehler in einem Teil den Diagnosezustand des anderen nicht verdeckt.
|
|
||||||
log.Write("WARN", "event=rollback_started activated=" + activated + " backup_created=" + backupCreated + " integration_mutation_started=" + integrationMutationStarted);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (activated && Directory.Exists(installDirectory))
|
if (activated && Directory.Exists(_installDirectory)) Directory.Delete(_installDirectory, true);
|
||||||
{
|
if (backupCreated && Directory.Exists(backup)) Directory.Move(backup, _installDirectory);
|
||||||
log.Write("WARN", "event=rollback_step step=remove_activated_directory path=\"" + installDirectory + "\"");
|
|
||||||
Directory.Delete(installDirectory, true);
|
|
||||||
}
|
}
|
||||||
if (backupCreated && Directory.Exists(backupDirectory))
|
catch (Exception rollback)
|
||||||
{
|
{
|
||||||
log.Write("WARN", "event=rollback_step step=restore_backup source=\"" + backupDirectory + "\" target=\"" + installDirectory + "\"");
|
rollbackErrors.Add(rollback.Message);
|
||||||
Directory.Move(backupDirectory, installDirectory);
|
|
||||||
}
|
|
||||||
write(backupCreated ? "Rollback: vorherige Programmversion wiederhergestellt." : "Rollback: unvollstaendige Neuinstallation entfernt.");
|
|
||||||
log.Write("WARN", "event=rollback_step_complete step=files result=success");
|
|
||||||
}
|
|
||||||
catch (Exception rollbackException)
|
|
||||||
{
|
|
||||||
fileRollbackSucceeded = false;
|
|
||||||
rollbackErrors.Add("Programmdateien: " + rollbackException.Message);
|
|
||||||
log.WriteException("SETUP-ROLLBACK-FILES", "Rollback Programmdateien", rollbackException);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (registerWindowsIntegration && integrationSnapshot != null)
|
|
||||||
{
|
|
||||||
log.Write("WARN", "event=rollback_step step=restore_windows_integration");
|
|
||||||
RestoreWindowsIntegration(integrationSnapshot);
|
|
||||||
log.Write("WARN", "event=rollback_step_complete step=windows_integration result=success");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception rollbackException)
|
|
||||||
{
|
|
||||||
rollbackErrors.Add("Windows-Integration: " + rollbackException.Message);
|
|
||||||
log.WriteException("SETUP-ROLLBACK-INTEGRATION", "Rollback Windows-Integration", rollbackException);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TryDeleteDirectory(stagingDirectory, write);
|
|
||||||
if (fileRollbackSucceeded && !Directory.Exists(installDirectory) && Directory.Exists(backupDirectory))
|
|
||||||
{
|
|
||||||
fileRollbackSucceeded = false;
|
|
||||||
rollbackErrors.Add("Programmdateien: Backup blieb ohne aktives Installationsverzeichnis bestehen: " + backupDirectory);
|
|
||||||
log.Write("ERROR", "event=rollback_invariant_failed active_directory=false backup_directory=true backup_path=\"" + backupDirectory + "\"");
|
|
||||||
}
|
|
||||||
var rollback = !mutationStarted
|
|
||||||
? "Kein Rollback erforderlich."
|
|
||||||
: rollbackErrors.Count == 0
|
|
||||||
? "Rollback erfolgreich."
|
|
||||||
: "Rollback unvollstaendig: " + string.Join(" | ", rollbackErrors);
|
|
||||||
log.Write("ERROR", "event=rollback_summary result=\"" + rollback + "\" backup_preserved=" + Directory.Exists(backupDirectory));
|
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Installation/Update fehlgeschlagen. Fehlercode=" + phaseCode + ". Phase='" + phase + "'. "
|
"Installation/Update fehlgeschlagen. "
|
||||||
+ rollback + " Ursache: " + ex.Message + " Diagnoselog: " + (log.FilePath.Length == 0 ? "nicht verfuegbar" : log.FilePath),
|
+ (rollbackErrors.Count == 0 ? "Die vorherige Version wurde wiederhergestellt. " : "Rollbackfehler: " + string.Join(" | ", rollbackErrors) + ". ")
|
||||||
|
+ "Ursache: " + ex.Message,
|
||||||
ex);
|
ex);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Entfernt das aktive Programmverzeichnis und die registrierte Windows-Integration.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="report">Optionale Fortschrittsausgabe für die Setup-Oberfläche.</param>
|
|
||||||
public void Uninstall(Action<string> report)
|
|
||||||
{
|
|
||||||
var uiReport = report ?? delegate { };
|
|
||||||
var log = SetupOperationLog.Create(dataDirectory, "uninstall");
|
|
||||||
Action<string> write = message => ReportSafely(log, uiReport, "INFO", message);
|
|
||||||
if (!string.IsNullOrEmpty(log.CreationError))
|
|
||||||
write((log.IsFallback ? "WARNUNG: Primaeres Diagnoselog nicht verfuegbar; Temp-Fallback wird verwendet: " : "WARNUNG: Diagnoselog konnte nicht angelegt werden: ") + log.CreationError);
|
|
||||||
if (log.FilePath.Length != 0)
|
|
||||||
{
|
|
||||||
lastInstallerLogDirectory = Path.GetDirectoryName(log.FilePath);
|
|
||||||
write("Diagnoselog: " + log.FilePath);
|
|
||||||
}
|
|
||||||
WindowsIntegrationSnapshot integrationSnapshot = null;
|
|
||||||
var removalDirectory = installDirectory + ".removed." + Guid.NewGuid().ToString("N");
|
|
||||||
var filesMoved = false;
|
|
||||||
var phaseCode = "UNINSTALL-INITIALIZATION";
|
|
||||||
var phase = "Initialisierung";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(dataDirectory);
|
|
||||||
log.Write("INFO", "event=uninstall_context install_directory=\"" + installDirectory + "\" removal_directory=\"" + removalDirectory + "\" data_directory=\"" + dataDirectory + "\"");
|
|
||||||
log.WriteFileDetails("installed_application", Path.Combine(installDirectory, ApplicationExeName));
|
|
||||||
integrationSnapshot = registerWindowsIntegration ? CaptureWindowsIntegration() : null;
|
|
||||||
|
|
||||||
phaseCode = "UNINSTALL-RUNNING-APPLICATION";
|
|
||||||
phase = "Laufende Anwendung ausschliessen";
|
|
||||||
write("Phase 1/3: " + phase + ".");
|
|
||||||
EnsureApplicationNotRunning(log);
|
|
||||||
|
|
||||||
phaseCode = "UNINSTALL-QUARANTINE";
|
|
||||||
phase = "Programmverzeichnis deaktivieren";
|
|
||||||
write("Phase 2/3: " + phase + ".");
|
|
||||||
// Die atomare Umbenennung deaktiviert die Anwendung, ohne die einzige
|
|
||||||
// wiederherstellbare Kopie vor Abschluss der Deinstallation zu löschen.
|
|
||||||
if (Directory.Exists(installDirectory))
|
|
||||||
{
|
|
||||||
log.Write("INFO", "event=directory_move role=uninstall_quarantine source=\"" + installDirectory + "\" target=\"" + removalDirectory + "\"");
|
|
||||||
Directory.Move(installDirectory, removalDirectory);
|
|
||||||
filesMoved = true;
|
|
||||||
log.Write("INFO", "event=directory_move_complete role=uninstall_quarantine");
|
|
||||||
}
|
|
||||||
|
|
||||||
phaseCode = "UNINSTALL-WINDOWS-INTEGRATION";
|
|
||||||
phase = "Windows-Integration entfernen";
|
|
||||||
write("Phase 3/3: " + phase + ".");
|
|
||||||
if (registerWindowsIntegration) RemoveWindowsIntegration(log);
|
|
||||||
var cleanupSucceeded = TryDeleteDirectory(removalDirectory, write);
|
|
||||||
log.Write("INFO", "event=uninstall_completed result=" + (cleanupSucceeded ? "success" : "success_with_quarantine_cleanup_warning")
|
|
||||||
+ " removal_directory_preserved=" + Directory.Exists(removalDirectory));
|
|
||||||
write("Deinstallation erfolgreich. Installer-Logs bleiben erhalten: " + Path.Combine(dataDirectory, "InstallerLogs"));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
log.WriteException(phaseCode, phase, ex);
|
|
||||||
var rollbackErrors = new List<string>();
|
|
||||||
if (filesMoved && !Directory.Exists(installDirectory) && Directory.Exists(removalDirectory))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Move(removalDirectory, installDirectory);
|
|
||||||
log.Write("WARN", "event=uninstall_rollback_step step=restore_program_directory result=success");
|
|
||||||
}
|
|
||||||
catch (Exception rollbackException)
|
|
||||||
{
|
|
||||||
rollbackErrors.Add("Programmverzeichnis: " + rollbackException.Message);
|
|
||||||
log.WriteException("UNINSTALL-ROLLBACK-FILES", "Deinstallationsrollback Programmverzeichnis", rollbackException);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (registerWindowsIntegration && integrationSnapshot != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
RestoreWindowsIntegration(integrationSnapshot);
|
|
||||||
log.Write("WARN", "event=uninstall_rollback_step step=restore_windows_integration result=success");
|
|
||||||
}
|
|
||||||
catch (Exception rollbackException)
|
|
||||||
{
|
|
||||||
rollbackErrors.Add("Windows-Integration: " + rollbackException.Message);
|
|
||||||
log.WriteException("UNINSTALL-ROLLBACK-INTEGRATION", "Deinstallationsrollback Windows-Integration", rollbackException);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var rollback = rollbackErrors.Count == 0 ? "Rollback erfolgreich." : "Rollback unvollstaendig: " + string.Join(" | ", rollbackErrors);
|
|
||||||
log.Write("ERROR", "event=uninstall_rollback_summary result=\"" + rollback + "\"");
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Deinstallation fehlgeschlagen. Fehlercode=" + phaseCode + ". Phase='" + phase + "'. " + rollback
|
|
||||||
+ " Ursache: " + ex.Message + " Diagnoselog: " + (log.FilePath.Length == 0 ? "nicht verfuegbar" : log.FilePath),
|
|
||||||
ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registriert Startmenü, optionale Desktop-Verknüpfung, Uninstaller und Uninstall-Schlüssel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="targetExe">Der vollständige Pfad der aktivierten Anwendung.</param>
|
|
||||||
/// <param name="createDesktopShortcut"><c>true</c>, wenn eine Desktop-Verknüpfung gewünscht ist.</param>
|
|
||||||
private void RegisterWindowsIntegration(string targetExe, bool createDesktopShortcut)
|
|
||||||
{
|
|
||||||
var programsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Entfernt und verifiziert die vom Setup verwaltete Windows-Integration.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="log">Das Diagnoseprotokoll des aktuellen Setup-Laufs.</param>
|
|
||||||
private void RemoveWindowsIntegration(SetupOperationLog log)
|
|
||||||
{
|
|
||||||
DeleteFileIfExists(DesktopShortcutPath);
|
|
||||||
var programsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName);
|
|
||||||
DeleteFileIfExists(Path.Combine(programsDirectory, ProductName + ".lnk"));
|
|
||||||
if (Directory.Exists(programsDirectory) && Directory.GetFileSystemEntries(programsDirectory).Length == 0) Directory.Delete(programsDirectory);
|
|
||||||
Registry.LocalMachine.DeleteSubKeyTree(UninstallKeyPath, false);
|
|
||||||
using (var remainingKey = Registry.LocalMachine.OpenSubKey(UninstallKeyPath, false))
|
|
||||||
{
|
|
||||||
if (File.Exists(DesktopShortcutPath) || File.Exists(StartMenuShortcutPath) || remainingKey != null)
|
|
||||||
throw new InvalidOperationException("Windows integration removal verification failed.");
|
|
||||||
}
|
|
||||||
log.Write("INFO", "event=windows_integration_removed registry_key=\"HKLM\\" + UninstallKeyPath + "\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Erstellt eine Windows-Verknüpfung über Windows Script Host und gibt COM-Objekte deterministisch frei.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="shortcutPath">Der vollständige Pfad der Verknüpfung.</param>
|
|
||||||
/// <param name="targetPath">Der Zielpfad der Verknüpfung.</param>
|
|
||||||
/// <param name="workingDirectory">Das Arbeitsverzeichnis des Ziels.</param>
|
|
||||||
/// <param name="description">Die in Windows sichtbare Beschreibung.</param>
|
|
||||||
private static void CreateShortcut(string shortcutPath, string targetPath, string workingDirectory, string description)
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(shortcutPath));
|
|
||||||
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
|
finally
|
||||||
{
|
{
|
||||||
if (shortcut != null && Marshal.IsComObject(shortcut)) Marshal.FinalReleaseComObject(shortcut);
|
TryDeleteDirectory(staging);
|
||||||
if (shell != null && Marshal.IsComObject(shell)) Marshal.FinalReleaseComObject(shell);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public void Uninstall(bool keepRuntimeData, Action<string> report)
|
||||||
/// Liest Registrywerte, Verknüpfungen und Uninstaller vor einer möglichen Mutation ein.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Ein wiederherstellbarer Snapshot der Windows-Integration.</returns>
|
|
||||||
private WindowsIntegrationSnapshot CaptureWindowsIntegration()
|
|
||||||
{
|
{
|
||||||
var snapshot = new WindowsIntegrationSnapshot
|
report = report ?? delegate { };
|
||||||
{
|
DeleteShortcuts();
|
||||||
RegistryValues = new Dictionary<string, Tuple<object, RegistryValueKind>>(StringComparer.OrdinalIgnoreCase),
|
if (Directory.Exists(_installDirectory)) Directory.Delete(_installDirectory, true);
|
||||||
DesktopShortcut = ReadFileOrNull(DesktopShortcutPath),
|
if (!keepRuntimeData && Directory.Exists(_runtimeDirectory)) Directory.Delete(_runtimeDirectory, true);
|
||||||
StartMenuShortcut = ReadFileOrNull(StartMenuShortcutPath),
|
report(keepRuntimeData
|
||||||
Uninstaller = ReadFileOrNull(UninstallerPath)
|
? "Programm entfernt; Wartungszustände und Logs wurden beibehalten."
|
||||||
};
|
: "Programm und Arbeitsdaten wurden entfernt.");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private void CreateShortcuts(bool desktop)
|
||||||
/// Stellt einen zuvor erfassten Zustand der Windows-Integration wieder her.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="snapshot">Der wiederherzustellende Integrationszustand.</param>
|
|
||||||
private void RestoreWindowsIntegration(WindowsIntegrationSnapshot snapshot)
|
|
||||||
{
|
{
|
||||||
if (snapshot == null) return;
|
var target = Path.Combine(_installDirectory, "BizTalkPlatformManagementTool.exe");
|
||||||
Registry.LocalMachine.DeleteSubKeyTree(UninstallKeyPath, false);
|
var programs = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), "JR IT Services");
|
||||||
if (snapshot.RegistryKeyExisted)
|
Directory.CreateDirectory(programs);
|
||||||
{
|
WriteInternetShortcut(Path.Combine(programs, "BizTalk Platform Management Tool.url"), target);
|
||||||
using (var key = Registry.LocalMachine.CreateSubKey(UninstallKeyPath))
|
var desktopPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), "BizTalk Platform Management Tool.url");
|
||||||
{
|
if (desktop) WriteInternetShortcut(desktopPath, target); else if (File.Exists(desktopPath)) File.Delete(desktopPath);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static void WriteInternetShortcut(string path, string target)
|
||||||
/// Liest eine Datei vollständig oder bildet ihr Nichtvorhandensein als <c>null</c> ab.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Der zu lesende Dateipfad.</param>
|
|
||||||
/// <returns>Der Dateiinhalt oder <c>null</c>, wenn die Datei nicht existiert.</returns>
|
|
||||||
private static byte[] ReadFileOrNull(string path)
|
|
||||||
{
|
{
|
||||||
return File.Exists(path) ? File.ReadAllBytes(path) : null;
|
var uri = new Uri(target).AbsoluteUri;
|
||||||
|
var content = "[InternetShortcut]\r\nURL=" + uri + "\r\nIconFile=" + target + "\r\nIconIndex=0\r\n";
|
||||||
|
File.WriteAllText(path, content, new UTF8Encoding(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static void DeleteShortcuts()
|
||||||
/// Stellt eine Datei exakt wieder her oder entfernt sie, wenn sie vorher nicht vorhanden war.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Der wiederherzustellende Dateipfad.</param>
|
|
||||||
/// <param name="content">Der vorherige Inhalt oder <c>null</c> für „nicht vorhanden“.</param>
|
|
||||||
private static void RestoreFile(string path, byte[] content)
|
|
||||||
{
|
{
|
||||||
if (content == null)
|
var start = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), "JR IT Services", "BizTalk Platform Management Tool.url");
|
||||||
{
|
var desktop = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), "BizTalk Platform Management Tool.url");
|
||||||
DeleteFileIfExists(path);
|
if (File.Exists(start)) File.Delete(start);
|
||||||
return;
|
if (File.Exists(desktop)) File.Delete(desktop);
|
||||||
}
|
|
||||||
// Beim Rollback des laufenden Uninstallers kann dessen Datei bereits exakt dem
|
|
||||||
// Snapshot entsprechen; dann vermeiden wir einen unnötigen Schreibzugriff auf die aktive EXE.
|
|
||||||
if (File.Exists(path) && File.ReadAllBytes(path).SequenceEqual(content)) return;
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
|
||||||
File.WriteAllBytes(path, content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static void CopyDirectory(string source, string destination)
|
||||||
/// Verhindert Mutation, solange die installierte Anwendung noch ausgeführt wird.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="log">Das Diagnoseprotokoll für Prozessfund und Zugriffsfehler.</param>
|
|
||||||
private void EnsureApplicationNotRunning(SetupOperationLog log)
|
|
||||||
{
|
{
|
||||||
var target = Path.Combine(installDirectory, ApplicationExeName);
|
Directory.CreateDirectory(destination);
|
||||||
if (!File.Exists(target))
|
foreach (var file in Directory.GetFiles(source))
|
||||||
{
|
File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), false);
|
||||||
log.Write("INFO", "event=running_application_check target_exists=false target=\"" + target + "\"");
|
foreach (var directory in Directory.GetDirectories(source))
|
||||||
return;
|
CopyDirectory(directory, Path.Combine(destination, Path.GetFileName(directory)));
|
||||||
}
|
|
||||||
var candidates = 0;
|
|
||||||
foreach (var process in Process.GetProcessesByName(Path.GetFileNameWithoutExtension(ApplicationExeName)))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
candidates++;
|
|
||||||
var processId = process.Id;
|
|
||||||
log.Write("INFO", "event=running_application_candidate_detected pid=" + processId.ToString(CultureInfo.InvariantCulture));
|
|
||||||
string processPath;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
processPath = process.MainModule.FileName;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
log.WriteException("SETUP-RUNNING-APPLICATION-INSPECTION", "Prozesspfad fuer PID " + processId + " pruefen", ex);
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Ein gleichnamiger Prozess (PID " + processId + ") konnte nicht sicher geprueft werden. Beenden Sie ihn oder pruefen Sie Berechtigungen und versuchen Sie es erneut.",
|
|
||||||
ex);
|
|
||||||
}
|
|
||||||
var matches = string.Equals(Path.GetFullPath(processPath), Path.GetFullPath(target), StringComparison.OrdinalIgnoreCase);
|
|
||||||
log.Write("INFO", "event=running_application_candidate pid=" + processId.ToString(CultureInfo.InvariantCulture)
|
|
||||||
+ " session_id=" + process.SessionId.ToString(CultureInfo.InvariantCulture)
|
|
||||||
+ " path=\"" + processPath + "\" matches_target=" + matches);
|
|
||||||
if (matches)
|
|
||||||
throw new InvalidOperationException(ProductName + " is still running. Close it before installation or removal.");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
process.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Write("INFO", "event=running_application_check target_exists=true candidate_count=" + candidates.ToString(CultureInfo.InvariantCulture) + " result=not_running");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static void RequireFile(string path)
|
||||||
/// Führt den WMI-freien Self-Test aus und validiert Exitcode sowie Erfolgstoken.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="executable">Die zu prüfende Anwendungs-EXE.</param>
|
|
||||||
/// <param name="label">Die Phasenbezeichnung für Log und Fehlermeldung.</param>
|
|
||||||
/// <param name="log">Das Diagnoseprotokoll für Metadaten und Prozessausgaben.</param>
|
|
||||||
private void RunAndValidateSelfTest(string executable, string label, SetupOperationLog log)
|
|
||||||
{
|
{
|
||||||
log.WriteFileDetails(label + "_self_test_executable", executable);
|
if (!File.Exists(path)) throw new FileNotFoundException("Installationspaket ist unvollständig. Datei fehlt: " + path, path);
|
||||||
var stopwatch = Stopwatch.StartNew();
|
|
||||||
if (selfTestRunner != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var successful = selfTestRunner(executable);
|
|
||||||
stopwatch.Stop();
|
|
||||||
log.Write(
|
|
||||||
successful ? "INFO" : "ERROR",
|
|
||||||
"event=self_test_completed label=" + label
|
|
||||||
+ " runner=injected elapsed_ms=" + stopwatch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture)
|
|
||||||
+ " result=" + (successful ? "success" : "failure"));
|
|
||||||
if (!successful) throw new InvalidOperationException("Der WMI-freie Self-Test '" + label + "' ist fehlgeschlagen.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
stopwatch.Stop();
|
|
||||||
log.WriteException("SETUP-SELFTEST-INJECTED", "Self-Test " + label, ex);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var startInfo = new ProcessStartInfo(executable, "--self-test")
|
private static void TryDeleteDirectory(string path)
|
||||||
{
|
{
|
||||||
WorkingDirectory = Path.GetDirectoryName(executable),
|
if (!Directory.Exists(path)) return;
|
||||||
UseShellExecute = false,
|
try { Directory.Delete(path, true); }
|
||||||
CreateNoWindow = true,
|
catch { }
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true
|
|
||||||
};
|
|
||||||
using (var process = Process.Start(startInfo))
|
|
||||||
{
|
|
||||||
if (process == null) throw new InvalidOperationException("Self-Test '" + label + "' konnte nicht gestartet werden.");
|
|
||||||
// Beide Kanäle werden parallel geleert, damit ein voller stdout-/stderr-Puffer
|
|
||||||
// den Kindprozess nicht blockiert und dadurch einen künstlichen Timeout erzeugt.
|
|
||||||
var outputRead = process.StandardOutput.ReadToEndAsync();
|
|
||||||
var errorRead = process.StandardError.ReadToEndAsync();
|
|
||||||
if (!process.WaitForExit(60000))
|
|
||||||
{
|
|
||||||
// Auch nach dem Timeout bleiben Prozessende und Stream-Erfassung begrenzt;
|
|
||||||
// ein nicht beendbarer Kindprozess darf das Setup nicht endlos festhalten.
|
|
||||||
var killResult = "sent";
|
|
||||||
try { process.Kill(); }
|
|
||||||
catch (Exception killException)
|
|
||||||
{
|
|
||||||
killResult = "failed";
|
|
||||||
log.WriteException("SETUP-SELFTEST-KILL", "Self-Test " + label + " nach Timeout beenden", killException);
|
|
||||||
}
|
|
||||||
var exitedAfterKill = false;
|
|
||||||
try { exitedAfterKill = process.WaitForExit(5000); } catch { }
|
|
||||||
var streamsCompleted = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
streamsCompleted = System.Threading.Tasks.Task.WaitAll(new System.Threading.Tasks.Task[] { outputRead, errorRead }, 5000);
|
|
||||||
}
|
|
||||||
catch (Exception captureException)
|
|
||||||
{
|
|
||||||
log.WriteException("SETUP-SELFTEST-CAPTURE", "Self-Test " + label + " Ausgabe nach Timeout erfassen", captureException);
|
|
||||||
}
|
|
||||||
stopwatch.Stop();
|
|
||||||
log.Write(
|
|
||||||
"ERROR",
|
|
||||||
"event=self_test_timeout label=" + label
|
|
||||||
+ " timeout_ms=60000 elapsed_ms=" + stopwatch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture)
|
|
||||||
+ " kill_result=" + killResult
|
|
||||||
+ " exited_after_kill=" + exitedAfterKill
|
|
||||||
+ " streams_completed=" + streamsCompleted
|
|
||||||
+ " stdout=\"" + (outputRead.Status == System.Threading.Tasks.TaskStatus.RanToCompletion ? outputRead.Result : "[capture incomplete]")
|
|
||||||
+ "\" stderr=\"" + (errorRead.Status == System.Threading.Tasks.TaskStatus.RanToCompletion ? errorRead.Result : "[capture incomplete]") + "\"");
|
|
||||||
throw new TimeoutException("Self-Test '" + label + "' hat das Zeitlimit von 60 Sekunden ueberschritten.");
|
|
||||||
}
|
|
||||||
System.Threading.Tasks.Task.WaitAll(outputRead, errorRead);
|
|
||||||
stopwatch.Stop();
|
|
||||||
var output = outputRead.Result;
|
|
||||||
var error = errorRead.Result;
|
|
||||||
log.Write(
|
|
||||||
process.ExitCode == 0 ? "INFO" : "ERROR",
|
|
||||||
"event=self_test_completed label=" + label
|
|
||||||
+ " runner=process elapsed_ms=" + stopwatch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture)
|
|
||||||
+ " exit_code=" + process.ExitCode.ToString(CultureInfo.InvariantCulture)
|
|
||||||
+ " exit_code_hex=0x" + process.ExitCode.ToString("X8", CultureInfo.InvariantCulture)
|
|
||||||
+ " stdout=\"" + output + "\" stderr=\"" + error + "\"");
|
|
||||||
if (process.ExitCode != 0 || output.IndexOf("SELF_TEST_OK", StringComparison.Ordinal) < 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Self-Test '" + label + "' fehlgeschlagen. Exitcode=" + process.ExitCode
|
|
||||||
+ " (0x" + process.ExitCode.ToString("X8", CultureInfo.InvariantCulture) + ")"
|
|
||||||
+ ", Stdout=" + CompactProcessText(output)
|
|
||||||
+ ", Stderr=" + CompactProcessText(error) + ".");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Liest die erzeugte Windows-Integration zurück und vergleicht sie mit dem Sollzustand.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="targetExe">Der erwartete Zielpfad der Anwendung.</param>
|
|
||||||
/// <param name="desktopRequested">Der gewünschte Zustand der Desktop-Verknüpfung.</param>
|
|
||||||
/// <param name="log">Das Diagnoseprotokoll für die validierten Dateien.</param>
|
|
||||||
private void ValidateWindowsIntegration(string targetExe, bool desktopRequested, SetupOperationLog log)
|
|
||||||
{
|
|
||||||
if (!File.Exists(StartMenuShortcutPath)) throw new FileNotFoundException("Start menu shortcut was not created.", StartMenuShortcutPath);
|
|
||||||
if (desktopRequested != File.Exists(DesktopShortcutPath))
|
|
||||||
throw new InvalidOperationException("Desktop shortcut state does not match the setup selection. Requested=" + desktopRequested + ".");
|
|
||||||
if (!File.Exists(UninstallerPath)) throw new FileNotFoundException("Uninstaller was not installed.", UninstallerPath);
|
|
||||||
|
|
||||||
using (var key = Registry.LocalMachine.OpenSubKey(UninstallKeyPath, false))
|
|
||||||
{
|
|
||||||
if (key == null) throw new InvalidOperationException("Windows uninstall registry key is missing after registration.");
|
|
||||||
RequireRegistryValue(key, "DisplayName", ProductName);
|
|
||||||
RequireRegistryValue(key, "DisplayVersion", ProductVersion);
|
|
||||||
RequireRegistryValue(key, "InstallLocation", installDirectory);
|
|
||||||
RequireRegistryValue(key, "DisplayIcon", targetExe);
|
|
||||||
RequireRegistryValue(key, "UninstallString", "\"" + UninstallerPath + "\" --uninstall");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WriteFileDetails("registered_uninstaller", UninstallerPath);
|
|
||||||
log.WriteFileDetails("start_menu_shortcut", StartMenuShortcutPath);
|
|
||||||
if (desktopRequested) log.WriteFileDetails("desktop_shortcut", DesktopShortcutPath);
|
|
||||||
log.Write("INFO", "event=windows_integration_validated registry_key=\"HKLM\\" + UninstallKeyPath + "\" desktop_shortcut=" + desktopRequested);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Vergleicht einen Registrywert mit seinem erwarteten Zeichenfolgenwert.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">Der geöffnete Uninstall-Schlüssel.</param>
|
|
||||||
/// <param name="name">Der Name des zu prüfenden Registrywerts.</param>
|
|
||||||
/// <param name="expected">Der erwartete Wert.</param>
|
|
||||||
private static void RequireRegistryValue(RegistryKey key, string name, string expected)
|
|
||||||
{
|
|
||||||
var actual = Convert.ToString(key.GetValue(name, null, RegistryValueOptions.DoNotExpandEnvironmentNames), CultureInfo.InvariantCulture);
|
|
||||||
if (!string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase))
|
|
||||||
throw new InvalidOperationException("Uninstall registry value '" + name + "' is invalid. Expected='" + expected + "', actual='" + actual + "'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Schreibt zuerst dauerhaft ins Log und isoliert anschließend Fehler des UI-Callbacks.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="log">Das dauerhafte Setup-Protokoll.</param>
|
|
||||||
/// <param name="report">Der optionale UI-Callback.</param>
|
|
||||||
/// <param name="level">Der Log-Level.</param>
|
|
||||||
/// <param name="message">Die auszugebende Nachricht.</param>
|
|
||||||
private static void ReportSafely(SetupOperationLog log, Action<string> report, string level, string message)
|
|
||||||
{
|
|
||||||
log.Write(level, message);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
report(message);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
log.Write("WARN", "event=ui_report_failed " + SetupOperationLog.FormatException(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Protokolliert Dateisystemtyp und freien Speicher des Zielvolumes bestmöglich.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="log">Das Setup-Protokoll.</param>
|
|
||||||
/// <param name="path">Ein Pfad auf dem zu untersuchenden Volume.</param>
|
|
||||||
private static void LogDriveSpace(SetupOperationLog log, string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var root = Path.GetPathRoot(Path.GetFullPath(path));
|
|
||||||
var drive = new DriveInfo(root);
|
|
||||||
log.Write(
|
|
||||||
"INFO",
|
|
||||||
"event=drive_space root=\"" + root + "\" format=\"" + drive.DriveFormat + "\""
|
|
||||||
+ " available_bytes=" + drive.AvailableFreeSpace.ToString(CultureInfo.InvariantCulture)
|
|
||||||
+ " total_bytes=" + drive.TotalSize.ToString(CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
log.Write("WARN", "event=drive_space_failed path=\"" + path + "\" " + SetupOperationLog.FormatException(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verdichtet Prozessausgabe für eine begrenzte Bedienermeldung; das Log bleibt vollständig.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">Die rohe Standard- oder Fehlerausgabe.</param>
|
|
||||||
/// <returns>Eine einzeilige Ausgabe mit maximal 2.000 Zeichen.</returns>
|
|
||||||
private static string CompactProcessText(string value)
|
|
||||||
{
|
|
||||||
var text = (value ?? string.Empty).Replace("\r", string.Empty).Replace("\n", " | ").Trim();
|
|
||||||
const int maxLength = 2000;
|
|
||||||
return text.Length <= maxLength ? text : text.Substring(0, maxLength) + "...[truncated]";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Kopiert deklarierte Payload-Dateien und prüft jede Zielkopie erneut per SHA-256.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sourceRoot">Das validierte Payload-Quellverzeichnis.</param>
|
|
||||||
/// <param name="targetRoot">Das isolierte Staging-Zielverzeichnis.</param>
|
|
||||||
/// <param name="files">Die validierten Manifesteinträge.</param>
|
|
||||||
private static void CopyPayload(string sourceRoot, string targetRoot, IEnumerable<PackageFile> files)
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(targetRoot);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stellt sicher, dass eine betriebsnotwendige Datei im Manifest enthalten ist.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="files">Die validierten Manifesteinträge.</param>
|
|
||||||
/// <param name="relativePath">Der erforderliche relative Dateipfad.</param>
|
|
||||||
private static void RequirePayload(IEnumerable<PackageFile> files, string relativePath)
|
|
||||||
{
|
|
||||||
if (!files.Any(x => string.Equals(x.RelativePath, relativePath, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
throw new InvalidDataException("Required payload file is missing from the manifest: " + relativePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Löscht ein temporäres Verzeichnis bestmöglich und meldet eine verbleibende Kopie als Warnung.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Das zu löschende Verzeichnis.</param>
|
|
||||||
/// <param name="report">Die ausfallsichere Fortschrittsausgabe.</param>
|
|
||||||
/// <returns><c>true</c>, wenn das Verzeichnis anschließend nicht mehr existiert.</returns>
|
|
||||||
private static bool TryDeleteDirectory(string path, Action<string> report)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Directory.Exists(path)) Directory.Delete(path, true);
|
|
||||||
return !Directory.Exists(path);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
report("WARNUNG: Verzeichnis konnte nicht bereinigt werden: " + path + " - " + ex.Message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Löscht eine Datei, sofern sie vorhanden ist, und lässt echte Löschfehler sichtbar werden.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Der zu löschende Dateipfad.</param>
|
|
||||||
private static void DeleteFileIfExists(string path)
|
|
||||||
{
|
|
||||||
if (File.Exists(path)) File.Delete(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Ruft den maschinenweiten Pfad der Desktop-Verknüpfung ab.</summary>
|
|
||||||
private static string DesktopShortcutPath
|
|
||||||
{
|
|
||||||
get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), ProductName + ".lnk"); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Ruft den maschinenweiten Pfad der Startmenü-Verknüpfung ab.</summary>
|
|
||||||
private static string StartMenuShortcutPath
|
|
||||||
{
|
|
||||||
get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), ProductName, ProductName + ".lnk"); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Ruft den dauerhaften Pfad der Uninstaller-Kopie ab.</summary>
|
|
||||||
private string UninstallerPath
|
|
||||||
{
|
|
||||||
get { return Path.Combine(dataDirectory, "Setup", "Uninstall.exe"); }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,204 +1,148 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Setup
|
namespace BizTalkPlatformManagementTool.Setup
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Stellt die Bedienoberfläche für Installation, Update, Deinstallation und Diagnosezugriff bereit.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class MainForm : Form
|
internal sealed class MainForm : Form
|
||||||
{
|
{
|
||||||
/// <summary>Ausführende Installer-Engine.</summary>
|
private readonly InstallerEngine _engine;
|
||||||
private readonly InstallerEngine engine;
|
private readonly CheckBox _desktop = new CheckBox();
|
||||||
|
private readonly CheckBox _keepData = new CheckBox();
|
||||||
|
private readonly Button _install = new Button();
|
||||||
|
private readonly Button _uninstall = new Button();
|
||||||
|
private readonly TextBox _status = new TextBox();
|
||||||
|
|
||||||
/// <summary>Gibt an, ob das Fenster direkt zur Deinstallation geöffnet wurde.</summary>
|
public MainForm(InstallerEngine engine)
|
||||||
private readonly bool uninstallMode;
|
|
||||||
|
|
||||||
/// <summary>Sichtbare Fortschritts- und Diagnoseausgabe.</summary>
|
|
||||||
private readonly TextBox output = new TextBox();
|
|
||||||
|
|
||||||
/// <summary>Auswahl für die optionale maschinenweite Desktop-Verknüpfung.</summary>
|
|
||||||
private readonly CheckBox desktopShortcut = new CheckBox();
|
|
||||||
|
|
||||||
/// <summary>Schaltfläche für Installation oder Update.</summary>
|
|
||||||
private readonly Button installButton = new Button();
|
|
||||||
|
|
||||||
/// <summary>Schaltfläche für die Deinstallation.</summary>
|
|
||||||
private readonly Button uninstallButton = new Button();
|
|
||||||
|
|
||||||
/// <summary>Verhindert parallele Aktionen und Schließen während einer Operation.</summary>
|
|
||||||
private bool busy;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialisiert das Setup-Fenster für den normalen oder direkten Deinstallationsmodus.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="engine">Die ausführende Installer-Engine.</param>
|
|
||||||
/// <param name="uninstallMode"><c>true</c>, wenn das Setup über den Uninstall-Eintrag gestartet wurde.</param>
|
|
||||||
public MainForm(InstallerEngine engine, bool uninstallMode)
|
|
||||||
{
|
{
|
||||||
this.engine = engine;
|
_engine = engine;
|
||||||
this.uninstallMode = uninstallMode;
|
Text = "BizTalk Platform Management Tool – Setup";
|
||||||
Text = "BizTalk Platform Management Tool Setup";
|
ClientSize = new Size(680, 500);
|
||||||
Width = 780;
|
MinimumSize = new Size(696, 539);
|
||||||
Height = 520;
|
|
||||||
MinimumSize = new Size(680, 420);
|
|
||||||
StartPosition = FormStartPosition.CenterScreen;
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
|
AutoScaleMode = AutoScaleMode.Dpi;
|
||||||
|
Font = new Font("Segoe UI", 9F);
|
||||||
|
BackColor = Color.White;
|
||||||
BuildUi();
|
BuildUi();
|
||||||
FormClosing += OnFormClosing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Erstellt und verdrahtet die vollständige Setup-Oberfläche.</summary>
|
|
||||||
private void BuildUi()
|
private void BuildUi()
|
||||||
{
|
{
|
||||||
var root = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(16), RowCount = 5, ColumnCount = 1 };
|
var header = new Panel { Dock = DockStyle.Top, Height = 112, BackColor = Color.FromArgb(28, 67, 102) };
|
||||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
header.Controls.Add(new Label { Text = "BizTalk Platform Management Tool", Location = new Point(24, 20), Size = new Size(620, 34), ForeColor = Color.White, Font = new Font(Font.FontFamily, 17F, FontStyle.Bold) });
|
||||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
header.Controls.Add(new Label { Text = "Installieren oder sicher auf eine neue Version aktualisieren", Location = new Point(26, 62), Size = new Size(620, 25), ForeColor = Color.FromArgb(220, 232, 242) });
|
||||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
Controls.Add(header);
|
||||||
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
|
||||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
|
||||||
|
|
||||||
root.Controls.Add(new Label
|
var body = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(24, 18, 24, 18), ColumnCount = 2, RowCount = 5 };
|
||||||
|
body.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
|
||||||
|
body.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
|
||||||
|
body.RowStyles.Add(new RowStyle(SizeType.Absolute, 48));
|
||||||
|
body.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
|
||||||
|
body.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
|
||||||
|
body.RowStyles.Add(new RowStyle(SizeType.Absolute, 54));
|
||||||
|
body.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||||
|
Controls.Add(body);
|
||||||
|
|
||||||
|
body.Controls.Add(new Label
|
||||||
{
|
{
|
||||||
AutoSize = true,
|
Text = _engine.IsInstalled
|
||||||
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
|
? "Eine vorhandene Installation wurde erkannt. Arbeitsdaten bleiben beim Update erhalten."
|
||||||
Text = "BizTalk Platform Management Tool 2.1.2"
|
: "Bereit zur Installation. Setup muss aus dem vollständig entpackten Paket gestartet werden.",
|
||||||
});
|
Dock = DockStyle.Fill,
|
||||||
root.Controls.Add(new Label
|
TextAlign = ContentAlignment.MiddleLeft
|
||||||
{
|
}, 0, 0);
|
||||||
AutoSize = true,
|
body.SetColumnSpan(body.GetControlFromPosition(0, 0), 2);
|
||||||
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";
|
_desktop.Text = "Desktop-Verknüpfung erstellen";
|
||||||
desktopShortcut.Checked = true;
|
_desktop.Checked = true;
|
||||||
desktopShortcut.AutoSize = true;
|
_desktop.Dock = DockStyle.Fill;
|
||||||
desktopShortcut.Enabled = !uninstallMode;
|
body.Controls.Add(_desktop, 0, 1);
|
||||||
root.Controls.Add(desktopShortcut);
|
body.SetColumnSpan(_desktop, 2);
|
||||||
|
_keepData.Text = "Wartungszustände und Logs bei Deinstallation behalten";
|
||||||
|
_keepData.Checked = true;
|
||||||
|
_keepData.Dock = DockStyle.Fill;
|
||||||
|
body.Controls.Add(_keepData, 0, 2);
|
||||||
|
body.SetColumnSpan(_keepData, 2);
|
||||||
|
|
||||||
output.Multiline = true;
|
ConfigureButton(_install, "Installieren / aktualisieren", Color.FromArgb(31, 111, 181));
|
||||||
output.ReadOnly = true;
|
ConfigureButton(_uninstall, "Deinstallieren", Color.FromArgb(120, 55, 55));
|
||||||
output.ScrollBars = ScrollBars.Vertical;
|
_install.Click += async delegate { await InstallAsync(); };
|
||||||
output.Dock = DockStyle.Fill;
|
_uninstall.Click += async delegate { await UninstallAsync(); };
|
||||||
output.Font = new Font(FontFamily.GenericMonospace, 9);
|
body.Controls.Add(_install, 0, 3);
|
||||||
root.Controls.Add(output);
|
body.Controls.Add(_uninstall, 1, 3);
|
||||||
|
|
||||||
var buttons = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft };
|
_status.Dock = DockStyle.Fill;
|
||||||
var closeButton = new Button { Text = "Schliessen", AutoSize = true };
|
_status.Multiline = true;
|
||||||
closeButton.Click += (sender, args) => Close();
|
_status.ReadOnly = true;
|
||||||
var logsButton = new Button { Text = "Diagnoselogs oeffnen", AutoSize = true };
|
_status.ScrollBars = ScrollBars.Vertical;
|
||||||
logsButton.Click += OpenLogs;
|
_status.BackColor = Color.FromArgb(247, 249, 252);
|
||||||
installButton.Text = engine.IsInstalled ? "Update installieren" : "Installieren";
|
_status.BorderStyle = BorderStyle.FixedSingle;
|
||||||
installButton.AutoSize = true;
|
body.Controls.Add(_status, 0, 4);
|
||||||
installButton.Enabled = engine.HasInstallPayload && !uninstallMode;
|
body.SetColumnSpan(_status, 2);
|
||||||
installButton.Click += (sender, args) => Run(false);
|
_uninstall.Enabled = _engine.IsInstalled;
|
||||||
uninstallButton.Text = "Deinstallieren";
|
|
||||||
uninstallButton.AutoSize = true;
|
|
||||||
uninstallButton.Enabled = engine.IsInstalled;
|
|
||||||
uninstallButton.Click += (sender, args) => Run(true);
|
|
||||||
buttons.Controls.Add(closeButton);
|
|
||||||
buttons.Controls.Add(logsButton);
|
|
||||||
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.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Öffnet das zuletzt verwendete Diagnoseverzeichnis im Windows Explorer.</summary>
|
private async Task InstallAsync()
|
||||||
/// <param name="sender">Die auslösende Schaltfläche.</param>
|
|
||||||
/// <param name="e">Die Ereignisargumente.</param>
|
|
||||||
private void OpenLogs(object sender, EventArgs e)
|
|
||||||
{
|
{
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(engine.InstallerLogDirectory);
|
|
||||||
Process.Start("explorer.exe", engine.InstallerLogDirectory);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBox.Show(this, "Diagnoseordner konnte nicht geoeffnet werden: " + ex.Message + "\n\n" + engine.InstallerLogDirectory, "Diagnoselogs", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Startet Installation oder Deinstallation außerhalb des UI-Threads.</summary>
|
|
||||||
/// <param name="uninstall"><c>true</c> für Deinstallation, <c>false</c> für Installation oder Update.</param>
|
|
||||||
private void Run(bool uninstall)
|
|
||||||
{
|
|
||||||
if (uninstall && MessageBox.Show(this, "BizTalk Platform Management Tool wirklich deinstallieren?", "Deinstallation bestaetigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var createDesktopShortcut = desktopShortcut.Checked;
|
|
||||||
SetBusy(true);
|
SetBusy(true);
|
||||||
// Dateisystem-, Registry- und Self-Test-Operationen dürfen die WinForms-Nachrichtenpumpe nicht blockieren.
|
|
||||||
Task.Run(() =>
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (uninstall) engine.Uninstall(Append);
|
await Task.Run(() => _engine.Install(_desktop.Checked, Report));
|
||||||
else engine.Install(createDesktopShortcut, Append);
|
Report("Installation erfolgreich abgeschlossen.");
|
||||||
Append(uninstall ? "FERTIG: Deinstallation erfolgreich." : "FERTIG: Installation/Update erfolgreich.");
|
MessageBox.Show(this, "Installation/Update erfolgreich abgeschlossen.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
Invoke(new Action(() =>
|
|
||||||
{
|
|
||||||
installButton.Text = engine.IsInstalled ? "Update installieren" : "Installieren";
|
|
||||||
uninstallButton.Enabled = engine.IsInstalled;
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Append("FEHLER: " + ex);
|
Report("FEHLER: " + ex.Message);
|
||||||
Invoke(new Action(() => MessageBox.Show(this, ex.Message, "Setup fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error)));
|
MessageBox.Show(this, ex.Message, "Installation fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
}
|
}
|
||||||
finally
|
finally { SetBusy(false); }
|
||||||
{
|
|
||||||
SetBusy(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Fügt der sichtbaren Ausgabe threadsicher eine zeitgestempelte Nachricht hinzu.</summary>
|
private async Task UninstallAsync()
|
||||||
/// <param name="message">Die anzuzeigende Nachricht.</param>
|
|
||||||
private void Append(string message)
|
|
||||||
{
|
{
|
||||||
if (IsDisposed || Disposing) return;
|
if (MessageBox.Show(this, "BizTalk Platform Management Tool wirklich deinstallieren?", Text, MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes) return;
|
||||||
if (InvokeRequired)
|
SetBusy(true);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try { BeginInvoke(new Action<string>(Append), message); } catch (InvalidOperationException) { }
|
await Task.Run(() => _engine.Uninstall(_keepData.Checked, Report));
|
||||||
return;
|
MessageBox.Show(this, "Deinstallation abgeschlossen.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
}
|
}
|
||||||
output.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message + Environment.NewLine);
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Report("FEHLER: " + ex.Message);
|
||||||
|
MessageBox.Show(this, ex.Message, "Deinstallation fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
finally { SetBusy(false); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Schaltet Steuerelemente und Wartecursor threadsicher in oder aus dem Arbeitszustand.</summary>
|
private void Report(string message)
|
||||||
/// <param name="value"><c>true</c>, solange eine Setup-Operation läuft.</param>
|
|
||||||
private void SetBusy(bool value)
|
|
||||||
{
|
{
|
||||||
if (InvokeRequired)
|
if (InvokeRequired) { BeginInvoke(new Action<string>(Report), message); return; }
|
||||||
{
|
_status.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message + Environment.NewLine);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Verhindert das Schließen des Fensters während einer laufenden Setup-Operation.</summary>
|
private void SetBusy(bool busy)
|
||||||
/// <param name="sender">Das zu schließende Setup-Fenster.</param>
|
|
||||||
/// <param name="e">Die abbrechbaren Argumente des Schließereignisses.</param>
|
|
||||||
private void OnFormClosing(object sender, FormClosingEventArgs e)
|
|
||||||
{
|
{
|
||||||
if (!busy) return;
|
if (InvokeRequired) { BeginInvoke(new Action<bool>(SetBusy), busy); return; }
|
||||||
e.Cancel = true;
|
_install.Enabled = !busy;
|
||||||
MessageBox.Show(this, "Das Setup arbeitet noch. Bitte warten Sie bis zum Abschluss.", "Setup laeuft", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
_uninstall.Enabled = !busy && _engine.IsInstalled;
|
||||||
|
_desktop.Enabled = !busy;
|
||||||
|
_keepData.Enabled = !busy;
|
||||||
|
UseWaitCursor = busy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureButton(Button button, string text, Color color)
|
||||||
|
{
|
||||||
|
button.Text = text;
|
||||||
|
button.Dock = DockStyle.Fill;
|
||||||
|
button.Margin = new Padding(4, 7, 4, 7);
|
||||||
|
button.FlatStyle = FlatStyle.Flat;
|
||||||
|
button.FlatAppearance.BorderSize = 0;
|
||||||
|
button.BackColor = color;
|
||||||
|
button.ForeColor = Color.White;
|
||||||
|
button.Font = new Font("Segoe UI", 9F, FontStyle.Bold);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
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
|
|
||||||
{
|
|
||||||
/// <summary>Beschreibt eine durch Länge und SHA-256 abgesicherte Payload-Datei.</summary>
|
|
||||||
public sealed class PackageFile
|
|
||||||
{
|
|
||||||
/// <summary>Gets or sets the normalized payload-relative path.</summary>
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Erzeugt und validiert das vollständige kryptografische Payload-Manifest.</summary>
|
|
||||||
public static class PackageManifest
|
|
||||||
{
|
|
||||||
/// <summary>Liest das Manifest und validiert jede sowie ausschließlich jede Payload-Datei.</summary>
|
|
||||||
/// <param name="applicationDirectory">Das Wurzelverzeichnis der Anwendungs-Payload.</param>
|
|
||||||
/// <param name="manifestPath">Der Pfad des zu prüfenden Manifests.</param>
|
|
||||||
/// <returns>Die validierten und normalisierten Manifesteinträge.</returns>
|
|
||||||
public static IList<PackageFile> ValidateAndRead(string applicationDirectory, string manifestPath)
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(applicationDirectory)) throw new DirectoryNotFoundException("Application payload missing: " + applicationDirectory);
|
|
||||||
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.");
|
|
||||||
// Der Mengenvergleich verhindert, dass nicht deklarierte DLLs oder Konfigurationen
|
|
||||||
// unbemerkt mit administrativen Rechten installiert werden.
|
|
||||||
var actualFiles = Directory.GetFiles(applicationDirectory, "*", SearchOption.AllDirectories)
|
|
||||||
.Select(x => NormalizeRelativePath(x.Substring(Path.GetFullPath(applicationDirectory).TrimEnd(Path.DirectorySeparatorChar).Length + 1)))
|
|
||||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray();
|
|
||||||
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>Erzeugt ein deterministisch sortiertes Manifest über die vollständige Payload.</summary>
|
|
||||||
/// <param name="applicationDirectory">Das Wurzelverzeichnis der Anwendungs-Payload.</param>
|
|
||||||
/// <param name="manifestPath">Der Zielpfad des Manifests.</param>
|
|
||||||
public static void Write(string applicationDirectory, string manifestPath)
|
|
||||||
{
|
|
||||||
var root = Path.GetFullPath(applicationDirectory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
|
||||||
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>Berechnet den kleingeschriebenen SHA-256-Hash einer Datei.</summary>
|
|
||||||
/// <param name="path">Der Pfad der zu prüfenden Datei.</param>
|
|
||||||
/// <returns>Der SHA-256-Hash als 64-stellige Hexadezimalzeichenfolge.</returns>
|
|
||||||
public static string Sha256(string path)
|
|
||||||
{
|
|
||||||
using (var stream = File.OpenRead(path))
|
|
||||||
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>Löst einen relativen Payload-Pfad auf und weist Directory Traversal zurück.</summary>
|
|
||||||
/// <param name="root">Das erlaubte Payload-Wurzelverzeichnis.</param>
|
|
||||||
/// <param name="relative">Der relative Pfad aus dem Manifest.</param>
|
|
||||||
/// <returns>Der vollständig aufgelöste, innerhalb von <paramref name="root"/> liegende Pfad.</returns>
|
|
||||||
public static string ResolveContainedPath(string root, string relative)
|
|
||||||
{
|
|
||||||
var normalizedRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalisiert Pfadtrenner und weist absolute, leere oder ausbrechende Pfade zurück.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Der zu normalisierende relative Pfad.</param>
|
|
||||||
/// <returns>Der normalisierte Pfad mit Schrägstrichen.</returns>
|
|
||||||
private static string NormalizeRelativePath(string path)
|
|
||||||
{
|
|
||||||
path = (path ?? string.Empty).Replace('\\', '/').Trim();
|
|
||||||
if (path.Length == 0 || path.StartsWith("/", StringComparison.Ordinal) || path.Contains("../") || path == ".." || Path.IsPathRooted(path))
|
|
||||||
throw new InvalidDataException("Unsafe package path: " + path);
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,20 +3,14 @@ using System.Windows.Forms;
|
|||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Setup
|
namespace BizTalkPlatformManagementTool.Setup
|
||||||
{
|
{
|
||||||
/// <summary>Enthält den Einstiegspunkt des administrativen Windows-Setups.</summary>
|
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
/// <summary>Startet die Setup-Oberfläche im Installations- oder Deinstallationsmodus.</summary>
|
|
||||||
/// <param name="args">Befehlszeilenargumente; <c>--uninstall</c> aktiviert die Deinstallation.</param>
|
|
||||||
/// <returns>Null nach regulärem Schließen der Oberfläche.</returns>
|
|
||||||
[STAThread]
|
[STAThread]
|
||||||
private static int Main(string[] args)
|
private static void Main()
|
||||||
{
|
{
|
||||||
Application.EnableVisualStyles();
|
Application.EnableVisualStyles();
|
||||||
Application.SetCompatibleTextRenderingDefault(false);
|
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)));
|
||||||
Application.Run(new MainForm(new InstallerEngine(AppDomain.CurrentDomain.BaseDirectory), uninstallMode));
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
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.2.0")]
|
|
||||||
[assembly: AssemblyFileVersion("2.1.2.0")]
|
|
||||||
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Security.Principal;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Setup
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Schreibt ein dauerhaftes, einzeiliges Diagnoseprotokoll für genau einen Setup-Lauf.
|
|
||||||
/// Die Komponente übernimmt keine Kennwörter oder andere Geheimnisse.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class SetupOperationLog
|
|
||||||
{
|
|
||||||
/// <summary>Serialisiert konkurrierende Schreibzugriffe innerhalb des Setup-Prozesses.</summary>
|
|
||||||
private readonly object sync = new object();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialisiert ein Protokoll für einen bereits festgelegten Dateipfad.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filePath">Der Logpfad oder eine leere Zeichenfolge bei vollständig ausgefallenem Logging.</param>
|
|
||||||
private SetupOperationLog(string filePath)
|
|
||||||
{
|
|
||||||
FilePath = filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Ruft den tatsächlich verwendeten Logpfad ab.</summary>
|
|
||||||
public string FilePath { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>Ruft die formatierte Ursache eines Fehlers beim primären Logaufbau ab.</summary>
|
|
||||||
public string CreationError { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>Ruft ab, ob das Log im temporären Rückfallverzeichnis liegt.</summary>
|
|
||||||
public bool IsFallback { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Erstellt ein Setup-Protokoll unter ProgramData oder ersatzweise im Temp-Verzeichnis.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dataDirectory">Das bevorzugte dauerhafte Datenverzeichnis.</param>
|
|
||||||
/// <param name="operation">Die kurze Operationsbezeichnung für den Dateinamen und Kontextkopf.</param>
|
|
||||||
/// <returns>Ein verwendbares Protokollobjekt, auch wenn keine Logdatei angelegt werden konnte.</returns>
|
|
||||||
public static SetupOperationLog Create(string dataDirectory, string operation)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return CreateInDirectory(Path.Combine(dataDirectory, "InstallerLogs"), operation);
|
|
||||||
}
|
|
||||||
catch (Exception primaryException)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Ohne primäres ProgramData-Log bleibt wenigstens im Benutzer-Temp ein
|
|
||||||
// Diagnosepfad erhalten; die ursprüngliche Ursache wird dort mitgeschrieben.
|
|
||||||
var fallback = CreateInDirectory(
|
|
||||||
Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool", "InstallerLogs"),
|
|
||||||
operation);
|
|
||||||
fallback.CreationError = FormatException(primaryException);
|
|
||||||
fallback.IsFallback = true;
|
|
||||||
fallback.Write(
|
|
||||||
"WARN",
|
|
||||||
"event=primary_log_creation_failed requested_data_directory=\"" + dataDirectory + "\" "
|
|
||||||
+ fallback.CreationError);
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
catch (Exception fallbackException)
|
|
||||||
{
|
|
||||||
return new SetupOperationLog(string.Empty)
|
|
||||||
{
|
|
||||||
CreationError = "primary={" + FormatException(primaryException) + "} fallback={" + FormatException(fallbackException) + "}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Legt eine eindeutig benannte Logdatei an und schreibt den technischen Kontextkopf.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="directory">Das Zielverzeichnis der Logdatei.</param>
|
|
||||||
/// <param name="operation">Die Bezeichnung des Setup-Laufs.</param>
|
|
||||||
/// <returns>Das initialisierte Setup-Protokoll.</returns>
|
|
||||||
private static SetupOperationLog CreateInDirectory(string directory, string operation)
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(directory);
|
|
||||||
CleanupOldLogs(directory);
|
|
||||||
var path = Path.Combine(
|
|
||||||
directory,
|
|
||||||
"setup-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmssfff", CultureInfo.InvariantCulture)
|
|
||||||
+ "-" + Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture)
|
|
||||||
+ "-" + operation + ".log");
|
|
||||||
using (new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read)) { }
|
|
||||||
var log = new SetupOperationLog(path);
|
|
||||||
log.Write(
|
|
||||||
"INFO",
|
|
||||||
"event=setup_started operation=" + operation
|
|
||||||
+ " setup_version=" + typeof(SetupOperationLog).Assembly.GetName().Version
|
|
||||||
+ " os=\"" + Environment.OSVersion.VersionString + "\""
|
|
||||||
+ " process_bitness=" + (Environment.Is64BitProcess ? "64" : "32")
|
|
||||||
+ " os_bitness=" + (Environment.Is64BitOperatingSystem ? "64" : "32")
|
|
||||||
+ " clr=" + Environment.Version
|
|
||||||
+ " machine=\"" + Environment.MachineName + "\""
|
|
||||||
+ " identity=\"" + CurrentIdentity() + "\""
|
|
||||||
+ " elevated=" + IsElevated()
|
|
||||||
+ " base_directory=\"" + AppDomain.CurrentDomain.BaseDirectory + "\"");
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Schreibt eine UTC-zeitgestempelte, einzeilige Nachricht ausfallsicher in die Logdatei.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="level">Der textuelle Log-Level.</param>
|
|
||||||
/// <param name="message">Die zu protokollierende Nachricht.</param>
|
|
||||||
public void Write(string level, string message)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(FilePath)) return;
|
|
||||||
var line = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)
|
|
||||||
+ " level=" + (level ?? "INFO").ToUpperInvariant()
|
|
||||||
+ " " + SingleLine(message)
|
|
||||||
+ Environment.NewLine;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
lock (sync)
|
|
||||||
{
|
|
||||||
File.AppendAllText(FilePath, line, new UTF8Encoding(false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Das Diagnose-Logging darf das eigentliche Setup-Ergebnis niemals ersetzen.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Protokolliert einen Fehler mit stabiler Kennung, Phase und vollständiger Exception-Kette.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="errorCode">Die stabile maschinenlesbare Fehlerkennung.</param>
|
|
||||||
/// <param name="phase">Die lesbare Setup-Phase.</param>
|
|
||||||
/// <param name="exception">Die zu protokollierende Ausnahme.</param>
|
|
||||||
public void WriteException(string errorCode, string phase, Exception exception)
|
|
||||||
{
|
|
||||||
Write("ERROR", "event=exception error_code=" + errorCode + " phase=\"" + phase + "\" " + FormatException(exception));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Protokolliert Existenz, Größe, Zeitstempel, Dateiversion und SHA-256 einer Datei.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="label">Die fachliche Rolle der Datei im Setup.</param>
|
|
||||||
/// <param name="path">Der zu untersuchende Dateipfad.</param>
|
|
||||||
public void WriteFileDetails(string label, string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!File.Exists(path))
|
|
||||||
{
|
|
||||||
Write("INFO", "event=file_details label=\"" + label + "\" path=\"" + path + "\" exists=false");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var info = new FileInfo(path);
|
|
||||||
var version = FileVersionInfo.GetVersionInfo(path).FileVersion ?? "(unknown)";
|
|
||||||
Write(
|
|
||||||
"INFO",
|
|
||||||
"event=file_details label=\"" + label + "\" path=\"" + path + "\" exists=true"
|
|
||||||
+ " size_bytes=" + info.Length.ToString(CultureInfo.InvariantCulture)
|
|
||||||
+ " modified_utc=" + info.LastWriteTimeUtc.ToString("o", CultureInfo.InvariantCulture)
|
|
||||||
+ " file_version=\"" + version + "\""
|
|
||||||
+ " sha256=" + PackageManifest.Sha256(path));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Write("WARN", "event=file_details_failed label=\"" + label + "\" path=\"" + path + "\" " + FormatException(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Formatiert eine Exception-Kette mit Typ, HRESULT, Nachricht und Stacktrace.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="exception">Die äußerste Ausnahme.</param>
|
|
||||||
/// <returns>Eine einzeilige Diagnose mit höchstens zwölf Exception-Ebenen.</returns>
|
|
||||||
internal static string FormatException(Exception exception)
|
|
||||||
{
|
|
||||||
var result = new StringBuilder();
|
|
||||||
var current = exception;
|
|
||||||
var depth = 0;
|
|
||||||
while (current != null && depth < 12)
|
|
||||||
{
|
|
||||||
if (depth > 0) result.Append(" | inner[").Append(depth).Append("] ");
|
|
||||||
result.Append("exception_type=").Append(current.GetType().FullName)
|
|
||||||
.Append(" hresult=0x").Append(current.HResult.ToString("X8", CultureInfo.InvariantCulture))
|
|
||||||
.Append(" message=\"").Append(current.Message).Append('"');
|
|
||||||
if (!string.IsNullOrWhiteSpace(current.StackTrace)) result.Append(" stack=\"").Append(current.StackTrace).Append('"');
|
|
||||||
current = current.InnerException;
|
|
||||||
depth++;
|
|
||||||
}
|
|
||||||
return SingleLine(result.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ermittelt die aktuelle Windows-Identität ohne einen Diagnosefehler weiterzureichen.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Der Identitätsname oder <c>(unknown)</c>.</returns>
|
|
||||||
private static string CurrentIdentity()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var identity = WindowsIdentity.GetCurrent())
|
|
||||||
{
|
|
||||||
return identity == null ? "(unknown)" : identity.Name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return "(unknown)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ermittelt, ob der aktuelle Prozess mit Administratorrechten läuft.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns><c>true</c>, <c>false</c> oder <c>unknown</c>.</returns>
|
|
||||||
private static string IsElevated()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var identity = WindowsIdentity.GetCurrent())
|
|
||||||
{
|
|
||||||
return identity != null && new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator) ? "true" : "false";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maskiert Steuerzeichen, damit jeder Logeintrag genau eine physische Zeile belegt.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">Der zu normalisierende Text.</param>
|
|
||||||
/// <returns>Der einzeilige Text.</returns>
|
|
||||||
private static string SingleLine(string value)
|
|
||||||
{
|
|
||||||
return (value ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Entfernt Setup-Protokolle, deren letzte Änderung mehr als 90 Tage zurückliegt.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="directory">Das zu bereinigende Installer-Logverzeichnis.</param>
|
|
||||||
private static void CleanupOldLogs(string directory)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var cutoff = DateTime.UtcNow.AddDays(-90);
|
|
||||||
foreach (var file in Directory.GetFiles(directory, "setup-*.log"))
|
|
||||||
{
|
|
||||||
if (File.GetLastWriteTimeUtc(file) < cutoff) File.Delete(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Die Aufbewahrungsbereinigung ist bestmöglich und blockiert kein Setup.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
<assemblyIdentity version="2.1.2.0" name="BizTalkPlatformManagementTool.Setup" />
|
<assemblyIdentity version="1.0.0.0" name="BizTalkPlatformManagementTool.Setup" />
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
|
<security>
|
||||||
|
<requestedPrivileges>
|
||||||
|
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
</trustInfo>
|
</trustInfo>
|
||||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
|
||||||
<application><supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /></application>
|
|
||||||
</compatibility>
|
|
||||||
</assembly>
|
</assembly>
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
<ErrorReport>prompt</ErrorReport>
|
<ErrorReport>prompt</ErrorReport>
|
||||||
<WarningLevel>4</WarningLevel>
|
<WarningLevel>4</WarningLevel>
|
||||||
<Prefer32Bit>false</Prefer32Bit>
|
<Prefer32Bit>false</Prefer32Bit>
|
||||||
<DocumentationFile>bin\Release\BizTalkPlatformManagementTool.xml</DocumentationFile>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
@@ -47,20 +46,19 @@
|
|||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
<Compile Include="RuntimeSelfTest.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Models\ArtifactStates.cs" />
|
<Compile Include="Models\ArtifactStates.cs" />
|
||||||
<Compile Include="Models\BizTalkSnapshot.cs" />
|
<Compile Include="Models\BizTalkSnapshot.cs" />
|
||||||
<Compile Include="Models\DiffModels.cs" />
|
<Compile Include="Models\DiffModels.cs" />
|
||||||
<Compile Include="Models\OperationModels.cs" />
|
<Compile Include="Models\OperationModels.cs" />
|
||||||
<Compile Include="Services\BizTalkWmiClient.cs" />
|
<Compile Include="Services\BizTalkWmiClient.cs" />
|
||||||
|
<Compile Include="Services\AppPaths.cs" />
|
||||||
<Compile Include="Services\CsvWriter.cs" />
|
<Compile Include="Services\CsvWriter.cs" />
|
||||||
<Compile Include="Services\HtmlReportWriter.cs" />
|
<Compile Include="Services\HtmlReportWriter.cs" />
|
||||||
<Compile Include="Services\JsonFileStore.cs" />
|
<Compile Include="Services\JsonFileStore.cs" />
|
||||||
<Compile Include="Services\OperationLogger.cs" />
|
<Compile Include="Services\OperationLogger.cs" />
|
||||||
<Compile Include="Services\SnapshotComparer.cs" />
|
<Compile Include="Services\SnapshotComparer.cs" />
|
||||||
<Compile Include="Services\SnapshotValidator.cs" />
|
|
||||||
<Compile Include="Services\SnapshotStore.cs" />
|
<Compile Include="Services\SnapshotStore.cs" />
|
||||||
<Compile Include="Services\BizTalkOperationService.cs" />
|
<Compile Include="Services\BizTalkOperationService.cs" />
|
||||||
<Compile Include="Ui\MainForm.cs" />
|
<Compile Include="Ui\MainForm.cs" />
|
||||||
|
|||||||
@@ -1,123 +1,58 @@
|
|||||||
namespace BizTalkPlatformManagementTool.Models
|
namespace BizTalkPlatformManagementTool.Models
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Contains BizTalk WMI state constants and display helpers used by snapshots,
|
|
||||||
/// plans and reports.
|
|
||||||
/// </summary>
|
|
||||||
public static class ArtifactStates
|
public static class ArtifactStates
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// WMI status value for a bound send port.
|
|
||||||
/// </summary>
|
|
||||||
public const int SendPortBound = 1;
|
public const int SendPortBound = 1;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// WMI status value for a stopped send port.
|
|
||||||
/// </summary>
|
|
||||||
public const int SendPortStopped = 2;
|
public const int SendPortStopped = 2;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// WMI status value for a started send port.
|
|
||||||
/// </summary>
|
|
||||||
public const int SendPortStarted = 3;
|
public const int SendPortStarted = 3;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// WMI status value for an unbound orchestration.
|
|
||||||
/// </summary>
|
|
||||||
public const int OrchestrationUnbound = 1;
|
public const int OrchestrationUnbound = 1;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// WMI status value for a bound orchestration.
|
|
||||||
/// </summary>
|
|
||||||
public const int OrchestrationBound = 2;
|
public const int OrchestrationBound = 2;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// WMI status value for a stopped orchestration.
|
|
||||||
/// </summary>
|
|
||||||
public const int OrchestrationStopped = 3;
|
public const int OrchestrationStopped = 3;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// WMI status value for a started orchestration.
|
|
||||||
/// </summary>
|
|
||||||
public const int OrchestrationStarted = 4;
|
public const int OrchestrationStarted = 4;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// WMI service state value for a stopped host instance.
|
|
||||||
/// </summary>
|
|
||||||
public const int HostStopped = 1;
|
public const int HostStopped = 1;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// WMI service state value for a host instance that is starting.
|
|
||||||
/// </summary>
|
|
||||||
public const int HostStartPending = 2;
|
public const int HostStartPending = 2;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// WMI service state value for a host instance that is stopping.
|
|
||||||
/// </summary>
|
|
||||||
public const int HostStopPending = 3;
|
public const int HostStopPending = 3;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// WMI service state value for a started host instance.
|
|
||||||
/// </summary>
|
|
||||||
public const int HostStarted = 4;
|
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)
|
public static string FormatReceiveLocation(bool enabled)
|
||||||
{
|
{
|
||||||
return enabled ? "Enabled" : "Disabled";
|
return enabled ? "Aktiviert" : "Deaktiviert";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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)
|
public static string FormatSendPort(int status)
|
||||||
{
|
{
|
||||||
switch (status)
|
switch (status)
|
||||||
{
|
{
|
||||||
case SendPortStarted: return "Started";
|
case SendPortStarted: return "Gestartet";
|
||||||
case SendPortStopped: return "Stopped";
|
case SendPortStopped: return "Gestoppt";
|
||||||
case SendPortBound: return "Bound";
|
case SendPortBound: return "Gebunden";
|
||||||
default: return "Unknown (" + status + ")";
|
default: return "Unbekannt (" + status + ")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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)
|
public static string FormatOrchestration(int status)
|
||||||
{
|
{
|
||||||
switch (status)
|
switch (status)
|
||||||
{
|
{
|
||||||
case OrchestrationStarted: return "Started";
|
case OrchestrationStarted: return "Gestartet";
|
||||||
case OrchestrationStopped: return "Stopped";
|
case OrchestrationStopped: return "Gestoppt";
|
||||||
case OrchestrationBound: return "Bound";
|
case OrchestrationBound: return "Gebunden";
|
||||||
case OrchestrationUnbound: return "Unbound";
|
case OrchestrationUnbound: return "Nicht gebunden";
|
||||||
default: return "Unknown (" + status + ")";
|
default: return "Unbekannt (" + status + ")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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)
|
public static string FormatHostInstance(int state)
|
||||||
{
|
{
|
||||||
switch (state)
|
switch (state)
|
||||||
{
|
{
|
||||||
case HostStarted: return "Started";
|
case HostStarted: return "Gestartet";
|
||||||
case HostStartPending: return "StartPending";
|
case HostStartPending: return "Wird gestartet";
|
||||||
case HostStopPending: return "StopPending";
|
case HostStopPending: return "Wird gestoppt";
|
||||||
case HostStopped: return "Stopped";
|
case HostStopped: return "Gestoppt";
|
||||||
default: return "Unknown (" + state + ")";
|
default: return "Unbekannt (" + state + ")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,62 +3,34 @@ using System.Runtime.Serialization;
|
|||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Models
|
namespace BizTalkPlatformManagementTool.Models
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Represents one captured BizTalk runtime state including application artifacts
|
|
||||||
/// and host instances.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public sealed class BizTalkSnapshot
|
public sealed class BizTalkSnapshot
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new snapshot with empty application and host instance collections.
|
|
||||||
/// </summary>
|
|
||||||
public BizTalkSnapshot()
|
public BizTalkSnapshot()
|
||||||
{
|
{
|
||||||
Applications = new List<ApplicationSnapshot>();
|
Applications = new List<ApplicationSnapshot>();
|
||||||
HostInstances = new List<HostInstanceState>();
|
HostInstances = new List<HostInstanceState>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the tool version that created the snapshot.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 1)]
|
[DataMember(Order = 1)]
|
||||||
public string ToolVersion { get; set; }
|
public string ToolVersion { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the local timestamp when the snapshot was created.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 2)]
|
[DataMember(Order = 2)]
|
||||||
public string CreatedAt { get; set; }
|
public string CreatedAt { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the BizTalk server name used for the WMI connection.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 3)]
|
[DataMember(Order = 3)]
|
||||||
public string Server { get; set; }
|
public string Server { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the BizTalk application snapshots captured from WMI.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 4)]
|
[DataMember(Order = 4)]
|
||||||
public List<ApplicationSnapshot> Applications { get; set; }
|
public List<ApplicationSnapshot> Applications { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the host instances captured from the BizTalk group.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 5)]
|
[DataMember(Order = 5)]
|
||||||
public List<HostInstanceState> HostInstances { get; set; }
|
public List<HostInstanceState> HostInstances { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Groups the captured artifact states for one BizTalk application.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public sealed class ApplicationSnapshot
|
public sealed class ApplicationSnapshot
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Initializes an application snapshot with empty artifact collections.
|
|
||||||
/// </summary>
|
|
||||||
public ApplicationSnapshot()
|
public ApplicationSnapshot()
|
||||||
{
|
{
|
||||||
ReceiveLocations = new List<ReceiveLocationState>();
|
ReceiveLocations = new List<ReceiveLocationState>();
|
||||||
@@ -66,169 +38,100 @@ namespace BizTalkPlatformManagementTool.Models
|
|||||||
Orchestrations = new List<OrchestrationState>();
|
Orchestrations = new List<OrchestrationState>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the BizTalk application name.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 1)]
|
[DataMember(Order = 1)]
|
||||||
public string Application { get; set; }
|
public string Application { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the receive locations that belong to the application.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 2)]
|
[DataMember(Order = 2)]
|
||||||
public List<ReceiveLocationState> ReceiveLocations { get; set; }
|
public List<ReceiveLocationState> ReceiveLocations { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the send ports that belong to the application.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 3)]
|
[DataMember(Order = 3)]
|
||||||
public List<SendPortState> SendPorts { get; set; }
|
public List<SendPortState> SendPorts { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the orchestrations that belong to the application.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 4)]
|
[DataMember(Order = 4)]
|
||||||
public List<OrchestrationState> Orchestrations { get; set; }
|
public List<OrchestrationState> Orchestrations { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Captures the relevant WMI state for a BizTalk receive location.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public sealed class ReceiveLocationState
|
public sealed class ReceiveLocationState
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the owning BizTalk application.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 1)]
|
[DataMember(Order = 1)]
|
||||||
public string Application { get; set; }
|
public string Application { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the receive location name.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 2)]
|
[DataMember(Order = 2)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the parent receive port name.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 3)]
|
[DataMember(Order = 3)]
|
||||||
public string ReceivePortName { get; set; }
|
public string ReceivePortName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether the receive location is enabled.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 4)]
|
[DataMember(Order = 4)]
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the receive adapter name.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 5)]
|
[DataMember(Order = 5)]
|
||||||
public string AdapterName { get; set; }
|
public string AdapterName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the receive location transport address.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 6)]
|
[DataMember(Order = 6)]
|
||||||
public string Address { get; set; }
|
public string Address { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Captures the relevant WMI state for a BizTalk send port.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public sealed class SendPortState
|
public sealed class SendPortState
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the owning BizTalk application.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 1)]
|
[DataMember(Order = 1)]
|
||||||
public string Application { get; set; }
|
public string Application { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the send port name.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 2)]
|
[DataMember(Order = 2)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the raw MSBTS_SendPort.Status value.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 3)]
|
[DataMember(Order = 3)]
|
||||||
public int Status { get; set; }
|
public int Status { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the primary transport adapter type.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 4)]
|
[DataMember(Order = 4)]
|
||||||
public string PrimaryTransportType { get; set; }
|
public string PrimaryTransportType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the primary transport address.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 5)]
|
[DataMember(Order = 5)]
|
||||||
public string PrimaryTransportAddress { get; set; }
|
public string PrimaryTransportAddress { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Captures the relevant WMI state for a BizTalk orchestration.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public sealed class OrchestrationState
|
public sealed class OrchestrationState
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the owning BizTalk application.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 1)]
|
[DataMember(Order = 1)]
|
||||||
public string Application { get; set; }
|
public string Application { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the orchestration name.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 2)]
|
[DataMember(Order = 2)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the raw MSBTS_Orchestration.OrchestrationStatus value.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 3)]
|
[DataMember(Order = 3)]
|
||||||
public int OrchestrationStatus { get; set; }
|
public int OrchestrationStatus { get; set; }
|
||||||
|
|
||||||
|
[DataMember(Order = 4, EmitDefaultValue = false)]
|
||||||
|
public string AssemblyName { get; set; }
|
||||||
|
|
||||||
|
[DataMember(Order = 5, EmitDefaultValue = false)]
|
||||||
|
public string AssemblyVersion { get; set; }
|
||||||
|
|
||||||
|
[DataMember(Order = 6, EmitDefaultValue = false)]
|
||||||
|
public string AssemblyCulture { get; set; }
|
||||||
|
|
||||||
|
[DataMember(Order = 7, EmitDefaultValue = false)]
|
||||||
|
public string AssemblyPublicKeyToken { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Captures the relevant WMI state for a BizTalk host instance.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public sealed class HostInstanceState
|
public sealed class HostInstanceState
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the host instance name used as the WMI key.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 1)]
|
[DataMember(Order = 1)]
|
||||||
public string InstanceName { get; set; }
|
public string InstanceName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the BizTalk host name.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 2)]
|
[DataMember(Order = 2)]
|
||||||
public string HostName { get; set; }
|
public string HostName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the server on which the host instance runs.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 3)]
|
[DataMember(Order = 3)]
|
||||||
public string Server { get; set; }
|
public string Server { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the raw MSBTS_HostInstance.ServiceState value.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 4)]
|
[DataMember(Order = 4)]
|
||||||
public int RawState { get; set; }
|
public int RawState { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the formatted host instance state.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 5)]
|
[DataMember(Order = 5)]
|
||||||
public string StateText { get; set; }
|
public string StateText { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,104 +3,56 @@ using System.Runtime.Serialization;
|
|||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Models
|
namespace BizTalkPlatformManagementTool.Models
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Contains all differences detected between two BizTalk snapshots.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public sealed class SnapshotDiff
|
public sealed class SnapshotDiff
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new diff with empty artifact and host instance collections.
|
|
||||||
/// </summary>
|
|
||||||
public SnapshotDiff()
|
public SnapshotDiff()
|
||||||
{
|
{
|
||||||
ArtifactDifferences = new List<ArtifactDiffEntry>();
|
ArtifactDifferences = new List<ArtifactDiffEntry>();
|
||||||
HostInstanceDifferences = new List<HostInstanceDiffEntry>();
|
HostInstanceDifferences = new List<HostInstanceDiffEntry>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets state differences for receive locations, send ports and orchestrations.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 1)]
|
[DataMember(Order = 1)]
|
||||||
public List<ArtifactDiffEntry> ArtifactDifferences { get; set; }
|
public List<ArtifactDiffEntry> ArtifactDifferences { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets state differences for host instances.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 2)]
|
[DataMember(Order = 2)]
|
||||||
public List<HostInstanceDiffEntry> HostInstanceDifferences { get; set; }
|
public List<HostInstanceDiffEntry> HostInstanceDifferences { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Describes a before/after state change for one BizTalk application artifact.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public sealed class ArtifactDiffEntry
|
public sealed class ArtifactDiffEntry
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the owning BizTalk application.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 1)]
|
[DataMember(Order = 1)]
|
||||||
public string Application { get; set; }
|
public string Application { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the artifact category, such as SendPort or ReceiveLocation.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 2)]
|
[DataMember(Order = 2)]
|
||||||
public string ArtifactType { get; set; }
|
public string ArtifactType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the artifact name.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 3)]
|
[DataMember(Order = 3)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the formatted state in the before snapshot.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 4)]
|
[DataMember(Order = 4)]
|
||||||
public string Before { get; set; }
|
public string Before { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the formatted state in the after snapshot.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 5)]
|
[DataMember(Order = 5)]
|
||||||
public string After { get; set; }
|
public string After { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Describes a before/after state change for one BizTalk host instance.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public sealed class HostInstanceDiffEntry
|
public sealed class HostInstanceDiffEntry
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the host instance name.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 1)]
|
[DataMember(Order = 1)]
|
||||||
public string InstanceName { get; set; }
|
public string InstanceName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the BizTalk host name.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 2)]
|
[DataMember(Order = 2)]
|
||||||
public string HostName { get; set; }
|
public string HostName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the server on which the host instance runs.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 3)]
|
[DataMember(Order = 3)]
|
||||||
public string Server { get; set; }
|
public string Server { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the formatted state in the before snapshot.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 4)]
|
[DataMember(Order = 4)]
|
||||||
public string Before { get; set; }
|
public string Before { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the formatted state in the after snapshot.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 5)]
|
[DataMember(Order = 5)]
|
||||||
public string After { get; set; }
|
public string After { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,211 +3,98 @@ using System.Runtime.Serialization;
|
|||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Models
|
namespace BizTalkPlatformManagementTool.Models
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Defines the supported operation plan modes.
|
|
||||||
/// </summary>
|
|
||||||
public enum OperationMode
|
public enum OperationMode
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Plan mode for stopping BizTalk runtime artifacts before maintenance.
|
|
||||||
/// </summary>
|
|
||||||
Shutdown,
|
Shutdown,
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Plan mode for returning BizTalk runtime artifacts to a captured state.
|
|
||||||
/// </summary>
|
|
||||||
Restore
|
Restore
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Defines the artifact categories that can appear in an operation plan.
|
|
||||||
/// </summary>
|
|
||||||
public enum OperationStepKind
|
public enum OperationStepKind
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// A receive location step.
|
|
||||||
/// </summary>
|
|
||||||
ReceiveLocation,
|
ReceiveLocation,
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A send port step.
|
|
||||||
/// </summary>
|
|
||||||
SendPort,
|
SendPort,
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An orchestration step.
|
|
||||||
/// </summary>
|
|
||||||
Orchestration,
|
Orchestration,
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A host instance step.
|
|
||||||
/// </summary>
|
|
||||||
HostInstance,
|
HostInstance,
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An informational step that is intentionally not executed.
|
|
||||||
/// </summary>
|
|
||||||
Note
|
Note
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the ordered shutdown or restore plan written before any
|
|
||||||
/// runtime-changing operation is executed.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public sealed class OperationPlan
|
public sealed class OperationPlan
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new operation plan with an empty step collection.
|
|
||||||
/// </summary>
|
|
||||||
public OperationPlan()
|
public OperationPlan()
|
||||||
{
|
{
|
||||||
Steps = new List<OperationStep>();
|
Steps = new List<OperationStep>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the plan mode as a serialized string.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 1)]
|
[DataMember(Order = 1)]
|
||||||
public string Mode { get; set; }
|
public string Mode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the local timestamp when the plan was created.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 2)]
|
[DataMember(Order = 2)]
|
||||||
public string CreatedAt { get; set; }
|
public string CreatedAt { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the target server for server-scoped plan steps.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 3)]
|
[DataMember(Order = 3)]
|
||||||
public string Server { get; set; }
|
public string Server { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the ordered operation steps.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 4)]
|
[DataMember(Order = 4)]
|
||||||
public List<OperationStep> Steps { get; set; }
|
public List<OperationStep> Steps { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Describes one executable or informational step in a shutdown or restore plan.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public sealed class OperationStep
|
public sealed class OperationStep
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the artifact kind for this step.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 1)]
|
[DataMember(Order = 1)]
|
||||||
public string Kind { get; set; }
|
public string Kind { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the owning BizTalk application, when applicable.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 2)]
|
[DataMember(Order = 2)]
|
||||||
public string Application { get; set; }
|
public string Application { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the artifact or host instance name displayed to the user.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 3)]
|
[DataMember(Order = 3)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the server associated with the step, when server scoped.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 4)]
|
[DataMember(Order = 4)]
|
||||||
public string Server { get; set; }
|
public string Server { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the human-readable action description.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 5)]
|
[DataMember(Order = 5)]
|
||||||
public string Action { get; set; }
|
public string Action { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the WMI class used to resolve the runtime object.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 6)]
|
[DataMember(Order = 6)]
|
||||||
public string WmiClass { get; set; }
|
public string WmiClass { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the WMI key property used to locate the runtime object.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 7)]
|
[DataMember(Order = 7)]
|
||||||
public string KeyProperty { get; set; }
|
public string KeyProperty { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the WMI key value used to locate the runtime object.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 8)]
|
[DataMember(Order = 8)]
|
||||||
public string KeyValue { get; set; }
|
public string KeyValue { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the WMI method or service pseudo-method to execute.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 9)]
|
[DataMember(Order = 9)]
|
||||||
public string MethodName { get; set; }
|
public string MethodName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the numeric arguments passed to the WMI method.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 10)]
|
[DataMember(Order = 10)]
|
||||||
public int[] Arguments { get; set; }
|
public int[] Arguments { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the raw WMI state expected after the method completes.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 11)]
|
[DataMember(Order = 11)]
|
||||||
public int? TargetState { get; set; }
|
public int? TargetState { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether the step should be executed.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 12)]
|
[DataMember(Order = 12)]
|
||||||
public bool Execute { get; set; }
|
public bool Execute { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets an operator-facing warning for skipped or risky steps.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 13)]
|
[DataMember(Order = 13)]
|
||||||
public string Warning { get; set; }
|
public string Warning { get; set; }
|
||||||
|
|
||||||
|
[DataMember(Order = 14, EmitDefaultValue = false)]
|
||||||
|
public string QualifierProperty { get; set; }
|
||||||
|
|
||||||
|
[DataMember(Order = 15, EmitDefaultValue = false)]
|
||||||
|
public string QualifierValue { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains user-selected runtime options for snapshot, shutdown and restore actions.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class OperationOptions
|
public sealed class OperationOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the BizTalk server used for WMI operations.
|
|
||||||
/// </summary>
|
|
||||||
public string Server { get; set; }
|
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; }
|
public string OutputDirectory { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the state file used as restore input.
|
|
||||||
/// </summary>
|
|
||||||
public string StateFile { get; set; }
|
public string StateFile { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether operations should only be logged.
|
|
||||||
/// </summary>
|
|
||||||
public bool DryRun { get; set; }
|
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; }
|
public int WaitTimeoutSeconds { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the number of seconds between WMI polling attempts.
|
|
||||||
/// </summary>
|
|
||||||
public int PollIntervalSeconds { get; set; }
|
public int PollIntervalSeconds { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,70 +1,38 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Security.Principal;
|
using System.Security.Principal;
|
||||||
using System.Threading;
|
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using BizTalkPlatformManagementTool.Ui;
|
using BizTalkPlatformManagementTool.Ui;
|
||||||
|
|
||||||
namespace BizTalkPlatformManagementTool
|
namespace BizTalkPlatformManagementTool
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Contains the WinForms application entry point and startup guard checks.
|
|
||||||
/// </summary>
|
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Starts the application after verifying that BizTalk WMI operations can run elevated.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="args">Befehlszeilenargumente; <c>--self-test</c> startet die WMI-freie Installerprüfung.</param>
|
|
||||||
/// <returns>Null bei erfolgreichem Abschluss, andernfalls ein prozessgeeigneter Fehlercode.</returns>
|
|
||||||
[STAThread]
|
[STAThread]
|
||||||
private static int Main(string[] args)
|
private static void Main()
|
||||||
{
|
{
|
||||||
// Der Self-Test muss ohne Administratorprüfung und ohne WinForms-Oberfläche laufen,
|
|
||||||
// damit der Installer ihn bereits im isolierten Staging-Verzeichnis ausführen kann.
|
|
||||||
if (args != null && args.Length == 1 && string.Equals(args[0], "--self-test", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return RuntimeSelfTest.Run();
|
|
||||||
}
|
|
||||||
|
|
||||||
Application.EnableVisualStyles();
|
Application.EnableVisualStyles();
|
||||||
Application.SetCompatibleTextRenderingDefault(false);
|
Application.SetCompatibleTextRenderingDefault(false);
|
||||||
|
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
|
||||||
|
Application.ThreadException += delegate(object sender, System.Threading.ThreadExceptionEventArgs args)
|
||||||
|
{
|
||||||
|
MessageBox.Show("Unerwarteter Fehler: " + args.Exception.Message, "BizTalk Platform Management Tool", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
};
|
||||||
|
|
||||||
if (!IsRunningAsAdministrator())
|
if (!IsRunningAsAdministrator())
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
MessageBox.Show(
|
||||||
"BizTalk Platform Management Tool must be started with administrator rights.\r\n\r\n" +
|
"Das BizTalk Platform Management Tool benötigt Administratorrechte.\r\n\r\n" +
|
||||||
"Please close the application and start it again with 'Run as administrator'. " +
|
"Bitte starten Sie die Anwendung über ‚Als Administrator ausführen‘ erneut. " +
|
||||||
"Without elevated rights, BizTalk WMI operations in root\\MicrosoftBizTalkServer will fail.",
|
"Ohne erhöhte Rechte sind BizTalk-WMI-Operationen nicht zuverlässig möglich.",
|
||||||
"Administrator Rights Required",
|
"Administratorrechte erforderlich",
|
||||||
MessageBoxButtons.OK,
|
MessageBoxButtons.OK,
|
||||||
MessageBoxIcon.Error);
|
MessageBoxIcon.Error);
|
||||||
return 1;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
bool createdNew;
|
|
||||||
// Der sitzungsbezogene Mutex verhindert konkurrierende Wartungsoperationen desselben Benutzers.
|
|
||||||
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());
|
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()
|
private static bool IsRunningAsAdministrator()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ using System.Reflection;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
[assembly: AssemblyTitle("BizTalk Platform Management Tool")]
|
[assembly: AssemblyTitle("BizTalk Platform Management Tool")]
|
||||||
[assembly: AssemblyDescription("Controlled BizTalk Server maintenance snapshots, plans and runtime operations")]
|
[assembly: AssemblyDescription("Kontrollierte BizTalk-Wartung mit Zustandswiederherstellung")]
|
||||||
[assembly: AssemblyCompany("BEW")]
|
[assembly: AssemblyCompany("JR IT Services")]
|
||||||
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
||||||
[assembly: AssemblyCopyright("Copyright © 2026")]
|
[assembly: AssemblyCopyright("Copyright © 2026")]
|
||||||
[assembly: ComVisible(false)]
|
[assembly: ComVisible(false)]
|
||||||
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
|
[assembly: Guid("DA63FBDA-4B7F-4D93-B63E-652F26A94796")]
|
||||||
[assembly: AssemblyVersion("2.1.2.0")]
|
[assembly: AssemblyVersion("2.1.0.0")]
|
||||||
[assembly: AssemblyFileVersion("2.1.2.0")]
|
[assembly: AssemblyFileVersion("2.1.0.0")]
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Prüft Serialisierung, Validierung, Vergleich und Report-Persistenz ohne BizTalk-WMI-Zugriff.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Null bei erfolgreicher Prüfung, andernfalls eins.</returns>
|
|
||||||
public static int Run()
|
|
||||||
{
|
|
||||||
var directory = Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool.SelfTest." + Guid.NewGuid().ToString("N"));
|
|
||||||
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
|
|
||||||
{
|
|
||||||
// Ein Bereinigungsfehler darf das bereits feststehende Self-Test-Ergebnis nicht überschreiben.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Erstellt einen minimalen, aber vollständig validierbaren Snapshot für den Self-Test.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sendPortState">Der rohe BizTalk-Status des enthaltenen Send Ports.</param>
|
|
||||||
/// <returns>Ein Snapshot mit genau einer Anwendung und einem Send Port.</returns>
|
|
||||||
private static BizTalkSnapshot SampleSnapshot(int sendPortState)
|
|
||||||
{
|
|
||||||
var snapshot = new BizTalkSnapshot
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace BizTalkPlatformManagementTool.Services
|
||||||
|
{
|
||||||
|
public static class AppPaths
|
||||||
|
{
|
||||||
|
public const string ProductName = "BizTalkPlatformManagementTool";
|
||||||
|
|
||||||
|
public static string RuntimeDirectory
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||||
|
ProductName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string WorkspaceDirectory
|
||||||
|
{
|
||||||
|
get { return Path.Combine(RuntimeDirectory, "Wartungen"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string LogDirectory
|
||||||
|
{
|
||||||
|
get { return Path.Combine(RuntimeDirectory, "Logs"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,39 +7,18 @@ using BizTalkPlatformManagementTool.Models;
|
|||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Services
|
namespace BizTalkPlatformManagementTool.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Coordinates BizTalk snapshot, plan, shutdown, restore and persistence operations.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class BizTalkOperationService
|
public sealed class BizTalkOperationService
|
||||||
{
|
{
|
||||||
/// <summary>
|
public const string Version = "2.1.0-net461";
|
||||||
/// Current tool version written into generated snapshots.
|
private const string UnknownApplication = "(Unbekannte Anwendung)";
|
||||||
/// </summary>
|
|
||||||
public const string Version = "2.1.2-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;
|
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)
|
public BizTalkOperationService(OperationLogger logger)
|
||||||
{
|
{
|
||||||
_logger = 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)
|
public void Diagnose(string server)
|
||||||
{
|
{
|
||||||
using (var client = CreateClient(server))
|
using (var client = CreateClient(server))
|
||||||
@@ -47,20 +26,15 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
var ports = client.Query("MSBTS_SendPort");
|
var ports = client.Query("MSBTS_SendPort");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.Success("WMI/CIM diagnostic succeeded. Send ports visible: " + ports.Count);
|
_logger.Success("WMI-Diagnose erfolgreich. Sichtbare Send Ports: " + ports.Count);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
DisposeAll(ports);
|
foreach (var port in ports) port.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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)
|
public BizTalkSnapshot CreateSnapshot(string server)
|
||||||
{
|
{
|
||||||
using (var client = CreateClient(server))
|
using (var client = CreateClient(server))
|
||||||
@@ -68,15 +42,14 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
var snapshot = new BizTalkSnapshot
|
var snapshot = new BizTalkSnapshot
|
||||||
{
|
{
|
||||||
ToolVersion = Version,
|
ToolVersion = Version,
|
||||||
CreatedAt = DateTimeOffset.Now.ToString("o"),
|
CreatedAt = DateTime.Now.ToString("s"),
|
||||||
Server = client.Server
|
Server = client.Server
|
||||||
};
|
};
|
||||||
|
|
||||||
var apps = new Dictionary<string, ApplicationSnapshot>(StringComparer.OrdinalIgnoreCase);
|
var apps = new Dictionary<string, ApplicationSnapshot>(StringComparer.OrdinalIgnoreCase);
|
||||||
var receiveLocations = client.Query("MSBTS_ReceiveLocation");
|
foreach (var item in client.Query("MSBTS_ReceiveLocation"))
|
||||||
try
|
|
||||||
{
|
{
|
||||||
foreach (var item in receiveLocations)
|
using (item)
|
||||||
{
|
{
|
||||||
var appName = GetApplicationName(item);
|
var appName = GetApplicationName(item);
|
||||||
var app = GetApplication(apps, appName);
|
var app = GetApplication(apps, appName);
|
||||||
@@ -87,19 +60,16 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
ReceivePortName = BizTalkWmiClient.SafeGetString(item, "ReceivePortName", string.Empty),
|
ReceivePortName = BizTalkWmiClient.SafeGetString(item, "ReceivePortName", string.Empty),
|
||||||
Enabled = !BizTalkWmiClient.SafeGetBoolean(item, "IsDisabled", true),
|
Enabled = !BizTalkWmiClient.SafeGetBoolean(item, "IsDisabled", true),
|
||||||
AdapterName = BizTalkWmiClient.SafeGetString(item, "AdapterName", string.Empty),
|
AdapterName = BizTalkWmiClient.SafeGetString(item, "AdapterName", string.Empty),
|
||||||
Address = BizTalkWmiClient.SafeGetString(item, "InboundTransportURL", string.Empty)
|
// Transport-URIs koennen Zugangsdaten enthalten und werden fuer die
|
||||||
|
// Zustandswiederherstellung nicht benoetigt. Daher nicht persistieren.
|
||||||
|
Address = string.Empty
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
DisposeAll(receiveLocations);
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendPorts = client.Query("MSBTS_SendPort");
|
foreach (var item in client.Query("MSBTS_SendPort"))
|
||||||
try
|
|
||||||
{
|
{
|
||||||
foreach (var item in sendPorts)
|
using (item)
|
||||||
{
|
{
|
||||||
var appName = GetApplicationName(item);
|
var appName = GetApplicationName(item);
|
||||||
var app = GetApplication(apps, appName);
|
var app = GetApplication(apps, appName);
|
||||||
@@ -109,19 +79,14 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty),
|
Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty),
|
||||||
Status = BizTalkWmiClient.SafeGetInt32(item, "Status", 0),
|
Status = BizTalkWmiClient.SafeGetInt32(item, "Status", 0),
|
||||||
PrimaryTransportType = BizTalkWmiClient.SafeGetString(item, "PTTransportType", string.Empty),
|
PrimaryTransportType = BizTalkWmiClient.SafeGetString(item, "PTTransportType", string.Empty),
|
||||||
PrimaryTransportAddress = BizTalkWmiClient.SafeGetString(item, "PTAddress", string.Empty)
|
PrimaryTransportAddress = string.Empty
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
DisposeAll(sendPorts);
|
|
||||||
}
|
|
||||||
|
|
||||||
var orchestrations = client.Query("MSBTS_Orchestration");
|
foreach (var item in client.Query("MSBTS_Orchestration"))
|
||||||
try
|
|
||||||
{
|
{
|
||||||
foreach (var item in orchestrations)
|
using (item)
|
||||||
{
|
{
|
||||||
var appName = GetApplicationName(item);
|
var appName = GetApplicationName(item);
|
||||||
var app = GetApplication(apps, appName);
|
var app = GetApplication(apps, appName);
|
||||||
@@ -129,19 +94,18 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
Application = app.Application,
|
Application = app.Application,
|
||||||
Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty),
|
Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty),
|
||||||
OrchestrationStatus = BizTalkWmiClient.SafeGetInt32(item, "OrchestrationStatus", 0)
|
OrchestrationStatus = BizTalkWmiClient.SafeGetInt32(item, "OrchestrationStatus", 0),
|
||||||
|
AssemblyName = BizTalkWmiClient.SafeGetString(item, "AssemblyName", string.Empty),
|
||||||
|
AssemblyVersion = BizTalkWmiClient.SafeGetString(item, "AssemblyVersion", string.Empty),
|
||||||
|
AssemblyCulture = BizTalkWmiClient.SafeGetString(item, "AssemblyCulture", string.Empty),
|
||||||
|
AssemblyPublicKeyToken = BizTalkWmiClient.SafeGetString(item, "AssemblyPublicKeyToken", string.Empty)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
DisposeAll(orchestrations);
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostInstances = client.Query("MSBTS_HostInstance");
|
foreach (var item in client.Query("MSBTS_HostInstance"))
|
||||||
try
|
|
||||||
{
|
{
|
||||||
foreach (var item in hostInstances)
|
using (item)
|
||||||
{
|
{
|
||||||
var state = BizTalkWmiClient.SafeGetInt32(item, "ServiceState", 0);
|
var state = BizTalkWmiClient.SafeGetInt32(item, "ServiceState", 0);
|
||||||
snapshot.HostInstances.Add(new HostInstanceState
|
snapshot.HostInstances.Add(new HostInstanceState
|
||||||
@@ -154,10 +118,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
DisposeAll(hostInstances);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var app in apps.Values.OrderBy(a => a.Application))
|
foreach (var app in apps.Values.OrderBy(a => a.Application))
|
||||||
{
|
{
|
||||||
@@ -168,50 +128,45 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
snapshot.HostInstances = snapshot.HostInstances.OrderBy(x => x.Server).ThenBy(x => x.InstanceName).ToList();
|
snapshot.HostInstances = snapshot.HostInstances.OrderBy(x => x.Server).ThenBy(x => x.InstanceName).ToList();
|
||||||
|
|
||||||
SnapshotValidator.Validate(snapshot);
|
_logger.Success("Snapshot erstellt. Anwendungen: " + snapshot.Applications.Count + ", Hostinstanzen: " + snapshot.HostInstances.Count);
|
||||||
_logger.Success("Snapshot created. Applications: " + snapshot.Applications.Count + ", host instances: " + snapshot.HostInstances.Count);
|
|
||||||
return snapshot;
|
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)
|
public OperationPlan CreateShutdownPlan(BizTalkSnapshot snapshot, string server)
|
||||||
{
|
{
|
||||||
SnapshotValidator.EnsureServerMatches(snapshot, server);
|
|
||||||
var plan = NewPlan(OperationMode.Shutdown, server);
|
var plan = NewPlan(OperationMode.Shutdown, server);
|
||||||
|
|
||||||
// Eingehenden Verkehr zuerst stoppen, bevor abhängige Verarbeitungsartefakte
|
|
||||||
// und zuletzt die Host Instances heruntergefahren werden.
|
|
||||||
foreach (var app in snapshot.Applications)
|
foreach (var app in snapshot.Applications)
|
||||||
{
|
{
|
||||||
foreach (var item in app.ReceiveLocations.Where(x => x.Enabled))
|
foreach (var item in app.ReceiveLocations.Where(x => x.Enabled))
|
||||||
{
|
{
|
||||||
plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, null));
|
var step = Step("ReceiveLocation", app.Application, item.Name, null, "Receive Location deaktivieren", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, null);
|
||||||
|
SetQualifier(step, "ReceivePortName", item.ReceivePortName);
|
||||||
|
plan.Steps.Add(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted))
|
foreach (var item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted))
|
||||||
{
|
{
|
||||||
plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Stop orchestration", "MSBTS_Orchestration", "Name", item.Name, "Stop", new[] { 1, 1 }, ArtifactStates.OrchestrationStopped));
|
var step = Step("Orchestration", app.Application, item.Name, null, "Orchestrierung stoppen", "MSBTS_Orchestration", "Name", item.Name, "Stop", new[] { 1, 1 }, ArtifactStates.OrchestrationStopped);
|
||||||
|
SetQualifier(step, "AssemblyName", item.AssemblyName);
|
||||||
|
plan.Steps.Add(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var item in app.SendPorts.Where(x => x.Status == ArtifactStates.SendPortStarted))
|
foreach (var item in app.SendPorts.Where(x => x.Status == ArtifactStates.SendPortStarted))
|
||||||
{
|
{
|
||||||
plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Stop send port", "MSBTS_SendPort", "Name", item.Name, "Stop", null, ArtifactStates.SendPortStopped));
|
plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Send Port stoppen", "MSBTS_SendPort", "Name", item.Name, "Stop", null, ArtifactStates.SendPortStopped));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted))
|
foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted))
|
||||||
{
|
{
|
||||||
var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Stop host instance", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Stop", null, ArtifactStates.HostStopped);
|
var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Hostinstanz stoppen", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Stop", null, ArtifactStates.HostStopped);
|
||||||
if (!string.IsNullOrEmpty(item.Server) && !string.Equals(item.Server, server, StringComparison.OrdinalIgnoreCase))
|
SetQualifier(step, "RunningServer", item.Server);
|
||||||
|
if (!string.IsNullOrEmpty(item.Server) && !SameServer(item.Server, server))
|
||||||
{
|
{
|
||||||
step.Execute = false;
|
step.Execute = false;
|
||||||
step.Warning = "Host instance belongs to server '" + item.Server + "'. Run this action on that server.";
|
step.Warning = "Die Hostinstanz gehört zum Server '" + item.Server + "'. Aktion dort ausführen.";
|
||||||
}
|
}
|
||||||
plan.Steps.Add(step);
|
plan.Steps.Add(step);
|
||||||
}
|
}
|
||||||
@@ -219,102 +174,51 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return plan;
|
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)
|
public OperationPlan CreateRestorePlan(BizTalkSnapshot snapshot, string server)
|
||||||
{
|
{
|
||||||
SnapshotValidator.EnsureServerMatches(snapshot, server);
|
|
||||||
var plan = NewPlan(OperationMode.Restore, server);
|
var plan = NewPlan(OperationMode.Restore, server);
|
||||||
|
|
||||||
// Beim Restore gilt die umgekehrte Abhängigkeitsrichtung: zuerst Laufzeit-Hosts,
|
|
||||||
// danach ausgehende Verarbeitung und Receive Locations bewusst ganz zum Schluss.
|
|
||||||
foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted))
|
foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted))
|
||||||
{
|
{
|
||||||
var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Start host instance", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Start", null, ArtifactStates.HostStarted);
|
var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Hostinstanz starten", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Start", null, ArtifactStates.HostStarted);
|
||||||
if (!string.IsNullOrEmpty(item.Server) && !string.Equals(item.Server, server, StringComparison.OrdinalIgnoreCase))
|
SetQualifier(step, "RunningServer", item.Server);
|
||||||
|
if (!string.IsNullOrEmpty(item.Server) && !SameServer(item.Server, server))
|
||||||
{
|
{
|
||||||
step.Execute = false;
|
step.Execute = false;
|
||||||
step.Warning = "Host instance belongs to server '" + item.Server + "'. Run this action on that server.";
|
step.Warning = "Die Hostinstanz gehört zum Server '" + item.Server + "'. Aktion dort ausführen.";
|
||||||
}
|
}
|
||||||
plan.Steps.Add(step);
|
plan.Steps.Add(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var app in snapshot.Applications)
|
foreach (var app in snapshot.Applications)
|
||||||
{
|
{
|
||||||
foreach (var item in app.SendPorts)
|
// Restore startet ausschliesslich Artefakte, die im Vorher-Snapshot aktiv waren.
|
||||||
|
// Bewusst gestoppte/gebundene Artefakte bleiben unangetastet.
|
||||||
|
foreach (var item in app.SendPorts.Where(x => x.Status == ArtifactStates.SendPortStarted))
|
||||||
{
|
{
|
||||||
if (item.Status == ArtifactStates.SendPortStarted)
|
plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Send Port starten", "MSBTS_SendPort", "Name", item.Name, "Start", null, ArtifactStates.SendPortStarted));
|
||||||
{
|
|
||||||
plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Start send port", "MSBTS_SendPort", "Name", item.Name, "Start", null, ArtifactStates.SendPortStarted));
|
|
||||||
}
|
|
||||||
else if (item.Status == ArtifactStates.SendPortStopped)
|
|
||||||
{
|
|
||||||
plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Ensure send port is stopped", "MSBTS_SendPort", "Name", item.Name, "StopOrEnlist", null, ArtifactStates.SendPortStopped));
|
|
||||||
}
|
|
||||||
else if (item.Status == ArtifactStates.SendPortBound)
|
|
||||||
{
|
|
||||||
plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Ensure send port is bound", "MSBTS_SendPort", "Name", item.Name, "UnEnlist", null, ArtifactStates.SendPortBound));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var item in app.Orchestrations)
|
foreach (var item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted))
|
||||||
{
|
{
|
||||||
if (item.OrchestrationStatus == ArtifactStates.OrchestrationStarted)
|
var step = Step("Orchestration", app.Application, item.Name, null, "Orchestrierung starten", "MSBTS_Orchestration", "Name", item.Name, "Start", new[] { 1, 1, 1 }, ArtifactStates.OrchestrationStarted);
|
||||||
{
|
SetQualifier(step, "AssemblyName", item.AssemblyName);
|
||||||
plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Start orchestration", "MSBTS_Orchestration", "Name", item.Name, "Start", new[] { 1, 1, 1 }, ArtifactStates.OrchestrationStarted));
|
plan.Steps.Add(step);
|
||||||
}
|
|
||||||
else if (item.OrchestrationStatus == ArtifactStates.OrchestrationStopped)
|
|
||||||
{
|
|
||||||
plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Ensure orchestration is stopped", "MSBTS_Orchestration", "Name", item.Name, "StopIfStarted", new[] { 1, 1 }, ArtifactStates.OrchestrationStopped));
|
|
||||||
}
|
|
||||||
else if (item.OrchestrationStatus == ArtifactStates.OrchestrationBound)
|
|
||||||
{
|
|
||||||
// Ein blindes Unenlist würde Bound nach Unbound verschieben und damit
|
|
||||||
// einen anderen Zustand als im Snapshot herstellen.
|
|
||||||
plan.Steps.Add(new OperationStep
|
|
||||||
{
|
|
||||||
Kind = "Note",
|
|
||||||
Application = app.Application,
|
|
||||||
Name = item.Name,
|
|
||||||
Action = "Orchestration was Bound in the snapshot; leaving unchanged to avoid making it Unbound.",
|
|
||||||
Execute = false,
|
|
||||||
Warning = "Manual check recommended."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (item.OrchestrationStatus == ArtifactStates.OrchestrationUnbound)
|
|
||||||
{
|
|
||||||
plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Unenlist orchestration", "MSBTS_Orchestration", "Name", item.Name, "UnenlistService", new[] { 1 }, ArtifactStates.OrchestrationUnbound));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var item in app.ReceiveLocations)
|
foreach (var item in app.ReceiveLocations.Where(x => x.Enabled))
|
||||||
{
|
{
|
||||||
plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, item.Enabled ? "Enable receive location" : "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, item.Enabled ? "Enable" : "Disable", null, null));
|
var step = Step("ReceiveLocation", app.Application, item.Name, null, "Receive Location aktivieren", "MSBTS_ReceiveLocation", "Name", item.Name, "Enable", null, null);
|
||||||
|
SetQualifier(step, "ReceivePortName", item.ReceivePortName);
|
||||||
|
plan.Steps.Add(step);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return plan;
|
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)
|
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))
|
using (var client = CreateClient(options.Server))
|
||||||
{
|
{
|
||||||
foreach (var step in plan.Steps)
|
foreach (var step in plan.Steps)
|
||||||
@@ -327,7 +231,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
|
|
||||||
if (options.DryRun)
|
if (options.DryRun)
|
||||||
{
|
{
|
||||||
// Dry-run löst das Objekt absichtlich nicht erneut per WMI auf und führt keine Methode aus.
|
|
||||||
_logger.Info("DRY RUN: " + step.Action + " '" + step.Name + "'");
|
_logger.Info("DRY RUN: " + step.Action + " '" + step.Name + "'");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -335,7 +238,7 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.Info("Executing step: " + DescribeStep(step));
|
_logger.Info("Executing step: " + DescribeStep(step));
|
||||||
using (var instance = client.FindByProperty(step.WmiClass, step.KeyProperty, step.KeyValue))
|
using (var instance = client.FindByProperties(step.WmiClass, step.KeyProperty, step.KeyValue, step.QualifierProperty, step.QualifierValue))
|
||||||
{
|
{
|
||||||
if (instance == null)
|
if (instance == null)
|
||||||
{
|
{
|
||||||
@@ -353,13 +256,6 @@ 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)
|
public string SaveSnapshot(string outputDirectory, string fileName, BizTalkSnapshot snapshot)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(outputDirectory);
|
Directory.CreateDirectory(outputDirectory);
|
||||||
@@ -369,13 +265,31 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public string SaveSnapshot(string path, BizTalkSnapshot snapshot)
|
||||||
/// Saves an operation plan as JSON.
|
{
|
||||||
/// </summary>
|
SnapshotStore.SaveSnapshotSet(path, snapshot);
|
||||||
/// <param name="outputDirectory">The directory where output files are written.</param>
|
_logger.Success("Zustand gespeichert: " + path);
|
||||||
/// <param name="fileName">The plan JSON file name.</param>
|
return path;
|
||||||
/// <param name="plan">The plan to persist.</param>
|
}
|
||||||
/// <returns>The saved plan file path.</returns>
|
|
||||||
|
public static void ValidateSnapshot(BizTalkSnapshot snapshot, string expectedServer)
|
||||||
|
{
|
||||||
|
if (snapshot == null)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Die Zustandsdatei enthält keinen Snapshot.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.Applications == null || snapshot.HostInstances == null)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Die Zustandsdatei ist unvollständig.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SameServer(snapshot.Server, expectedServer))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Die Zustandsdatei gehört zum Server '" + snapshot.Server + "', ausgewählt ist jedoch '" + expectedServer + "'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string SavePlan(string outputDirectory, string fileName, OperationPlan plan)
|
public string SavePlan(string outputDirectory, string fileName, OperationPlan plan)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(outputDirectory);
|
Directory.CreateDirectory(outputDirectory);
|
||||||
@@ -385,13 +299,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return path;
|
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)
|
public string SaveDiff(string outputDirectory, string fileName, SnapshotDiff diff)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(outputDirectory);
|
Directory.CreateDirectory(outputDirectory);
|
||||||
@@ -401,15 +308,14 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return path;
|
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)
|
private void ExecuteStep(BizTalkWmiClient client, ManagementObject instance, OperationStep step, OperationOptions options)
|
||||||
{
|
{
|
||||||
|
if (IsTargetReached(instance, step))
|
||||||
|
{
|
||||||
|
_logger.Success("Bereits im Zielzustand: " + step.Name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(step.MethodName, "StopOrEnlist", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(step.MethodName, "StopOrEnlist", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var status = BizTalkWmiClient.SafeGetInt32(instance, "Status", 0);
|
var status = BizTalkWmiClient.SafeGetInt32(instance, "Status", 0);
|
||||||
@@ -446,12 +352,33 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
WaitForTarget(client, step, options);
|
WaitForTarget(client, step, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static bool IsTargetReached(ManagementObject instance, OperationStep step)
|
||||||
/// Waits until a WMI object reaches the target state described by a plan step.
|
{
|
||||||
/// </summary>
|
if (instance == null || step == null)
|
||||||
/// <param name="client">The connected WMI client.</param>
|
{
|
||||||
/// <param name="step">The step whose target state should be verified.</param>
|
return false;
|
||||||
/// <param name="options">The runtime options controlling timeout and polling interval.</param>
|
}
|
||||||
|
|
||||||
|
if (step.Kind == "ReceiveLocation")
|
||||||
|
{
|
||||||
|
var shouldBeEnabled = string.Equals(step.MethodName, "Enable", StringComparison.OrdinalIgnoreCase);
|
||||||
|
return !BizTalkWmiClient.SafeGetBoolean(instance, "IsDisabled", true) == shouldBeEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!step.TargetState.HasValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var property = step.Kind == "SendPort"
|
||||||
|
? "Status"
|
||||||
|
: step.Kind == "Orchestration"
|
||||||
|
? "OrchestrationStatus"
|
||||||
|
: step.Kind == "HostInstance" ? "ServiceState" : string.Empty;
|
||||||
|
return property.Length > 0
|
||||||
|
&& BizTalkWmiClient.SafeGetInt32(instance, property, 0) == step.TargetState.Value;
|
||||||
|
}
|
||||||
|
|
||||||
private void WaitForTarget(BizTalkWmiClient client, OperationStep step, OperationOptions options)
|
private void WaitForTarget(BizTalkWmiClient client, OperationStep step, OperationOptions options)
|
||||||
{
|
{
|
||||||
if (!step.TargetState.HasValue)
|
if (!step.TargetState.HasValue)
|
||||||
@@ -459,30 +386,25 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
if (step.Kind == "ReceiveLocation")
|
if (step.Kind == "ReceiveLocation")
|
||||||
{
|
{
|
||||||
var shouldBeEnabled = string.Equals(step.MethodName, "Enable", StringComparison.OrdinalIgnoreCase);
|
var shouldBeEnabled = string.Equals(step.MethodName, "Enable", StringComparison.OrdinalIgnoreCase);
|
||||||
client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => !BizTalkWmiClient.SafeGetBoolean(o, "IsDisabled", true) == shouldBeEnabled, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
|
client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, step.QualifierProperty, step.QualifierValue, o => !BizTalkWmiClient.SafeGetBoolean(o, "IsDisabled", true) == shouldBeEnabled, step.Action + " abgeschlossen", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step.Kind == "SendPort")
|
if (step.Kind == "SendPort")
|
||||||
{
|
{
|
||||||
client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "Status", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
|
client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, step.QualifierProperty, step.QualifierValue, o => BizTalkWmiClient.SafeGetInt32(o, "Status", 0) == step.TargetState.Value, step.Action + " abgeschlossen", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
|
||||||
}
|
}
|
||||||
else if (step.Kind == "Orchestration")
|
else if (step.Kind == "Orchestration")
|
||||||
{
|
{
|
||||||
client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "OrchestrationStatus", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
|
client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, step.QualifierProperty, step.QualifierValue, o => BizTalkWmiClient.SafeGetInt32(o, "OrchestrationStatus", 0) == step.TargetState.Value, step.Action + " abgeschlossen", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
|
||||||
}
|
}
|
||||||
else if (step.Kind == "HostInstance")
|
else if (step.Kind == "HostInstance")
|
||||||
{
|
{
|
||||||
client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "ServiceState", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
|
client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, step.QualifierProperty, step.QualifierValue, o => BizTalkWmiClient.SafeGetInt32(o, "ServiceState", 0) == step.TargetState.Value, step.Action + " abgeschlossen", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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)
|
private BizTalkWmiClient CreateClient(string server)
|
||||||
{
|
{
|
||||||
var client = new BizTalkWmiClient(server, _logger);
|
var client = new BizTalkWmiClient(server, _logger);
|
||||||
@@ -490,37 +412,16 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return client;
|
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)
|
private static OperationPlan NewPlan(OperationMode mode, string server)
|
||||||
{
|
{
|
||||||
return new OperationPlan
|
return new OperationPlan
|
||||||
{
|
{
|
||||||
Mode = mode.ToString(),
|
Mode = mode.ToString(),
|
||||||
CreatedAt = DateTimeOffset.Now.ToString("o"),
|
CreatedAt = DateTime.Now.ToString("s"),
|
||||||
Server = server
|
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)
|
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
|
return new OperationStep
|
||||||
@@ -540,11 +441,24 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static void SetQualifier(OperationStep step, string property, string value)
|
||||||
/// Converts integer method arguments into the object array required by WMI.
|
{
|
||||||
/// </summary>
|
if (step == null || string.IsNullOrWhiteSpace(value))
|
||||||
/// <param name="values">The integer values from the operation step.</param>
|
{
|
||||||
/// <returns>An object array suitable for ManagementObject.InvokeMethod.</returns>
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
step.QualifierProperty = property;
|
||||||
|
step.QualifierValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SameServer(string left, string right)
|
||||||
|
{
|
||||||
|
var first = (left ?? string.Empty).Trim().TrimStart('\\').Split('.')[0];
|
||||||
|
var second = (right ?? string.Empty).Trim().TrimStart('\\').Split('.')[0];
|
||||||
|
return first.Length > 0 && string.Equals(first, second, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
private static object[] ToObjects(int[] values)
|
private static object[] ToObjects(int[] values)
|
||||||
{
|
{
|
||||||
if (values == null || values.Length == 0)
|
if (values == null || values.Length == 0)
|
||||||
@@ -560,11 +474,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return result;
|
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)
|
private static string DescribeStep(OperationStep step)
|
||||||
{
|
{
|
||||||
if (step == null)
|
if (step == null)
|
||||||
@@ -582,12 +491,6 @@ 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)
|
private static ApplicationSnapshot GetApplication(Dictionary<string, ApplicationSnapshot> apps, string appName)
|
||||||
{
|
{
|
||||||
ApplicationSnapshot app;
|
ApplicationSnapshot app;
|
||||||
@@ -599,11 +502,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return app;
|
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)
|
private static string GetApplicationName(ManagementObject item)
|
||||||
{
|
{
|
||||||
var names = new[] { "ApplicationName", "Application", "BizTalkApplication" };
|
var names = new[] { "ApplicationName", "Application", "BizTalkApplication" };
|
||||||
@@ -617,24 +515,5 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
return UnknownApplication;
|
return UnknownApplication;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gibt alle von einer WMI-Abfrage übernommenen Objekte deterministisch frei.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="items">Die freizugebenden WMI-Objekte oder <c>null</c>.</param>
|
|
||||||
private static void DisposeAll(IEnumerable<ManagementObject> items)
|
|
||||||
{
|
|
||||||
if (items == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
if (item != null)
|
|
||||||
{
|
|
||||||
item.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,53 +6,24 @@ using System.Threading;
|
|||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Services
|
namespace BizTalkPlatformManagementTool.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Provides the WMI access layer for BizTalk Server objects in root\MicrosoftBizTalkServer.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class BizTalkWmiClient : IDisposable
|
public sealed class BizTalkWmiClient : IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// BizTalk WMI namespace used for all platform management queries.
|
|
||||||
/// </summary>
|
|
||||||
private const string NamespacePath = "root\\MicrosoftBizTalkServer";
|
private const string NamespacePath = "root\\MicrosoftBizTalkServer";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Target server for the WMI connection.
|
|
||||||
/// </summary>
|
|
||||||
private readonly string _server;
|
private readonly string _server;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Logger used for WMI diagnostics and operation traces.
|
|
||||||
/// </summary>
|
|
||||||
private readonly OperationLogger _logger;
|
private readonly OperationLogger _logger;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connected WMI management scope, created lazily or by Connect.
|
|
||||||
/// </summary>
|
|
||||||
private ManagementScope _scope;
|
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)
|
public BizTalkWmiClient(string server, OperationLogger logger)
|
||||||
{
|
{
|
||||||
_server = string.IsNullOrWhiteSpace(server) ? Environment.MachineName : server.Trim();
|
_server = string.IsNullOrWhiteSpace(server) ? Environment.MachineName : server.Trim();
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the normalized target server name used by this client.
|
|
||||||
/// </summary>
|
|
||||||
public string Server
|
public string Server
|
||||||
{
|
{
|
||||||
get { return _server; }
|
get { return _server; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connects to the BizTalk WMI namespace on the target server.
|
|
||||||
/// </summary>
|
|
||||||
public void Connect()
|
public void Connect()
|
||||||
{
|
{
|
||||||
var path = "\\\\" + _server + "\\" + NamespacePath;
|
var path = "\\\\" + _server + "\\" + NamespacePath;
|
||||||
@@ -62,22 +33,11 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
_logger.Success("WMI connection established.");
|
_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)
|
public List<ManagementObject> Query(string className)
|
||||||
{
|
{
|
||||||
return Query(className, true);
|
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)
|
private List<ManagementObject> Query(string className, bool logQuery)
|
||||||
{
|
{
|
||||||
EnsureConnected();
|
EnsureConnected();
|
||||||
@@ -103,38 +63,23 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
catch (ManagementException ex)
|
catch (ManagementException ex)
|
||||||
{
|
{
|
||||||
foreach (var item in result)
|
|
||||||
{
|
|
||||||
item.Dispose();
|
|
||||||
}
|
|
||||||
throw new InvalidOperationException("WMI query failed. Query: " + queryText + ". WMI error: " + ex.Message, ex);
|
throw new InvalidOperationException("WMI query failed. Query: " + queryText + ". WMI error: " + ex.Message, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
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)
|
public ManagementObject FindByProperty(string className, string propertyName, string value)
|
||||||
{
|
{
|
||||||
return FindByProperty(className, propertyName, value, true);
|
return FindByProperties(className, propertyName, value, null, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public ManagementObject FindByProperties(string className, string propertyName, string value, string qualifierProperty, string qualifierValue)
|
||||||
/// Finds one WMI object using SELECT * plus a client-side filter so special
|
{
|
||||||
/// characters in BizTalk names cannot break a WQL WHERE clause.
|
return FindByProperties(className, propertyName, value, qualifierProperty, qualifierValue, true);
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="className">The WMI class name to query.</param>
|
|
||||||
/// <param name="propertyName">The property used as the lookup key.</param>
|
private ManagementObject FindByProperties(string className, string propertyName, string value, string qualifierProperty, string qualifierValue, bool logLookup)
|
||||||
/// <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();
|
EnsureConnected();
|
||||||
|
|
||||||
@@ -144,6 +89,14 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
"{0} = \"{1}\"",
|
"{0} = \"{1}\"",
|
||||||
propertyName,
|
propertyName,
|
||||||
value ?? string.Empty);
|
value ?? string.Empty);
|
||||||
|
if (!string.IsNullOrWhiteSpace(qualifierProperty))
|
||||||
|
{
|
||||||
|
filter += string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
" AND {0} = \"{1}\"",
|
||||||
|
qualifierProperty,
|
||||||
|
qualifierValue ?? string.Empty);
|
||||||
|
}
|
||||||
if (logLookup)
|
if (logLookup)
|
||||||
{
|
{
|
||||||
_logger.Info("Resolving WMI object: " + queryText + " with client-side filter " + filter);
|
_logger.Info("Resolving WMI object: " + queryText + " with client-side filter " + filter);
|
||||||
@@ -151,28 +104,37 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Bewusst keine WQL-WHERE-Klausel: BizTalk-Namen können Zeichen enthalten,
|
|
||||||
// die sonst eine fehlerhafte oder anders interpretierte Query erzeugen.
|
|
||||||
ManagementObject match = null;
|
|
||||||
var items = Query(className, false);
|
var items = Query(className, false);
|
||||||
|
var matches = new List<ManagementObject>();
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
if (MatchesProperty(item, className, propertyName, value))
|
if (MatchesProperty(item, className, propertyName, value)
|
||||||
|
&& (string.IsNullOrWhiteSpace(qualifierProperty)
|
||||||
|
|| MatchesProperty(item, className, qualifierProperty, qualifierValue)))
|
||||||
{
|
{
|
||||||
match = item;
|
matches.Add(item);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
if (!object.ReferenceEquals(item, match))
|
if (!matches.Contains(item))
|
||||||
{
|
{
|
||||||
item.Dispose();
|
item.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return match;
|
if (matches.Count > 1)
|
||||||
|
{
|
||||||
|
foreach (var item in matches)
|
||||||
|
{
|
||||||
|
item.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("WMI lookup is ambiguous. Class: " + className + ", filter: " + filter + ", matches: " + matches.Count + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches.Count == 0 ? null : matches[0];
|
||||||
}
|
}
|
||||||
catch (ManagementException ex)
|
catch (ManagementException ex)
|
||||||
{
|
{
|
||||||
@@ -180,13 +142,6 @@ 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)
|
public uint InvokeMethod(ManagementObject instance, string methodName, params object[] arguments)
|
||||||
{
|
{
|
||||||
if (instance == null)
|
if (instance == null)
|
||||||
@@ -207,9 +162,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
throw new InvalidOperationException("WMI method failed. Class: " + instance.Path.ClassName + ", method: " + methodToCall + ", object: " + SafeObjectName(instance) + ". WMI error: " + ex.Message, ex);
|
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);
|
var returnCode = ExtractReturnCode(result);
|
||||||
if (returnCode != 0)
|
if (returnCode != 0)
|
||||||
{
|
{
|
||||||
@@ -218,33 +170,15 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
|
|
||||||
return returnCode;
|
return returnCode;
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (output != null)
|
|
||||||
{
|
|
||||||
output.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public void WaitForState(string className, string keyProperty, string keyValue, string qualifierProperty, string qualifierValue, Func<ManagementObject, bool> isReached, string description, int timeoutSeconds, int pollIntervalSeconds)
|
||||||
/// 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));
|
var deadline = DateTime.UtcNow.AddSeconds(Math.Max(1, timeoutSeconds));
|
||||||
var delay = Math.Max(1, pollIntervalSeconds);
|
var delay = Math.Max(1, pollIntervalSeconds);
|
||||||
|
|
||||||
while (DateTime.UtcNow <= deadline)
|
while (DateTime.UtcNow <= deadline)
|
||||||
{
|
{
|
||||||
using (var current = FindByProperty(className, keyProperty, keyValue, false))
|
using (var current = FindByProperties(className, keyProperty, keyValue, qualifierProperty, qualifierValue, false))
|
||||||
{
|
{
|
||||||
if (current != null && isReached(current))
|
if (current != null && isReached(current))
|
||||||
{
|
{
|
||||||
@@ -253,25 +187,12 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var remaining = deadline - DateTime.UtcNow;
|
Thread.Sleep(TimeSpan.FromSeconds(delay));
|
||||||
if (remaining <= TimeSpan.Zero)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Am Timeout-Ende nur noch die tatsächlich verbleibende Zeit schlafen.
|
|
||||||
Thread.Sleep(remaining < TimeSpan.FromSeconds(delay) ? remaining : TimeSpan.FromSeconds(delay));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new TimeoutException("Timeout while waiting for " + description + " [" + className + "." + keyProperty + "=" + keyValue + "]");
|
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)
|
public static string SafeGetString(ManagementBaseObject item, string propertyName, string fallback)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -290,13 +211,6 @@ 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)
|
public static int SafeGetInt32(ManagementBaseObject item, string propertyName, int fallback)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -315,13 +229,6 @@ 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)
|
public static bool SafeGetBoolean(ManagementBaseObject item, string propertyName, bool fallback)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -340,9 +247,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures that the management scope is connected before a WMI operation runs.
|
|
||||||
/// </summary>
|
|
||||||
private void EnsureConnected()
|
private void EnsureConnected()
|
||||||
{
|
{
|
||||||
if (_scope == null || !_scope.IsConnected)
|
if (_scope == null || !_scope.IsConnected)
|
||||||
@@ -351,12 +255,6 @@ 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)
|
private static bool HasProperty(ManagementBaseObject item, string propertyName)
|
||||||
{
|
{
|
||||||
foreach (PropertyData property in item.Properties)
|
foreach (PropertyData property in item.Properties)
|
||||||
@@ -370,14 +268,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return false;
|
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)
|
private static bool MatchesProperty(ManagementBaseObject item, string className, string propertyName, string expectedValue)
|
||||||
{
|
{
|
||||||
foreach (var candidate in CandidatePropertyNames(className, propertyName))
|
foreach (var candidate in CandidatePropertyNames(className, propertyName))
|
||||||
@@ -397,12 +287,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return false;
|
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)
|
private static IEnumerable<string> CandidatePropertyNames(string className, string propertyName)
|
||||||
{
|
{
|
||||||
yield return propertyName;
|
yield return propertyName;
|
||||||
@@ -414,11 +298,6 @@ 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)
|
private static string SafeObjectName(ManagementObject instance)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -431,12 +310,6 @@ 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)
|
private static string ResolveMethodName(ManagementObject instance, string requestedName)
|
||||||
{
|
{
|
||||||
using (var managementClass = new ManagementClass(instance.Scope, new ManagementPath(instance.Path.ClassName), null))
|
using (var managementClass = new ManagementClass(instance.Scope, new ManagementPath(instance.Path.ClassName), null))
|
||||||
@@ -453,11 +326,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
throw new MissingMethodException(instance.Path.ClassName, requestedName);
|
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)
|
private static uint ExtractReturnCode(object result)
|
||||||
{
|
{
|
||||||
if (result == null)
|
if (result == null)
|
||||||
@@ -474,12 +342,8 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return Convert.ToUInt32(result, CultureInfo.InvariantCulture);
|
return Convert.ToUInt32(result, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Releases resources owned by this client.
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_scope = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,8 @@ using BizTalkPlatformManagementTool.Models;
|
|||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Services
|
namespace BizTalkPlatformManagementTool.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Writes snapshot and diff data to CSV files for review outside the GUI.
|
|
||||||
/// </summary>
|
|
||||||
public static class CsvWriter
|
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)
|
public static void WriteSnapshotArtifacts(string path, BizTalkSnapshot snapshot)
|
||||||
{
|
{
|
||||||
var lines = new List<string> { "Application,Type,Name,Status" };
|
var lines = new List<string> { "Application,Type,Name,Status" };
|
||||||
@@ -36,11 +28,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
File.WriteAllLines(path, lines, Encoding.UTF8);
|
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)
|
public static void WriteSnapshotHosts(string path, BizTalkSnapshot snapshot)
|
||||||
{
|
{
|
||||||
var lines = new List<string> { "InstanceName,HostName,Server,State" };
|
var lines = new List<string> { "InstanceName,HostName,Server,State" };
|
||||||
@@ -51,11 +38,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
File.WriteAllLines(path, lines, Encoding.UTF8);
|
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)
|
public static void WriteDiff(string path, SnapshotDiff diff)
|
||||||
{
|
{
|
||||||
var lines = new List<string> { "Scope,Application,Type,Name,Server,Before,After" };
|
var lines = new List<string> { "Scope,Application,Type,Name,Server,Before,After" };
|
||||||
@@ -70,11 +52,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
File.WriteAllLines(path, lines, Encoding.UTF8);
|
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)
|
private static string Row(params string[] values)
|
||||||
{
|
{
|
||||||
var escaped = new string[values.Length];
|
var escaped = new string[values.Length];
|
||||||
@@ -85,19 +62,9 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return string.Join(",", escaped);
|
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)
|
private static string Escape(string value)
|
||||||
{
|
{
|
||||||
value = value ?? string.Empty;
|
value = value ?? string.Empty;
|
||||||
if (value.Length > 0 && (value[0] == '=' || value[0] == '+' || value[0] == '-' || value[0] == '@' || value[0] == '\t'))
|
|
||||||
{
|
|
||||||
// Tabellenkalkulationen dürfen exportierte Namen nicht als Formel ausführen.
|
|
||||||
value = "'" + value;
|
|
||||||
}
|
|
||||||
if (value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) < 0)
|
if (value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) < 0)
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
@@ -4,16 +4,8 @@ using BizTalkPlatformManagementTool.Models;
|
|||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Services
|
namespace BizTalkPlatformManagementTool.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Writes HTML reports for BizTalk snapshots and snapshot differences.
|
|
||||||
/// </summary>
|
|
||||||
public static class HtmlReportWriter
|
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)
|
public static void WriteSnapshot(string path, BizTalkSnapshot snapshot)
|
||||||
{
|
{
|
||||||
var html = new StringBuilder();
|
var html = new StringBuilder();
|
||||||
@@ -58,11 +50,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
System.IO.File.WriteAllText(path, html.ToString(), Encoding.UTF8);
|
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)
|
public static void WriteDiff(string path, SnapshotDiff diff)
|
||||||
{
|
{
|
||||||
var html = new StringBuilder();
|
var html = new StringBuilder();
|
||||||
@@ -87,26 +74,12 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
System.IO.File.WriteAllText(path, html.ToString(), Encoding.UTF8);
|
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)
|
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))
|
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>");
|
.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)
|
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>")
|
html.Append("<!doctype html><html><head><meta charset='utf-8'><title>").Append(Encode(title)).Append("</title><style>")
|
||||||
@@ -114,23 +87,13 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
.Append("</style></head><body><h1>").Append(Encode(title)).Append("</h1>");
|
.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)
|
private static void Footer(StringBuilder html)
|
||||||
{
|
{
|
||||||
html.Append("</body></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)
|
private static string Encode(string value)
|
||||||
{
|
{
|
||||||
// Alle aus BizTalk gelesenen Werte werden vor der Aufnahme in HTML neutralisiert.
|
|
||||||
return WebUtility.HtmlEncode(value ?? string.Empty);
|
return WebUtility.HtmlEncode(value ?? string.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Runtime.Serialization.Json;
|
using System.Runtime.Serialization.Json;
|
||||||
@@ -6,51 +5,57 @@ using System.Text;
|
|||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Services
|
namespace BizTalkPlatformManagementTool.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Persists DataContract models as JSON with repository-defined encoding rules.
|
|
||||||
/// </summary>
|
|
||||||
public static class JsonFileStore
|
public static class JsonFileStore
|
||||||
{
|
{
|
||||||
/// <summary>
|
private const long MaximumJsonBytes = 16L * 1024L * 1024L;
|
||||||
/// UTF-8 encoding instance that writes JSON without a byte order mark.
|
|
||||||
/// </summary>
|
|
||||||
private static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
|
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)
|
public static void Save<T>(string path, T value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(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
|
var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
|
||||||
{
|
{
|
||||||
UseSimpleDictionaryFormat = true
|
UseSimpleDictionaryFormat = true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var fullPath = Path.GetFullPath(path);
|
||||||
|
var temporaryPath = fullPath + ".tmp." + System.Guid.NewGuid().ToString("N");
|
||||||
|
try
|
||||||
|
{
|
||||||
using (var stream = new MemoryStream())
|
using (var stream = new MemoryStream())
|
||||||
{
|
{
|
||||||
serializer.WriteObject(stream, value);
|
serializer.WriteObject(stream, value);
|
||||||
var json = Utf8NoBom.GetString(stream.ToArray());
|
var payload = stream.ToArray();
|
||||||
WriteAtomically(fullPath, json);
|
if (payload.LongLength > MaximumJsonBytes)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Die JSON-Ausgabe überschreitet die Sicherheitsgrenze von 16 MiB.");
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var file = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough))
|
||||||
|
{
|
||||||
|
file.Write(payload, 0, payload.Length);
|
||||||
|
file.Flush(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
File.Replace(temporaryPath, fullPath, null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File.Move(temporaryPath, fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(temporaryPath))
|
||||||
|
{
|
||||||
|
File.Delete(temporaryPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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)
|
public static T Load<T>(string path)
|
||||||
{
|
{
|
||||||
var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
|
var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
|
||||||
@@ -58,6 +63,16 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
UseSimpleDictionaryFormat = true
|
UseSimpleDictionaryFormat = true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var information = new FileInfo(path);
|
||||||
|
if (!information.Exists)
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("Die JSON-Datei wurde nicht gefunden.", path);
|
||||||
|
}
|
||||||
|
if (information.Length == 0 || information.Length > MaximumJsonBytes)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Die JSON-Datei ist leer oder überschreitet die Sicherheitsgrenze von 16 MiB: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
var json = NormalizeJson(File.ReadAllText(path, Encoding.UTF8));
|
var json = NormalizeJson(File.ReadAllText(path, Encoding.UTF8));
|
||||||
using (var stream = new MemoryStream(Utf8NoBom.GetBytes(json)))
|
using (var stream = new MemoryStream(Utf8NoBom.GetBytes(json)))
|
||||||
{
|
{
|
||||||
@@ -67,16 +82,11 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
catch (SerializationException ex)
|
catch (SerializationException ex)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException("Failed to load JSON file '" + path + "'. " + DescribeJsonStart(json), ex);
|
throw new InvalidDataException("Die JSON-Datei '" + path + "' konnte nicht geladen werden. " + DescribeJsonStart(json), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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)
|
private static string NormalizeJson(string json)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(json))
|
if (string.IsNullOrEmpty(json))
|
||||||
@@ -97,110 +107,21 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return json;
|
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)
|
private static string DescribeJsonStart(string json)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
{
|
{
|
||||||
return "The file is empty or contains only whitespace.";
|
return "Die Datei ist leer oder enthält nur Leerzeichen.";
|
||||||
}
|
}
|
||||||
|
|
||||||
var trimmed = json.TrimStart();
|
var trimmed = json.TrimStart();
|
||||||
var first = trimmed.Length == 0 ? '\0' : trimmed[0];
|
var first = trimmed.Length == 0 ? '\0' : trimmed[0];
|
||||||
if (first != '{' && first != '[')
|
if (first != '{' && first != '[')
|
||||||
{
|
{
|
||||||
return "The first JSON character is '" + first + "' instead of '{' or '['.";
|
return "Das erste JSON-Zeichen ist '" + first + "' statt '{' oder '['.";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "The file starts with valid JSON syntax but could not be deserialized into the expected model.";
|
return "Die Datei beginnt mit gültiger JSON-Syntax, passt aber nicht zum erwarteten Datenmodell.";
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes a file through a same-directory temporary file so an interrupted
|
|
||||||
/// save cannot leave a truncated snapshot or operation plan behind.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Der endgültige Zielpfad.</param>
|
|
||||||
/// <param name="content">Der vollständig serialisierte Dateiinhalt.</param>
|
|
||||||
private static void WriteAtomically(string path, string content)
|
|
||||||
{
|
|
||||||
// Temporärdatei und Backup liegen absichtlich im Zielverzeichnis. Dadurch bleiben
|
|
||||||
// Umbenennung und Austausch auf demselben Volume und können atomar erfolgen.
|
|
||||||
var temporaryPath = path + ".tmp." + Guid.NewGuid().ToString("N");
|
|
||||||
var backupPath = path + ".bak." + Guid.NewGuid().ToString("N");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ersetzt eine vorhandene Datei über Umbenennungen, wenn <see cref="File.Replace(string, string, string, bool)"/> nicht unterstützt wird.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Der endgültige Zielpfad.</param>
|
|
||||||
/// <param name="temporaryPath">Die vollständig geschriebene Temporärdatei.</param>
|
|
||||||
/// <param name="backupPath">Der temporäre Sicherungspfad der vorherigen Datei.</param>
|
|
||||||
private static void ReplaceWithRenameFallback(string path, string temporaryPath, string backupPath)
|
|
||||||
{
|
|
||||||
File.Move(path, backupPath);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Move(temporaryPath, path);
|
|
||||||
TryDelete(backupPath);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
if (!File.Exists(path) && File.Exists(backupPath))
|
|
||||||
{
|
|
||||||
File.Move(backupPath, path);
|
|
||||||
}
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Löscht eine temporäre Datei bestmöglich, ohne das primäre Speicherergebnis zu verändern.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Der zu löschende Dateipfad.</param>
|
|
||||||
private static void TryDelete(string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
File.Delete(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Die Bereinigung ist nachrangig und darf einen erfolgreichen Schreibvorgang nicht verdecken.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,97 +5,32 @@ using System.Threading;
|
|||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Services
|
namespace BizTalkPlatformManagementTool.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Defines the severity used for visible and file-based operation log entries.
|
|
||||||
/// </summary>
|
|
||||||
public enum LogLevel
|
public enum LogLevel
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Informational progress or diagnostic entry.
|
|
||||||
/// </summary>
|
|
||||||
Info,
|
Info,
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Non-fatal condition that needs operator attention.
|
|
||||||
/// </summary>
|
|
||||||
Warning,
|
Warning,
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Failed operation or exception entry.
|
|
||||||
/// </summary>
|
|
||||||
Error,
|
Error,
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Successful operation entry.
|
|
||||||
/// </summary>
|
|
||||||
Success
|
Success
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents one operation log entry displayed in the GUI and written to disk.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class LogEntry
|
public sealed class LogEntry
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the local time when the entry was created.
|
|
||||||
/// </summary>
|
|
||||||
public DateTime Timestamp { get; set; }
|
public DateTime Timestamp { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the entry severity.
|
|
||||||
/// </summary>
|
|
||||||
public LogLevel Level { get; set; }
|
public LogLevel Level { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the operator-facing message.
|
|
||||||
/// </summary>
|
|
||||||
public string Message { get; set; }
|
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
|
public sealed class OperationLogger
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Prefix used for daily log files written to the resolved log directory.
|
|
||||||
/// </summary>
|
|
||||||
private const string LogFilePrefix = "BizTalkPlatformManagementTool-";
|
private const string LogFilePrefix = "BizTalkPlatformManagementTool-";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// File extension used for operation log files.
|
|
||||||
/// </summary>
|
|
||||||
private const string LogFileExtension = ".log";
|
private const string LogFileExtension = ".log";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Number of daily log files retained, including the current day.
|
|
||||||
/// </summary>
|
|
||||||
private const int RetentionDays = 5;
|
private const int RetentionDays = 5;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process-wide lock that serializes log file appends.
|
|
||||||
/// </summary>
|
|
||||||
private static readonly object FileLock = new object();
|
private static readonly object FileLock = new object();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process-wide flag that ensures log cleanup runs only once.
|
|
||||||
/// </summary>
|
|
||||||
private static int _cleanupDone;
|
private static int _cleanupDone;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional callback for forwarding entries to the UI.
|
|
||||||
/// </summary>
|
|
||||||
private readonly Action<LogEntry> _sink;
|
private readonly Action<LogEntry> _sink;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Directory where daily log files are written.
|
|
||||||
/// </summary>
|
|
||||||
private readonly string _logDirectory;
|
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)
|
public OperationLogger(Action<LogEntry> sink)
|
||||||
{
|
{
|
||||||
_sink = sink;
|
_sink = sink;
|
||||||
@@ -103,9 +38,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
CleanupOldLogs();
|
CleanupOldLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the path of the daily log file for the current date.
|
|
||||||
/// </summary>
|
|
||||||
public string LogFilePath
|
public string LogFilePath
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -114,47 +46,26 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes an informational entry.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">The message to log.</param>
|
|
||||||
public void Info(string message)
|
public void Info(string message)
|
||||||
{
|
{
|
||||||
Write(LogLevel.Info, message);
|
Write(LogLevel.Info, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes a warning entry.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">The message to log.</param>
|
|
||||||
public void Warning(string message)
|
public void Warning(string message)
|
||||||
{
|
{
|
||||||
Write(LogLevel.Warning, message);
|
Write(LogLevel.Warning, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes an error entry.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">The message to log.</param>
|
|
||||||
public void Error(string message)
|
public void Error(string message)
|
||||||
{
|
{
|
||||||
Write(LogLevel.Error, message);
|
Write(LogLevel.Error, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes a success entry.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">The message to log.</param>
|
|
||||||
public void Success(string message)
|
public void Success(string message)
|
||||||
{
|
{
|
||||||
Write(LogLevel.Success, 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)
|
private void Write(LogLevel level, string message)
|
||||||
{
|
{
|
||||||
var entry = new LogEntry
|
var entry = new LogEntry
|
||||||
@@ -174,10 +85,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
_sink(entry);
|
_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)
|
private void WriteToFile(LogEntry entry)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -197,13 +104,10 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Ein Logfehler darf niemals eine fachliche BizTalk-Operation abbrechen.
|
// Logging must never interrupt BizTalk operations.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes log files older than the configured retention window.
|
|
||||||
/// </summary>
|
|
||||||
private void CleanupOldLogs()
|
private void CleanupOldLogs()
|
||||||
{
|
{
|
||||||
if (Interlocked.Exchange(ref _cleanupDone, 1) == 1)
|
if (Interlocked.Exchange(ref _cleanupDone, 1) == 1)
|
||||||
@@ -225,27 +129,33 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Die Aufbewahrungsbereinigung ist bestmöglich und beeinflusst den aktuellen Lauf nicht.
|
// Log retention cleanup is best-effort.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ermittelt das bevorzugte maschinenweite Logverzeichnis mit Rückfall auf das EXE-Verzeichnis.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Ein verwendbares Verzeichnis für die täglichen Laufzeitlogs.</returns>
|
|
||||||
private static string ResolveLogDirectory()
|
private static string ResolveLogDirectory()
|
||||||
{
|
{
|
||||||
var commonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
var candidates = new[]
|
||||||
var preferred = Path.Combine(commonData, "BizTalkPlatformManagementTool", "Logs");
|
{
|
||||||
|
AppPaths.LogDirectory,
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), AppPaths.ProductName, "Logs"),
|
||||||
|
AppDomain.CurrentDomain.BaseDirectory
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(preferred);
|
Directory.CreateDirectory(candidate);
|
||||||
return preferred;
|
return candidate;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
// Das Logging darf die Bedienoberflaeche nicht am Start hindern.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return AppDomain.CurrentDomain.BaseDirectory;
|
return AppDomain.CurrentDomain.BaseDirectory;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,12 @@ using BizTalkPlatformManagementTool.Models;
|
|||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Services
|
namespace BizTalkPlatformManagementTool.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Compares two BizTalk snapshots and returns the state differences that matter
|
|
||||||
/// after a maintenance window.
|
|
||||||
/// </summary>
|
|
||||||
public static class SnapshotComparer
|
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)
|
public static SnapshotDiff Compare(BizTalkSnapshot before, BizTalkSnapshot after)
|
||||||
{
|
{
|
||||||
SnapshotValidator.Validate(before);
|
if (before == null) throw new ArgumentNullException("before");
|
||||||
SnapshotValidator.Validate(after);
|
if (after == null) throw new ArgumentNullException("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();
|
var diff = new SnapshotDiff();
|
||||||
|
|
||||||
CompareReceiveLocations(diff, FlattenReceiveLocations(before), FlattenReceiveLocations(after));
|
CompareReceiveLocations(diff, FlattenReceiveLocations(before), FlattenReceiveLocations(after));
|
||||||
@@ -34,12 +20,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return diff;
|
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)
|
private static void CompareReceiveLocations(SnapshotDiff diff, Dictionary<string, ReceiveLocationState> before, Dictionary<string, ReceiveLocationState> after)
|
||||||
{
|
{
|
||||||
foreach (var pair in after)
|
foreach (var pair in after)
|
||||||
@@ -64,12 +44,6 @@ 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)
|
private static void CompareSendPorts(SnapshotDiff diff, Dictionary<string, SendPortState> before, Dictionary<string, SendPortState> after)
|
||||||
{
|
{
|
||||||
foreach (var pair in after)
|
foreach (var pair in after)
|
||||||
@@ -94,12 +68,6 @@ 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)
|
private static void CompareOrchestrations(SnapshotDiff diff, Dictionary<string, OrchestrationState> before, Dictionary<string, OrchestrationState> after)
|
||||||
{
|
{
|
||||||
foreach (var pair in after)
|
foreach (var pair in after)
|
||||||
@@ -124,12 +92,6 @@ 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)
|
private static void CompareHostInstances(SnapshotDiff diff, List<HostInstanceState> before, List<HostInstanceState> after)
|
||||||
{
|
{
|
||||||
var beforeMap = new Dictionary<string, HostInstanceState>(StringComparer.OrdinalIgnoreCase);
|
var beforeMap = new Dictionary<string, HostInstanceState>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -137,12 +99,11 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
|
|
||||||
foreach (var item in before)
|
foreach (var item in before)
|
||||||
{
|
{
|
||||||
// Hostname allein ist gruppenweit nicht eindeutig; der Server gehört zur Identität.
|
beforeMap[Key(item.Server, item.InstanceName)] = item;
|
||||||
beforeMap[SnapshotValidator.ArtifactKey(item.Server, item.InstanceName)] = item;
|
|
||||||
}
|
}
|
||||||
foreach (var item in after)
|
foreach (var item in after)
|
||||||
{
|
{
|
||||||
afterMap[SnapshotValidator.ArtifactKey(item.Server, item.InstanceName)] = item;
|
afterMap[Key(item.Server, item.InstanceName)] = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var pair in afterMap)
|
foreach (var pair in afterMap)
|
||||||
@@ -167,15 +128,6 @@ 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)
|
private static void AddArtifact(SnapshotDiff diff, string application, string type, string name, string before, string after)
|
||||||
{
|
{
|
||||||
diff.ArtifactDifferences.Add(new ArtifactDiffEntry
|
diff.ArtifactDifferences.Add(new ArtifactDiffEntry
|
||||||
@@ -188,11 +140,6 @@ 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)
|
private static Dictionary<string, ReceiveLocationState> FlattenReceiveLocations(BizTalkSnapshot snapshot)
|
||||||
{
|
{
|
||||||
var map = new Dictionary<string, ReceiveLocationState>(StringComparer.OrdinalIgnoreCase);
|
var map = new Dictionary<string, ReceiveLocationState>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -200,17 +147,12 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
foreach (var item in app.ReceiveLocations)
|
foreach (var item in app.ReceiveLocations)
|
||||||
{
|
{
|
||||||
map[SnapshotValidator.ArtifactKey(app.Application, item.Name)] = item;
|
map[Key(item.Application, item.ReceivePortName, item.Name)] = item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
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)
|
private static Dictionary<string, SendPortState> FlattenSendPorts(BizTalkSnapshot snapshot)
|
||||||
{
|
{
|
||||||
var map = new Dictionary<string, SendPortState>(StringComparer.OrdinalIgnoreCase);
|
var map = new Dictionary<string, SendPortState>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -218,17 +160,12 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
foreach (var item in app.SendPorts)
|
foreach (var item in app.SendPorts)
|
||||||
{
|
{
|
||||||
map[SnapshotValidator.ArtifactKey(app.Application, item.Name)] = item;
|
map[Key(item.Application, item.Name)] = item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
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)
|
private static Dictionary<string, OrchestrationState> FlattenOrchestrations(BizTalkSnapshot snapshot)
|
||||||
{
|
{
|
||||||
var map = new Dictionary<string, OrchestrationState>(StringComparer.OrdinalIgnoreCase);
|
var map = new Dictionary<string, OrchestrationState>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -236,10 +173,15 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
foreach (var item in app.Orchestrations)
|
foreach (var item in app.Orchestrations)
|
||||||
{
|
{
|
||||||
map[SnapshotValidator.ArtifactKey(app.Application, item.Name)] = item;
|
map[Key(item.Application, item.Name, item.AssemblyName, item.AssemblyVersion, item.AssemblyCulture, item.AssemblyPublicKeyToken)] = item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string Key(params string[] parts)
|
||||||
|
{
|
||||||
|
return string.Join("|", Array.ConvertAll(parts, value => (value ?? string.Empty).Trim().ToUpperInvariant()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,8 @@ using BizTalkPlatformManagementTool.Models;
|
|||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Services
|
namespace BizTalkPlatformManagementTool.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Saves snapshot and diff models together with their sidecar report formats.
|
|
||||||
/// </summary>
|
|
||||||
public static class SnapshotStore
|
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)
|
public static void SaveSnapshotSet(string jsonPath, BizTalkSnapshot snapshot)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath)));
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath)));
|
||||||
@@ -22,11 +14,6 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
HtmlReportWriter.WriteSnapshot(jsonPath + ".html", snapshot);
|
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)
|
public static void SaveDiffSet(string jsonPath, SnapshotDiff diff)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath)));
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath)));
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
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>
|
|
||||||
/// Normalisiert optionale Sammlungen und weist fehlende oder doppelte Artefaktidentitäten zurück.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="snapshot">Der zu normalisierende und zu validierende Snapshot.</param>
|
|
||||||
public static void Validate(BizTalkSnapshot snapshot)
|
|
||||||
{
|
|
||||||
if (snapshot == null)
|
|
||||||
{
|
|
||||||
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>
|
|
||||||
/// Validiert einen Snapshot und stellt sicher, dass er zum angeforderten Zielserver gehört.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="snapshot">Der als Operationsgrundlage verwendete Snapshot.</param>
|
|
||||||
/// <param name="targetServer">Der für die Operation ausgewählte BizTalk-Server.</param>
|
|
||||||
public static void EnsureServerMatches(BizTalkSnapshot snapshot, string targetServer)
|
|
||||||
{
|
|
||||||
Validate(snapshot);
|
|
||||||
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>
|
|
||||||
/// Vergleicht Servernamen und akzeptiert Kurzname und FQDN desselben Hosts als identisch.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="left">Der erste Servername.</param>
|
|
||||||
/// <param name="right">Der zweite Servername.</param>
|
|
||||||
/// <returns><c>true</c>, wenn beide Namen denselben Server bezeichnen; andernfalls <c>false</c>.</returns>
|
|
||||||
public static bool ServerNamesEqual(string left, string right)
|
|
||||||
{
|
|
||||||
var normalizedLeft = NormalizeServer(left);
|
|
||||||
var normalizedRight = NormalizeServer(right);
|
|
||||||
if (string.Equals(normalizedLeft, normalizedRight, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Equals(ShortName(normalizedLeft), ShortName(normalizedRight), StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Erstellt die kollisionsarme Identität für anwendungsbezogene BizTalk-Artefakte.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="application">Der Name der BizTalk-Anwendung.</param>
|
|
||||||
/// <param name="name">Der Artefaktname.</param>
|
|
||||||
/// <returns>Ein zusammengesetzter Schlüssel aus Anwendung und Artefaktname.</returns>
|
|
||||||
public static string ArtifactKey(string application, string name)
|
|
||||||
{
|
|
||||||
// Das nicht druckbare Trennzeichen kann in normalen BizTalk-Namen nicht mit der
|
|
||||||
// sichtbaren Verkettung von Anwendung und Artefakt verwechselt werden.
|
|
||||||
return (application ?? string.Empty).Trim() + "\u001f" + (name ?? string.Empty).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prüft eine Artefaktsammlung auf leere Namen und doppelte Identitäten.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Der Typ des zu prüfenden Snapshot-Artefakts.</typeparam>
|
|
||||||
/// <param name="application">Die besitzende BizTalk-Anwendung.</param>
|
|
||||||
/// <param name="type">Die lesbare Artefaktbezeichnung für Fehlermeldungen.</param>
|
|
||||||
/// <param name="values">Die zu prüfenden Artefakte.</param>
|
|
||||||
/// <param name="getName">Funktion zum Ermitteln des Artefaktnamens.</param>
|
|
||||||
/// <param name="keys">Die bereits bekannten Identitäten dieses Artefakttyps.</param>
|
|
||||||
private static void ValidateArtifacts<T>(string application, string type, IEnumerable<T> values, Func<T, string> getName, HashSet<string> keys)
|
|
||||||
{
|
|
||||||
foreach (var value in values)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalisiert lokale Serveraliasnamen auf den tatsächlichen Rechnernamen.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">Der eingegebene Servername.</param>
|
|
||||||
/// <returns>Der getrimmte und normalisierte Servername.</returns>
|
|
||||||
private static string NormalizeServer(string value)
|
|
||||||
{
|
|
||||||
value = (value ?? string.Empty).Trim().TrimStart('\\');
|
|
||||||
if (value == "." || string.Equals(value, "localhost", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return Environment.MachineName;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Entfernt den DNS-Suffix eines Servernamens.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">Ein normalisierter Kurzname oder FQDN.</param>
|
|
||||||
/// <returns>Der Hostanteil vor dem ersten Punkt.</returns>
|
|
||||||
private static string ShortName(string value)
|
|
||||||
{
|
|
||||||
var index = value.IndexOf('.');
|
|
||||||
return index < 0 ? value : value.Substring(0, index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
<assemblyIdentity version="2.1.2.0" name="BizTalkPlatformManagementTool" />
|
<assemblyIdentity version="1.0.0.0" name="BizTalkPlatformManagementTool.app" />
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
<security>
|
<security>
|
||||||
<requestedPrivileges>
|
<requestedPrivileges>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
</trustInfo>
|
</trustInfo>
|
||||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
<application>
|
<application>
|
||||||
|
<supportedOS Id="{4f476546-937d-4f22-9fc8-4b416b2f8f9f}" />
|
||||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
</application>
|
</application>
|
||||||
</compatibility>
|
</compatibility>
|
||||||
|
|||||||
+37
-7
@@ -1,14 +1,44 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
|
||||||
<PropertyGroup><Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration><Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform><ProjectGuid>{318F4307-F62C-47C9-9B90-F0C9BF2F812A}</ProjectGuid><OutputType>Exe</OutputType><RootNamespace>BizTalkPlatformManagementTool.Tests</RootNamespace><AssemblyName>BizTalkPlatformManagementTool.Tests</AssemblyName><TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion><FileAlignment>512</FileAlignment><Deterministic>true</Deterministic></PropertyGroup>
|
<PropertyGroup>
|
||||||
<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>
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "><DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel><DocumentationFile>bin\Release\BizTalkPlatformManagementTool.Tests.xml</DocumentationFile></PropertyGroup>
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
<ItemGroup><Reference Include="System" /><Reference Include="System.Core" /></ItemGroup>
|
<ProjectGuid>{B2AD340B-859F-413B-8811-7FBA4BD2BC02}</ProjectGuid>
|
||||||
<ItemGroup><Compile Include="Program.cs" /></ItemGroup>
|
<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>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj"><Project>{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}</Project><Name>BizTalkPlatformManagementTool</Name></ProjectReference>
|
<Reference Include="System" />
|
||||||
<ProjectReference Include="..\..\src\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj"><Project>{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}</Project><Name>BizTalkPlatformManagementTool.Setup</Name></ProjectReference>
|
<Reference Include="System.Core" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj">
|
||||||
|
<Project>{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}</Project>
|
||||||
|
<Name>BizTalkPlatformManagementTool</Name>
|
||||||
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,351 +1,136 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using BizTalkPlatformManagementTool.Models;
|
using BizTalkPlatformManagementTool.Models;
|
||||||
using BizTalkPlatformManagementTool.Services;
|
using BizTalkPlatformManagementTool.Services;
|
||||||
using BizTalkPlatformManagementTool.Setup;
|
|
||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Tests
|
namespace BizTalkPlatformManagementTool.Tests
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Enthält die portable Regressionstestsuite ohne Abhängigkeit von einem externen Testframework.
|
|
||||||
/// </summary>
|
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
/// <summary>Anzahl der im aktuellen Testlauf fehlgeschlagenen Prüfungen.</summary>
|
private static int _failed;
|
||||||
private static int failures;
|
|
||||||
|
|
||||||
/// <summary>Führt alle Regressionstests aus und liefert einen CI-tauglichen Exitcode.</summary>
|
|
||||||
/// <returns>Null, wenn alle Tests bestanden wurden; andernfalls eins.</returns>
|
|
||||||
private static int Main()
|
private static int Main()
|
||||||
{
|
{
|
||||||
Run("JsonRoundTripIsBomTolerantAndAtomic", JsonRoundTripIsBomTolerantAndAtomic);
|
Run("RestoreStartsOnlyPreviouslyActiveArtifacts", RestoreStartsOnlyPreviouslyActiveArtifacts);
|
||||||
Run("DiffUsesApplicationAndNameIdentity", DiffUsesApplicationAndNameIdentity);
|
Run("ShutdownStopsOnlyPreviouslyActiveArtifacts", ShutdownStopsOnlyPreviouslyActiveArtifacts);
|
||||||
Run("RestoreRejectsDifferentServer", RestoreRejectsDifferentServer);
|
Run("SnapshotComparisonUsesCompositeIdentity", SnapshotComparisonUsesCompositeIdentity);
|
||||||
Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder);
|
Run("SnapshotServerValidationIsSafe", SnapshotServerValidationIsSafe);
|
||||||
Run("CsvNeutralizesFormulaValues", CsvNeutralizesFormulaValues);
|
Run("JsonStoreReplacesAtomically", JsonStoreReplacesAtomically);
|
||||||
Run("PackageManifestRejectsTampering", PackageManifestRejectsTampering);
|
Console.WriteLine(_failed == 0 ? "PASS: 5 tests" : "FAIL: " + _failed + " test(s)");
|
||||||
Run("PackageManifestRejectsUndeclaredAndTraversalFiles", PackageManifestRejectsUndeclaredAndTraversalFiles);
|
return _failed == 0 ? 0 : 1;
|
||||||
Run("InstallerActivatesValidatedPayload", InstallerActivatesValidatedPayload);
|
|
||||||
Run("InstallerDoesNotMutateOnStagingFailure", InstallerDoesNotMutateOnStagingFailure);
|
|
||||||
Run("InstallerRollsBackFailedActivatedSelfTest", InstallerRollsBackFailedActivatedSelfTest);
|
|
||||||
Run("InstallerUninstallRemovesProgramDirectory", InstallerUninstallRemovesProgramDirectory);
|
|
||||||
Run("InstallerDiagnosticLogContainsContextAndExceptionChain", InstallerDiagnosticLogContainsContextAndExceptionChain);
|
|
||||||
Run("InstallerDiagnosticLogFallsBackToTemp", InstallerDiagnosticLogFallsBackToTemp);
|
|
||||||
Run("InstallerSurvivesUiReportFailure", InstallerSurvivesUiReportFailure);
|
|
||||||
Console.WriteLine(failures == 0 ? "ALL TESTS PASSED" : failures + " TEST(S) FAILED");
|
|
||||||
return failures == 0 ? 0 : 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Führt einen einzelnen Test isoliert aus und protokolliert sein Ergebnis.</summary>
|
private static void RestoreStartsOnlyPreviouslyActiveArtifacts()
|
||||||
/// <param name="name">Der stabile Testname für die Konsolenausgabe.</param>
|
|
||||||
/// <param name="test">Die auszuführende Testfunktion.</param>
|
|
||||||
private static void Run(string name, Action test)
|
|
||||||
{
|
{
|
||||||
try { test(); Console.WriteLine("PASS " + name); }
|
var snapshot = FullSnapshot();
|
||||||
catch (Exception ex) { failures++; Console.Error.WriteLine("FAIL " + name + ": " + ex); }
|
var plan = new BizTalkOperationService(null).CreateRestorePlan(snapshot, "BIZ01");
|
||||||
|
AssertEqual(4, plan.Steps.Count(x => x.Execute), "restore executable step count");
|
||||||
|
Assert(plan.Steps.Any(x => x.Kind == "HostInstance" && x.Name == "HostStarted"), "started host missing");
|
||||||
|
Assert(plan.Steps.Any(x => x.Kind == "SendPort" && x.Name == "SendStarted"), "started send port missing");
|
||||||
|
Assert(plan.Steps.Any(x => x.Kind == "Orchestration" && x.Name == "OrchStarted"), "started orchestration missing");
|
||||||
|
AssertEqual("AssemblyName", plan.Steps.Single(x => x.Kind == "Orchestration").QualifierProperty, "orchestration qualifier property");
|
||||||
|
var receive = plan.Steps.Single(x => x.Kind == "ReceiveLocation");
|
||||||
|
AssertEqual("ReceivePortName", receive.QualifierProperty, "receive qualifier property");
|
||||||
|
AssertEqual("RP1", receive.QualifierValue, "receive qualifier value");
|
||||||
|
Assert(!plan.Steps.Any(x => x.Name == "SendStopped" || x.Name == "SendBound" || x.Name == "ReceiveDisabled"), "inactive artifacts must remain untouched");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Prüft BOM-tolerantes Lesen und rückstandsfreies atomisches JSON-Schreiben.</summary>
|
private static void ShutdownStopsOnlyPreviouslyActiveArtifacts()
|
||||||
private static void JsonRoundTripIsBomTolerantAndAtomic()
|
|
||||||
{
|
{
|
||||||
InTemp(directory =>
|
var plan = new BizTalkOperationService(null).CreateShutdownPlan(FullSnapshot(), "BIZ01");
|
||||||
{
|
AssertEqual(4, plan.Steps.Count(x => x.Execute), "shutdown executable step count");
|
||||||
var path = Path.Combine(directory, "snapshot.json");
|
Assert(!plan.Steps.Any(x => x.Name == "SendStopped" || x.Name == "SendBound" || x.Name == "ReceiveDisabled"), "inactive artifacts must not be stopped");
|
||||||
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");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Prüft, dass gleichnamige Artefakte verschiedener Anwendungen getrennt verglichen werden.</summary>
|
private static void SnapshotComparisonUsesCompositeIdentity()
|
||||||
private static void DiffUsesApplicationAndNameIdentity()
|
|
||||||
{
|
{
|
||||||
var before = Snapshot("APP-A", "SHARED", ArtifactStates.SendPortStarted);
|
var before = EmptySnapshot("BIZ01");
|
||||||
before.Applications.Add(Snapshot("APP-B", "SHARED", ArtifactStates.SendPortStarted).Applications[0]);
|
var after = EmptySnapshot("BIZ01");
|
||||||
var after = Snapshot("APP-A", "SHARED", ArtifactStates.SendPortStopped);
|
before.Applications.Add(App("AppA", ArtifactStates.SendPortStarted));
|
||||||
after.Applications.Add(Snapshot("APP-B", "SHARED", ArtifactStates.SendPortStarted).Applications[0]);
|
before.Applications.Add(App("AppB", ArtifactStates.SendPortStopped));
|
||||||
|
after.Applications.Add(App("AppA", ArtifactStates.SendPortStopped));
|
||||||
|
after.Applications.Add(App("AppB", ArtifactStates.SendPortStopped));
|
||||||
var diff = SnapshotComparer.Compare(before, after);
|
var diff = SnapshotComparer.Compare(before, after);
|
||||||
Assert(diff.ArtifactDifferences.Count == 1, "expected one application-scoped difference");
|
AssertEqual(1, diff.ArtifactDifferences.Count, "composite send port comparison");
|
||||||
Assert(diff.ArtifactDifferences[0].Application == "APP-A", "wrong application was compared");
|
AssertEqual("AppA", diff.ArtifactDifferences[0].Application, "changed application");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Prüft Kurzname/FQDN-Kompatibilität und Ablehnung eines fremden Restore-Zielservers.</summary>
|
private static void SnapshotServerValidationIsSafe()
|
||||||
private static void RestoreRejectsDifferentServer()
|
|
||||||
{
|
{
|
||||||
var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted);
|
BizTalkOperationService.ValidateSnapshot(EmptySnapshot("biz01.domain.local"), "BIZ01");
|
||||||
snapshot.Server = "BIZTALK-A.example.local";
|
AssertThrows<InvalidDataException>(() => BizTalkOperationService.ValidateSnapshot(EmptySnapshot("BIZ02"), "BIZ01"), "foreign server must be rejected");
|
||||||
SnapshotValidator.EnsureServerMatches(snapshot, "BIZTALK-A");
|
|
||||||
Expect<InvalidOperationException>(() => SnapshotValidator.EnsureServerMatches(snapshot, "BIZTALK-B"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Prüft die sichere Restore-Reihenfolge und den Schutz gebundener Orchestrierungen.</summary>
|
private static void JsonStoreReplacesAtomically()
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Prüft die Neutralisierung formelartiger CSV-Feldwerte.</summary>
|
|
||||||
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");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Prüft, dass eine nach Manifestbildung veränderte Payload abgelehnt wird.</summary>
|
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Prüft die Aktivierung einer vollständig validierten Neuinstallation.</summary>
|
|
||||||
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");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Prüft die Ablehnung nicht deklarierter Dateien und ausbrechender Manifestpfade.</summary>
|
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Prüft, dass ein Staging-Fehler die aktive Installation nicht mutiert.</summary>
|
|
||||||
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 data = Path.Combine(directory, "data");
|
|
||||||
var messages = new List<string>();
|
|
||||||
var engine = new InstallerEngine(package, install, data, false, path => false);
|
|
||||||
var exception = Capture<InvalidOperationException>(() => engine.Install(false, messages.Add));
|
|
||||||
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");
|
|
||||||
Assert(exception.Message.Contains("Fehlercode=SETUP-STAGING-VALIDATION"), "staging error code missing");
|
|
||||||
Assert(exception.Message.Contains("Kein Rollback erforderlich"), "pre-mutation failure reported a rollback");
|
|
||||||
Assert(messages.Any(x => x.Contains("Kein Rollback erforderlich")), "operator output did not distinguish pre-mutation failure");
|
|
||||||
var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single());
|
|
||||||
Assert(log.Contains("event=self_test_completed label=staging"), "staging self-test result missing from log");
|
|
||||||
Assert(log.Contains("event=rollback_summary result=not_required"), "no-rollback evidence missing from log");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Prüft die Wiederherstellung der Vorversion nach fehlgeschlagenem aktiviertem Self-Test.</summary>
|
|
||||||
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);
|
|
||||||
var exception = Capture<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");
|
|
||||||
Assert(exception.Message.Contains("Fehlercode=SETUP-ACTIVATED-SELFTEST"), "activated self-test error code missing");
|
|
||||||
Assert(exception.Message.Contains("Rollback erfolgreich"), "successful rollback not reported");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Prüft die Deinstallation über ein atomar umbenanntes Quarantäneverzeichnis.</summary>
|
|
||||||
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");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Prüft technischen Kontext, Fehlercode, HRESULT und innere Ausnahme im Setup-Log.</summary>
|
|
||||||
private static void InstallerDiagnosticLogContainsContextAndExceptionChain()
|
|
||||||
{
|
|
||||||
InTemp(directory =>
|
|
||||||
{
|
|
||||||
var log = SetupOperationLog.Create(directory, "diagnostic-test");
|
|
||||||
Assert(!string.IsNullOrEmpty(log.FilePath), "diagnostic log was not created");
|
|
||||||
Exception failure;
|
|
||||||
try { throw new InvalidOperationException("outer", new IOException("inner")); }
|
|
||||||
catch (Exception ex) { failure = ex; }
|
|
||||||
log.WriteException("TEST-CODE", "Test phase", failure);
|
|
||||||
var content = File.ReadAllText(log.FilePath);
|
|
||||||
Assert(content.Contains("event=setup_started"), "setup context header missing");
|
|
||||||
Assert(content.Contains("process_bitness="), "process bitness missing");
|
|
||||||
Assert(content.Contains("identity="), "identity missing");
|
|
||||||
Assert(content.Contains("error_code=TEST-CODE"), "error code missing");
|
|
||||||
Assert(content.Contains("inner[1]"), "inner exception missing");
|
|
||||||
Assert(content.Contains("hresult=0x"), "HRESULT missing");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Prüft, dass ein fehlerhafter UI-Callback die Installation nicht beeinflusst.</summary>
|
|
||||||
private static void InstallerSurvivesUiReportFailure()
|
|
||||||
{
|
|
||||||
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 => true);
|
|
||||||
engine.Install(false, message => { throw new InvalidOperationException("UI unavailable"); });
|
|
||||||
Assert(File.Exists(Path.Combine(install, InstallerEngine.ApplicationExeName)), "UI report failure interrupted installation");
|
|
||||||
var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single());
|
|
||||||
Assert(log.Contains("event=ui_report_failed"), "UI report failure was not diagnosed");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Prüft den Diagnose-Log-Fallback bei einem nicht verwendbaren ProgramData-Pfad.</summary>
|
|
||||||
private static void InstallerDiagnosticLogFallsBackToTemp()
|
|
||||||
{
|
|
||||||
InTemp(directory =>
|
|
||||||
{
|
|
||||||
var blockedDataDirectory = Path.Combine(directory, "blocked-data-directory");
|
|
||||||
File.WriteAllText(blockedDataDirectory, "not a directory");
|
|
||||||
var log = SetupOperationLog.Create(blockedDataDirectory, "fallback-test");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Assert(log.IsFallback, "diagnostic log did not use the Temp fallback");
|
|
||||||
Assert(!string.IsNullOrEmpty(log.CreationError), "primary logging failure was not retained");
|
|
||||||
Assert(File.Exists(log.FilePath), "fallback diagnostic log was not created");
|
|
||||||
Assert(File.ReadAllText(log.FilePath).Contains("event=primary_log_creation_failed"), "fallback reason missing from log");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (File.Exists(log.FilePath)) File.Delete(log.FilePath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Erstellt einen minimalen Snapshot für Vergleiche und Planprüfungen.</summary>
|
|
||||||
/// <param name="application">Der Name der Testanwendung.</param>
|
|
||||||
/// <param name="port">Der Name des Test-Send-Ports.</param>
|
|
||||||
/// <param name="state">Der rohe Send-Port-Status.</param>
|
|
||||||
/// <returns>Ein Snapshot mit einer Anwendung und einem Send Port.</returns>
|
|
||||||
private static BizTalkSnapshot Snapshot(string application, string port, int state)
|
|
||||||
{
|
|
||||||
var result = new BizTalkSnapshot { ToolVersion = "test", CreatedAt = DateTimeOffset.Now.ToString("o"), Server = Environment.MachineName };
|
|
||||||
var app = new ApplicationSnapshot { Application = application };
|
|
||||||
app.SendPorts.Add(new SendPortState { Application = application, Name = port, Status = state });
|
|
||||||
result.Applications.Add(app);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Erzeugt eine minimale, manifestierte Installer-Payload.</summary>
|
|
||||||
/// <param name="root">Das temporäre Testwurzelverzeichnis.</param>
|
|
||||||
/// <param name="payload">Der simulierte Inhalt der Anwendungs-EXE.</param>
|
|
||||||
/// <returns>Das Verzeichnis des erzeugten Testpakets.</returns>
|
|
||||||
private static string CreatePackage(string root, string payload)
|
|
||||||
{
|
|
||||||
var package = Path.Combine(root, "package");
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Führt einen Test in einem eindeutigen temporären Verzeichnis mit garantierter Bereinigung aus.</summary>
|
|
||||||
/// <param name="action">Die Testfunktion, die den temporären Pfad erhält.</param>
|
|
||||||
private static void InTemp(Action<string> action)
|
|
||||||
{
|
{
|
||||||
var directory = Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool.Tests." + Guid.NewGuid().ToString("N"));
|
var directory = Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool.Tests." + Guid.NewGuid().ToString("N"));
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
try { action(directory); }
|
try
|
||||||
finally { if (Directory.Exists(directory)) Directory.Delete(directory, true); }
|
{
|
||||||
|
var path = Path.Combine(directory, "state.json");
|
||||||
|
JsonFileStore.Save(path, EmptySnapshot("BIZ01"));
|
||||||
|
JsonFileStore.Save(path, EmptySnapshot("BIZ02"));
|
||||||
|
var loaded = JsonFileStore.Load<BizTalkSnapshot>(path);
|
||||||
|
AssertEqual("BIZ02", loaded.Server, "atomic replacement payload");
|
||||||
|
AssertEqual(0, Directory.GetFiles(directory, "*.tmp.*").Length, "temporary files");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(directory, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Bricht den Test ab, wenn eine erwartete Bedingung nicht erfüllt ist.</summary>
|
private static BizTalkSnapshot FullSnapshot()
|
||||||
/// <param name="condition">Die erwartete Bedingung.</param>
|
{
|
||||||
/// <param name="message">Die Fehlermeldung bei nicht erfüllter Bedingung.</param>
|
var snapshot = EmptySnapshot("BIZ01");
|
||||||
private static void Assert(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); }
|
var app = new ApplicationSnapshot { Application = "Orders" };
|
||||||
|
app.ReceiveLocations.Add(new ReceiveLocationState { Application = "Orders", Name = "ReceiveEnabled", ReceivePortName = "RP1", Enabled = true });
|
||||||
|
app.ReceiveLocations.Add(new ReceiveLocationState { Application = "Orders", Name = "ReceiveDisabled", ReceivePortName = "RP1", Enabled = false });
|
||||||
|
app.SendPorts.Add(new SendPortState { Application = "Orders", Name = "SendStarted", Status = ArtifactStates.SendPortStarted });
|
||||||
|
app.SendPorts.Add(new SendPortState { Application = "Orders", Name = "SendStopped", Status = ArtifactStates.SendPortStopped });
|
||||||
|
app.SendPorts.Add(new SendPortState { Application = "Orders", Name = "SendBound", Status = ArtifactStates.SendPortBound });
|
||||||
|
app.Orchestrations.Add(new OrchestrationState { Application = "Orders", Name = "OrchStarted", OrchestrationStatus = ArtifactStates.OrchestrationStarted, AssemblyName = "Orders.Processes" });
|
||||||
|
app.Orchestrations.Add(new OrchestrationState { Application = "Orders", Name = "OrchStopped", OrchestrationStatus = ArtifactStates.OrchestrationStopped });
|
||||||
|
snapshot.Applications.Add(app);
|
||||||
|
snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HostStarted", Server = "BIZ01", RawState = ArtifactStates.HostStarted });
|
||||||
|
snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HostStopped", Server = "BIZ01", RawState = ArtifactStates.HostStopped });
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Prüft, dass eine Aktion eine bestimmte Ausnahme auslöst.</summary>
|
private static BizTalkSnapshot EmptySnapshot(string server)
|
||||||
/// <typeparam name="T">Der erwartete Ausnahmetyp.</typeparam>
|
{
|
||||||
/// <param name="action">Die auszuführende Aktion.</param>
|
return new BizTalkSnapshot { Server = server, CreatedAt = DateTime.Now.ToString("s"), ToolVersion = BizTalkOperationService.Version };
|
||||||
private static void Expect<T>(Action action) where T : Exception
|
}
|
||||||
|
|
||||||
|
private static ApplicationSnapshot App(string name, int sendStatus)
|
||||||
|
{
|
||||||
|
var app = new ApplicationSnapshot { Application = name };
|
||||||
|
app.SendPorts.Add(new SendPortState { Application = name, Name = "SharedName", Status = sendStatus });
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Run(string name, Action test)
|
||||||
|
{
|
||||||
|
try { test(); Console.WriteLine("PASS: " + name); }
|
||||||
|
catch (Exception ex) { _failed++; Console.WriteLine("FAIL: " + name + " - " + ex.Message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Assert(bool condition, string message)
|
||||||
|
{
|
||||||
|
if (!condition) throw new InvalidOperationException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertEqual<T>(T expected, T actual, string label)
|
||||||
|
{
|
||||||
|
if (!object.Equals(expected, actual)) throw new InvalidOperationException(label + ": expected=" + expected + ", actual=" + actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertThrows<T>(Action action, string label) where T : Exception
|
||||||
{
|
{
|
||||||
try { action(); }
|
try { action(); }
|
||||||
catch (T) { return; }
|
catch (T) { return; }
|
||||||
throw new InvalidOperationException("Expected exception " + typeof(T).Name);
|
throw new InvalidOperationException(label);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Führt eine Aktion aus und gibt die erwartete Ausnahme für weitere Prüfungen zurück.</summary>
|
|
||||||
/// <typeparam name="T">Der erwartete Ausnahmetyp.</typeparam>
|
|
||||||
/// <param name="action">Die auszuführende Aktion.</param>
|
|
||||||
/// <returns>Die von der Aktion ausgelöste Ausnahme.</returns>
|
|
||||||
private static T Capture<T>(Action action) where T : Exception
|
|
||||||
{
|
|
||||||
try { action(); }
|
|
||||||
catch (T ex) { return ex; }
|
|
||||||
throw new InvalidOperationException("Expected exception " + typeof(T).Name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user