Compare commits
14
Commits
0cb3fda151
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b1c14b892 | ||
|
|
61b5b7e452 | ||
|
|
b18adab26d | ||
|
|
fe3c84a27f | ||
|
|
999f69bcb9 | ||
|
|
3b4a621cd3 | ||
|
|
3219c3f1bc | ||
|
|
4af64d8b41 | ||
|
|
1228f71765 | ||
|
|
9ce7e8d45a | ||
|
|
08626197be | ||
|
|
b32cc61e43 | ||
|
|
bb2d2f8484 | ||
|
|
a74528e5c6 |
@@ -1,15 +0,0 @@
|
||||
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 +1,2 @@
|
||||
|
||||
* text=auto eol=crlf
|
||||
|
||||
+1
-1
@@ -10,7 +10,6 @@ bin/
|
||||
obj/
|
||||
|
||||
# Logs / Outputs
|
||||
artifacts/
|
||||
*.log
|
||||
*.json
|
||||
*.html
|
||||
@@ -19,6 +18,7 @@ artifacts/
|
||||
*.png
|
||||
work/
|
||||
release/*
|
||||
artifacts/
|
||||
|
||||
# PowerShell specifics
|
||||
*.ps1.orig
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# AI-Maintainer-Handoff
|
||||
|
||||
## Projekt und Sicherheitsziel
|
||||
|
||||
Das Repository enthält ein .NET-Framework-4.6.1-WinForms-Tool für kontrollierte BizTalk-2020-Wartungsoperationen. Änderungen müssen Dry-run, explizite Freigabe realer Aktionen, sichere Reihenfolgen und wiederherstellbare Installergrenzen erhalten. Die aktuelle Produktversion ist 2.3.4.
|
||||
|
||||
## Installerinvarianten
|
||||
|
||||
- Vor der ersten Mutation müssen Manifest, Länge, SHA-256 und Staging-Self-Test erfolgreich sein.
|
||||
- Eine laufende installierte Anwendung blockiert Update und Deinstallation.
|
||||
- Ein Update darf die aktive Vorversion erst nach erfolgreichem Staging verändern.
|
||||
- Das Backup einer bestehenden Installation wird nur atomar per `Directory.Move` erzeugt. Bei dauerhaftem Fehler bleibt die Vorversion aktiv; es gibt keinen In-place-Kopierfallback.
|
||||
- Nach ausgeschöpften Aktivierungs-Move-Retries darf eine erneut SHA-256-geprüfte Kopie nur in ein nicht vorhandenes Ziel geschrieben werden: bei einer Neuinstallation oder nachdem eine Update-Vorversion vollständig atomar ins Backup verschoben wurde.
|
||||
- Nach jeder Aktivierungsart läuft der Self-Test erneut aus dem endgültigen Installationsziel.
|
||||
- Sobald ein Zielverzeichnis teilweise angelegt sein kann, muss `activated=true` gesetzt sein, damit der Catch-Pfad es entfernt.
|
||||
- Scheitert der Ziel-Self-Test nach einem Update-Kopierfallback, muss die teilweise neue Version entfernt und das atomare Backup wieder als aktives Verzeichnis eingesetzt werden.
|
||||
- Windows-Integration wird erst nach bestandenem Ziel-Self-Test verändert und bei Folgefehlern aus dem Snapshot restauriert.
|
||||
- Startmenü-Link, Uninstaller und Uninstall-Registry sind verpflichtend; Fehler müssen weiterhin den vollständigen Rollback auslösen.
|
||||
- Der gemeinsame Desktop-Link ist optional und standardmäßig abgewählt. Snapshot, Erstellen/Entfernen, Validierung, Rollback und Uninstall müssen jeden Fehler als sichtbare Warnung isolieren und dürfen keine Kernoperation abbrechen.
|
||||
- Diagnose-Logging darf das eigentliche Setup-Ergebnis nie ersetzen.
|
||||
|
||||
Die zentrale Implementierung liegt in `src/BizTalkPlatformManagementTool.Setup/InstallerEngine.cs`. Move-Retries sind auf acht Versuche und 19,75 Sekunden Wartezeit begrenzt. Die injizierbaren `directoryMover`- und `retryDelay`-Delegates existieren ausschließlich, damit Sperrpfade ohne echte Wartezeit portabel getestet werden können.
|
||||
|
||||
## Runtime- und Recovery-Invarianten
|
||||
|
||||
- Ein isolierter WMI-, Adapter- oder Servicefehler darf keine späteren unabhängigen Planschritte verhindern.
|
||||
- Teilfehler werden vollständig pro Schritt persistiert; der Gesamtlauf bleibt sichtbar fehlgeschlagen und darf nicht als Erfolg ausgegeben werden.
|
||||
- Vor jeder Mutation wird der aktuelle Zustand geprüft. Bereits erreichte Sollzustände werden ohne Methodenaufruf als `AlreadySatisfied` erfasst.
|
||||
- Der Nachher-Snapshot wird unabhängig von Einzelfehlern versucht; sein Fehler gehört in denselben Ergebnisreport.
|
||||
- Die Runtime-WMI-Klassen liefern keine verlässliche Application-Zuordnung. `BizTalkApplicationResolver` liest deshalb die Management-DB-Position aus `MSBTS_GroupSetting` und indiziert die read-only ExplorerOM-Anwendungshierarchie. WMI bleibt Quelle für Zustand und Mutationen; ExplorerOM wird ohne Compile-time-Abhängigkeit und nur mit geprüfter Microsoft-Assemblyidentität geladen.
|
||||
- Scheitert die Katalogauflösung, bleibt die WMI-Zustandserfassung verfügbar, aber alle betroffenen Artefakte werden sichtbar unter `(Unknown Application)` gruppiert und Diagnose, Grid sowie Laufzeitlog müssen eine verwertbare Warnung ausgeben.
|
||||
- Shutdown-Kategorien gelten global über alle Anwendungen: Receive Locations, Orchestrations, Send Ports, Host Instances.
|
||||
- Zwischen der globalen Receive-Location-Phase und allen späteren Shutdown-Kategorien liegt ein persistierter Operator-Checkpoint. Ein realer Lauf darf nur nach explizitem Continue fortsetzen; Stop oder Handlerfehler markiert alle späteren Zeilen als `NotExecuted` und arbeitet fail-closed.
|
||||
- Dry-run zeigt den Checkpoint ohne Callback. Der Ergebnisreport muss Entscheidung, Zeitpunkt, OperatorStopped und NotExecutedCount dauerhaft enthalten.
|
||||
- Ein BizTalk-Application-Stop wird nicht zusätzlich ausgeführt: `Partially Started` ist während des Drains erwartbar, während Application-Stop-Optionen eine breitere, teils destruktive Semantik als das reine Setzen eines Statuswerts besitzen.
|
||||
- Restore-Kategorien gelten global über alle Anwendungen: Host Instances, Send Ports, Orchestrations, Receive Locations.
|
||||
- Emergency Restore überschreibt niemals die Eingabe-`before.json`, erzeugt eine timestamp-basierte Kopie und stellt `ENTSSO` vor Host Instances sicher.
|
||||
- Emergency Restore muss mit genau einer validen `before.json` funktionieren; Dateien eines vorherigen fehlgeschlagenen Laufs dürfen keine Voraussetzung sein.
|
||||
- Ein älterer kompatibler Snapshot, insbesondere aus 2.1.3, darf nicht allein anhand seines `ToolVersion`-Werts abgelehnt werden.
|
||||
- Alle laufzeitrelevanten Bestätigungsdialoge müssen mit dem Hauptformular als Owner angezeigt werden. Vor dem echten Drain-Checkpoint muss der Status sichtbar `ACTION REQUIRED`/pausiert melden; Dry-run muss ausdrücklich erklären, dass keine Runtime-Änderung und keine echte Drain-Entscheidung erfolgt.
|
||||
- Der echte Emergency Restore erzeugt nach Möglichkeit automatisch einen timestamp-basierten Soll/Ist-Diff aus Recovery-Quelle und Nachher-Snapshot.
|
||||
- Die zentrale best-effort Orchestrierung liegt in `OperationPlanExecutor`; die WMI-/Service-Zustandsprüfung bleibt im produktiven Runtime-Adapter.
|
||||
- Datei-Logging und optionale GUI-Logweiterleitung sind Diagnosekanäle und dürfen niemals einen fachlichen Planschritt oder dessen Fehlerbehandlung unterbrechen.
|
||||
- Ein Laufzeitlogpfad gilt erst nach erfolgreichem Create/Write/Flush/Delete-Test als verfügbar. Die Reihenfolge ProgramData, LocalAppData, EXE-`Logs`, Temp ist stabil; ein Append-Fehler muss denselben Datensatz auf dem nächsten Kandidaten erneut versuchen.
|
||||
- Fallback und Totalausfall des Dateiloggings müssen mit Pfad und Exception im Grid sichtbar sein. Jeder GUI-Start persistiert einen Verifikationsdatensatz; der WMI-freie Self-Test prüft einen vollständigen Log-Write/Read-Roundtrip.
|
||||
- Scheduler-Preflight darf nur für Receive Locations mit passendem Adaptername oder `scheduler:`-URI laufen; normale Adapter dürfen keine zusätzliche Abhängigkeit erhalten.
|
||||
- Adapter-DLLs dürfen nur aus explizit konfigurierten, per Registry erkannten oder konventionellen Produktverzeichnissen und nur nach vollständiger Assemblyidentitätsprüfung geladen werden. Der Toolprozess verändert niemals den GAC.
|
||||
- Laufzeitlogs halten 30 Kalendertage, komprimieren abgeschlossene Tage und rehydrieren maximal 10.000 neue Einträge ins Grid. Ein Archiv-/Lesefehler darf keine BizTalk-Operation beeinflussen.
|
||||
|
||||
## Versionierung
|
||||
|
||||
Bei einem Release sind mindestens diese Stellen konsistent zu ändern:
|
||||
|
||||
- `InstallerEngine.ProductVersion`
|
||||
- Setup-Titel in `MainForm`
|
||||
- beide `Properties/AssemblyInfo.cs`
|
||||
- beide `app.manifest`
|
||||
- `BizTalkOperationService.Version`
|
||||
- `CHANGELOG.md`
|
||||
|
||||
## Verifikation
|
||||
|
||||
Portable Befehle:
|
||||
|
||||
```sh
|
||||
msbuild BizTalkPlatformManagementTool.sln /t:Rebuild /p:Configuration=Release '/p:Platform=Any CPU' /m:1
|
||||
mono tests/BizTalkPlatformManagementTool.Tests/bin/Release/BizTalkPlatformManagementTool.Tests.exe
|
||||
mono src/BizTalkPlatformManagementTool/bin/Release/BizTalkPlatformManagementTool.exe --self-test
|
||||
mono src/BizTalkPlatformManagementTool.Packager/bin/Release/BizTalkPlatformManagementTool.Packager.exe . Release
|
||||
```
|
||||
|
||||
Nach der Paketierung müssen ZIP, Base64-TXT und SHA-256-Datei gegengeprüft werden. Die abschließende Freigabe braucht zusätzlich einen repräsentativen Windows-/ACC-Test von UAC, `Program Files`-ACL/EDR, Registry, Verknüpfungen, Update, Rollback und Deinstallation sowie BizTalk-Diagnose und Dry-run.
|
||||
@@ -4,11 +4,11 @@ VisualStudioVersion = 15.0.28307.168
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool", "src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj", "{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Tests", "tests\BizTalkPlatformManagementTool.Tests\BizTalkPlatformManagementTool.Tests.csproj", "{B2AD340B-859F-413B-8811-7FBA4BD2BC02}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Setup", "src\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj", "{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Setup", "src\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj", "{38E61630-11AB-4F8D-B421-F8A88831C4BD}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Packager", "src\BizTalkPlatformManagementTool.Packager\BizTalkPlatformManagementTool.Packager.csproj", "{74A5D422-0BA5-4559-BD81-C89C071A8FE4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Packager", "src\BizTalkPlatformManagementTool.Packager\BizTalkPlatformManagementTool.Packager.csproj", "{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool.Tests", "tests\BizTalkPlatformManagementTool.Tests\BizTalkPlatformManagementTool.Tests.csproj", "{318F4307-F62C-47C9-9B90-F0C9BF2F812A}"
|
||||
EndProject
|
||||
Global
|
||||
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}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B2AD340B-859F-413B-8811-7FBA4BD2BC02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2AD340B-859F-413B-8811-7FBA4BD2BC02}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2AD340B-859F-413B-8811-7FBA4BD2BC02}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2AD340B-859F-413B-8811-7FBA4BD2BC02}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{38E61630-11AB-4F8D-B421-F8A88831C4BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{38E61630-11AB-4F8D-B421-F8A88831C4BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{38E61630-11AB-4F8D-B421-F8A88831C4BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{38E61630-11AB-4F8D-B421-F8A88831C4BD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{74A5D422-0BA5-4559-BD81-C89C071A8FE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{74A5D422-0BA5-4559-BD81-C89C071A8FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{74A5D422-0BA5-4559-BD81-C89C071A8FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{74A5D422-0BA5-4559-BD81-C89C071A8FE4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{318F4307-F62C-47C9-9B90-F0C9BF2F812A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{318F4307-F62C-47C9-9B90-F0C9BF2F812A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{318F4307-F62C-47C9-9B90-F0C9BF2F812A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{318F4307-F62C-47C9-9B90-F0C9BF2F812A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
+177
-28
@@ -1,55 +1,204 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## [Unreleased] - 2026-08-06
|
||||
## [2.3.4] - 2026-08-27
|
||||
### Fixed
|
||||
- Shutdown plan review, inbound-drain and emergency-restore confirmations are now owned by the main window, preventing a modal decision from being hidden behind the application in RDP and multi-monitor sessions.
|
||||
- The shutdown status now changes from a generic preparation message to an explicit `ACTION REQUIRED` / paused state before the real inbound-drain decision.
|
||||
|
||||
### Changed
|
||||
- Dry-run and real execution are distinguished with prominent mode text, color and persistent status guidance; execution settings cannot be changed while an operation is active.
|
||||
- A shutdown Dry-run now displays an owned informational notice explaining that it changes no receive locations and therefore does not open the real drain decision dialog.
|
||||
- The prepared real-shutdown confirmation explicitly announces the second decision after the receive-location phase; closing the real checkpoint dialog is explicitly documented as a safe stop.
|
||||
|
||||
### Added
|
||||
- Regression coverage for checkpoint creation when later shutdown work exists without enabled receive locations, and for distinct Dry-run, prepared-plan and paused-checkpoint operator guidance.
|
||||
- A dedicated PROD root-cause analysis, validation runbook and plain-text handoff for shutdown-dialog visibility.
|
||||
|
||||
- Überarbeitete deutschsprachige GUI mit einheitlichem Workflow und DPI-Skalierung.
|
||||
- .NET-Installer mit sicherem Update, Rollback und Deinstallation.
|
||||
- Release-Packager für ZIP, Base64-TXT und SHA-256.
|
||||
- Regressionstestprojekt für Plan-, Vergleichs-, Validierungs- und Dateilogik.
|
||||
## [2.3.3] - 2026-08-27
|
||||
### Fixed
|
||||
- Snapshot, shutdown and restore rows no longer show every Send Port, Receive Location and Orchestration under `(Unknown Application)` when the BizTalk runtime WMI classes omit application ownership.
|
||||
|
||||
### Changed
|
||||
- Application ownership is now read from the official read-only ExplorerOM application hierarchy, while WMI remains authoritative for runtime state and mutations.
|
||||
- **Diagnose** reports visible Send Ports and the exact number with a resolved application association; incomplete catalog access remains a visible warning instead of being hidden.
|
||||
- ExplorerOM is loaded process-locally from the GAC or identity-checked BizTalk installation paths, preserving the portable build and WMI-free installer self-test.
|
||||
|
||||
- Restore startet ausschließlich Artefakte, die im Vorher-Snapshot aktiv waren.
|
||||
- Vorhandene Vorher-Snapshots werden beim Herunterfahren wiederverwendet.
|
||||
- WMI-Auflösung nutzt zusätzliche Qualifier und bricht bei Mehrdeutigkeit ab.
|
||||
- Snapshot-Vergleich verwendet zusammengesetzte Artefaktidentitäten.
|
||||
- JSON-Dateien werden atomar und größenbegrenzt geschrieben.
|
||||
- Logs und Wartungsdateien liegen unter `%ProgramData%` und überleben Updates.
|
||||
- Transport-URIs werden nicht mehr in neuen Snapshots persistiert.
|
||||
### Added
|
||||
- Regression coverage for Send Port, Receive Port/Location and Orchestration mapping, case-insensitive lookup and rejection of ambiguous cross-application names.
|
||||
- A dedicated PROD root-cause analysis, validation runbook and plain-text handoff for the application-association fix.
|
||||
|
||||
## [2.0.0] - 2026-04-27
|
||||
## [2.3.2] - 2026-08-26
|
||||
### Added
|
||||
- A persisted, fail-closed operator drain checkpoint between the global receive-location phase and all later real shutdown phases.
|
||||
- Durable `Continue`, `Stop` or `Error` checkpoint evidence plus `NotExecuted` results for every protected downstream plan row.
|
||||
- Regression coverage for checkpoint ordering, continuation after an early receive-location failure, explicit operator stop, missing-handler fail-closed behavior and dry-run isolation.
|
||||
- A dedicated PROD runbook and plain-text handoff for drain verification and BizTalk application status semantics.
|
||||
|
||||
### Changed
|
||||
- Real shutdown now pauses with a default-No dialog that summarizes receive-location outcomes and asks the operator to verify Group Hub and enterprise monitoring before continuing.
|
||||
- A safe operator stop still attempts the partial-state snapshot and execution report and is shown as stopped rather than a completed shutdown.
|
||||
|
||||
### Security
|
||||
- The tool deliberately does not issue a broad application-level stop merely to change `Partially Started`; stronger BizTalk application stop modes can remove subscriptions, unenlist artifacts, undeploy policies or terminate running instances.
|
||||
|
||||
## [2.3.1] - 2026-08-26
|
||||
### Added
|
||||
- A real runtime-log write/read roundtrip to the WMI-free installer self-test and a durable startup verification record naming the active daily file.
|
||||
- Regression coverage for startup fallback, append-time failover without record loss and total storage failure diagnostics.
|
||||
- A dedicated PROD storage-validation runbook in Markdown and plain text.
|
||||
|
||||
### Changed
|
||||
- Runtime logging now verifies actual writes and uses the deterministic fallback order ProgramData, LocalAppData, executable-local `Logs`, then Temp.
|
||||
- **Log Folder** opens the currently verified path; fallback and total failure diagnostics include attempted paths and exceptions in the Operation Log grid.
|
||||
|
||||
### Fixed
|
||||
- Successful directory creation can no longer be mistaken for working file logging when the subsequent append is denied.
|
||||
- Runtime append failures are no longer swallowed silently; the failed record is retried on the next local candidate and a durable/visible warning is emitted.
|
||||
|
||||
## [2.3.0] - 2026-08-26
|
||||
### Added
|
||||
- Process-local, assembly-identity-checked resolution and preflight for ScheduledTask `scheduler:` receive locations, including automatic BizTalk/adapter directory and registry discovery plus an explicit configuration override.
|
||||
- Persistent Operation Log grid restoration from retained plain and GZip-compressed daily logs.
|
||||
- **Log Folder** action and a dedicated PROD validation/support runbook in Markdown and plain text.
|
||||
- Regression coverage for Scheduler plan metadata, assembly identity rejection, restart persistence, compression, 30-day retention and detailed exception diagnostics.
|
||||
|
||||
### Changed
|
||||
- Runtime logs now retain 30 calendar days and compress completed daily files instead of deleting everything older than five days.
|
||||
- Grid, result JSON and file logs now preserve exception type, HRESULT, inner exceptions, Fusion loader details and stack traces for support analysis.
|
||||
|
||||
### Fixed
|
||||
- ScheduledTask 7.0.2 receive locations can resolve `Microsoft.BizTalk.Scheduler.dll` from the installed BizTalk environment before WMI `Enable`/`Disable`, without requiring the management tool to mutate the GAC.
|
||||
|
||||
## [2.2.4] - 2026-08-24
|
||||
### Fixed
|
||||
- Permission, Windows Script Host, validation and rollback errors affecting the optional all-users desktop shortcut no longer abort installation, update or uninstallation.
|
||||
- Failure to capture or restore the optional desktop shortcut can no longer prevent restoration of mandatory registry, Start menu and uninstaller state.
|
||||
|
||||
### Changed
|
||||
- The all-users desktop shortcut is now opt-in and unchecked by default.
|
||||
- Optional desktop warnings are visible in the setup UI and durable log, while completion remains successful with an explicit warning result.
|
||||
|
||||
### Added
|
||||
- Failure-injection coverage proving that both an optional shortcut exception and a failing warning sink remain non-fatal.
|
||||
- Dedicated operator and maintainer documentation for the optional Windows-integration boundary.
|
||||
|
||||
## [2.2.3] - 2026-08-24
|
||||
### Fixed
|
||||
- A persistent Windows/EDR denial of the final staging-directory rename no longer aborts activation after an existing installation has already been atomically moved to its backup.
|
||||
- The SHA-256-verified copy fallback now covers both first installation and update activation, while refusing to overwrite an existing target or an update without a completed backup.
|
||||
|
||||
### Added
|
||||
- Regression coverage for successful update copy fallback and complete restoration of the previous version when the post-copy target self-test fails.
|
||||
- PROD incident analysis and a plain-text operator handoff for the observed 2.1.2 `SETUP-ACTIVATION` failure.
|
||||
|
||||
## [2.2.2] - 2026-08-19
|
||||
### Fixed
|
||||
- A failure in the optional GUI log sink can no longer escape step-level error handling or stop later independent shutdown/restore steps.
|
||||
|
||||
### Added
|
||||
- Failure-injection coverage for multiple consecutive unexpected exception types and a permanently failing log-display sink.
|
||||
|
||||
## [2.2.1] - 2026-08-19
|
||||
### Added
|
||||
- WMI-free **Validate State** preflight with snapshot metadata, server matching and artifact counts.
|
||||
- Direct **State...** file selection for a preserved recovery snapshot.
|
||||
- Automatic timestamped JSON/CSV/HTML target/actual diff after a real Emergency Restore.
|
||||
- Regression coverage proving that a `before.json` created by version 2.1.3 remains a valid Emergency Restore source.
|
||||
|
||||
### Changed
|
||||
- Emergency-recovery documentation now provides a dedicated operator runbook for the ACC restart and explicitly distinguishes dry-run evidence from real execution evidence.
|
||||
|
||||
## [2.2.0] - 2026-08-19
|
||||
### Added
|
||||
- State-aware **Emergency Restore** from an existing `before.json` without overwriting the recovery source.
|
||||
- Enterprise Single Sign-On (`ENTSSO`) startup as the first emergency-recovery prerequisite.
|
||||
- Durable per-step `shutdown-result.json`, `restore-result.json` and timestamped emergency result reports.
|
||||
- Regression coverage for the ACC `Microsoft.BizTalk.Scheduler` failure, best-effort continuation, already-satisfied states, emergency ordering and global cross-application ordering.
|
||||
|
||||
### Changed
|
||||
- Shutdown and restore continue with remaining independent steps after an isolated WMI or adapter failure and finish with an explicit operator-review result.
|
||||
- Runtime operations skip artifacts that already match the requested target state, making recovery from partial shutdowns idempotent.
|
||||
- Post-operation snapshots are attempted and diagnosed independently after partial execution failures.
|
||||
- Shutdown order is now global across all applications: receive locations, orchestrations, send ports, then host instances.
|
||||
- Restore order is now global across all applications: host instances, send ports, orchestrations, then receive locations.
|
||||
|
||||
### Fixed
|
||||
- An ACC shutdown no longer aborts every later step when `RV_PMP_Trigger_Schedule` fails validation because `Microsoft.BizTalk.Scheduler` or a dependency cannot be loaded.
|
||||
- A partial shutdown no longer prevents recovery evidence from being written after the first failed step.
|
||||
- Host-instance steps no longer treat the short name and FQDN of the same target server as different machines.
|
||||
|
||||
## [2.1.3] - 2026-08-11
|
||||
### Added
|
||||
- Bounded retry/backoff diagnostics for transient `Directory.Move` locks caused by the Windows loader, antivirus or endpoint protection.
|
||||
- A SHA-256-verified copy activation fallback limited to first-time installations with no existing active version.
|
||||
- Regression coverage for transient recovery, permanent new-install fallback and permanent update denial without mutation.
|
||||
- ACC incident analysis and an AI maintainer handoff document.
|
||||
|
||||
### Changed
|
||||
- Install, update, rollback and uninstall directory moves now use eight bounded attempts over at most 19.75 seconds.
|
||||
- Setup completion logs record the activation method and preservation state of both backup and staging directories.
|
||||
- Product and setup version advanced to 2.1.3.
|
||||
|
||||
## [2.1.2] - 2026-08-11
|
||||
### Added
|
||||
- Complete XML documentation for types and methods across application, setup, packager and regression projects, including parameters, generic type parameters and return values.
|
||||
- Focused German inline comments for non-obvious BizTalk ordering, WMI compatibility, persistence, security and installer transaction decisions.
|
||||
- XML documentation output in every Release project configuration for compiler-side validation.
|
||||
|
||||
### Changed
|
||||
- README, technical documentation and installation/build guidance now describe the source-documentation standard and generated XML developer artifacts.
|
||||
|
||||
## [2.1.1] - 2026-08-11
|
||||
### Added
|
||||
- Stable setup/uninstall phase codes, environment and file metadata, complete self-test output, exception chains with HRESULT/stacktrace, and per-step rollback diagnostics.
|
||||
- Installer-log fallback from ProgramData to Temp plus a setup button for opening diagnostic logs.
|
||||
- Post-write verification of shortcuts, uninstall executable and essential uninstall-registry values.
|
||||
- Regression coverage for pre-mutation rollback wording, diagnostic context, log fallback and UI-report isolation.
|
||||
|
||||
### 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.
|
||||
- Solution, Projektdatei, Namespace, Assemblyname und Standard-Ausgabepfad angepasst.
|
||||
- Dokumentation aktualisiert und `Installation.md` ergänzt.
|
||||
|
||||
## [1.3.0] - 2026-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- HTML-Report mit farblicher Kennzeichnung
|
||||
- farbige Statusausgabe
|
||||
- Remote-Server-Parameter und Ausgabeverzeichnis
|
||||
- verbesserte Diff-Erkennung für neue und entfernte Artefakte
|
||||
- HTML Report mit farblicher Kennzeichnung (grün/rot)
|
||||
- Farbige Statusausgabe auf STDOUT
|
||||
- Remote‑Server Parameter (-Server) und Ausgabeverzeichnis (-OutDir)
|
||||
- Verbesserte Diff‑Erkennung (neu/entfernt)
|
||||
|
||||
## [1.2.0] - 2026-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- Remote-fähige Snapshot-Funktion und Logging
|
||||
- Remote‑fähige Snapshot‑Funktion, Logging (Konsole + Datei)
|
||||
|
||||
## [1.1.0] - 2026-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- textuelle Diff-Ausgabe und HTML-Report
|
||||
- Textuelle Diff‑Ausgabe auf STDOUT + HTML Report
|
||||
|
||||
## [1.0.0] - 2026-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- Snapshot und Diff für Receive Locations, Send Ports und Orchestrierungen
|
||||
- Grundfunktion: Snapshot & Diff (Receive Locations, Send Ports, Orchestrations)
|
||||
|
||||
+209
-117
@@ -1,159 +1,251 @@
|
||||
# Technische Dokumentation
|
||||
# BizTalk Platform Management Tool Dokumentation
|
||||
|
||||
**Stand:** 2026-08-06
|
||||
**Stand:** 2026-08-26
|
||||
**Implementierung:** C# WinForms, .NET Framework 4.6.1
|
||||
**Archivierte PowerShell-Version:** `archive/powershell/BizTalkPlatformManagementTool.ps1`
|
||||
|
||||
**Version:** 2.1.0
|
||||
## Zweck
|
||||
|
||||
**Technik:** C# WinForms, .NET Framework 4.6.1
|
||||
Das BizTalk Platform Management Tool unterstützt kontrollierte Wartungsfenster für Microsoft BizTalk Server 2020:
|
||||
|
||||
## Ziel und Sicherheitsprinzip
|
||||
- Exakten Laufzeitstatus vor einer Wartung erfassen.
|
||||
- Receive Locations, Orchestrations, Send Ports und Host Instances in definierter Reihenfolge herunterfahren.
|
||||
- Den Laufzeitstatus aus einer gespeicherten `before.json` wiederherstellen.
|
||||
- Den Status nach der Wartung erfassen und beide Snapshots vergleichen.
|
||||
- Plan- und Ergebnisdateien für Review, Audit und Nachvollziehbarkeit schreiben.
|
||||
|
||||
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:
|
||||
## Projektstruktur
|
||||
|
||||
- Nur Artefakte, die im Snapshot aktiv waren, werden heruntergefahren.
|
||||
- Nur Artefakte, die im Snapshot aktiv waren, werden wieder gestartet.
|
||||
- Zuvor gestoppte, gebundene oder deaktivierte Artefakte werden beim Restore nicht verändert.
|
||||
- Ein vorhandener Vorher-Snapshot wird durch **Herunterfahren** nicht ersetzt.
|
||||
- Snapshot und Zielserver müssen übereinstimmen; Kurzname und FQDN desselben Servers werden akzeptiert.
|
||||
- Bei mehrdeutiger WMI-Auflösung wird abgebrochen, statt möglicherweise das falsche Objekt zu verändern.
|
||||
- Solution: `BizTalkPlatformManagementTool.sln`
|
||||
- Projekt: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj`
|
||||
- UI: `src/BizTalkPlatformManagementTool/Ui/MainForm.cs`
|
||||
- WMI-Zugriff: `src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs`
|
||||
- Anwendungszuordnung: `src/BizTalkPlatformManagementTool/Services/BizTalkApplicationResolver.cs`
|
||||
- Adapter-Assemblyauflösung: `src/BizTalkPlatformManagementTool/Services/AdapterAssemblyResolver.cs`
|
||||
- Persistentes Laufzeitlogging: `src/BizTalkPlatformManagementTool/Services/OperationLogger.cs`
|
||||
- Operationslogik: `src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs`
|
||||
- Snapshot-Validierung: `src/BizTalkPlatformManagementTool/Services/SnapshotValidator.cs`
|
||||
- Installer: `src/BizTalkPlatformManagementTool.Setup`
|
||||
- Release-Paketierung: `src/BizTalkPlatformManagementTool.Packager`
|
||||
- Regressionstests: `tests/BizTalkPlatformManagementTool.Tests`
|
||||
- PowerShell-Archiv: `archive/powershell/BizTalkPlatformManagementTool.ps1`
|
||||
|
||||
## Oberfläche
|
||||
## Code-Dokumentationsstandard
|
||||
|
||||
Die Oberfläche führt von links nach rechts durch sechs Schritte:
|
||||
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.
|
||||
|
||||
1. **Verbindung prüfen**
|
||||
2. **Zustand erfassen**
|
||||
3. **Herunterfahren**
|
||||
4. **Wiederherstellen**
|
||||
5. **Nachher erfassen**
|
||||
6. **Vergleichen**
|
||||
Deutsche Inline-Kommentare stehen gezielt an Stellen, deren Zweck nicht allein aus dem Code hervorgeht. Dazu zählen insbesondere:
|
||||
|
||||
Kopfbereich, Status-Badge, einheitliche Eingabefelder, gleich große
|
||||
Workflow-Schaltflächen, farblich getrennte destruktive und wiederherstellende
|
||||
Aktionen sowie responsive Ergebnis- und Logtabellen sorgen für eine klare
|
||||
Bedienung auch bei kleineren Bildschirmgrößen und DPI-Skalierung.
|
||||
- sichere Shutdown-/Restore-Reihenfolge und Schutz gebundener Orchestrierungen,
|
||||
- WMI-Auflösung über breite Abfrage mit clientseitigem Filter,
|
||||
- read-only Anwendungszuordnung über die ExplorerOM-Anwendungshierarchie,
|
||||
- atomare JSON-Ersetzung auf demselben Volume,
|
||||
- Neutralisierung formelartiger CSV-Werte,
|
||||
- Persistenz eines Operationsplans vor der Benutzerbestätigung,
|
||||
- Staging-, Aktivierungs-, Quarantäne- und Rollbackgrenzen des Installers,
|
||||
- begrenzte Self-Test-Prozess- und Streambehandlung.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Erfasster Zustand
|
||||
## UI Workflow
|
||||
|
||||
Über `root\MicrosoftBizTalkServer` werden gelesen:
|
||||
1. Anwendung mit Administratorrechten starten.
|
||||
2. BizTalk-Servernamen eintragen.
|
||||
3. Ausgabeverzeichnis wählen.
|
||||
4. **Dry run** aktiviert lassen.
|
||||
5. Mit **Diagnose** den Zugriff auf `root\MicrosoftBizTalkServer` prüfen.
|
||||
6. Mit **Snapshot Before** die Datei `before.json` erzeugen.
|
||||
7. Mit **Shutdown** den Plan `shutdown-plan.json` erzeugen und prüfen.
|
||||
8. **Dry run** erst deaktivieren, wenn der Plan korrekt ist.
|
||||
9. Beim echten Shutdown nach den Receive Locations den Drain-Dialog geöffnet lassen, Group Hub und Enterprise-Monitoring prüfen und erst bei leer gelaufener Verarbeitung **Yes** wählen.
|
||||
10. Nach der Wartung mit **Restore** aus `before.json` wiederherstellen.
|
||||
11. Mit **Snapshot After** und **Compare** die Umgebung validieren.
|
||||
|
||||
| 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 |
|
||||
Wenn ein echter Shutdown oder Restore nur teilweise ausgeführt wurde und lediglich die ursprüngliche `before.json` verfügbar ist, wird **Emergency Restore** verwendet. Die Quelle kann über **State...** ausgewählt und vorab über **Validate State** ohne WMI-Zugriff geprüft werden. Der Modus kopiert die Quelle unter einem timestamp-basierten Namen, überschreibt `before.json` nicht, stellt zuerst `ENTSSO` sicher und gleicht danach den gespeicherten Sollzustand zustandsbewusst ab. Der erste Lauf muss als Dry-run erfolgen.
|
||||
|
||||
Transportadressen werden bewusst nicht persistiert. Sie sind für Start/Stop
|
||||
nicht erforderlich und können Benutzerinformationen, Kennwörter oder andere
|
||||
sensible Bestandteile enthalten.
|
||||
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. Beim nächsten Start wird die aufbewahrte Historie erneut geladen. **Log Folder** öffnet den lokalen Ablageordner, **Close** beendet die Anwendung.
|
||||
|
||||
## Zustandswerte
|
||||
## Sicherheitsdesign
|
||||
|
||||
- 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`
|
||||
- Dry-run ist standardmäßig aktiviert.
|
||||
- Beim Start wird geprüft, ob die Anwendung mit Administratorrechten läuft. Ohne erhöhte Rechte wird eine Fehlermeldung angezeigt und die Anwendung beendet.
|
||||
- Echte Shutdown-/Restore-Aktionen verlangen erst nach Erzeugung und Speicherung des frischen Plans eine zusätzliche Bestätigung mit Zielserver, Plandatei und exakter Zahl ausführbarer Schritte.
|
||||
- Ein echter Shutdown hält nach der globalen Receive-Location-Phase an einem im Plan gespeicherten Operator-Checkpoint. Nur ein explizites **Yes** setzt Orchestrierungen, Send Ports und Host Instances fort.
|
||||
- **No** oder ein Fehler des Bestätigungsdialogs arbeitet fail-closed: spätere Planzeilen werden ohne Mutation als `NotExecuted` gespeichert; Nachher-Snapshot und Ergebnisreport werden trotzdem versucht.
|
||||
- Jede Operation schreibt Einträge in das sichtbare Operation Log.
|
||||
- Primär wird unter `%ProgramData%\BizTalkPlatformManagementTool\Logs` eine tägliche Logdatei `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben. Jeder Kandidat muss einen echten Create/Write/Flush/Delete-Test bestehen. Die Fallbackreihenfolge ist LocalAppData, `Logs` unter dem EXE-Verzeichnis und Temp.
|
||||
- Jeder GUI-Start hängt einen Prüfdatensatz an die aktive Tagesdatei an. **Log Folder** öffnet diesen tatsächlich beschreibbaren Pfad; ein Fallback oder der vollständige Ausfall des Dateiloggings wird mit Pfaden und Exception im Grid angezeigt.
|
||||
- Der aktuelle Tag bleibt als `.log` lesbar. Abgeschlossene Vortage werden als `.log.gz` komprimiert; aktueller Tag plus 29 Vortage bleiben erhalten.
|
||||
- Beim Programmstart werden bis zu 10.000 der neuesten Einträge aus `.log` und `.log.gz` chronologisch in das Operation Log geladen.
|
||||
- Der Kopfbereich zeigt den zuletzt erkannten Umgebungsstatus aus den Host Instances.
|
||||
- Operationspläne werden vor Laufzeitänderungen gespeichert.
|
||||
- Jeder ausführbare Schritt prüft vor der Mutation, ob sein Sollzustand bereits erreicht ist.
|
||||
- Ein Einzelfehler stoppt nicht mehr die restlichen unabhängigen Schritte; alle Ergebnisse werden einzeln persistiert.
|
||||
- Ein Teilfehler bleibt im GUI und im Ergebnisreport ausdrücklich fehlgeschlagen und wird nicht als Gesamterfolg ausgegeben.
|
||||
- Der Nachher-Snapshot wird auch nach Einzelfehlern separat versucht; ein Snapshotfehler wird im Ergebnisreport gesichert.
|
||||
- WMI-Methodenrückgaben werden geprüft.
|
||||
- **Diagnose** meldet neben sichtbaren Send Ports auch die Zahl erfolgreich zugeordneter Anwendungen; eine Differenz erzeugt eine Warnung.
|
||||
- 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.
|
||||
- Fehler protokollieren Typ, HRESULT, vollständige innere Exception-Kette, vorhandenes Fusion-Loaderprotokoll und Stacktrace identisch im Grid, Ergebnisreport und Laufzeitlog.
|
||||
- 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.
|
||||
|
||||
Weitere Host-Servicezustände werden als unbekannt angezeigt und nicht
|
||||
automatisch als gestartet behandelt.
|
||||
## Shutdown-Reihenfolge
|
||||
|
||||
## Shutdown-Plan
|
||||
1. Receive Locations deaktivieren, die im Snapshot aktiviert waren.
|
||||
2. Operator-Checkpoint: eingehenden Verkehr, Group Hub, laufende Service Instances und Enterprise-Monitoring prüfen; erst nach dem Leerfahren fortsetzen.
|
||||
3. Orchestrations stoppen, die gestartet waren.
|
||||
4. Send Ports stoppen, die gestartet waren.
|
||||
5. Host Instances stoppen, die auf dem ausgewählten Server gestartet waren.
|
||||
|
||||
1. Alle im Snapshot aktivierten Receive Locations deaktivieren.
|
||||
2. Alle im Snapshot gestarteten Orchestrierungen kontrolliert stoppen.
|
||||
3. Alle im Snapshot gestarteten Send Ports stoppen.
|
||||
4. Alle im Snapshot gestarteten Hostinstanzen des ausgewählten Servers stoppen.
|
||||
`Partially Started` auf Anwendungsebene ist während der Drain-Phase erwartbar: deaktivierte Receive Locations bei noch laufenden Orchestrierungen/Send Ports entsprechen dem BizTalk-Modell „Partial Stop - Allow running instances to continue“. Das Tool setzt Anwendungen nicht zusätzlich pauschal auf `Stopped`. Ein Application-Stop ist kein reines Statusflag, sondern führt abhängig von der Auswahl weitere Mutationen bis hin zu Unenlist, Policy-Undeploy oder Instanzterminierung aus. Die artefaktgenaue Steuerung bleibt deshalb die sichere, aus `before.json` reproduzierbare Grenze.
|
||||
|
||||
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.
|
||||
## Restore-Reihenfolge
|
||||
|
||||
## Restore-Plan
|
||||
1. Host Instances starten, die auf dem ausgewählten Server zuvor gestartet waren.
|
||||
2. Send Ports wiederherstellen.
|
||||
3. Orchestrations wiederherstellen, soweit dies sicher möglich ist.
|
||||
4. Receive Locations zuletzt wiederherstellen.
|
||||
|
||||
1. Zuvor gestartete Hostinstanzen starten.
|
||||
2. Zuvor gestartete Send Ports starten.
|
||||
3. Zuvor gestartete Orchestrierungen starten.
|
||||
4. Zuvor aktivierte Receive Locations zuletzt aktivieren.
|
||||
Die Kategorienreihenfolge gilt global über alle BizTalk-Anwendungen. Dadurch wird keine Receive Location einer alphabetisch früheren Anwendung aktiviert, bevor Send Ports und Orchestrations späterer Anwendungen behandelt wurden.
|
||||
|
||||
Gestoppte oder gebundene Send Ports, gestoppte/gebundene/ungebundene
|
||||
Orchestrierungen, deaktivierte Receive Locations und gestoppte Hostinstanzen
|
||||
erzeugen keine Restore-Aktion.
|
||||
## Emergency Restore
|
||||
|
||||
## Eindeutige WMI-Auflösung
|
||||
Der Emergency Restore benötigt nur eine valide `before.json` und denselben Zielserver. Er erzeugt keine neue Vorheraufnahme und überschreibt die Recovery-Quelle nicht.
|
||||
|
||||
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:
|
||||
1. Timestamp-basierte Kopie der Eingabedatei sichern.
|
||||
2. Enterprise Single Sign-On (`ENTSSO`) auf dem Zielserver auf `Running` bringen.
|
||||
3. Zuvor gestartete Host Instances zustandsbewusst starten.
|
||||
4. Send Ports, Orchestrations und Receive Locations in global sicherer Reihenfolge auf den Snapshotzustand abgleichen.
|
||||
5. Bereits korrekte Zustände als `AlreadySatisfied` überspringen.
|
||||
6. Isolierte WMI-/Adapterfehler als `Failed` erfassen und mit dem nächsten unabhängigen Schritt fortfahren.
|
||||
7. Nachher-Snapshot unabhängig versuchen und vollständigen Ergebnisreport schreiben.
|
||||
8. Bei erfolgreichem Nachher-Snapshot automatisch einen timestamp-basierten Soll/Ist-Diff als JSON, CSV und HTML erzeugen.
|
||||
|
||||
- Receive Location: `Name` plus `ReceivePortName`
|
||||
- Orchestrierung: `Name` plus `AssemblyName`
|
||||
- Hostinstanz: `InstanceName` plus `RunningServer`
|
||||
Der Best-effort-Ansatz bedeutet nicht, dass Fehler ignoriert werden: Jeder Teilfehler erzeugt einen roten/fehlgeschlagenen Abschluss und erfordert die Prüfung des Ergebnisreports. Er verhindert lediglich, dass beispielsweise eine einzelne nicht validierbare Receive Location alle späteren Host-, Port- oder Receive-Location-Schritte blockiert.
|
||||
|
||||
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.
|
||||
## Restore-Grenzen
|
||||
|
||||
## Dateisicherheit
|
||||
- Send Ports werden auf `Started`, `Stopped` oder `Bound` zurückgesetzt.
|
||||
- Orchestrations mit Status `Started`, `Stopped` oder `Unbound` werden direkt behandelt.
|
||||
- Orchestrations mit Status `Bound` bleiben unverändert und werden zur manuellen Prüfung markiert, weil ein blindes Unenlist zu `Unbound` führen kann.
|
||||
- Receive Locations werden zuletzt aktiviert, damit eingehender Verkehr erst nach Host Instances und abhängigen Artefakten wieder aufgenommen wird.
|
||||
|
||||
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.
|
||||
## ScheduledTask-Adapter
|
||||
|
||||
Vergleiche verwenden zusammengesetzte Identitäten. Gleichnamige Artefakte in
|
||||
unterschiedlichen Anwendungen, Receive Ports oder Assembly-Versionen werden
|
||||
nicht miteinander vermischt.
|
||||
Beim Fremdadapter `Schedule`/ScheduledTask 7.0.2 wird `MSBTS_ReceiveLocation.Enable` beziehungsweise `.Disable` erst ausgeführt, nachdem die für die Transportdatenvalidierung erforderliche `Microsoft.BizTalk.Scheduler.dll` prozesslokal geladen werden konnte. Die Planzeile enthält dafür Adaptername und `scheduler:`-Adresse aus dem Snapshot.
|
||||
|
||||
## Verzeichnisse
|
||||
Der Resolver sucht nur in vorhandenen, vertrauenswürdigen Installationsverzeichnissen und akzeptiert eine DLL erst nach Prüfung der Assemblyidentität. Er berücksichtigt:
|
||||
|
||||
```text
|
||||
%ProgramFiles%\BizTalkPlatformManagementTool\
|
||||
BizTalkPlatformManagementTool.exe
|
||||
BizTalkPlatformManagementTool.exe.config
|
||||
1. `AdapterAssemblySearchPaths` aus der EXE-Konfiguration,
|
||||
2. Installations- und Assemblypfade aus beiden BizTalk-Registryansichten,
|
||||
3. lokale Verzeichnisse `Microsoft BizTalk Server*` und `BizTalk ScheduledTask Adapter*` unter Program Files (x86).
|
||||
|
||||
%ProgramData%\BizTalkPlatformManagementTool\
|
||||
Wartungen\
|
||||
Logs\
|
||||
```
|
||||
Die Standardkonfiguration enthält BizTalk Server 2020 und den im PROD-Screenshot bestätigten Pfad `C:\Program Files (x86)\BizTalk ScheduledTask Adapter 7.0.2`. Die Auflösung gilt nur im Toolprozess. Es werden weder GAC noch Adapterdateien verändert. Ist die erforderliche Assembly nicht auffindbar oder nicht zur angeforderten Identität passend, scheitert nur dieser Schritt mit vollständiger Pfad-, Versions- und Loaderdiagnose; der Executor arbeitet die restlichen unabhängigen Schritte weiter ab.
|
||||
|
||||
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.
|
||||
## Ausgabedateien
|
||||
|
||||
## Installer und Rollback
|
||||
- Snapshots: `before.json`, `after.json`
|
||||
- Operationspläne: `shutdown-plan.json`, `restore-plan.json`
|
||||
- Nachher-Snapshots: `shutdown-after.json`, `restore-after.json`
|
||||
- Ergebnisreports: `shutdown-result.json`, `restore-result.json`
|
||||
- Emergency-Dateien: `emergency-source-before-*`, `emergency-restore-plan-*`, `emergency-restore-result-*`, `emergency-restore-after-*`
|
||||
- Diff: `diff.json`, `diff.csv`, `diff.html`
|
||||
- Snapshot-Reports: `*.csv`, `*.hosts.csv`, `*.html`
|
||||
- Laufzeitlogs: primär `%ProgramData%\BizTalkPlatformManagementTool\Logs\BizTalkPlatformManagementTool-yyyy-MM-dd.log`, andernfalls der im Grid und über **Log Folder** ausgewiesene verifizierte Fallback; komprimierte Vortage `*.log.gz`
|
||||
- Installerlogs: `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs\setup-*.log`
|
||||
|
||||
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.
|
||||
## Fehleranalyse
|
||||
|
||||
Der Release-Packager erzeugt den Installationsordner, ein ZIP, eine
|
||||
Base64-Transportdatei und eine SHA-256-Datei.
|
||||
`MSBTS_SendPort`, `MSBTS_ReceiveLocation` und `MSBTS_Orchestration` enthalten gemäß BizTalk-WMI-Schema keine verlässliche Application-Property. Die Anwendung liest deshalb zuerst über `MSBTS_GroupSetting` den BizTalk-Managementdatenbankserver und -namen. Danach wird `Microsoft.BizTalk.ExplorerOM.dll` ohne Compile-time-Referenz aus dem GAC oder einem identitätsgeprüften BizTalk-Installationspfad geladen. Der read-only Anwendungskatalog liefert die Hierarchie Application → Send Ports, Receive Ports/Locations und Orchestrations; die eigentlichen Zustände und alle Mutationen bleiben ausschließlich WMI-basiert.
|
||||
|
||||
## Tests
|
||||
Kann ExplorerOM nicht geladen werden, fehlen Management-DB-Rechte oder ist ein Name nicht eindeutig, bleibt die Zustandserfassung best-effort verfügbar. Die betroffenen Zeilen stehen dann bewusst unter `(Unknown Application)`. Das Grid und die Laufzeitdatei enthalten die vollständige Lade-/DB-/Reflection-Diagnose; **Diagnose** zeigt eine kleinere Zahl zugeordneter als sichtbarer Send Ports. Ein Dry-run darf erst nach geklärter Zuordnung freigegeben werden.
|
||||
|
||||
Die Regressionstests prüfen:
|
||||
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.
|
||||
|
||||
- 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.
|
||||
Bei `scheduler:` erscheinen zusätzlich `ScheduledTask adapter preflight`, alle tatsächlich verwendeten Suchverzeichnisse, Prozessbitness und bei Erfolg die vollständige geladene Assemblyidentität samt Pfad. Ein Ladefehler enthält die anfordernde Assembly, HRESULT, Fusion-Details und Stacktrace. Für eine Supportanalyse sind die Tagesdatei, relevante `.log.gz`, der passende `*-result.json`, Plan, Vorher-/Nachher-Snapshot und die unveränderte EXE-Konfiguration zu sichern.
|
||||
|
||||
Ein echter WMI-Integrationstest muss auf einem BizTalk-2020-System im Dry-run
|
||||
und anschließend in einem freigegebenen Wartungsfenster erfolgen.
|
||||
Snapshot- und Plan-JSON-Dateien werden als UTF-8 ohne BOM über eine temporäre Datei im Zielverzeichnis und anschließenden atomaren Austausch geschrieben. Beim Laden werden vorhandene Dateien mit UTF-8-BOM oder durch Encoding-Konvertierung sichtbar gewordenem BOM-Marker toleriert. Deserialisierte Snapshots werden normalisiert und auf leere Namen, Duplikate und fehlende Strukturen geprüft.
|
||||
|
||||
Diffs verwenden den zusammengesetzten Schlüssel aus Anwendung und Artefaktname. Gleichnamige Artefakte in verschiedenen BizTalk-Anwendungen überschreiben sich daher nicht mehr. CSV-Werte mit Präfix `=`, `+`, `-`, `@` oder Tab werden mit einem Apostroph neutralisiert, damit Tabellenkalkulationen sie nicht als Formel ausführen.
|
||||
|
||||
WMI-Abfrageobjekte sowie Rückgabeobjekte von WMI-Methoden werden deterministisch freigegeben. Polling wartet am Timeout-Ende nur noch für die tatsächlich verbleibende Zeit.
|
||||
|
||||
## Installer- und Updatearchitektur
|
||||
|
||||
Das Releasepaket enthält `Setup.exe`, `application.manifest`, den Payload-Ordner `application` und die Installationsanleitung. Vor jeder Änderung prüft der Installer, dass jede Payload-Datei vollständig im Manifest enthalten ist und Länge sowie SHA-256 entsprechen. Nicht deklarierte Zusatzdateien führen zum Abbruch.
|
||||
|
||||
Die Aktivierung ist transaktional aufgebaut:
|
||||
|
||||
1. Payload validieren.
|
||||
2. Eindeutiges Staging-Verzeichnis neben dem Installationsziel erstellen.
|
||||
3. Kopierte Dateien nochmals per SHA-256 und die Staging-EXE per `--self-test` prüfen.
|
||||
4. Laufende Toolinstanz ausschließen.
|
||||
5. Bestehendes Verzeichnis in ein eindeutiges Backup verschieben.
|
||||
6. Staging auf demselben Volume als produktives Verzeichnis aktivieren; kurzzeitige Rename-Sperren werden mit acht begrenzten Versuchen und insgesamt höchstens 19,75 Sekunden Backoff behandelt.
|
||||
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.
|
||||
|
||||
Wenn ausschließlich die atomare Staging-Umbenennung dauerhaft gesperrt bleibt, ist ein eng begrenzter Kopierfallback zulässig: Bei einer Neuinstallation muss das Ziel fehlen; bei einem Update muss die vollständige Vorversion bereits atomar ins Backup verschoben worden sein und das aktive Ziel fehlen. Jede Zieldatei wird erneut anhand des Manifests per SHA-256 geprüft, und der zweite Self-Test läuft weiterhin aus dem endgültigen Ziel. Der Fallback ist nie ein In-place-Update. Ein Teilfehler entfernt die Zielkopie und stellt beim Update das Backup wieder her. Jeder Retry, der Geltungsbereich und die verwendete Aktivierungsmethode stehen im Setup-Log.
|
||||
|
||||
Die Windows-Integration besitzt ab Version 2.2.4 eine explizite Kritikalitätsgrenze. Startmenü-Link, Uninstaller und Registry sind für einen wartbaren Maschinenzustand verpflichtend und bleiben Teil der Transaktion. Der gemeinsame Desktop-Link ist dagegen eine standardmäßig abgewählte Komfortfunktion. Sämtliche Zugriffe darauf – Eingangssnapshot, Erstellen/Entfernen, Rücklesevalidierung, Rollback und Uninstall – laufen innerhalb einer ausfallsicheren optionalen Exception-Grenze. Ein Fehler erzeugt UI- und Logwarnung sowie einen erfolgreichen Abschluss mit Warnstatus, aber keinen Anwendungsrollback. Auch eine fehlerhafte GUI-Warnungsweiterleitung ist isoliert.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Das äußere ZIP erhält zusätzlich eine SHA-256-Datei und eine Certutil-kompatible Base64-TXT-Datei. Diese äußere Prüfsumme erkennt Übertragungsfehler; sie ist keine digitale Herausgebersignatur. Details und Befehle stehen in `Installation.md`.
|
||||
|
||||
## Automatisierte Verifikation
|
||||
|
||||
`tests/BizTalkPlatformManagementTool.Tests` prüft derzeit:
|
||||
|
||||
- Atomare JSON-Aktualisierung und BOM-Kompatibilität.
|
||||
- Anwendungsbezogene Diff-Identität bei gleichnamigen Artefakten.
|
||||
- ExplorerOM-basierte Artefaktzuordnung einschließlich Receive-Port-Fallback und Ambiguitätsschutz.
|
||||
- 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.
|
||||
- Erholung nach einer vorübergehenden Rename-Sperre mit korrektem Backoff und Diagnoseereignissen.
|
||||
- Verifizierter Kopierfallback einer dauerhaft move-gesperrten Neuinstallation und eines bereits atomar gesicherten Updates.
|
||||
- Vollständiger Update-Rollback, wenn der Ziel-Self-Test nach dem Kopierfallback fehlschlägt.
|
||||
- Nichtfataler gemeinsamer Desktop-Link bei simuliertem ACL-Fehler, Zustandsabweichung und ausfallender Warnungsweiterleitung.
|
||||
- Begrenzter Abbruch eines dauerhaft move-gesperrten Updates ohne Veränderung der Vorversion.
|
||||
- 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.
|
||||
- Fortsetzung nach einer simulierten `Microsoft.BizTalk.Scheduler`-Exception.
|
||||
- Idempotentes Überspringen bereits erreichter Sollzustände.
|
||||
- `ENTSSO` als erster Emergency-Restore-Schritt.
|
||||
- Globale Kategorie-Reihenfolge über mehrere BizTalk-Anwendungen.
|
||||
- Kurzname/FQDN-Gleichheit für Host-Instance-Schritte.
|
||||
- Persistenz vollständiger Teilfehlerreports.
|
||||
|
||||
Der portable Build, die Tests, der Anwendungsselftest und die Paketkonsistenz sind lokal unter Mono prüfbar. Die endgültige Freigabe erfordert zusätzlich einen Windows-Test von UAC, Registry, Verknüpfungen und Setup-Rollback sowie einen repräsentativen BizTalk-2020-Test von Diagnose, Dry-run, Shutdown und Restore.
|
||||
|
||||
## Status Mapping
|
||||
|
||||
- `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
|
||||
|
||||
+158
-39
@@ -2,58 +2,177 @@
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Windows Server 2019/2022 oder Administrationshost
|
||||
- Microsoft BizTalk Server 2020 oder installierte BizTalk Administration Tools
|
||||
- .NET Framework 4.6.1 oder neuer
|
||||
- lokales Administratorkonto
|
||||
- Mitgliedschaft in der Gruppe **BizTalk Server Administrators**
|
||||
- WMI-Zugriff auf `root\MicrosoftBizTalkServer`
|
||||
- Windows Server 2019/2022 oder ein Windows-Administrationshost
|
||||
- Microsoft BizTalk Server 2020 oder BizTalk Administration Tools
|
||||
- .NET Framework 4.6.1 Runtime
|
||||
- Lokale Administratorrechte; `Setup.exe` und die Anwendung fordern diese per UAC-Manifest an
|
||||
- Zugriff auf `root\MicrosoftBizTalkServer` mit den erforderlichen BizTalk-Rechten
|
||||
|
||||
## Installation
|
||||
## Übergabe als TXT
|
||||
|
||||
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.
|
||||
Das Release erzeugt folgende Dateien unter `artifacts`:
|
||||
|
||||
Installiert wird nach `%ProgramFiles%\BizTalkPlatformManagementTool`.
|
||||
Persistente Wartungszustände und Logs liegen getrennt unter
|
||||
`%ProgramData%\BizTalkPlatformManagementTool`.
|
||||
- `BizTalkPlatformManagementTool-Setup.zip.b64.txt`: Certutil-kompatible Base64-Übertragung
|
||||
- `BizTalkPlatformManagementTool-Setup.zip.sha256.txt`: SHA-256 des ZIP-Archivs
|
||||
- `BizTalkPlatformManagementTool-Setup.zip`: direkt entpackbares Installationspaket
|
||||
- `BizTalkPlatformManagementTool-2.3.4-source.zip.b64.txt`: vollständiger Quellstand als Base64-TXT
|
||||
- `BizTalkPlatformManagementTool-2.3.4-source.zip.sha256.txt`: SHA-256 des Quellarchivs
|
||||
|
||||
## Sicheres Update
|
||||
Auf dem Zielsystem wird die TXT-Datei so rekonstruiert und geprüft:
|
||||
|
||||
Der Installer validiert das Paket, kopiert die neue Version zunächst in ein
|
||||
Staging-Verzeichnis und aktiviert sie erst danach. Eine vorhandene Installation
|
||||
wird vorübergehend umbenannt. Schlägt das Update fehl, wird die vorherige
|
||||
Programmversion wiederhergestellt. Wartungszustände und Logs unter
|
||||
`%ProgramData%` bleiben unberührt.
|
||||
```bat
|
||||
certutil -decode BizTalkPlatformManagementTool-Setup.zip.b64.txt BizTalkPlatformManagementTool-Setup.zip
|
||||
certutil -hashfile BizTalkPlatformManagementTool-Setup.zip SHA256
|
||||
type BizTalkPlatformManagementTool-Setup.zip.sha256.txt
|
||||
```
|
||||
|
||||
Vor dem Update muss das Management Tool geschlossen sein. Laufende
|
||||
Wartungsoperationen dürfen nicht durch ein Update unterbrochen werden.
|
||||
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.
|
||||
|
||||
## 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. Die standardmäßig abgewählte, optionale Desktop-Verknüpfung nur bei Bedarf für alle Benutzer aktivieren.
|
||||
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 primär: `%ProgramData%\BizTalkPlatformManagementTool\Logs`
|
||||
- Installerlogs: `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs`
|
||||
- Uninstaller: `%ProgramData%\BizTalkPlatformManagementTool\Setup\Uninstall.exe`
|
||||
- Startmenü: `BizTalk Platform Management Tool`
|
||||
|
||||
## Laufzeitlog-Ablage prüfen
|
||||
|
||||
Der normale Laufzeitlogordner liegt nicht im Programmverzeichnis. `%ProgramData%` entspricht üblicherweise `C:\ProgramData` und ist im Explorer standardmäßig ausgeblendet. Dass unter `C:\Program Files\BizTalkPlatformManagementTool` nur EXE, Konfiguration und Installationsstatus sichtbar sind, ist daher allein noch kein Logfehler.
|
||||
|
||||
Ab Version 2.3.1 akzeptiert die Anwendung einen Ablageort erst nach einem echten Create/Write/Flush/Delete-Test. Sie prüft in dieser Reihenfolge:
|
||||
|
||||
1. `%ProgramData%\BizTalkPlatformManagementTool\Logs`
|
||||
2. `%LocalAppData%\BizTalkPlatformManagementTool\Logs`
|
||||
3. `%ProgramFiles%\BizTalkPlatformManagementTool\Logs`
|
||||
4. `%TEMP%\BizTalkPlatformManagementTool\Logs`
|
||||
|
||||
Danach wird bei jedem Programmstart die Zeile `Runtime log storage verified by startup append. Active file: ...` wirklich in die Tagesdatei geschrieben und im Grid angezeigt. **Log Folder** öffnet genau den aktiven Pfad. Bei einem Fallback nennt eine gelbe Grid-Warnung den verworfenen Pfad samt Exception. Kann kein Kandidat beschrieben werden, erscheint `RUNTIME FILE LOGGING UNAVAILABLE`; vor einer echten Wartungsoperation müssen dann Berechtigungen, Datenträger oder Sicherheitssoftware geklärt werden.
|
||||
|
||||
Der WMI-freie Installer-Self-Test schreibt und liest zusätzlich einen eigenen temporären Runtime-Logmarker. Damit wird eine Version, deren Logger grundsätzlich keine Datei erzeugen kann, bereits vor der Aktivierung abgelehnt. Die PROD-Abnahme des realen Maschinenpfads bleibt nach dem Update erforderlich.
|
||||
|
||||
## Anwendungszuordnung ab 2.3.3
|
||||
|
||||
Die Runtime-WMI-Klassen für Send Ports, Receive Locations und Orchestrierungen liefern keine verlässliche BizTalk-Anwendungszuordnung. Version 2.3.3 liest deshalb `MgmtDbServerName` und `MgmtDbName` aus `MSBTS_GroupSetting` und verwendet die lokal installierte `Microsoft.BizTalk.ExplorerOM.dll` ausschließlich read-only zum Aufbau der Application-/Artefakthierarchie. Zustandsabfrage sowie Shutdown-/Restore-Mutationen bleiben WMI-basiert.
|
||||
|
||||
Es wird keine BizTalk-DLL mit dem Paket ausgeliefert. Die Microsoft-Assembly wird aus dem GAC oder aus einem vorhandenen, identitätsgeprüften BizTalk-Installationspfad geladen. Das ausführende Konto benötigt neben WMI-Zugriff Leserechte auf den BizTalk-Anwendungskatalog in der Management-Datenbank. Nach dem Update muss **Diagnose** für PROD dieselbe Zahl bei `Send ports visible` und `application associations resolved` melden. Andernfalls zeigt das Log den Lade-, Berechtigungs- oder Zuordnungsfehler; ein realer Lauf darf erst nach Klärung freigegeben werden.
|
||||
|
||||
## Shutdown-Drain-Checkpoint ab 2.3.2
|
||||
|
||||
Ein echter Shutdown pausiert nach allen Receive Locations und vor Orchestrierungen, Send Ports und Host Instances. Der Dialog zeigt erfolgreiche, bereits deaktivierte und fehlgeschlagene Receive Locations. Er darf geöffnet bleiben, während Group Hub, laufende Service Instances, MessageBox-/Queue-Metriken und das Enterprise-Monitoring geprüft werden.
|
||||
|
||||
- **Yes** protokolliert `CheckpointDecision=Continue` und setzt den gespeicherten Plan fort.
|
||||
- **No** beendet den Lauf sicher; alle späteren Zeilen werden als `NotExecuted` gespeichert.
|
||||
- Ein fehlender oder fehlerhafter Dialog stoppt ebenfalls fail-closed und wird als Fehler protokolliert.
|
||||
- Dry-run zeigt den Checkpoint im Plan/Grid, fordert aber keine echte Entscheidung an.
|
||||
- Nach einem Stopp werden `shutdown-after.json` und `shutdown-result.json` soweit möglich trotzdem geschrieben.
|
||||
|
||||
Der Dialog behauptet nicht automatisch, die Umgebung sei leer. Diese fachliche Entscheidung bleibt bewusst beim Operator und den vorhandenen Enterprise-Monitoringquellen. `Partially Started` in der BizTalk-Konsole ist direkt nach dem Abschalten der Receive Locations erwartbar. Es wird kein pauschaler Application-Stop ergänzt, da dieser je nach Modus weitere Artefaktzustände, Subscriptions, Policies oder laufende Instanzen verändert.
|
||||
|
||||
## 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. Atomare Verzeichnisverschiebungen werden bei `IOException` oder `UnauthorizedAccessException` mit begrenztem Backoff bis zu achtmal versucht (insgesamt höchstens 19,75 Sekunden Wartezeit).
|
||||
6. Die aktivierte EXE führt den Self-Test erneut aus.
|
||||
7. Erst danach werden Verknüpfungen und Windows-Uninstall-Eintrag aktualisiert.
|
||||
|
||||
Kann ausschließlich die Umbenennung des geprüften Staging-Verzeichnisses dauerhaft nicht ausgeführt werden, darf der Installer nach vollständig ausgeschöpften Move-Versuchen auf eine verifizierte Kopie zurückfallen. Dabei wird jede Datei aus dem Staging nochmals gegen ihren Manifest-SHA-256 geprüft; anschließend läuft auch der zweite Self-Test aus dem endgültigen Ziel. Bei einer Neuinstallation muss das Ziel noch fehlen. Bei einem Update ist der Fallback erst zulässig, nachdem die vollständige Vorversion atomar ins Backup verschoben wurde und das Ziel ebenfalls nicht mehr existiert. Er ist damit kein In-place-Update und überschreibt keine aktive Version.
|
||||
|
||||
Scheitern Kopie, Hashprüfung oder Ziel-Self-Test, entfernt das Setup das teilweise neue Ziel und stellt beim Update das zuvor atomar erzeugte Backup wieder her. Kann bereits die Vorversion nicht atomar ins Backup verschoben werden, bleibt sie unverändert aktiv und das Update bricht ohne Kopierfallback ab.
|
||||
|
||||
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 temporäres Verzeichnis wegen einer Dateisperre nicht gelöscht werden, bleibt die erfolgreiche Installation aktiv und das Log kennzeichnet den Abschluss als `success_with_temporary_cleanup_warning`.
|
||||
|
||||
### Optionale Desktop-Verknüpfung für alle Benutzer
|
||||
|
||||
Der gemeinsame Desktop-Link unter `%PUBLIC%\Desktop` ist eine Komfortfunktion und ab Version 2.2.4 standardmäßig abgewählt. Fehlende Rechte, eine WSH-/COM-Sperre, eine abweichende Linkvalidierung oder ein Fehler beim Sichern, Entfernen beziehungsweise Wiederherstellen dieses optionalen Links dürfen Installation, Update, Rollback und Deinstallation nicht abbrechen. Das Setup zeigt eine Warnung, schreibt die vollständige Exception ins Diagnoselog und setzt die Kernoperation fort.
|
||||
|
||||
Startmenü-Link, Uninstaller unter ProgramData und Uninstall-Registry bleiben verpflichtende Windows-Integration. Scheitert einer dieser Bestandteile, wird die Installation weiterhin als fehlgeschlagen behandelt und die vorherige Version vollständig wiederhergestellt. Der Installer verändert keine ACLs und umgeht keine Sicherheitsrichtlinie; bei einem Desktop-Link-Fehler bleibt der Link lediglich im tatsächlich erreichbaren Zustand.
|
||||
|
||||
## 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.
|
||||
|
||||
Bei einer gesperrten Verzeichnisverschiebung erscheinen `event=directory_move_retry` und nach einer Erholung `event=directory_move_recovered`. Ein Kopierfallback protokolliert `event=activation_fallback_started`, `event=activation_fallback_complete`, den Geltungsbereich `scope=new_install` oder `scope=update_after_backup` und im Abschluss `activation_method=verified_copy_fallback`. Kann bei einem Update bereits das Backup nicht erzeugt werden, endet es weiterhin mit `SETUP-ACTIVATION`, bevor die aktive Version verändert wurde.
|
||||
|
||||
Ein Fehler des gemeinsamen Desktop-Links erscheint als `event=optional_windows_integration_warning` mit einer Rolle wie `desktop_shortcut_snapshot`, `desktop_shortcut_create`, `desktop_shortcut_remove`, `desktop_shortcut_validate`, `desktop_shortcut_rollback_restore` oder `desktop_shortcut_uninstall_remove`. Der erfolgreiche Gesamtabschluss enthält dann `optional_desktop_shortcut_warning=True` und beispielsweise `result=success_with_optional_desktop_shortcut_warning`; er ist kein Rollback und kein fehlgeschlagenes Setup.
|
||||
|
||||
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.
|
||||
|
||||
## Emergency Restore nach einem Teilabbruch
|
||||
|
||||
Version 2.3.1 kann einen Wiederanlauf allein aus einer erhaltenen `before.json` vorbereiten und ausführen. Eine mit Version 2.1.3 erzeugte Datei ist kompatibel; zusätzliche Plan- oder Nachher-Dateien des fehlgeschlagenen Laufs sind nicht erforderlich. Auch ein Fehler der GUI-Logweiterleitung ist vom fachlichen Executor isoliert und kann die späteren Planschritte nicht abbrechen.
|
||||
|
||||
Für ScheduledTask-Receive-Locations enthält die installierte EXE-Konfiguration `AdapterAssemblySearchPaths`. Normalerweise ist keine Anpassung nötig: BizTalk- und ScheduledTask-Installationsverzeichnisse werden zusätzlich automatisch erkannt. Nur bei einem abweichenden lokalen Installationspfad darf der vorhandene Wert um diesen Pfad ergänzt werden. Die Assembly muss aus genau dieser BizTalk-/Adapterinstallation stammen; das Tool verändert den GAC nicht.
|
||||
|
||||
1. Die erhaltene `before.json` außerhalb des Arbeitsverzeichnisses zusätzlich sichern.
|
||||
2. Anwendung als Administrator starten und denselben Zielserver wählen, der im Snapshot gespeichert ist.
|
||||
3. Über **State...** die erhaltene Datei auswählen oder im Feld **State** ihren vollständigen Pfad eintragen.
|
||||
4. **Validate State** ausführen. Diese Prüfung öffnet keine WMI-Verbindung und verändert keinen Laufzeitzustand.
|
||||
5. **Dry run** aktiviert lassen und **Emergency Restore** wählen.
|
||||
6. Den timestamp-basierten `emergency-restore-plan-*` prüfen. Die bestehende `before.json` wird dabei nicht überschrieben.
|
||||
7. Dry run deaktivieren, **Emergency Restore** erneut wählen und den expliziten Dialog bestätigen.
|
||||
|
||||
Der Emergency Restore stellt zuerst sicher, dass der Windows-Dienst `ENTSSO` läuft. Danach folgen Host Instances, Send Ports, Orchestrations und zuletzt Receive Locations. Vor jeder Mutation wird der aktuelle Zustand geprüft; bereits korrekte Zustände werden als `AlreadySatisfied` protokolliert. Ein isolierter Fehler wird als `Failed` festgehalten, während alle späteren unabhängigen Schritte weiter versucht werden.
|
||||
|
||||
Jeder Lauf schreibt eine unveränderte Snapshot-Kopie sowie timestamp-basierte Plan- und Ergebnisdateien. Der echte Lauf versucht zusätzlich Nachher-Snapshot und `emergency-restore-diff-*` als JSON, CSV und HTML. Ein Ergebnis mit mindestens einem fehlgeschlagenen Schritt bleibt im GUI ausdrücklich `Failed` und verlangt Operator-Review, auch wenn alle anderen Schritte erfolgreich waren.
|
||||
|
||||
## Deinstallation
|
||||
|
||||
Im Setup kann die Anwendung wieder deinstalliert werden. Standardmäßig bleiben
|
||||
Wartungszustände und Logs erhalten. Die Option kann deaktiviert werden, wenn
|
||||
auch diese Daten bewusst entfernt werden sollen.
|
||||
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.
|
||||
|
||||
## Transport-TXT zurückwandeln
|
||||
## Build, Test und Paketierung
|
||||
|
||||
Die Datei `BizTalkPlatformManagementTool-Setup.zip.b64.txt` kann auf dem
|
||||
Zielsystem beispielsweise mit `certutil.exe` zurückgewandelt werden:
|
||||
In einer Visual-Studio-Developer-Eingabeaufforderung mit .NET Framework 4.6.1 Developer Pack:
|
||||
|
||||
```cmd
|
||||
certutil.exe -decode BizTalkPlatformManagementTool-Setup.zip.b64.txt BizTalkPlatformManagementTool-Setup.zip
|
||||
```bat
|
||||
scripts\test-release.cmd
|
||||
scripts\package-release.cmd
|
||||
```
|
||||
|
||||
Anschließend die SHA-256-Prüfsumme mit
|
||||
`BizTalkPlatformManagementTool-Setup.zip.sha256.txt` vergleichen.
|
||||
`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.
|
||||
|
||||
## Erster Funktionstest
|
||||
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.
|
||||
|
||||
1. Tool über Startmenü oder Desktop-Verknüpfung starten.
|
||||
2. Dry-run aktiviert lassen.
|
||||
3. **Verbindung prüfen** ausführen.
|
||||
4. **Zustand erfassen** ausführen.
|
||||
5. JSON-, CSV- und HTML-Dateien im Arbeitsordner prüfen.
|
||||
6. **Herunterfahren** im Dry-run ausführen und den Plan kontrollieren.
|
||||
Unter Mono kann der portable Anteil lokal geprüft werden:
|
||||
|
||||
```sh
|
||||
msbuild BizTalkPlatformManagementTool.sln /p:Configuration=Release /p:Platform="Any CPU" /m:1
|
||||
mono tests/BizTalkPlatformManagementTool.Tests/bin/Release/BizTalkPlatformManagementTool.Tests.exe
|
||||
mono src/BizTalkPlatformManagementTool/bin/Release/BizTalkPlatformManagementTool.exe --self-test
|
||||
```
|
||||
|
||||
Mono ersetzt nicht die abschließende Prüfung von UAC, Registry, Verknüpfungen und BizTalk-WMI auf einem repräsentativen Windows-/BizTalk-System.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
PROD-HANDOFF: APPLICATION-ZUORDNUNG - VERSION 2.3.3
|
||||
Stand: 2026-08-27
|
||||
|
||||
FEHLER
|
||||
Version 2.3.2 zeigte Send Ports, Receive Locations und Orchestrierungen unter
|
||||
"(Unknown Application)". Die Objekte und Zustaende kamen korrekt aus WMI,
|
||||
aber die erwarteten Application-Properties existieren in diesen Runtime-WMI-
|
||||
Klassen nicht.
|
||||
|
||||
FIX
|
||||
Version 2.3.3 liest MgmtDbServerName/MgmtDbName aus MSBTS_GroupSetting und
|
||||
baut ueber die lokal installierte, read-only verwendete ExplorerOM-Hierarchie
|
||||
einen Application-/Artefaktindex. WMI bleibt alleinige Quelle fuer Zustaende
|
||||
und Mutationen. Es wird keine BizTalk-DLL mitgeliefert oder in den GAC kopiert.
|
||||
|
||||
PROD-ABNAHME
|
||||
1. Setup 2.3.3 installieren; setup_version=2.3.3.0 und Ziel-Self-Test pruefen.
|
||||
2. Tool als Administrator starten; PROD-Ziel und aktiviertes Dry run pruefen.
|
||||
3. Diagnose ausfuehren. Erwartet:
|
||||
Send ports visible: N; application associations resolved: N.
|
||||
Beide Zahlen muessen identisch sein.
|
||||
4. Im Runtime-Log "BizTalk application catalog loaded through ExplorerOM"
|
||||
und plausible Application-/Artefaktzaehler pruefen.
|
||||
5. Snapshot Before erzeugen. Keine Unknown-Application-Gruppe fuer regulaere
|
||||
katalogisierte Artefakte; mehrere Ports, Receive Locations und
|
||||
Orchestrierungen gegen die BizTalk Administration Console abgleichen.
|
||||
6. Shutdown nur als Dry-run ausfuehren. Echte Application-Namen und korrekte
|
||||
globale Reihenfolge in Grid und shutdown-plan.json pruefen.
|
||||
7. Erst danach einen echten Wartungslauf freigeben.
|
||||
|
||||
STOPPKRITERIUM
|
||||
Sind sichtbare und zugeordnete Send-Port-Zahl verschieden oder bleibt Unknown
|
||||
Application sichtbar, keinen echten Shutdown starten. Tageslog, Diagnose-
|
||||
Screenshot, before.json, shutdown-plan.json, Setup-Log, Prozessidentitaet und
|
||||
vollstaendige Exceptiondiagnose sichern. ExplorerOM-Installation, Management-
|
||||
DB-Erreichbarkeit und Leserechte pruefen.
|
||||
|
||||
LOKALE VERIFIKATION
|
||||
Release-Rebuild erfolgreich; 41 Regressionstests erfolgreich; WMI-freier
|
||||
Self-Test erfolgreich; Paket/Base64/SHA-256 ueber die Release-Chain geprueft.
|
||||
|
||||
DETAILS
|
||||
docs\PROD-Application-Zuordnung-2026-08-27.md
|
||||
@@ -0,0 +1,38 @@
|
||||
PROD-RUNBOOK: LAUFZEITLOG-ABLAGE - VERSION 2.3.1
|
||||
Stand: 2026-08-26
|
||||
|
||||
EINORDNUNG
|
||||
Der Installationsordner C:\Program Files\BizTalkPlatformManagementTool ist nicht
|
||||
der primaere Logordner. Dieser liegt unter
|
||||
%ProgramData%\BizTalkPlatformManagementTool\Logs. ProgramData ist im Explorer
|
||||
normalerweise ausgeblendet. Im Program-Files-Ordner wird daher im Normalfall
|
||||
kein Log erwartet.
|
||||
|
||||
FIX IN 2.3.1
|
||||
- Echter Create/Write/Flush/Delete-Test vor Auswahl eines Logpfads.
|
||||
- Reihenfolge: ProgramData, LocalAppData, EXE-Unterordner Logs, Temp.
|
||||
- Append-Fehler: derselbe Datensatz wird im naechsten Kandidaten wiederholt.
|
||||
- Fallback/Totalausfall erscheinen mit Pfad und Exception im Grid.
|
||||
- Jeder GUI-Start schreibt einen Startup-Verifikationseintrag in Datei und Grid.
|
||||
- Der Installer-Self-Test prueft einen temporaeren Write/Read-Roundtrip.
|
||||
- 30 Kalendertage, abgeschlossene Tage GZip, maximal 10.000 Grid-Eintraege.
|
||||
|
||||
PROD-ABNAHME
|
||||
1. Setup 2.3.1 als Administrator installieren/aktualisieren.
|
||||
2. Erfolgreichen Ziel-Self-Test im Setup-Log pruefen.
|
||||
3. Tool starten, noch keine reale BizTalk-Operation ausfuehren.
|
||||
4. Gruenen Eintrag "Runtime log storage verified by startup append" pruefen.
|
||||
5. Gelbe Fallbackwarnung bzw. RUNTIME FILE LOGGING UNAVAILABLE ausschliessen.
|
||||
6. Log Folder oeffnen. Genannte Tagesdatei muss existieren und den Eintrag
|
||||
dieses Starts enthalten.
|
||||
7. Diagnose ausfuehren, Tool schliessen und neu starten.
|
||||
8. Diagnosehistorie muss im Grid und in der Tagesdatei vorhanden sein.
|
||||
9. Erst danach ScheduledTask zuerst als Dry-run und nach Planreview real testen.
|
||||
|
||||
BEI TOTALAUSFALL
|
||||
Keine reale Wartungsoperation beginnen. Vollstaendigen Grid-Eintrag, Benutzer,
|
||||
freien Speicher, Endpoint-Security-Ereignisse und ACLs der vier Kandidaten
|
||||
sichern. ACLs nicht eigenmaechtig aufweiten.
|
||||
|
||||
DETAILFASSUNG
|
||||
docs\PROD-Laufzeitlog-Ablage-2026-08-26.md
|
||||
@@ -0,0 +1,80 @@
|
||||
PROD-RUNBOOK: SCHEDULEDTASK-STEUERUNG UND PERSISTENTES LAUFZEITLOGGING
|
||||
Stand: 2026-08-26
|
||||
Zielversion: 2.3.4
|
||||
Adapter: BizTalk ScheduledTask Adapter 7.0.2
|
||||
Adapterpfad: C:\Program Files (x86)\BizTalk ScheduledTask Adapter 7.0.2
|
||||
|
||||
URSACHE
|
||||
MSBTS_ReceiveLocation.Enable/Disable validiert die Transportdaten. Der Adapter
|
||||
benoetigt dabei Microsoft.BizTalk.Scheduler.dll (beobachtet: Version 3.13.0.0).
|
||||
Die Datei liegt in der BizTalk-Installation, ist aber nicht zwingend im GAC
|
||||
aufloesbar. Version 2.3.1 laedt sie nach Assemblyidentitaetspruefung nur in den
|
||||
laufenden Toolprozess. Das Tool kopiert keine DLL und aendert den GAC nicht.
|
||||
|
||||
VORBEREITUNG
|
||||
1. before.json, Reports und vorhandene Logs extern sichern.
|
||||
2. Setup 2.3.4 als Administrator installieren/aktualisieren.
|
||||
3. setup_version=2.3.4.0 und erfolgreichen Ziel-Self-Test pruefen.
|
||||
4. Tool als Administrator starten, gruenen Startup-Log-Pruefeintrag kontrollieren,
|
||||
dann Log Folder oeffnen. Normalfall: %ProgramData%\BizTalkPlatformManagementTool\Logs.
|
||||
5. Im lokalen BizTalk-Ordner Microsoft.BizTalk.Scheduler.dll pruefen.
|
||||
6. Nur bei Sonderpfad AdapterAssemblySearchPaths in der EXE.config erweitern.
|
||||
Mehrere Pfade mit Semikolon trennen. Keine fremde/alte DLL kopieren.
|
||||
|
||||
FUNKTIONSTEST
|
||||
1. Diagnose.
|
||||
2. Snapshot Before; Scheduler-Receive-Location und scheduler:-URI pruefen.
|
||||
3. Shutdown zuerst mit Dry run; shutdown-plan.json reviewen.
|
||||
4. Dry run deaktivieren, Shutdown bestaetigen.
|
||||
5. Erwartete Logzeilen:
|
||||
ScheduledTask adapter preflight
|
||||
ProcessBitness=...; SearchDirectories=...
|
||||
ScheduledTask dependency loaded process-locally / resolved through CLR/GAC
|
||||
(oder already loaded)
|
||||
Calling MSBTS_ReceiveLocation.Disable
|
||||
Reached: Disable receive location completed
|
||||
6. shutdown-result.json: Succeeded oder AlreadySatisfied.
|
||||
7. Drain-Checkpoint: Group Hub/Monitoring leer laufen lassen, dann Yes.
|
||||
8. Restore zuerst Dry run, dann real.
|
||||
9. Calling MSBTS_ReceiveLocation.Enable und Reached pruefen.
|
||||
10. Snapshot After und Compare; Sollzustand muss wiederhergestellt sein.
|
||||
|
||||
LOGGING-TEST
|
||||
1. Lauf mit erkennbarer Uhrzeit erzeugen, Tool schliessen und neu starten.
|
||||
2. Vorherige Eintraege muessen mit Datum/Uhrzeit wieder im Grid stehen.
|
||||
3. Clear leert nur das Grid; Neustart laedt die Historie erneut.
|
||||
4. Aktueller Tag: BizTalkPlatformManagementTool-yyyy-MM-dd.log
|
||||
5. Abgeschlossene Vortage: .log.gz
|
||||
6. Aufbewahrung: aktueller Tag plus 29 Vortage.
|
||||
7. Grid: neueste maximal 10.000 Eintraege; Dateien bleiben vollstaendig.
|
||||
|
||||
HINWEIS ZUM PFAD
|
||||
ProgramData ist nicht Program Files und im Explorer normalerweise ausgeblendet.
|
||||
Unter Program Files ist im Normalfall kein Log sichtbar. Die Reihenfolge lautet:
|
||||
ProgramData, LocalAppData, EXE-Unterordner Logs, Temp. Fallback oder Totalausfall
|
||||
muss ab 2.3.1 mit Pfad und Exception im Grid erscheinen. Die Tagesdatei muss den
|
||||
bei jedem Start geschriebenen Verifikationseintrag enthalten.
|
||||
|
||||
FEHLERFALL SICHERN
|
||||
1. Aktuelle .log und relevante .log.gz aus Log Folder.
|
||||
2. shutdown-plan.json oder restore-plan.json.
|
||||
3. Passenden *-result.json-Report.
|
||||
4. before.json und vorhandenen Nachher-Snapshot.
|
||||
5. Unveraenderte BizTalkPlatformManagementTool.exe.config.
|
||||
6. Screenshot des vollstaendigen roten Grid-Eintrags.
|
||||
7. Dateiversion von Microsoft.BizTalk.Scheduler.dll.
|
||||
|
||||
Fehler enthalten Typ, HRESULT, innere Ausnahmen, Fusion-Informationen,
|
||||
Stacktrace, angeforderte/anfragende Assembly und alle Suchpfade. GAC oder DLLs
|
||||
im Fehlerfall nicht spontan veraendern; zuerst dieses Supportpaket auswerten.
|
||||
|
||||
ABNAHME
|
||||
- Scheduler-Receive-Location real deaktiviert und wieder aktiviert.
|
||||
- Normale Adapter ohne Scheduler-Preflight weiterhin funktionsfaehig.
|
||||
- Teilfehler bleibt Failed, spaetere unabhaengige Schritte laufen weiter.
|
||||
- Historie erscheint nach Neustart im Grid.
|
||||
- Vortage komprimiert, 30 Kalendertage aufbewahrt.
|
||||
- Setup/Tool/Reports zeigen 2.3.4 bzw. 2.3.4-net461.
|
||||
|
||||
Detailfassung:
|
||||
docs\PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md
|
||||
@@ -0,0 +1,55 @@
|
||||
PROD-HANDOFF: SHUTDOWN-DIALOG-SICHTBARKEIT - VERSION 2.3.4
|
||||
Datum: 2026-08-27
|
||||
|
||||
BEFUND
|
||||
Nach der Receive-Location-Phase wurde kein sichtbarer Dialog zur Bestaetigung
|
||||
des Leerlaufens wahrgenommen.
|
||||
|
||||
URSACHEN
|
||||
1. Dry run ist standardmaessig aktiv. Er deaktiviert keine Receive Location und
|
||||
ruft deshalb bewusst keinen echten Drain-Entscheidungsdialog auf. Dieser
|
||||
Unterschied war waehrend der Ausfuehrung nicht deutlich genug sichtbar.
|
||||
2. Die bisherigen MessageBoxen hatten keinen Owner. In RDP-/Multi-Monitor-
|
||||
Sitzungen konnte ein modaler Plan- oder Drain-Dialog hinter dem Hauptfenster
|
||||
liegen, waehrend dort nur "Preparing shutdown..." sichtbar blieb.
|
||||
|
||||
SICHERHEIT
|
||||
Der persistierte OperatorCheckpoint war vorhanden. Ohne Dialog-Callback, bei
|
||||
Fehler, Nein oder Schliessen stoppt der echte Shutdown fail-closed. Spaetere
|
||||
Orchestrierungen, Send Ports und Host Instances bleiben NotExecuted.
|
||||
|
||||
FIX 2.3.4
|
||||
- Alle runtime-relevanten Bestaetigungen sind an das Hauptfenster gebunden.
|
||||
- Am echten Checkpoint steht sichtbar ACTION REQUIRED / Shutdown paused.
|
||||
- Der erste Realbetrieb-Dialog kuendigt die zweite Drain-Entscheidung an.
|
||||
- Dry run und REAL EXECUTION sind farblich und textlich eindeutig.
|
||||
- Dry run zeigt einen eigenen Hinweis: keine Mutation, keine echte Drain-
|
||||
Entscheidung.
|
||||
- Einstellungen sind waehrend einer laufenden Operation gesperrt.
|
||||
|
||||
PROD-ABNAHME
|
||||
1. Setup 2.3.4 installieren; setup_version=2.3.4.0 und Ziel-Self-Test pruefen.
|
||||
2. Dry run: Shutdown Dry Run - No Runtime Changes muss sichtbar sein; keine
|
||||
BizTalk-Mutation.
|
||||
3. shutdown-plan.json: OperatorCheckpoint muss vor jeder spaeteren Shutdown-
|
||||
Phase stehen.
|
||||
4. Freigegebenes Wartungsfenster: Dry run deaktivieren; REAL EXECUTION muss
|
||||
sichtbar sein.
|
||||
5. Ersten gespeicherten Plan bestaetigen.
|
||||
6. Nach Receive Locations: ACTION REQUIRED und owner-gebundener Drain-Dialog.
|
||||
7. Zuerst Nein testen: CheckpointDecision=Stop, alle spaeteren Zeilen
|
||||
NotExecuted.
|
||||
8. Danach kontrolliert wiederholen; Group Hub und Monitoring pruefen; erst bei
|
||||
bestaetigtem Leerlauf Ja auswaehlen.
|
||||
9. RDP minimieren/wiederherstellen und ggf. Monitorwechsel testen.
|
||||
|
||||
ALTE LAEUFE UNTERSCHEIDEN
|
||||
- DryRun=true + keine CheckpointDecision: erwartete Simulation.
|
||||
- Kein Ergebnis + UI bleibt Preparing shutdown: moeglicherweise verdeckter
|
||||
Dialog; Alt+Tab pruefen, Prozess nicht blind beenden.
|
||||
- CheckpointDecision=Stop/Error: sicher vor spaeteren Phasen gestoppt.
|
||||
- CheckpointDecision=Continue: positive Bedienerentscheidung wurde erfasst.
|
||||
|
||||
Portable Verifikation: Release-Build aller vier Projekte, 43 Regressionstests,
|
||||
Runtime-Self-Test und manifest-/hashgepruefte Paketierung. Die finale Dialog-
|
||||
und BizTalk-Pruefung erfolgt auf Windows/PROD.
|
||||
@@ -0,0 +1,42 @@
|
||||
PROD-RUNBOOK: SHUTDOWN-DRAIN-CHECKPOINT - VERSION 2.3.2
|
||||
Stand: 2026-08-26
|
||||
|
||||
ZIEL
|
||||
Nach allen Receive Locations pausiert der echte Shutdown. Erst wenn BizTalk
|
||||
sauber leer gelaufen ist, setzt ein ausdrueckliches Yes Orchestrierungen,
|
||||
Send Ports und Host Instances fort.
|
||||
|
||||
DRY-RUN
|
||||
1. Setup 2.3.2 installieren; setup_version=2.3.2.0 und Ziel-Self-Test pruefen.
|
||||
2. Diagnose, Snapshot Before und Shutdown mit aktiviertem Dry run ausfuehren.
|
||||
3. shutdown-plan.json: ReceiveLocations, OperatorCheckpoint, Orchestrations,
|
||||
SendPorts, HostInstances.
|
||||
4. Dry-run zeigt den Checkpoint, oeffnet aber keinen Entscheidungsdialog.
|
||||
|
||||
ECHTER SHUTDOWN
|
||||
1. Gesamtplan bestaetigen und Receive-Location-Ergebnisse beobachten.
|
||||
2. Dialog "Confirm BizTalk Inbound Drain" geoeffnet lassen.
|
||||
3. In Group Hub und Enterprise-Monitoring pruefen:
|
||||
- kein neuer Eingang,
|
||||
- laufende Service Instances kontrolliert gegen null,
|
||||
- keine unerwarteten Suspended/Resumable Instances,
|
||||
- MessageBox-, Queue-, Adapter- und Fremdsystemmetriken leer/stabil,
|
||||
- mehrteilige Prozesse fachlich vollstaendig.
|
||||
4. Nur bei leerer Umgebung Yes waehlen.
|
||||
5. Bei Unsicherheit, Last oder Receive-Location-Fehlern No waehlen.
|
||||
|
||||
ERGEBNIS
|
||||
Yes: Outcome=Confirmed, CheckpointDecision=Continue, Fortsetzung.
|
||||
No: Outcome=Declined, CheckpointDecision=Stop, OperatorStopped=true.
|
||||
Dialogfehler: CheckpointDecision=Error; sicherer Stopp.
|
||||
Nach No/Error sind alle spaeteren Zeilen NotExecuted. shutdown-after.json und
|
||||
shutdown-result.json werden trotzdem soweit moeglich geschrieben.
|
||||
|
||||
APPLICATION STATUS
|
||||
Partially Started direkt nach dem Abschalten der Receive Locations ist der
|
||||
erwartete Drain-Zwischenzustand. Kein zusaetzlicher Application-Stop wird
|
||||
ausgefuehrt. Dessen staerkere Modi koennen Subscriptions entfernen, Artefakte
|
||||
unenlisten, Policies undeployen oder laufende Instanzen terminieren.
|
||||
|
||||
DETAILS
|
||||
docs\PROD-Shutdown-Drain-Checkpoint-2026-08-26.md
|
||||
@@ -1,105 +1,154 @@
|
||||
# BizTalk Platform Management Tool
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Wichtigste Eigenschaften
|
||||
## Current State
|
||||
|
||||
- übersichtliche deutschsprachige WinForms-Oberfläche mit klarer Sechs-Schritt-Führung
|
||||
- Dry-run standardmäßig aktiv
|
||||
- Vorher-Snapshot als verbindliche Wiederherstellungsgrundlage
|
||||
- vorhandener Vorher-Snapshot wird beim Herunterfahren nicht überschrieben
|
||||
- servergebundene Validierung verhindert die Verwendung einer Zustandsdatei für den falschen BizTalk-Server
|
||||
- Receive Locations, Send Ports, Orchestrierungen und Hostinstanzen
|
||||
- kontrollierte Reihenfolge mit Zustandsprüfung und konfigurierbarem Timeout
|
||||
- nur vorher aktive Artefakte werden heruntergefahren und wieder gestartet
|
||||
- WMI-Objekte werden eindeutig aufgelöst; mehrdeutige Treffer führen sicher zum Abbruch
|
||||
- wiederholbare Ausführung: bereits erreichte Zielzustände werden übersprungen
|
||||
- atomar geschriebene JSON-Dateien mit Größenbegrenzung
|
||||
- keine Transport-URIs, Benutzernamen oder Kennwörter im Snapshot
|
||||
- CSV-/HTML-Berichte und Vorher-/Nachher-Vergleich
|
||||
- tägliche Logs unter `%ProgramData%\BizTalkPlatformManagementTool\Logs`
|
||||
- eigenständiger .NET-Installer für Installation, Update und Deinstallation
|
||||
- ZIP-, SHA-256- und Base64-TXT-Paket werden beim Release-Build erzeugt
|
||||
- Application: C# WinForms
|
||||
- Target framework: .NET Framework 4.6.1
|
||||
- Solution: `BizTalkPlatformManagementTool.sln`
|
||||
- Project: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj`
|
||||
- Primary namespace and assembly: `BizTalkPlatformManagementTool`
|
||||
- Legacy PowerShell archive: `archive/powershell/BizTalkPlatformManagementTool.ps1`
|
||||
|
||||
## Sicherer Wartungsablauf
|
||||
## Features
|
||||
|
||||
1. Tool als Administrator starten.
|
||||
2. Zielserver und Arbeitsordner prüfen.
|
||||
3. **Verbindung prüfen** ausführen.
|
||||
4. Mit aktiviertem Dry-run **Zustand erfassen**.
|
||||
5. **Herunterfahren** im Dry-run ausführen und `shutdown-plan.json` prüfen.
|
||||
6. Dry-run deaktivieren und **Herunterfahren** bestätigen.
|
||||
7. Server manuell patchen.
|
||||
8. Dieselbe Vorher-Zustandsdatei auswählen.
|
||||
9. **Wiederherstellen** zunächst im Dry-run prüfen und danach real ausführen.
|
||||
10. **Nachher erfassen** und **Vergleichen**.
|
||||
- Snapshot before and after downtime as JSON, CSV and HTML
|
||||
- Diff between `before.json` and `after.json`
|
||||
- Controlled shutdown from the current runtime state
|
||||
- Controlled restore from `before.json`
|
||||
- State-aware emergency restore from a preserved `before.json`, including Enterprise SSO startup
|
||||
- WMI-free validation and file selection for preserved recovery snapshots, including 2.1.3 snapshots
|
||||
- Host instance handling for the selected BizTalk server
|
||||
- Dry-run mode enabled by default
|
||||
- WMI access through `root\MicrosoftBizTalkServer`
|
||||
- Correct application ownership from the read-only ExplorerOM application hierarchy, with WMI retained for runtime state and mutations
|
||||
- Startup check for administrator rights
|
||||
- Detailed operation logging in the GUI and daily rolling log files in a write-verified local directory
|
||||
- Automatic operation-log restoration into the grid after an application restart
|
||||
- Thirty-day runtime-log retention with GZip compression for completed daily logs
|
||||
- Process-local ScheduledTask adapter dependency resolution for `scheduler:` receive locations without modifying the GAC
|
||||
- Fail-closed operator drain checkpoint after all receive locations and before later shutdown phases
|
||||
- Owner-bound shutdown confirmations with an explicit paused/action-required status and an unambiguous Dry-run notice
|
||||
- Best-effort plan execution: one isolated WMI failure is recorded while remaining independent steps continue
|
||||
- Idempotent execution that skips artifacts already in the requested target state
|
||||
- Durable per-step result reports even when a shutdown or restore completes only partially
|
||||
- Environment status indicator based on host instance state
|
||||
- Clear and Close actions in the main toolbar
|
||||
- No compile-time dependency on BizTalk ExplorerOM assemblies; the installed Microsoft assembly is loaded process-locally with identity validation
|
||||
- Transactional Windows installer/updater with SHA-256 payload validation and rollback
|
||||
- Bounded activation retries plus a verified copy fallback for first installs and atomically backed-up updates when Windows/EDR blocks only the staging rename
|
||||
- Opt-in all-users desktop shortcut whose ACL, WSH, validation or rollback failures remain visible but cannot roll back the core installation
|
||||
- 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
|
||||
|
||||
Die Zustandsdatei ist sicherheitskritisch: Sie definiert exakt, welche
|
||||
Artefakte wieder gestartet werden. Bewusst gestoppte, gebundene oder
|
||||
deaktivierte Artefakte bleiben beim Restore unangetastet.
|
||||
## Safe Usage
|
||||
|
||||
## Reihenfolge
|
||||
1. Start the app with **Run as administrator** on a BizTalk server or a management host with WMI access.
|
||||
2. Keep **Dry run** enabled.
|
||||
3. Click **Diagnose** to verify WMI access.
|
||||
4. Click **Snapshot Before**.
|
||||
5. Click **Shutdown** and review `shutdown-plan.json`.
|
||||
6. Disable **Dry run** only when the plan is correct.
|
||||
7. During a real shutdown, keep the drain dialog open until Group Hub and enterprise monitoring show that in-flight processing has emptied; choose **Yes** only then.
|
||||
8. After maintenance, click **Restore** using the saved `before.json`.
|
||||
9. Click **Snapshot After** and **Compare**.
|
||||
|
||||
Herunterfahren:
|
||||
If a shutdown was interrupted and only the original `before.json` remains, select that file with **State...**, run **Validate State**, keep **Dry run** enabled and click **Emergency Restore**. The recovery plan never overwrites the source snapshot, ensures the `ENTSSO` service is running first, skips already-correct runtime states and continues after isolated step failures. Disable Dry run only after reviewing the timestamped emergency plan. A real run automatically writes a timestamped target/actual diff when the post-operation snapshot succeeds.
|
||||
|
||||
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
|
||||
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. **Log Folder** opens the persistent local runtime-log directory.
|
||||
|
||||
Wiederherstellen:
|
||||
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 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
|
||||
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).
|
||||
|
||||
## Dateien
|
||||
## Operation Order
|
||||
|
||||
Standard-Arbeitsbereich:
|
||||
Shutdown:
|
||||
- Disable receive locations that were enabled.
|
||||
- Pause at the persisted inbound-drain checkpoint and require an explicit operator decision.
|
||||
- Stop orchestrations that were started.
|
||||
- Stop send ports that were started.
|
||||
- Stop host instances that were started on the selected server.
|
||||
|
||||
```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
|
||||
```
|
||||
Restore:
|
||||
- Start host instances that were previously started.
|
||||
- Restore send ports to Started, Stopped or Bound.
|
||||
- Restore orchestrations where safe.
|
||||
- Restore receive locations last.
|
||||
|
||||
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.
|
||||
Orchestrations that were `Bound` are deliberately left unchanged during restore to avoid accidentally making them `Unbound`.
|
||||
|
||||
## Build und Tests
|
||||
The drain checkpoint is part of `shutdown-plan.json`. In a real run, the status changes to **ACTION REQUIRED**, and the owned dialog remains in front of the main window. **Yes** records `CheckpointDecision=Continue`; **No**, a closed dialog or a callback error fails closed before orchestrations, send ports and host instances. All later rows are persisted as `NotExecuted`, followed by a partial-state snapshot and `shutdown-result.json`. Dry-run shows the checkpoint but never opens a decision dialog; it displays a separate notice explaining that no receive location was changed.
|
||||
|
||||
Voraussetzung ist das .NET Framework 4.6.1 Developer Pack:
|
||||
BizTalk applications displaying `Partially Started` immediately after receive locations are disabled is expected. Microsoft defines “Partial Stop - Allow running instances to continue” as disabling only receive locations so in-flight instances can finish. The tool deliberately does not issue an additional application-level stop: its stronger variants can stop or unenlist artifacts, remove subscriptions, undeploy policies or terminate instances. The existing artifact-level plan preserves exact snapshot state and controlled global ordering.
|
||||
|
||||
```cmd
|
||||
msbuild BizTalkPlatformManagementTool.sln /t:Rebuild /p:Configuration=Release /p:Platform="Any CPU"
|
||||
tests\BizTalkPlatformManagementTool.Tests\bin\Release\BizTalkPlatformManagementTool.Tests.exe
|
||||
```
|
||||
## Outputs
|
||||
|
||||
Der Release-Build erzeugt automatisch:
|
||||
- `before.json`, `after.json`
|
||||
- `shutdown-plan.json`, `restore-plan.json`
|
||||
- `shutdown-after.json`, `restore-after.json`
|
||||
- `shutdown-result.json`, `restore-result.json`
|
||||
- Timestamped `emergency-source-before-*`, `emergency-restore-plan-*`, `emergency-restore-result-*`, `emergency-restore-after-*` and `emergency-restore-diff-*` files
|
||||
- `diff.json`, `diff.csv`, `diff.html`
|
||||
- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html`
|
||||
- Runtime logs primarily under `%ProgramData%\BizTalkPlatformManagementTool\Logs`
|
||||
|
||||
```text
|
||||
artifacts\BizTalkPlatformManagementTool-Setup\
|
||||
artifacts\BizTalkPlatformManagementTool-Setup.zip
|
||||
artifacts\BizTalkPlatformManagementTool-Setup.zip.b64.txt
|
||||
artifacts\BizTalkPlatformManagementTool-Setup.zip.sha256.txt
|
||||
```
|
||||
The current day remains a plain `BizTalkPlatformManagementTool-yyyy-MM-dd.log`. Completed daily logs are compressed to `.log.gz`; the current day plus the previous 29 calendar days are retained. Older records are removed on startup. Up to the newest 10,000 retained entries are automatically restored from plain and compressed files into the Operation Log grid after a restart.
|
||||
|
||||
## Dokumentation
|
||||
At every start, the application performs an actual create/write/flush/delete probe and then appends a visible startup verification record. The ordered locations are ProgramData, the current account's LocalAppData, `Logs` below the executable directory, and Temp. **Log Folder** opens the path that really accepted the write. A fallback or complete storage failure is shown with attempted paths and exception details in the Operation Log; it is never silently ignored.
|
||||
|
||||
`C:\Program Files\BizTalkPlatformManagementTool` therefore normally contains no log file. ProgramData is a separate, normally hidden Windows directory. An install-local `Logs` subdirectory appears only when both ProgramData and LocalAppData were rejected and Program Files itself permits the fallback write.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
The BizTalk runtime WMI classes for Send Ports, Receive Locations and Orchestrations do not expose a reliable application property. The tool therefore reads `MgmtDbServerName`/`MgmtDbName` from `MSBTS_GroupSetting`, opens the installed read-only ExplorerOM application catalog and builds an application/artifact index before collecting WMI state. **Diagnose** reports both visible and application-resolved Send Port counts. A mismatch is never silently presented as a valid association: affected rows stay under `(Unknown Application)` and the Operation Log records whether ExplorerOM loading, Management database access or catalog matching failed.
|
||||
|
||||
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.
|
||||
|
||||
Execution is deliberately best-effort. A failure such as an adapter-specific validation exception is written as `Failed` in the result report, but later independent plan steps are still attempted. The GUI ends in a failed/operator-review state when any step failed; it never reports a partial execution as an unconditional success. The post-operation snapshot is attempted independently and its own failure is preserved in the same report.
|
||||
|
||||
The operator checkpoint is the explicit exception to best-effort continuation: declining it or losing the confirmation mechanism intentionally prevents every later shutdown phase. This is a safe incomplete operation, not a successful full shutdown, and remains visible in the report and grid.
|
||||
|
||||
For a ScheduledTask receive location, the operation plan retains adapter name and address. Immediately before a real `Enable` or `Disable`, the tool preloads `Microsoft.BizTalk.Scheduler.dll` from the locally installed BizTalk directory and resolves further dependencies from identity-checked BizTalk/ScheduledTask adapter directories. Conventional BizTalk 2020 and `BizTalk ScheduledTask Adapter 7.x` folders plus BizTalk registry paths are discovered automatically. An exceptional installation path can be added to the semicolon-delimited `AdapterAssemblySearchPaths` value in `BizTalkPlatformManagementTool.exe.config`. This is process-local: the tool neither copies DLLs nor changes the GAC. Never point the setting at assemblies from a different BizTalk version.
|
||||
|
||||
Error records in the grid, execution report and file log include exception type, HRESULT, complete inner-exception chain, available Fusion loader information and stack trace. For the PROD validation and support bundle, follow [ScheduledTask control and runtime logging runbook](docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md).
|
||||
|
||||
For the specific PROD finding "no logs visible below Program Files" and the version 2.3.1 storage verification, follow [PROD runtime-log storage validation](docs/PROD-Laufzeitlog-Ablage-2026-08-26.md).
|
||||
|
||||
Snapshot and plan JSON files are written as UTF-8 without BOM. Loading is tolerant of existing files that contain a UTF-8 BOM or a visible BOM marker from previous encoding conversions.
|
||||
|
||||
JSON snapshots and plans are written through a same-directory temporary file and atomic replacement. Snapshot comparison keys artifacts by application plus name, preventing collisions between equal artifact names in different applications. CSV fields that could be interpreted as spreadsheet formulas are neutralized.
|
||||
|
||||
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)
|
||||
- [Technische Dokumentation](Dokumentation.md)
|
||||
- [Microsoft-Referenzen](REFERENCES.md)
|
||||
- [Dokumentation](Dokumentation.md)
|
||||
- [Installer stability analysis](docs/Installer-Stabilitaetsanalyse-2026-08-11.md)
|
||||
- [ACC activation incident analysis](docs/ACC-Installer-Aktivierungsfehler-2026-08-11.md)
|
||||
- [PROD activation incident analysis and 2.2.3 fix](docs/PROD-Installer-Aktivierungsfehler-2026-08-24.md)
|
||||
- [Optional all-users desktop shortcut stability](docs/Installer-Optionale-Desktopverknuepfung-2026-08-24.md)
|
||||
- [ACC runtime shutdown incident and recovery fix](docs/ACC-Runtime-Shutdown-Exception-2026-08-19.md)
|
||||
- [PROD ScheduledTask control and runtime logging runbook](docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md)
|
||||
- [PROD runtime-log storage validation and 2.3.1 fix](docs/PROD-Laufzeitlog-Ablage-2026-08-26.md)
|
||||
- [PROD shutdown drain checkpoint and application-status analysis](docs/PROD-Shutdown-Drain-Checkpoint-2026-08-26.md)
|
||||
- [PROD application-association analysis and 2.3.3 validation](docs/PROD-Application-Zuordnung-2026-08-27.md)
|
||||
- [PROD shutdown-dialog visibility analysis and 2.3.4 validation](docs/PROD-Shutdown-Dialog-Sichtbarkeit-2026-08-27.md)
|
||||
- [AI maintainer handoff](AI-README.md)
|
||||
- [References](REFERENCES.md)
|
||||
|
||||
+11
-3
@@ -3,6 +3,11 @@
|
||||
|
||||
- MSBTS_SendPort.Status (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-status-property-wmi
|
||||
- MSBTS_SendPort (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-wmi
|
||||
- MSBTS_GroupSetting (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-groupsetting-wmi
|
||||
- BtsCatalogExplorer (ExplorerOM): https://learn.microsoft.com/en-us/dotnet/api/microsoft.biztalk.explorerom.btscatalogexplorer?view=bts-2020
|
||||
- Application (ExplorerOM): https://learn.microsoft.com/en-us/dotnet/api/microsoft.biztalk.explorerom.application?view=bts-2020
|
||||
- IBizTalkApplication (ExplorerOM): https://learn.microsoft.com/en-us/dotnet/api/microsoft.biztalk.explorerom.ibiztalkapplication?view=bts-dotnet
|
||||
- BtsOrchestration (ExplorerOM): https://learn.microsoft.com/en-us/dotnet/api/microsoft.biztalk.explorerom.btsorchestration?view=bts-2020
|
||||
- MSBTS_SendPort.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-start-method-wmi
|
||||
- MSBTS_SendPort.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-stop-method-wmi
|
||||
- MSBTS_SendPort.Enlist (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-enlist-method-wmi
|
||||
@@ -12,11 +17,14 @@
|
||||
- MSBTS_Orchestration.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-stop-method-wmi
|
||||
- MSBTS_Orchestration.Unenlist (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-unenlist-method-wmi
|
||||
- MSBTS_ReceiveLocation (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi
|
||||
- MSBTS_ReceiveLocation.Name / zusammengesetzter Schlüssel: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-name-property-wmi
|
||||
- MSBTS_ReceiveLocation.Enable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-enable-method-wmi
|
||||
- MSBTS_ReceiveLocation.Disable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-disable-method-wmi
|
||||
- Microsoft.BizTalk.Scheduler.ISchedule: https://learn.microsoft.com/en-us/dotnet/api/microsoft.biztalk.scheduler.ischedule?view=bts-2020
|
||||
- Managed BizTalk adapter assembly deployment and resolution: https://learn.microsoft.com/en-us/biztalk/core/how-biztalk-server-instantiates-an-adapter
|
||||
- BizTalk Scheduled Task Adapter 7.0.2 release repository: https://github.com/sandroasp/BizTalk-Scheduled-Task-Adapter
|
||||
- MSBTS_HostInstance (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-wmi
|
||||
- MSBTS_HostInstance.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-start-method-wmi
|
||||
- MSBTS_HostInstance.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-stop-method-wmi
|
||||
- MSBTS_HostInstance.ServiceState (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-servicestate-property-wmi
|
||||
- MSBTS_Orchestration.AssemblyName / zusammengesetzter Schlüssel: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-assemblyname-property-wmi
|
||||
- Start and stop a BizTalk application: https://learn.microsoft.com/en-us/biztalk/core/how-to-start-and-stop-a-biztalk-application
|
||||
- Stop Application dialog semantics: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/stop-application-dialog-box
|
||||
- BizTalk Server shutdown sequence: https://learn.microsoft.com/en-us/biztalk/core/how-to-start-stop-pause-resume-or-restart-biztalk-server-services
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# ACC Emergency Restore aus `before.json`
|
||||
|
||||
Stand: 19.08.2026
|
||||
Ausführung: Donnerstag, 20.08.2026, oder Montag, 24.08.2026
|
||||
Zielserver: `AV23AGPWBI01`
|
||||
Benötigte Toolversion: `2.2.2`
|
||||
|
||||
## Kurzantwort
|
||||
|
||||
Ja. Die vorhandene, mit Version 2.1.3 erzeugte `before.json` kann von Version 2.2.2 direkt als Recovery-Quelle verwendet werden. Zusätzliche Dateien des abgebrochenen Shutdowns sind nicht erforderlich. Die Kompatibilität wird durch einen Regressionstest abgedeckt; die echte BizTalk-/ACC-Ausführung muss noch vor Ort bestätigt werden.
|
||||
|
||||
Für diesen Teilzustand ausschließlich **Emergency Restore** verwenden. Dieser Modus startet zuerst `ENTSSO`, arbeitet danach den gespeicherten Sollzustand idempotent ab, setzt nach Einzelfehlern fort und überschreibt die Quell-`before.json` nicht.
|
||||
|
||||
## 1. Dateien vorbereiten
|
||||
|
||||
- [ ] Originale `before.json` an einem zweiten Ort unverändert sichern.
|
||||
- [ ] Optional den Hash der Originaldatei protokollieren:
|
||||
|
||||
```bat
|
||||
certutil -hashfile before.json SHA256
|
||||
```
|
||||
|
||||
- [ ] Die Release-Datei `BizTalkPlatformManagementTool-Setup.zip.b64.txt` und die zugehörige SHA-256-Datei auf ACC übertragen.
|
||||
- [ ] Die TXT-Datei rekonstruieren und das ZIP prüfen:
|
||||
|
||||
```bat
|
||||
certutil -decode BizTalkPlatformManagementTool-Setup.zip.b64.txt BizTalkPlatformManagementTool-Setup.zip
|
||||
certutil -hashfile BizTalkPlatformManagementTool-Setup.zip SHA256
|
||||
type BizTalkPlatformManagementTool-Setup.zip.sha256.txt
|
||||
```
|
||||
|
||||
- [ ] Nur fortfahren, wenn beide ZIP-Hashes exakt gleich sind.
|
||||
- [ ] ZIP in einen neuen Ordner entpacken, die laufende Toolinstanz schließen und `Setup.exe` als Administrator starten.
|
||||
- [ ] Installation/Update auf Version `2.2.2` vollständig abschließen.
|
||||
- [ ] Ein neues, leeres Ausgabeverzeichnis anlegen, zum Beispiel `C:\BizTalk-Recovery\2026-08-20` oder `C:\BizTalk-Recovery\2026-08-24`.
|
||||
|
||||
Wichtig: Die Recovery-Quelle außerhalb dieses Ausgabeverzeichnisses lassen. Während der Recovery nicht **Snapshot Before** anklicken, damit kein neuer Teilzustand als vermeintlicher Sollzustand gespeichert wird.
|
||||
|
||||
## 2. `before.json` ohne WMI prüfen
|
||||
|
||||
- [ ] Anwendung als Administrator starten.
|
||||
- [ ] Im Feld **Server** `AV23AGPWBI01` eintragen. Kurzname und FQDN desselben Hosts werden akzeptiert.
|
||||
- [ ] Unter **Output** das neue Recovery-Ausgabeverzeichnis auswählen.
|
||||
- [ ] Über **State...** die gesicherte `before.json` auswählen.
|
||||
- [ ] **Dry run** aktiviert lassen.
|
||||
- [ ] **Validate State** anklicken.
|
||||
- [ ] Im Erfolgsdialog prüfen:
|
||||
- Snapshotversion ist erwartungsgemäß `2.1.3-net461` oder `2.1.3`.
|
||||
- Snapshotserver ist `AV23AGPWBI01` beziehungsweise dessen FQDN.
|
||||
- Erstellzeit und Anzahl von Applications, Host Instances, Receive Locations, Send Ports und Orchestrations wirken plausibel.
|
||||
|
||||
**Validate State** öffnet keine WMI-Verbindung und verändert weder BizTalk noch Windows-Dienste. Bei einer roten Fehlermeldung nicht real ausführen, sondern zuerst Pfad, JSON-Datei und Serverauswahl korrigieren.
|
||||
|
||||
## 3. Emergency-Dry-run prüfen
|
||||
|
||||
- [ ] **Dry run** bleibt aktiviert.
|
||||
- [ ] **Emergency Restore** anklicken.
|
||||
- [ ] Den erzeugten `emergency-restore-plan-<Zeitstempel>.json` öffnen.
|
||||
- [ ] Prüfen, dass der Plan zu `AV23AGPWBI01` gehört.
|
||||
- [ ] Reihenfolge prüfen:
|
||||
1. Windows-Dienst `ENTSSO`,
|
||||
2. alle Host Instances,
|
||||
3. alle Send Ports,
|
||||
4. alle Orchestrations,
|
||||
5. alle Receive Locations.
|
||||
- [ ] Prüfen, dass der Dry-run-Report `emergency-restore-result-<Zeitstempel>.json` geschrieben wurde und keine Initialisierungsfehler enthält.
|
||||
- [ ] Die timestamp-basierte Datei `emergency-source-before-<Zeitstempel>.json` als zusätzliche Recovery-Evidenz sichern.
|
||||
|
||||
Der Dry-run führt keine Zustandsänderung aus. Er bestätigt aber, dass genau die erhaltene Datei gelesen, validiert und in einen ausführbaren Emergency-Plan übersetzt werden kann.
|
||||
|
||||
## 4. Echten Wiederanlauf ausführen
|
||||
|
||||
- [ ] Sicherstellen, dass SQL Server, Netzwerk und die übrigen BizTalk-Abhängigkeiten erreichbar sind.
|
||||
- [ ] Noch einmal kontrollieren: Server, Output-Pfad und State-Datei sind korrekt.
|
||||
- [ ] **Dry run** deaktivieren.
|
||||
- [ ] **Emergency Restore** anklicken.
|
||||
- [ ] Im Bestätigungsdialog Quellpfad, Zielserver, Plandatei und Schrittzahl lesen.
|
||||
- [ ] Erst danach mit **Yes** bestätigen.
|
||||
- [ ] Anwendung bis zum Abschluss geöffnet lassen.
|
||||
|
||||
Die reale Reihenfolge ist:
|
||||
|
||||
1. `ENTSSO` auf `Running` bringen.
|
||||
2. Host Instances gemäß `before.json` abgleichen.
|
||||
3. Send Ports abgleichen.
|
||||
4. Orchestrations abgleichen; im Snapshot nur `Bound` gebliebene Orchestrations werden bewusst zur manuellen Kontrolle markiert.
|
||||
5. Receive Locations zuletzt abgleichen.
|
||||
|
||||
Vor jeder Mutation liest Version 2.2.2 den Istzustand. Ein bereits korrekter Zustand wird als `AlreadySatisfied` protokolliert und nicht erneut per WMI geändert. Ein einzelner Fehler wird als `Failed` gespeichert; alle späteren unabhängigen Schritte werden trotzdem versucht. Auch ein Fehler der optionalen GUI-Loganzeige kann die fachliche Ausführung nicht unterbrechen.
|
||||
|
||||
## 5. Ergebnis abnehmen
|
||||
|
||||
- [ ] `emergency-restore-result-<Zeitstempel>.json` prüfen.
|
||||
- [ ] Idealfall: `FailedCount` ist `0`, `InitializationError` und `PostSnapshotError` sind leer.
|
||||
- [ ] `AlreadySatisfied` ist normal und bedeutet, dass keine unnötige Mutation ausgeführt wurde.
|
||||
- [ ] `emergency-restore-after-<Zeitstempel>.json` muss nach einem echten Lauf vorhanden sein, sofern der Nachher-Snapshot gelang.
|
||||
- [ ] `emergency-restore-diff-<Zeitstempel>.json`, `.csv` und `.html` prüfen. Im vollständigen Sollzustand sind keine fachlichen Unterschiede enthalten.
|
||||
- [ ] Danach **Diagnose** ausführen und den BizTalk-Zustand zusätzlich in der BizTalk Administration Console kontrollieren.
|
||||
- [ ] Empfang, Verarbeitung und Versand mit dem für ACC vorgesehenen fachlichen Smoke-Test prüfen; Suspended Instances kontrollieren.
|
||||
- [ ] Recovery-Quelle, Plan, Ergebnis, Nachher-Snapshot, Diff und Laufzeitlog gemeinsam sichern.
|
||||
|
||||
## Sonderfall `RV_PMP_Trigger_Schedule`
|
||||
|
||||
Wenn die Receive Location noch aktiviert ist und `before.json` ebenfalls `Enabled=true` vorgibt, wird sie als `AlreadySatisfied` übersprungen. Damit wird der fehlerhafte Scheduler-WMI-Aufruf nicht erneut ausgelöst.
|
||||
|
||||
Wenn sie deaktiviert ist, obwohl `before.json` sie aktiviert erwartet, versucht das Tool die Aktivierung. Tritt der bekannte Fehler zu `Microsoft.BizTalk.Scheduler, Version=3.13.0.0` erneut auf, wird genau dieser Schritt als `Failed` protokolliert; die übrigen Receive Locations werden weiterbearbeitet. Dann:
|
||||
|
||||
- [ ] Ergebnisreport und vollständige Exception sichern.
|
||||
- [ ] Aktuellen Zustand der Receive Location in der BizTalk Administration Console prüfen.
|
||||
- [ ] Scheduler-Assembly/Abhängigkeiten auf dem Administrations-/BizTalk-Host reparieren oder den Schritt nach fachlicher Freigabe manuell ausführen.
|
||||
- [ ] PROD erst freigeben, wenn der verbleibende Einzelfehler behoben oder ausdrücklich akzeptiert und getestet ist.
|
||||
|
||||
## Wenn der Lauf erneut unterbrochen wird
|
||||
|
||||
Die ursprüngliche `before.json` bleibt unverändert. Version 2.2.2 kann mit derselben Recovery-Quelle erneut gestartet werden, weil bereits erreichte Zustände als `AlreadySatisfied` übersprungen werden. Vor dem Wiederholen immer den jüngsten Ergebnisreport sichern und prüfen, welcher Schritt tatsächlich fehlgeschlagen ist.
|
||||
|
||||
## Go/No-Go für PROD
|
||||
|
||||
ACC ist erst ein belastbarer PROD-Nachweis, wenn der echte Emergency Restore abgeschlossen wurde, der Ergebnisreport vollständig ist, der automatische Diff geprüft wurde und jeder verbliebene `Failed`-Schritt fachlich behandelt wurde. Die lokalen 27 Regressionstests und der Self-Test ersetzen diesen realen BizTalk-/Windows-Nachweis nicht.
|
||||
@@ -0,0 +1,36 @@
|
||||
# ACC-Installer-Aktivierungsfehler vom 11.08.2026
|
||||
|
||||
## Befund
|
||||
|
||||
Die Installation von Version 2.1.2 auf `AV23AGPWBI01` scheiterte in Phase 3 mit `SETUP-ACTIVATION`, `System.IO.IOException` und HRESULT `0x80070005`. Betroffen war die Verschiebung
|
||||
|
||||
`C:\Program Files\BizTalkPlatformManagementTool.staging.<guid>` → `C:\Program Files\BizTalkPlatformManagementTool`.
|
||||
|
||||
Das Log belegt gleichzeitig:
|
||||
|
||||
- Setup lief 64-Bit und erhöht (`elevated=true`).
|
||||
- Das Ziel war eine Neuinstallation (`existing_installation=False`).
|
||||
- Paketmanifest, Dateilängen und SHA-256 waren korrekt.
|
||||
- Das Staging-Verzeichnis konnte unter `Program Files` angelegt und vollständig beschrieben werden.
|
||||
- Die Staging-EXE startete und beendete ihren Self-Test erfolgreich.
|
||||
- Zwischen dem protokollierten Self-Test-Ende und dem ersten `Directory.Move` lagen nur rund 15 ms.
|
||||
|
||||
Damit sind ein beschädigtes Paket, fehlender Speicherplatz, eine laufende Altversion und ein generelles Fehlen von Schreibrechten als unmittelbare Ursache nicht plausibel. `0x80070005` beweist jedoch nicht, welcher Prozess oder welche Policy die Verschiebung blockierte. Der wahrscheinlichste Befund ist ein noch kurz gehaltenes Delete-/Rename-Handle des Windows Loaders, Virenscanners oder ACC-Endpoint-Schutzes unmittelbar nach Ausführung der neuen EXE. Eine dauerhaft auf Rename beschränkte Endpoint-Policy bleibt als zweite Möglichkeit bestehen.
|
||||
|
||||
## Fix in Version 2.1.3
|
||||
|
||||
Alle transaktionalen Verzeichnisverschiebungen verwenden jetzt höchstens acht Versuche. Nach einem `IOException` oder `UnauthorizedAccessException` wartet das Setup 250, 500, 1.000, 2.000, 3.000, 5.000 und 8.000 ms. Die gesamte zusätzliche Wartezeit ist damit auf 19,75 Sekunden begrenzt. Jeder Fehlversuch und eine spätere Erholung werden mit Exceptiontyp, HRESULT, Rolle, Versuch und Wartezeit protokolliert.
|
||||
|
||||
Bleibt bei einer reinen Neuinstallation die Staging-Umbenennung dauerhaft gesperrt, kopiert das Setup die bereits validierte Payload in das noch nicht vorhandene Installationsziel. Jede Zieldatei wird dabei erneut gegen den Manifest-SHA-256 geprüft. Danach läuft der zweite Self-Test unverändert aus dem endgültigen Ziel. Ein Teilfehler gilt als begonnene Mutation und entfernt das unvollständige Ziel per Rollback.
|
||||
|
||||
Für Updates existiert dieser Kopierfallback absichtlich nicht. Lässt sich die aktive Version nicht atomar ins Backup verschieben, bleibt sie unangetastet und das Setup bricht nach den begrenzten Versuchen mit `SETUP-ACTIVATION` ab.
|
||||
|
||||
## Erwartete ACC-Abnahme
|
||||
|
||||
1. `BizTalkPlatformManagementTool-Setup.zip.b64.txt` dekodieren und den SHA-256 des ZIP prüfen.
|
||||
2. In einen neuen Ordner entpacken und `Setup.exe` starten.
|
||||
3. Im Erfolgslog `setup_version=2.1.3.0` und `activation_method=atomic_move` oder `activation_method=verified_copy_fallback` prüfen.
|
||||
4. Anwendung starten, **Diagnose** ausführen und danach einen Dry-run erstellen.
|
||||
5. Bei erneutem Fehler das vollständige neue `setup-*.log` sichern. Die `directory_move_retry`-Ereignisse zeigen dann, ob und wie lange ACC die Operation blockiert hat.
|
||||
|
||||
Die lokale Mono-Prüfung deckt Build, 17 Regressionstests, Anwendungsselftest, Paketmanifest und Transportartefakte ab. UAC, Endpoint-Schutz, Windows-Registry, Verknüpfungen und BizTalk-WMI müssen weiterhin auf ACC geprüft werden.
|
||||
@@ -0,0 +1,103 @@
|
||||
# ACC-Runtime-Abbruch beim Shutdown vom 19.08.2026
|
||||
|
||||
## Befund
|
||||
|
||||
Der reale Shutdown auf `AV23AGPWBI01` mit Version 2.1.3 wurde bei der Receive Location `RV_PMP_Trigger_Schedule` abgebrochen. Der WMI-Aufruf `MSBTS_ReceiveLocation.Disable` meldete sinngemäß:
|
||||
|
||||
```text
|
||||
Could not validate TransportTypeData, Address or Public Address properties for Receive Location.
|
||||
Could not load file or assembly Microsoft.BizTalk.Scheduler, Version=3.13.0.0 or one of its dependencies.
|
||||
```
|
||||
|
||||
Die unmittelbar vorher erzeugte `before.json` blieb vorhanden. Weil der Executor die erste Schrittausnahme erneut warf, wurden alle späteren Planschritte und der Nachher-Snapshot nicht mehr ausgeführt. Anschließend wurden die Host Instances und Enterprise Single Sign-On manuell gestoppt. Damit entstand ein gemischter Teilzustand.
|
||||
|
||||
Die Exception belegt einen Validierungs-/Assemblyfehler des Scheduler-Adapters oder einer Abhängigkeit im BizTalk-WMI-Pfad. Sie belegt nicht, dass `before.json` beschädigt wurde. Der Snapshot wurde bereits vor jeder Laufzeitmutation atomar gespeichert und bleibt die maßgebliche Wiederherstellungsquelle.
|
||||
|
||||
## Sicherheitsproblem in Version 2.1.3
|
||||
|
||||
- Ein isolierter Adapterfehler stoppte alle späteren unabhängigen Schritte.
|
||||
- Der erste Fehler verhinderte `shutdown-after.json` und einen strukturierten Ergebnisreport.
|
||||
- Restore-Pläne beschrieben zwar den Sollzustand aus `before.json`, prüften aber vor `Start`, `Stop`, `Enable` oder `Disable` nicht allgemein, ob dieser Zustand bereits erreicht war.
|
||||
- Ein erneuter Restore nach einem Teilabbruch konnte deshalb an einer redundanten Operation erneut stoppen.
|
||||
- Die dokumentierte Kategorie-Reihenfolge war nur innerhalb einer einzelnen Anwendung umgesetzt; über mehrere Anwendungen hinweg konnten Kategorien ineinandergreifen.
|
||||
- Enterprise SSO lag außerhalb des Snapshots und wurde bei einer manuellen Abschaltung nicht automatisch als Wiederanlaufvoraussetzung behandelt.
|
||||
|
||||
## Fix in Version 2.2.0
|
||||
|
||||
### Best-effort mit klarer Fehlergrenze
|
||||
|
||||
Jeder Planschritt hat nun einen eigenen Fehlerfang. Ein Fehler erhält das Ergebnis `Failed` mit kompletter Exception-Kette; anschließend wird der nächste unabhängige Schritt versucht. Der Gesamtlauf bleibt bei mindestens einem Fehler ausdrücklich fehlgeschlagen und verlangt Operator-Review.
|
||||
|
||||
### Zustandsbewusste und idempotente Ausführung
|
||||
|
||||
Vor jeder Mutation wird der aktuelle WMI- beziehungsweise Servicezustand gelesen. Entspricht er bereits dem Ziel, wird keine Methode aufgerufen und das Ergebnis lautet `AlreadySatisfied`. Dadurch kann ein Restore sicher an einem gemischten Zustand ansetzen. Im konkreten ACC-Fall wird die weiterhin aktiv gebliebene Scheduler-Receive-Location beim Restore nicht erneut über WMI aktiviert, wenn `before.json` ebenfalls `Enabled=true` enthält.
|
||||
|
||||
### Dauerhafte Evidenz
|
||||
|
||||
Reguläre Operationen schreiben `shutdown-result.json` beziehungsweise `restore-result.json`. Jeder Report enthält Ergebnis, Zeit und Exception pro Schritt. Der Nachher-Snapshot wird auch nach Einzelfehlern separat versucht; ein Fehler dieser Aufnahme wird ebenfalls im Report gesichert.
|
||||
|
||||
### Emergency Restore nur aus `before.json`
|
||||
|
||||
Der neue Button **Emergency Restore** benötigt außer der erhaltenen `before.json` keine Datei des fehlgeschlagenen Laufs. Er:
|
||||
|
||||
1. validiert Snapshot und Zielserver,
|
||||
2. schreibt eine timestamp-basierte, unveränderte Recovery-Kopie,
|
||||
3. erzeugt und speichert einen neuen Emergency-Plan,
|
||||
4. stellt zuerst den Windows-Dienst `ENTSSO` auf `Running`,
|
||||
5. gleicht Host Instances, Send Ports, Orchestrations und Receive Locations zustandsbewusst ab,
|
||||
6. arbeitet nach isolierten Fehlern weiter,
|
||||
7. versucht einen Nachher-Snapshot und schreibt immer den vollständigen Ergebnisreport, soweit das Ausgabeverzeichnis verfügbar ist.
|
||||
|
||||
## Recovery-Ergänzungen in Version 2.2.1
|
||||
|
||||
- **State...** wählt die erhaltene `before.json` direkt aus.
|
||||
- **Validate State** prüft JSON-Struktur und Zielserver ohne WMI-Verbindung oder Laufzeitänderung und zeigt Snapshotversion, Erstellzeit sowie Artefaktzahlen an.
|
||||
- Ein 2.1.3-Snapshot wird ausdrücklich als kompatible Recovery-Quelle regressionsgetestet; `ToolVersion` ist keine Versionssperre.
|
||||
- Nach einem echten Emergency Restore wird bei erfolgreichem Nachher-Snapshot automatisch `emergency-restore-diff-*` als JSON, CSV und HTML geschrieben.
|
||||
|
||||
## Zusätzliche Exception-Grenze in Version 2.2.2
|
||||
|
||||
Ein erneuter Audit des gesamten Shutdown-Kontrollflusses bestätigte, dass WMI-Lookup, Istzustandsprüfung, Methodenaufruf, Zustandswartezeit und Objektfreigabe innerhalb der Per-Step-Exception-Grenze liegen. Zusätzlich ist nun auch die optionale GUI-Weiterleitung des Loggers vollständig isoliert: Selbst wenn die Anzeige eines Logeintrags eine Exception wirft, laufen der aktuelle fachliche Schritt und alle späteren unabhängigen Schritte weiter. Failure-Injection-Tests decken mehrere aufeinanderfolgende `InvalidOperationException`-, `IOException`- und `TimeoutException`-Fehler sowie einen dauerhaft werfenden Log-Sink ab.
|
||||
|
||||
### Globale Reihenfolge
|
||||
|
||||
Shutdown:
|
||||
|
||||
1. alle Receive Locations,
|
||||
2. alle Orchestrations,
|
||||
3. alle Send Ports,
|
||||
4. alle Host Instances.
|
||||
|
||||
Restore und Emergency Restore:
|
||||
|
||||
1. bei Emergency Restore zuerst `ENTSSO`,
|
||||
2. alle Host Instances,
|
||||
3. alle Send Ports,
|
||||
4. alle Orchestrations,
|
||||
5. alle Receive Locations.
|
||||
|
||||
Diese Reihenfolge gilt nun über alle Anwendungen hinweg.
|
||||
|
||||
## Verifikation
|
||||
|
||||
Die portable Regressionstestsuite enthält ab Version 2.2.0 unter anderem folgende Vorfallsprüfungen:
|
||||
|
||||
- simulierte `Microsoft.BizTalk.Scheduler`-Exception in einem mittleren Schritt,
|
||||
- erfolgreiche Ausführung aller späteren Schritte,
|
||||
- vollständige Persistenz des Teilfehlerreports,
|
||||
- Überspringen eines bereits erreichten Sollzustands ohne Mutation,
|
||||
- `ENTSSO` als erster Emergency-Schritt,
|
||||
- globale Shutdown-/Restore-Reihenfolge mit mehreren Anwendungen,
|
||||
- Kurzname/FQDN-Gleichheit für Host-Instance-Schritte.
|
||||
|
||||
Die lokale Suite umfasst ab Version 2.2.2 insgesamt 27 Tests einschließlich der expliziten 2.1.3-Snapshot-Kompatibilität, zusätzlicher Failure-Injection für unerwartete Exceptions und eines vollständigen Shutdown-Fortsetzungstests über Receive Location, Orchestration, Send Port und Host Instance. Die abschließende Freigabe für PROD benötigt weiterhin einen Windows-/BizTalk-Test auf ACC: echter Scheduler-Fehlerpfad, vollständige Fortsetzung, Ergebnisreport, Emergency-Dry-run und realer Wiederanlauf aus einer Kopie der vorhandenen `before.json`.
|
||||
|
||||
## PROD-Freigabekriterien
|
||||
|
||||
- Version 2.2.2 installieren und Installer-Self-Test bestätigen.
|
||||
- `Diagnose` und einen vollständigen Shutdown-Dry-run ausführen.
|
||||
- Kontrolliert nachweisen, dass eine isolierte Scheduler-Exception spätere Schritte nicht blockiert.
|
||||
- `shutdown-result.json` und Nachher-Snapshot prüfen.
|
||||
- Emergency Restore zuerst ausschließlich als Dry-run aus einer Snapshot-Kopie prüfen.
|
||||
- Realen Emergency Restore auf ACC abschließen und `FailedCount=0` oder jeden verbleibenden Einzelfehler fachlich freigeben.
|
||||
- Erst danach dasselbe Releasepaket für PROD verwenden.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Optionale Desktop-Verknüpfung für alle Benutzer
|
||||
|
||||
## Befund
|
||||
|
||||
Bis Version 2.2.3 war die Option **Desktop-Verknüpfung für alle Benutzer erstellen** standardmäßig aktiviert. Der Installer verwaltet den Link über Windows Script Host im von `Environment.SpecialFolder.CommonDesktopDirectory` gelieferten gemeinsamen Desktop-Verzeichnis, üblicherweise `%PUBLIC%\Desktop`.
|
||||
|
||||
Obwohl der Link fachlich optional war, lagen alle Zugriffe innerhalb der transaktionskritischen Windows-Integrationsphase. Eine `UnauthorizedAccessException`, COM-/WSH-Exception oder Zustandsabweichung konnte daher nach bereits erfolgreicher Payload-Aktivierung einen `SETUP-WINDOWS-INTEGRATION`-Fehler und den vollständigen Rollback auslösen. Betroffen waren nicht nur das Erstellen, sondern fünf Pfade:
|
||||
|
||||
1. Lesen des vorhandenen Links für den Rollback-Snapshot.
|
||||
2. Erstellen oder Entfernen entsprechend der Setup-Auswahl.
|
||||
3. Rücklesende Validierung des gewünschten Zustands.
|
||||
4. Wiederherstellung des vorherigen Links während eines Rollbacks.
|
||||
5. Entfernen und Prüfen bei der Deinstallation.
|
||||
|
||||
Der konkrete Berechtigungsgeber kann erst anhand des vollständigen `setup-*.log`, der effektiven ACL und gegebenenfalls EDR-Ereignisse bestimmt werden. Unabhängig davon war die bisherige Kritikalität für eine reine Komfortfunktion zu hoch.
|
||||
|
||||
## Fix in Version 2.2.4
|
||||
|
||||
- Die Desktop-Option ist standardmäßig abgewählt und muss bewusst aktiviert werden.
|
||||
- Alle fünf Desktop-Pfade laufen über eine gemeinsame optionale Exception-Grenze.
|
||||
- Jeder Fehler wird mit Exceptiontyp, HRESULT, Stacktrace, Rolle und Pfad als `optional_windows_integration_warning` protokolliert.
|
||||
- Die Setup-Oberfläche zeigt eine Warnung und erklärt ausdrücklich, dass die Kerninstallation fortgesetzt wird.
|
||||
- Auch eine Exception der Warnungsweiterleitung wird isoliert und als `optional_windows_integration_report_failed` protokolliert.
|
||||
- Ein Desktop-Snapshot, der wegen fehlender Rechte nicht gelesen werden konnte, wird beim späteren Rollback nicht erfunden oder überschrieben.
|
||||
- Ein fehlgeschlagener Desktop-Zugriff bei der Deinstallation kann einen alten Link zurücklassen, verhindert aber nicht die Entfernung der Anwendung.
|
||||
|
||||
Der Installer ändert keine ACLs, übernimmt keinen Besitz und umgeht keine Sicherheitsrichtlinie. Die Sicherheitsentscheidung des Systems bleibt bestehen; lediglich die optionale Verknüpfung wird aus der Kerntransaktion entfernt.
|
||||
|
||||
## Weiterhin verpflichtende Integration
|
||||
|
||||
Diese Bestandteile bleiben transaktionskritisch:
|
||||
|
||||
- Startmenü-Verknüpfung für alle Benutzer,
|
||||
- Uninstaller unter `%ProgramData%\BizTalkPlatformManagementTool\Setup`,
|
||||
- Uninstall-Eintrag unter `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`.
|
||||
|
||||
Ein Fehler dieser Elemente verhindert weiterhin einen inkonsistenten Erfolg und löst bei Installation oder Update den Rollback aus.
|
||||
|
||||
## Diagnose und Abnahme
|
||||
|
||||
Bei nicht angeforderter oder erfolgreicher Desktop-Operation enthält das Log `event=optional_windows_integration_completed`. Bei einem tolerierten Fehler enthält es:
|
||||
|
||||
```text
|
||||
event=optional_windows_integration_warning role=desktop_shortcut_... path="..." exception_type=... hresult=...
|
||||
event=setup_completed result=success_with_optional_desktop_shortcut_warning optional_desktop_shortcut_warning=True
|
||||
```
|
||||
|
||||
Für die Windows-Abnahme:
|
||||
|
||||
1. Setup 2.2.4 zunächst mit abgewählter Desktop-Option installieren oder aktualisieren.
|
||||
2. `setup_version=2.2.4.0`, erfolgreichen Kernabschluss, Startmenü-Link, Uninstaller und Registry prüfen.
|
||||
3. Die Option in einem weiteren Update gezielt aktivieren. Wird der gemeinsame Desktop blockiert, muss das Setup mit Warnung erfolgreich bleiben und die Anwendung startbar sein.
|
||||
4. Danach **Diagnose** und einen Dry-run ausführen.
|
||||
5. Deinstallation separat prüfen; ein nicht löschbarer optionaler Desktop-Link darf höchstens als Warnung verbleiben.
|
||||
|
||||
Die portable Regressionstestsuite injiziert einen ACL-Fehler, eine Validierungsabweichung und einen fehlerhaften Warnungs-Sink. Reale `%PUBLIC%\Desktop`-ACLs, WSH/COM, Endpoint-Schutz und Gruppenrichtlinien bleiben auf Windows zu validieren.
|
||||
@@ -0,0 +1,57 @@
|
||||
# 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.
|
||||
- Acht begrenzte Move-Versuche mit 250, 500, 1.000, 2.000, 3.000, 5.000 und 8.000 ms Backoff zwischen den Versuchen.
|
||||
- SHA-256-verifizierter Kopierfallback für eine Neuinstallation ohne Ziel oder für eine Update-Aktivierung nach vollständig atomar gesicherter Vorversion; niemals als In-place-Update.
|
||||
- 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.
|
||||
- Explizite optionale Exception-Grenze für sämtliche Zugriffe auf den gemeinsamen Desktop-Link; Startmenü, Uninstaller und Registry bleiben transaktionskritisch.
|
||||
- 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.
|
||||
- Dreißig Regressionstests einschließlich manipulierter/zusätzlicher/ausbrechender Payload-Pfade, Staging-Abbruch ohne Mutation, transienter Move-Erholung, verifiziertem Neuinstallations- und Updatefallback, begrenztem Updateabbruch, erzwungenem Fehler des zweiten Self-Tests mit Wiederherstellung der Vorversion, Deinstallation über ein Quarantäneverzeichnis, Diagnosekontext, Log-Fallback sowie Fehlerisolierung der UI- und optionalen Desktop-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 nicht durch den neuen Retry/Fallback aufgelösten Dateisperren 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.
|
||||
@@ -0,0 +1,81 @@
|
||||
# PROD-Analyse: `(Unknown Application)` bei Runtime-Artefakten
|
||||
|
||||
**Stand:** 2026-08-27
|
||||
|
||||
**Fehlerstand:** 2.3.2
|
||||
|
||||
**Zielversion:** 2.3.3
|
||||
|
||||
## Beobachtung und Bewertung
|
||||
|
||||
Im PROD-Shutdown-Plan wurden die gefundenen Send Ports mit korrekten Namen und Zuständen angezeigt, die Spalte **Application** enthielt jedoch durchgehend `(Unknown Application)`. Damit war der WMI-Zugriff selbst erfolgreich; fehlerhaft war die nachgelagerte Zuordnung der Artefakte zu BizTalk-Anwendungen.
|
||||
|
||||
Der Lauf wäre technisch weiterhin in der global sicheren Kategorie-Reihenfolge ausführbar gewesen, weil Send Ports über ihren gruppenweit eindeutigen WMI-Namen adressiert werden. Die Anzeige ist dennoch ein Produktfehler: Planreview und anwendungsbezogene Snapshot-/Diff-Auswertung benötigen die echte Ownership. Ein realer Lauf mit ungeklärter Zuordnung ist daher nicht freizugeben.
|
||||
|
||||
## Root Cause
|
||||
|
||||
Version 2.3.2 versuchte bei `MSBTS_SendPort`, `MSBTS_ReceiveLocation` und `MSBTS_Orchestration` Properties namens `ApplicationName`, `Application` oder `BizTalkApplication` auszulesen. Diese Properties gehören nicht zum dokumentierten Runtime-WMI-Vertrag der drei Klassen. Der Code fiel deshalb für jedes betroffene Objekt auf `(Unknown Application)` zurück.
|
||||
|
||||
Die BizTalk-Anwendung ist eine Kataloghierarchie, keine verlässliche Property dieser Runtime-WMI-Objekte. Microsoft stellt sie über `Microsoft.BizTalk.ExplorerOM` bereit: Eine Application enthält ihre Send Ports, Receive Ports samt Receive Locations und Orchestrierungen. `MSBTS_GroupSetting` liefert den Server und Namen der BizTalk Management-Datenbank, mit der `BtsCatalogExplorer` verbunden wird.
|
||||
|
||||
## Korrektur in 2.3.3
|
||||
|
||||
`BizTalkApplicationResolver` baut vor der WMI-Zustandserfassung einen read-only Index auf:
|
||||
|
||||
1. `MSBTS_GroupSetting` lesen und genau einen Management-DB-Endpunkt verlangen.
|
||||
2. Die lokal installierte `Microsoft.BizTalk.ExplorerOM.dll` über CLR/GAC oder vorhandene BizTalk-Verzeichnisse laden.
|
||||
3. Vor einem dateibasierten Fallback Simple Name und Microsoft-Public-Key-Token prüfen.
|
||||
4. Applications und deren Send Ports, Receive Ports/Locations sowie Orchestrierungen enumerieren.
|
||||
5. WMI-Artefakte case-insensitiv gegen diesen Index auflösen; bei Receive Locations dient der Parent-Receive-Port als zusätzlicher Fallback.
|
||||
6. Namen, die widersprüchlich unter mehreren Anwendungen auftauchen, nicht raten, sondern weiter als unbekannt markieren.
|
||||
|
||||
Es gibt weiterhin keine Compile-time-Referenz und keine mitgelieferte Kopie der BizTalk-DLL. Der Installer-Self-Test bleibt WMI- und BizTalk-frei. ExplorerOM wird ausschließlich gelesen; Zustände, Wait-Logik und sämtliche Shutdown-/Restore-Mutationen bleiben beim bestehenden WMI-Adapter.
|
||||
|
||||
Wenn Assembly, Management-Datenbank oder Berechtigung nicht verfügbar sind, bricht die Zustandserfassung nicht verdeckt ab. Die betroffenen Zeilen bleiben sichtbar unter `(Unknown Application)`, und Grid sowie Laufzeitlog enthalten die vollständige Ursache. **Diagnose** meldet künftig sowohl die Zahl sichtbarer Send Ports als auch die Zahl erfolgreicher Application-Zuordnungen.
|
||||
|
||||
## Lokale Verifikation
|
||||
|
||||
- Release-Rebuild aller vier .NET-Framework-4.6.1-Projekte erfolgreich.
|
||||
- 41 portable Regressionstests erfolgreich.
|
||||
- Neuer Test deckt Send Port, Receive Location, Receive-Port-Fallback, Orchestration, case-insensitive Namen und mehrdeutige Cross-Application-Namen ab.
|
||||
- WMI-freier Anwendungsselftest erfolgreich mit `2.3.3-net461`.
|
||||
- Paketmanifest, Installer-ZIP, Certutil-Base64 und SHA-256 werden durch die Release-Chain erzeugt und separat gegengeprüft.
|
||||
|
||||
Die lokale Suite kann den echten BizTalk-2020-ExplorerOM-Katalog nicht simulieren. Die abschließende Freigabe benötigt deshalb die folgende PROD-Abnahme.
|
||||
|
||||
## PROD-Abnahme nach dem Update
|
||||
|
||||
1. Setup 2.3.3 als Administrator ausführen und im Setup-Log `setup_version=2.3.3.0` sowie erfolgreichen Ziel-Self-Test prüfen.
|
||||
2. Tool starten, PROD-Zielserver kontrollieren und **Dry run** aktiviert lassen.
|
||||
3. **Diagnose** ausführen. Erwartung: `Send ports visible: N; application associations resolved: N.` mit identischen Zahlen und ohne nachfolgende Association-Warnung.
|
||||
4. Laufzeitlog über **Log Folder** öffnen und den Eintrag `BizTalk application catalog loaded through ExplorerOM` prüfen. Er muss plausible Zähler für Applications, Send Ports, Receive Ports/Locations und Orchestrations enthalten.
|
||||
5. **Snapshot Before** erzeugen und `before.json` prüfen:
|
||||
- keine `(Unknown Application)`-Gruppe für regulär katalogisierte Runtime-Artefakte,
|
||||
- stichprobenartig mehrere Send Ports aus unterschiedlichen Anwendungen gegen die BizTalk Administration Console abgleichen,
|
||||
- Receive Locations und Orchestrierungen ebenfalls anwendungsrichtig gruppiert.
|
||||
6. **Shutdown** nur als Dry-run ausführen. Im Grid und in `shutdown-plan.json` müssen die echten Application-Namen erscheinen; Reihenfolge und Schrittzahl gegen den Snapshot prüfen.
|
||||
7. Erst nach erfolgreichem Katalog-, Snapshot- und Dry-run-Abgleich über einen echten Wartungslauf entscheiden.
|
||||
|
||||
## Fehlerpfad und Supportdaten
|
||||
|
||||
Sind die Diagnosezahlen unterschiedlich oder erscheinen weiterhin unbekannte Anwendungen, keinen echten Shutdown starten. Sichern:
|
||||
|
||||
- aktuelle Tageslogdatei,
|
||||
- Diagnose-Screenshot,
|
||||
- `before.json` und Sidecars,
|
||||
- `shutdown-plan.json` aus dem Dry-run,
|
||||
- Setup-Log,
|
||||
- genaue Windows-Identität des Toolprozesses,
|
||||
- WMI-/ExplorerOM-/SQL-Exception einschließlich HRESULT und Inner Exceptions.
|
||||
|
||||
Dann insbesondere prüfen: lokale BizTalk-Administrationskomponenten/ExplorerOM-Installation, Erreichbarkeit der in `MSBTS_GroupSetting` gemeldeten Management-Datenbank und Leseberechtigung des ausführenden Kontos.
|
||||
|
||||
## Microsoft-Quellen
|
||||
|
||||
- [MSBTS_SendPort (WMI)](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-wmi)
|
||||
- [MSBTS_ReceiveLocation (WMI)](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi)
|
||||
- [MSBTS_Orchestration (WMI)](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-wmi)
|
||||
- [MSBTS_GroupSetting (WMI)](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-groupsetting-wmi)
|
||||
- [BtsCatalogExplorer (ExplorerOM)](https://learn.microsoft.com/en-us/dotnet/api/microsoft.biztalk.explorerom.btscatalogexplorer?view=bts-2020)
|
||||
- [Application (ExplorerOM)](https://learn.microsoft.com/en-us/dotnet/api/microsoft.biztalk.explorerom.application?view=bts-2020)
|
||||
- [BtsOrchestration (ExplorerOM)](https://learn.microsoft.com/en-us/dotnet/api/microsoft.biztalk.explorerom.btsorchestration?view=bts-2020)
|
||||
@@ -0,0 +1,52 @@
|
||||
# PROD-Installer-Aktivierungsfehler vom 24.08.2026
|
||||
|
||||
## Kurzbefund
|
||||
|
||||
Der Screenshot zeigt nicht den aktuellen Repository-Stand, sondern `BizTalk Platform Management Tool 2.1.2`. Der Lauf bestand um 12:40:59 die Staging-Prüfung und scheiterte unmittelbar danach in Phase 3 mit:
|
||||
|
||||
```text
|
||||
SETUP-ACTIVATION
|
||||
System.InvalidOperationException: Installation/Update fehlgeschlagen
|
||||
System.IO.IOException: Access to the path
|
||||
'C:\Program Files\BizTalkPlatformManagementTool.staging.<guid>' is denied.
|
||||
at System.IO.Directory.InternalMove(...)
|
||||
```
|
||||
|
||||
Die Abschlussmeldung enthält `Kein Rollback erforderlich`. Im 2.1.2-Code bedeutet das, dass weder ein vorhandenes Installationsverzeichnis ins Backup verschoben noch ein neues Ziel aktiviert wurde. Der Fehler trat somit an der ersten Systemmutation auf; Paketkopie und Staging-Self-Test waren bereits erfolgreich.
|
||||
|
||||
## Ursachenbewertung
|
||||
|
||||
Version 2.1.2 führte für die Aktivierung genau einen `Directory.Move` aus. `Access denied` beim Umbenennen des vollständig erstellten und bereits ausführbaren Staging-Verzeichnisses ist mit einer kurzzeitigen Loader-/Virenscanner-/EDR-Sperre oder einer auf Rename/Delete beschränkten Richtlinie vereinbar. Der Screenshot allein identifiziert den blockierenden Prozess nicht. Er belegt aber:
|
||||
|
||||
- kein Manifest- oder SHA-256-Fehler,
|
||||
- keinen Fehler des WMI-freien Staging-Self-Tests,
|
||||
- keinen BizTalk-Laufzeitfehler,
|
||||
- keinen erforderlichen Rollback einer aktiven Version,
|
||||
- einen Fehler beim Verzeichnis-Rename unter `C:\Program Files`.
|
||||
|
||||
Der gleiche 2.1.2-Codepfad war bereits am 11.08.2026 in ACC aufgefallen. Der damals ab 2.1.3 ergänzte Retry- und Neuinstallationsfallback war in der auf dem Screenshot ausgeführten Datei noch nicht enthalten.
|
||||
|
||||
## Fix in Version 2.2.3
|
||||
|
||||
Version 2.2.3 enthält die bereits vorhandenen acht begrenzten Move-Versuche mit 250, 500, 1.000, 2.000, 3.000, 5.000 und 8.000 ms Backoff. Bleibt ausschließlich der Staging-Rename gesperrt, wird die validierte Payload in das fehlende Ziel kopiert und jede Zieldatei erneut gegen das Manifest geprüft.
|
||||
|
||||
Der Fix deckt jetzt beide sicheren Situationen ab:
|
||||
|
||||
1. Neuinstallation: Es existiert kein Installationsziel.
|
||||
2. Update: Die vollständige Vorversion wurde zuvor atomar ins Backup verschoben und das Installationsziel existiert nicht mehr.
|
||||
|
||||
Der Fallback überschreibt niemals eine aktive Installation. Bei einem Update bleibt das Backup während Kopie, Hashprüfung und zweitem Self-Test erhalten. Scheitert einer dieser Schritte, entfernt der Installer die teilweise neue Version und stellt das Backup wieder her. Kann schon das Backup nicht atomar erzeugt werden, bleibt die Vorversion unverändert aktiv und es gibt keinen Kopierfallback.
|
||||
|
||||
Zwei neue Failure-Injection-Regressionstests belegen den erfolgreichen Updatefallback und die vollständige Wiederherstellung der Vorversion nach einem erzwungen fehlgeschlagenen Ziel-Self-Test.
|
||||
|
||||
## Vorgehen in PROD
|
||||
|
||||
1. Die 2.1.2-Datei nicht erneut starten.
|
||||
2. Screenshot und vollständiges `setup-*.log` aus `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs` als Vorfallevidenz sichern.
|
||||
3. `artifacts\BizTalkPlatformManagementTool-Setup.zip.b64.txt` aus Version 2.2.4 mit `certutil -decode` rekonstruieren.
|
||||
4. Den SHA-256 des ZIPs mit `BizTalkPlatformManagementTool-Setup.zip.sha256.txt` vergleichen.
|
||||
5. Das ZIP in einen neuen Ordner entpacken und die dortige `Setup.exe` starten. Der Fenstertitel muss `2.2.4` zeigen.
|
||||
6. Im Erfolgslog `setup_version=2.2.4.0` und `activation_method=atomic_move` oder `activation_method=verified_copy_fallback` prüfen. Beim Fallback muss zusätzlich `scope=new_install` oder `scope=update_after_backup` protokolliert sein.
|
||||
7. Danach in der Anwendung zuerst **Diagnose** und anschließend mit aktiviertem **Dry run** den vorgesehenen Betriebsablauf prüfen.
|
||||
|
||||
Eine Windows-/BizTalk-/EDR-Abnahme in PROD bleibt erforderlich; die lokale Mono-Suite kann reale ACLs, Endpoint-Schutz, Registry und Verknüpfungen nicht simulieren.
|
||||
@@ -0,0 +1,44 @@
|
||||
# PROD-Runbook: Laufzeitlog-Ablage und Version 2.3.1
|
||||
|
||||
**Stand:** 2026-08-26
|
||||
**Zielversion:** 2.3.1
|
||||
|
||||
## Einordnung des Screenshots
|
||||
|
||||
Der gezeigte Ordner `C:\Program Files\BizTalkPlatformManagementTool` ist der Programmordner. Der primäre Laufzeitlogordner ist dagegen `%ProgramData%\BizTalkPlatformManagementTool\Logs`, üblicherweise `C:\ProgramData\BizTalkPlatformManagementTool\Logs`. `C:\ProgramData` ist im Explorer standardmäßig ausgeblendet. Im Programmordner wird deshalb im Normalfall kein Log erwartet.
|
||||
|
||||
Die frühere Implementierung konnte dennoch einen echten Fehler verdecken: Sie prüfte nur, ob sich der Ordner anlegen ließ, und ignorierte einen Fehler beim nachfolgenden `AppendAllText`. Damit konnte das Grid funktionieren, obwohl kein dauerhaftes Log geschrieben wurde.
|
||||
|
||||
## Fix in 2.3.1
|
||||
|
||||
- Jeder Kandidat muss einen echten Create/Write/Flush/Delete-Test bestehen.
|
||||
- Die Reihenfolge ist ProgramData, LocalAppData, EXE-Unterordner `Logs`, Temp.
|
||||
- Ein späterer Append-Fehler wiederholt denselben Datensatz auf dem nächsten Kandidaten.
|
||||
- Fallback und vollständiger Ausfall erscheinen mit Pfad und Exception im Grid.
|
||||
- Jeder GUI-Start schreibt `Runtime log storage verified by startup append. Active file: ...` in Grid und Tagesdatei.
|
||||
- Der Installer-Self-Test prüft einen vollständigen temporären Write/Read-Roundtrip.
|
||||
- Tagesdateien werden nach Abschluss komprimiert; aktueller Tag plus 29 Vortage bleiben erhalten. Bis zu 10.000 Einträge werden nach Neustart ins Grid geladen.
|
||||
|
||||
## PROD-Abnahme nach Update
|
||||
|
||||
1. Setup 2.3.1 als Administrator ausführen und erfolgreichen Ziel-Self-Test prüfen.
|
||||
2. Tool als Administrator starten, noch keine reale BizTalk-Operation ausführen.
|
||||
3. Im Grid den grünen Startup-Verifikationseintrag prüfen und seinen vollständigen Dateipfad notieren.
|
||||
4. Auf eine gelbe Pfad-Fallbackwarnung oder `RUNTIME FILE LOGGING UNAVAILABLE` achten.
|
||||
5. **Log Folder** öffnen und prüfen, dass exakt die genannte Tagesdatei existiert.
|
||||
6. Datei öffnen und den Startup-Verifikationseintrag dieses Starts prüfen.
|
||||
7. **Diagnose** ausführen, Tool regulär schließen und neu starten.
|
||||
8. Prüfen, dass Diagnoseeinträge wieder im Grid erscheinen und auch in der Tagesdatei stehen.
|
||||
9. Erst danach den ScheduledTask-Test zunächst als Dry-run und nach Planreview real durchführen.
|
||||
|
||||
Bei `RUNTIME FILE LOGGING UNAVAILABLE` keine reale Wartungsoperation beginnen. Screenshot des vollständigen Grid-Eintrags, aktiven Benutzer, freien Speicher, Endpoint-Security-Ereignisse und die ACLs der vier genannten Kandidaten sichern. Keine ACL eigenmächtig aufweiten; die Ursache mit dem Serverbetrieb klären.
|
||||
|
||||
## Supportpaket
|
||||
|
||||
- Aktuelle `.log` und relevante `.log.gz` aus **Log Folder**
|
||||
- Screenshot der Startup-Verifikation beziehungsweise vollständigen Speicherwarnung
|
||||
- `shutdown-plan.json`/`restore-plan.json` und passender `*-result.json`-Report
|
||||
- `before.json`, Nachher-Snapshot und unveränderte EXE-Konfiguration
|
||||
- Setup-Log mit `setup_version=2.3.1.0` und erfolgreichem Ziel-Self-Test
|
||||
|
||||
Die Windows-/BizTalk-/PROD-Prüfung bleibt die endgültige Abnahme; die portable Regressionstoolchain simuliert die drei Speicherfehlerpfade ohne BizTalk.
|
||||
@@ -0,0 +1,83 @@
|
||||
# PROD-Runbook: ScheduledTask-Steuerung und persistentes Laufzeitlogging
|
||||
|
||||
**Stand:** 2026-08-26
|
||||
|
||||
**Zielversion:** 2.3.4
|
||||
|
||||
**Betroffener Adapter:** BizTalk ScheduledTask Adapter 7.0.2
|
||||
|
||||
**Bestätigter Adapterpfad:** `C:\Program Files (x86)\BizTalk ScheduledTask Adapter 7.0.2`
|
||||
|
||||
## Ursache und Fix
|
||||
|
||||
`MSBTS_ReceiveLocation.Enable` und `.Disable` validieren die Transportdaten einer ScheduledTask-Receive-Location. Der Adapter benötigt dafür `Microsoft.BizTalk.Scheduler.dll`. Diese BizTalk-Assembly liegt im BizTalk-Installationsverzeichnis, ist aber nicht in jeder Umgebung im GAC auflösbar. Der Fehler lautete deshalb sinngemäß `Could not load file or assembly Microsoft.BizTalk.Scheduler, Version=3.13.0.0`.
|
||||
|
||||
Version 2.3.1 erkennt ScheduledTask-Schritte an Adaptername oder `scheduler:`-URI. Unmittelbar vor der WMI-Mutation sucht sie die Assembly in der lokalen BizTalk-/Adapterinstallation, prüft ihre .NET-Assemblyidentität und lädt sie nur für den laufenden Toolprozess. Weitere angeforderte Abhängigkeiten werden nach derselben Identitätsprüfung aufgelöst. Das Tool kopiert keine Fremd-DLL, registriert nichts und verändert den GAC nicht.
|
||||
|
||||
## Vorbereitung
|
||||
|
||||
1. Vorhandene `before.json`, Ergebnisdateien und Laufzeitlogs außerhalb des Installationsordners sichern.
|
||||
2. Setup 2.3.4 als Administrator installieren beziehungsweise aktualisieren.
|
||||
3. Im Setup-Log `setup_version=2.3.4.0` und erfolgreichen Ziel-Self-Test prüfen.
|
||||
4. Tool als Administrator starten, den grünen Eintrag `Runtime log storage verified by startup append` prüfen und mit **Log Folder** den dort genannten aktiven Pfad öffnen. Normalfall ist `%ProgramData%\BizTalkPlatformManagementTool\Logs`.
|
||||
5. Prüfen, dass der lokale BizTalk-Installationsordner `Microsoft.BizTalk.Scheduler.dll` enthält.
|
||||
6. Nur wenn BizTalk oder der Adapter abweichend installiert wurde: `AdapterAssemblySearchPaths` in `BizTalkPlatformManagementTool.exe.config` um den vorhandenen lokalen Ordner ergänzen. Mehrere Pfade werden mit Semikolon getrennt. Keine DLL aus ACC, einer alten BizTalk-Version oder einem Downloadordner kopieren.
|
||||
|
||||
## Kontrollierter Funktionstest
|
||||
|
||||
1. **Diagnose** ausführen.
|
||||
2. **Snapshot Before** ausführen und im Statusgrid die betroffene Receive Location mit Adapter und `scheduler:`-Adresse prüfen.
|
||||
3. **Dry run** aktiviert lassen und **Shutdown** ausführen.
|
||||
4. In `shutdown-plan.json` bei der betroffenen Receive Location `AdapterName` und `Address` prüfen.
|
||||
5. Erst nach Planreview Dry-run deaktivieren, **Shutdown** erneut ausführen und bestätigen.
|
||||
6. Im Operation Log müssen für den Scheduler-Schritt mindestens folgende Inhalte erscheinen:
|
||||
- `ScheduledTask adapter preflight`
|
||||
- `ProcessBitness=` und `SearchDirectories=`
|
||||
- `ScheduledTask dependency loaded process-locally`, `resolved through CLR/GAC` oder `already loaded`
|
||||
- `Calling MSBTS_ReceiveLocation.Disable`
|
||||
- `Reached: Disable receive location completed`
|
||||
7. In `shutdown-result.json` muss der Schritt `Succeeded` oder bei bereits erreichtem Zustand `AlreadySatisfied` sein.
|
||||
8. Am Drain-Checkpoint Receive-Location-Ergebnisse prüfen, Group Hub/Monitoring leer laufen lassen und erst dann **Yes** wählen.
|
||||
9. Nach der Wartung **Restore** zunächst im Dry-run, danach real ausführen.
|
||||
10. Entsprechend `Calling MSBTS_ReceiveLocation.Enable`, den erreichten Zustand und `Succeeded`/`AlreadySatisfied` prüfen.
|
||||
11. **Snapshot After** und **Compare** ausführen; die Receive Location muss dem gespeicherten Sollzustand entsprechen.
|
||||
|
||||
## Logging-Test über Neustart
|
||||
|
||||
1. Einen erkennbaren Diagnose- oder Dry-run-Lauf durchführen und Uhrzeit notieren.
|
||||
2. Tool regulär schließen und erneut starten.
|
||||
3. Das Operation Log muss die Einträge des vorherigen Prozesses mit vollständigem Datum und Uhrzeit wieder anzeigen.
|
||||
4. **Clear** leert nur das Grid. Nach erneutem Programmstart wird die aufbewahrte Historie wieder geladen.
|
||||
5. **Log Folder** muss den lokalen Ordner öffnen.
|
||||
6. Die aktuelle Tagesdatei heißt `BizTalkPlatformManagementTool-yyyy-MM-dd.log` und enthält den bei diesem Start erzeugten Verifikationseintrag.
|
||||
7. Nach dem ersten Start an einem Folgetag wird der abgeschlossene Vortag zu `.log.gz` komprimiert.
|
||||
8. Aktueller Tag plus 29 Vortage bleiben erhalten. Einträge außerhalb dieses Fensters werden beim Start entfernt.
|
||||
9. Das Grid lädt höchstens die neuesten 10.000 Einträge; die 30-Tage-Dateien bleiben davon unabhängig vollständig erhalten.
|
||||
|
||||
ProgramData ist ein separates, üblicherweise ausgeblendetes Windows-Verzeichnis. Im Installationsordner unter Program Files wird im Normalfall kein Log erwartet. Version 2.3.1 prüft ProgramData durch einen echten Schreibzugriff; danach folgen LocalAppData, ein `Logs`-Unterordner im Installationsverzeichnis und Temp. Jeder Fallback oder vollständige Ausfall muss im Grid mit dem betroffenen Pfad und der Exception sichtbar sein.
|
||||
|
||||
## Fehlerfall und Supportpaket
|
||||
|
||||
Bei einem Fehler keine Assembly austauschen und den GAC nicht spontan verändern. Folgende unveränderte Evidenz sichern:
|
||||
|
||||
1. Aktuelle `.log` sowie relevante `.log.gz` aus dem über **Log Folder** geöffneten Ordner.
|
||||
2. `shutdown-plan.json` oder `restore-plan.json`.
|
||||
3. `shutdown-result.json`, `restore-result.json` oder den timestamp-basierten Emergency-Report.
|
||||
4. `before.json` und vorhandenen Nachher-Snapshot.
|
||||
5. `BizTalkPlatformManagementTool.exe.config`.
|
||||
6. Screenshot des vollständigen roten Grid-Eintrags und der Umgebung.
|
||||
7. Dateieigenschaften/Version von `Microsoft.BizTalk.Scheduler.dll`; die Datei selbst nur nach interner Freigabe übermitteln.
|
||||
|
||||
Der Fehlerdatensatz enthält Exceptiontyp, HRESULT, innere Ausnahmen, vorhandenes Fusion-Loaderprotokoll, Stacktrace, angeforderte Assembly, anfordernde Assembly und alle geprüften Suchpfade. Damit lässt sich unterscheiden zwischen fehlendem Pfad, falscher BizTalk-Version, nicht passender Strong-Name-Identität, Bitness-/Abhängigkeitsproblem und eigentlichem WMI-Fehler.
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
- ScheduledTask-Receive-Location lässt sich real deaktivieren und wieder auf den Snapshotzustand aktivieren.
|
||||
- Andere Receive Locations bleiben unverändert funktionsfähig und benötigen keinen Scheduler-Preflight.
|
||||
- Ein einzelner Fehler bleibt `Failed`, verhindert aber keine späteren unabhängigen Planschritte.
|
||||
- Ergebnisreport und Nachher-Snapshot werden auch bei einem Teilfehler soweit möglich geschrieben.
|
||||
- Historisches Log erscheint nach Neustart wieder im Grid.
|
||||
- Vortagslogs werden komprimiert und exakt 30 Kalendertage aufbewahrt.
|
||||
- Setup, Tool und Ergebnisdateien melden Version 2.3.4 beziehungsweise `2.3.4-net461`.
|
||||
|
||||
Die lokale Mono-Toolchain prüft Resolverlogik, Identitätsgrenze, Persistenz, Kompression und Aufbewahrung ohne BizTalk. Die endgültige Freigabe erfordert diesen realen Windows-/BizTalk-/PROD-Test.
|
||||
@@ -0,0 +1,57 @@
|
||||
# PROD-Analyse: Shutdown-Dialog nicht sichtbar
|
||||
|
||||
**Datum:** 27.08.2026
|
||||
|
||||
**Zielversion:** 2.3.4
|
||||
|
||||
**Befund:** Nach der Receive-Location-Phase wurde kein sichtbarer Dialog zur Bestätigung des Leerlaufens wahrgenommen.
|
||||
|
||||
## Ergebnis der Ursachenanalyse
|
||||
|
||||
Der fachliche Checkpoint war in Version 2.3.3 vorhanden: Jeder Shutdown-Plan mit nachgelagerter Arbeit enthält genau eine persistierte `OperatorCheckpoint`-Zeile vor Orchestrierungen, Send Ports und lokalen Host Instances. Ohne Callback, bei einem Fehler oder bei **Nein** stoppt der Executor fail-closed; keine geschützte spätere Phase wird ausgeführt.
|
||||
|
||||
Die Oberfläche hatte aber zwei voneinander unabhängige Sichtbarkeitsprobleme:
|
||||
|
||||
1. **Dry run ist standardmäßig aktiv.** In diesem Modus wird keine Receive Location deaktiviert. Deshalb wird der echte Entscheidungsdialog absichtlich nicht aufgerufen; nur die Checkpoint-Zeile wird simuliert. Der kleine Haken und der generische Status machten diesen Unterschied während der Ausführung nicht eindeutig.
|
||||
2. **Die Bestätigungen hatten keinen Fenster-Owner.** `MessageBox.Show(...)` wurde aus dem UI-Thread aufgerufen, aber nicht an das Hauptformular gebunden. Unter RDP, nach Monitorwechseln oder bei abweichender Z-Order konnte der modale Dialog hinter dem Hauptfenster beziehungsweise außerhalb des erwarteten Vordergrunds liegen. Der Background-Worker wartete dann korrekt auf `Invoke`, während das Hauptfenster weiterhin nur `Preparing shutdown...` zeigte und deaktiviert wirkte.
|
||||
|
||||
Der Screenshot allein unterscheidet die beiden Fälle nicht, weil der Einstellungsbereich mit **Dry run** nicht sichtbar und kein `shutdown-result.json` beigefügt ist. Der sichtbare Zustand `Preparing shutdown...` bei bereits dargestelltem Plan ist jedoch mit einem wartenden, owner-losen Plan- oder Drain-Dialog vereinbar. Ein abgeschlossenes `DryRun=true` mit leerer `CheckpointDecision` belegt dagegen die beabsichtigte Simulation.
|
||||
|
||||
## Korrektur in 2.3.4
|
||||
|
||||
- Planfreigabe, Drain-Entscheidung und Emergency-Restore-Bestätigung verwenden das Hauptformular explizit als Owner und holen es vor dem Anzeigen nach vorn.
|
||||
- Unmittelbar vor der echten Drain-Entscheidung meldet die Statuszeile: `ACTION REQUIRED: Shutdown paused`.
|
||||
- Der Titel des Checkpoints beginnt mit `ACTION REQUIRED`; der Text erklärt, dass der Shutdown pausiert ist und **Nein oder Schließen** sicher stoppt.
|
||||
- Die erste Freigabe eines echten Shutdowns kündigt den zweiten Dialog nach der Receive-Location-Phase ausdrücklich an.
|
||||
- Dry-run und Realbetrieb haben gut sichtbare, farblich getrennte Texte. Während einer Operation sind diese Einstellungen gesperrt.
|
||||
- Ein Shutdown-Dry-run zeigt einen eigenen owner-gebundenen Hinweis: keine Runtime-Änderung, keine deaktivierte Receive Location und daher keine echte Drain-Entscheidung.
|
||||
|
||||
Die Sicherheitssemantik bleibt unverändert: Das Tool behauptet keinen automatisch erkannten Leerlauf. Nur der Operator kann nach Prüfung von Group Hub und Enterprise-Monitoring mit **Ja** fortsetzen.
|
||||
|
||||
## Diagnose vorhandener 2.3.2/2.3.3-Läufe
|
||||
|
||||
`shutdown-result.json` und `shutdown-plan.json` sichern und wie folgt bewerten:
|
||||
|
||||
| Evidenz | Bedeutung |
|
||||
|---|---|
|
||||
| `DryRun=true`, Checkpoint-Outcome `DryRun`, keine `CheckpointDecision` | Erwartete Simulation; es gab keine Runtime-Änderung und bewusst keinen Entscheidungsdialog. |
|
||||
| Kein Ergebnisreport, UI bleibt bei `Preparing shutdown...` | Lauf wartet möglicherweise auf einen verdeckten Plan- oder Drain-Dialog; Taskwechsel/Alt+Tab prüfen. Prozess nicht blind beenden. |
|
||||
| `DryRun=false`, `CheckpointDecision=Stop` | Operator hat Nein gewählt oder sicher gestoppt; spätere Zeilen müssen `NotExecuted` sein. |
|
||||
| `DryRun=false`, `CheckpointDecision=Error` | Dialogmechanismus fehlte/fehlerhaft; fail-closed, spätere Zeilen müssen `NotExecuted` sein. |
|
||||
| `DryRun=false`, `CheckpointDecision=Continue` | Eine positive Entscheidung wurde erfasst; spätere Phasen durften laufen. |
|
||||
| Plan ohne `OperatorCheckpoint` | Nur zulässig, wenn keine Orchestrierung, kein Send Port und keine lokale Host Instance nachgelagert herunterzufahren war. |
|
||||
|
||||
## PROD-Abnahme 2.3.4
|
||||
|
||||
1. Setup 2.3.4 als Administrator installieren und im Setup-Log `setup_version=2.3.4.0` sowie den erfolgreichen Ziel-Self-Test prüfen.
|
||||
2. Tooldateiversion und Self-Test-Ausgabe `2.3.4-net461` prüfen.
|
||||
3. **Dry run** aktiviert lassen, **Shutdown** wählen und den Hinweis `Shutdown Dry Run - No Runtime Changes` prüfen. Es darf keine BizTalk-Mutation stattfinden.
|
||||
4. `shutdown-plan.json` prüfen: Bei späterer Shutdown-Arbeit steht `OperatorCheckpoint` vor Orchestrierungen, Send Ports und Host Instances.
|
||||
5. In einem freigegebenen Wartungsfenster **Dry run** deaktivieren. Der Modus muss `REAL EXECUTION - drain dialog required` anzeigen.
|
||||
6. Den gespeicherten Plan im ersten owner-gebundenen Dialog bestätigen.
|
||||
7. Nach den Receive Locations muss die Statuszeile `ACTION REQUIRED` zeigen und der Dialog `ACTION REQUIRED - Confirm BizTalk Inbound Drain` vor dem Hauptfenster stehen.
|
||||
8. Zuerst **Nein** testen. `shutdown-result.json` muss `CheckpointDecision=Stop` und für alle späteren Zeilen `NotExecuted` enthalten.
|
||||
9. Den kontrollierten Test wiederholen, Group Hub/Monitoring prüfen und erst nach bestätigtem Leerlauf **Ja** wählen. Danach dürfen Orchestrierungen, Send Ports und Host Instances folgen.
|
||||
10. RDP minimieren/wiederherstellen und, falls im Betrieb üblich, Monitorwechsel testen. Der owner-gebundene Dialog muss dem Hauptfenster zugeordnet bleiben.
|
||||
|
||||
Die portable Verifikation umfasst Release-Build aller vier Projekte, 43 Regressionstests, Runtime-Self-Test und die manifest-/hashgeprüfte Paketierung. Die abschließende Sichtbarkeits- und BizTalk-Abnahme bleibt ein Windows-/PROD-Test.
|
||||
@@ -0,0 +1,81 @@
|
||||
# PROD-Runbook: Shutdown-Drain-Checkpoint und Application-Status
|
||||
|
||||
**Stand:** 2026-08-26
|
||||
|
||||
**Zielversion:** 2.3.2
|
||||
|
||||
## Ziel
|
||||
|
||||
Nach dem Abschalten aller zuvor aktivierten Receive Locations muss BizTalk Zeit erhalten, bereits angenommene Nachrichten und laufende Service Instances kontrolliert zu verarbeiten. Version 2.3.2 hält deshalb vor Orchestrierungen, Send Ports und Host Instances an und verlangt eine explizite Operatorentscheidung.
|
||||
|
||||
Der Checkpoint ist als `OperatorCheckpoint` in `shutdown-plan.json` gespeichert. Er ist kein automatischer Nachweis, dass die Umgebung leer ist. Die Entscheidung stützt sich auf BizTalk Group Hub und die in der Enterprise-Umgebung vorhandenen Queue-, MessageBox-, Prozess- und Schnittstellenmetriken.
|
||||
|
||||
## Warum `Partially Started` erwartbar ist
|
||||
|
||||
Microsoft beschreibt **Partial Stop - Allow running instances to continue** als Deaktivieren der Receive Locations bei unverändert laufenden übrigen Artefakten. Genau dieser Zwischenzustand ermöglicht das Leerfahren und wird in der Administration Console erwartbar als teilweise gestartet sichtbar.
|
||||
|
||||
Ein pauschaler Application-Stop wird nicht ergänzt. Je nach ausgewähltem BizTalk-Modus würde er zusätzlich Orchestrierungen und Send Ports stoppen, Subscriptions entfernen, Artefakte unenlisten, Policies undeployen oder laufende Instanzen terminieren. Das wäre keine kosmetische Statuskorrektur und könnte die gerade gewünschte Drain-Phase abbrechen. Microsoft empfiehlt einen Full Stop nur zum Entfernen oder erneuten Bereitstellen einer Anwendung.
|
||||
|
||||
Quellen:
|
||||
|
||||
- [How to Start and Stop a BizTalk Application](https://learn.microsoft.com/en-us/biztalk/core/how-to-start-and-stop-a-biztalk-application)
|
||||
- [Stop Application Dialog Box](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/stop-application-dialog-box)
|
||||
- [Restart services or shut down BizTalk Server](https://learn.microsoft.com/en-us/biztalk/core/how-to-start-stop-pause-resume-or-restart-biztalk-server-services)
|
||||
|
||||
## Dry-run und Planreview
|
||||
|
||||
1. Version 2.3.2 installieren und `setup_version=2.3.2.0` sowie erfolgreichen Ziel-Self-Test prüfen.
|
||||
2. **Diagnose** und **Snapshot Before** ausführen.
|
||||
3. **Dry run** aktiviert lassen und **Shutdown** wählen.
|
||||
4. In `shutdown-plan.json` die globale Reihenfolge prüfen:
|
||||
- alle `ReceiveLocation`-Zeilen,
|
||||
- genau ein `OperatorCheckpoint`, sofern spätere Shutdownarbeit existiert,
|
||||
- Orchestrierungen,
|
||||
- Send Ports,
|
||||
- Host Instances.
|
||||
5. Dry-run darf keinen Entscheidungsdialog öffnen; der Checkpoint erscheint als `DryRun`.
|
||||
|
||||
## Echter Shutdown
|
||||
|
||||
1. Dry-run deaktivieren, den gespeicherten Gesamtplan bestätigen und Shutdown starten.
|
||||
2. Die Receive-Location-Ergebnisse im Operation Log beobachten. Ein Scheduler-Fehler bleibt rot und wird im Checkpoint gezählt.
|
||||
3. Beim Dialog **Confirm BizTalk Inbound Drain** zunächst nichts wählen. Der Dialog darf während der gesamten Leerfahrzeit geöffnet bleiben.
|
||||
4. Mindestens prüfen:
|
||||
- keine neuen eingehenden Nachrichten an den abgeschalteten Schnittstellen,
|
||||
- Group Hub: laufende Service Instances entwickeln sich kontrolliert gegen null,
|
||||
- keine unerwarteten suspended/resumable Instances,
|
||||
- relevante MessageBox-/Queue-/Adapter- und Fremdsystemmetriken sind leer beziehungsweise stabil,
|
||||
- fachlich mehrteilige Prozesse haben alle benötigten Eingaben erhalten.
|
||||
5. Bei leer gelaufener und fehlerfreier Umgebung **Yes** wählen. Erst dann folgen Orchestrierungen, Send Ports und Host Instances.
|
||||
6. Bei Unsicherheit, Receive-Location-Fehlern oder weiter laufender Last **No** wählen. Das Tool verändert danach keine spätere Kategorie.
|
||||
|
||||
## Nachweis im Ergebnisreport
|
||||
|
||||
Bei **Yes**:
|
||||
|
||||
- Checkpointzeile: `Outcome=Confirmed`
|
||||
- `CheckpointDecision=Continue`
|
||||
- `OperatorStopped=false`
|
||||
|
||||
Bei **No**:
|
||||
|
||||
- Checkpointzeile: `Outcome=Declined`
|
||||
- `CheckpointDecision=Stop`
|
||||
- `OperatorStopped=true`
|
||||
- jede spätere Zeile: `Outcome=NotExecuted`
|
||||
- `NotExecutedCount` entspricht den geschützten Restzeilen
|
||||
|
||||
Bei fehlendem oder fehlerhaftem Bestätigungsmechanismus lautet die Entscheidung `Error`; der Checkpoint ist `Failed` und alle späteren Zeilen bleiben ebenfalls `NotExecuted`. In allen Fällen versucht das Tool anschließend `shutdown-after.json` und speichert `shutdown-result.json`.
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
- Checkpoint steht im gespeicherten Plan exakt zwischen Receive Locations und Orchestrierungen.
|
||||
- Dry-run öffnet keinen echten Entscheidungsdialog.
|
||||
- Dialog verwendet **No** als sichere Standardauswahl.
|
||||
- **Yes** setzt ausschließlich die bereits gespeicherten späteren Schritte fort.
|
||||
- **No** und Dialogfehler führen zu keiner späteren WMI-Mutation.
|
||||
- Entscheidung, Zeitpunkt, Teilzustand und alle nicht ausgeführten Schritte sind in Grid, Log und Report nachvollziehbar.
|
||||
- Es wird kein zusätzlicher Application-Stop und insbesondere kein Full Stop ausgeführt.
|
||||
- Setup, Tool und neue Reports melden 2.3.2 beziehungsweise `2.3.2-net461`.
|
||||
|
||||
Die portable Toolchain prüft die Ablaufgrenzen ohne BizTalk. Die endgültige Freigabe benötigt einen Windows-/BizTalk-/PROD-Test mit den realen Enterprise-Monitoringquellen.
|
||||
@@ -0,0 +1,9 @@
|
||||
@echo off
|
||||
setlocal
|
||||
where msbuild.exe >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo MSBuild.exe was not found in PATH. Run this from a Visual Studio Developer Command Prompt.
|
||||
exit /b 1
|
||||
)
|
||||
msbuild "%~dp0..\BizTalkPlatformManagementTool.sln" /p:Configuration=Release /p:Platform="Any CPU" /m:1 /v:minimal
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
setlocal
|
||||
call "%~dp0test-release.cmd"
|
||||
if errorlevel 1 exit /b 1
|
||||
"%~dp0..\src\BizTalkPlatformManagementTool.Packager\bin\Release\BizTalkPlatformManagementTool.Packager.exe" "%~dp0.." Release
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
setlocal
|
||||
call "%~dp0build-release.cmd"
|
||||
if errorlevel 1 exit /b 1
|
||||
"%~dp0..\tests\BizTalkPlatformManagementTool.Tests\bin\Release\BizTalkPlatformManagementTool.Tests.exe"
|
||||
exit /b %ERRORLEVEL%
|
||||
+8
-25
@@ -2,32 +2,15 @@
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{AF67FD76-8F5D-42DB-9D52-20EF6332D1F9}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>BizTalkPlatformManagementTool.Packager</RootNamespace>
|
||||
<AssemblyName>BizTalkPlatformManagementTool.Packager</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration><Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{74A5D422-0BA5-4559-BD81-C89C071A8FE4}</ProjectGuid><OutputType>Exe</OutputType>
|
||||
<RootNamespace>BizTalkPlatformManagementTool.Packager</RootNamespace><AssemblyName>BizTalkPlatformManagementTool.Packager</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion><FileAlignment>512</FileAlignment><Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" /><Reference Include="System.Core" /><Reference Include="System.IO.Compression" /><Reference Include="System.IO.Compression.FileSystem" /><Reference Include="System.Security" />
|
||||
</ItemGroup>
|
||||
<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><DocumentationFile>bin\Release\BizTalkPlatformManagementTool.Packager.xml</DocumentationFile></PropertyGroup>
|
||||
<ItemGroup><Reference Include="System" /><Reference Include="System.Core" /><Reference Include="System.IO.Compression" /><Reference Include="System.IO.Compression.FileSystem" /><Reference Include="System.Security" /></ItemGroup>
|
||||
<ItemGroup><Compile Include="Program.cs" /></ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BizTalkPlatformManagementTool\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>
|
||||
<ItemGroup><ProjectReference Include="..\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj"><Project>{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}</Project><Name>BizTalkPlatformManagementTool.Setup</Name></ProjectReference></ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Target Name="CreateInstallPackage" AfterTargets="Build" Condition=" '$(Configuration)' == 'Release' ">
|
||||
<Exec Command=""$(TargetPath)" "$(MSBuildProjectDirectory)\..\.." "$(Configuration)"" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -1,92 +1,81 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using BizTalkPlatformManagementTool.Setup;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Packager
|
||||
{
|
||||
/// <summary>Erzeugt aus den Release-Binärdateien das übertragbare Setup-Paket.</summary>
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// Erstellt Payload, Manifest, ZIP, Certutil-kompatible Base64-TXT und SHA-256-Datei.
|
||||
/// </summary>
|
||||
/// <param name="args">Repository-Wurzel und Build-Konfiguration.</param>
|
||||
/// <returns>Null bei erfolgreicher Paketierung, andernfalls eins.</returns>
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (args.Length != 2) throw new ArgumentException("Usage: Packager.exe <repository-root> <configuration>");
|
||||
if (args.Length != 2) throw new ArgumentException("Usage: BizTalkPlatformManagementTool.Packager.exe <repository-root> <configuration>");
|
||||
var root = Path.GetFullPath(args[0]);
|
||||
var configuration = args[1];
|
||||
var artifacts = Path.Combine(root, "artifacts");
|
||||
var package = Path.Combine(artifacts, "BizTalkPlatformManagementTool-Setup");
|
||||
var zip = package + ".zip";
|
||||
var base64 = zip + ".b64.txt";
|
||||
var checksum = zip + ".sha256.txt";
|
||||
RecreateDirectory(package);
|
||||
Directory.CreateDirectory(Path.Combine(package, "application"));
|
||||
Directory.CreateDirectory(Path.Combine(package, "documentation"));
|
||||
var application = Path.Combine(package, "application");
|
||||
var zip = Path.Combine(artifacts, "BizTalkPlatformManagementTool-Setup.zip");
|
||||
|
||||
var applicationOutput = Path.Combine(root, "src", "BizTalkPlatformManagementTool", "bin", configuration);
|
||||
var setupOutput = Path.Combine(root, "src", "BizTalkPlatformManagementTool.Setup", "bin", configuration);
|
||||
CopyRequired(Path.Combine(applicationOutput, "BizTalkPlatformManagementTool.exe"), Path.Combine(package, "application", "BizTalkPlatformManagementTool.exe"));
|
||||
CopyRequired(Path.Combine(applicationOutput, "BizTalkPlatformManagementTool.exe.config"), Path.Combine(package, "application", "BizTalkPlatformManagementTool.exe.config"));
|
||||
CopyRequired(Path.Combine(setupOutput, "BizTalkPlatformManagementTool.Setup.exe"), Path.Combine(package, "Setup.exe"));
|
||||
CopyRequired(Path.Combine(root, "README.md"), Path.Combine(package, "documentation", "README.md"));
|
||||
CopyRequired(Path.Combine(root, "Dokumentation.md"), Path.Combine(package, "documentation", "Dokumentation.md"));
|
||||
CopyRequired(Path.Combine(root, "Installation.md"), Path.Combine(package, "documentation", "Installation.md"));
|
||||
if (Directory.Exists(package)) Directory.Delete(package, true);
|
||||
Directory.CreateDirectory(application);
|
||||
Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool.Setup", "bin", configuration, "BizTalkPlatformManagementTool.Setup.exe"), Path.Combine(package, "Setup.exe"));
|
||||
Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool", "bin", configuration, "BizTalkPlatformManagementTool.exe"), Path.Combine(application, "BizTalkPlatformManagementTool.exe"));
|
||||
Copy(Path.Combine(root, "src", "BizTalkPlatformManagementTool", "bin", configuration, "BizTalkPlatformManagementTool.exe.config"), Path.Combine(application, "BizTalkPlatformManagementTool.exe.config"));
|
||||
Copy(Path.Combine(root, "Installation.md"), Path.Combine(package, "INSTALLATION.md"));
|
||||
PackageManifest.Write(application, Path.Combine(package, "application.manifest"));
|
||||
// Das frisch erzeugte Manifest wird vor dem äußeren ZIP sofort gegen die Payload geprüft.
|
||||
PackageManifest.ValidateAndRead(application, Path.Combine(package, "application.manifest"));
|
||||
|
||||
DeleteIfExists(zip);
|
||||
DeleteIfExists(base64);
|
||||
DeleteIfExists(checksum);
|
||||
if (File.Exists(zip)) File.Delete(zip);
|
||||
ZipFile.CreateFromDirectory(package, zip, CompressionLevel.Optimal, false);
|
||||
WriteBase64(zip, base64);
|
||||
var hash = ComputeSha256(zip);
|
||||
File.WriteAllText(checksum, hash + " " + Path.GetFileName(zip) + "\r\n", new UTF8Encoding(false));
|
||||
Console.WriteLine("Installationsordner: " + package);
|
||||
Console.WriteLine("Installations-ZIP: " + zip);
|
||||
Console.WriteLine("Transport-TXT: " + base64);
|
||||
Console.WriteLine("SHA-256: " + hash);
|
||||
WriteBase64(zip, zip + ".b64.txt");
|
||||
File.WriteAllText(zip + ".sha256.txt", PackageManifest.Sha256(zip) + " " + Path.GetFileName(zip) + Environment.NewLine, new UTF8Encoding(false));
|
||||
Console.WriteLine("SETUP_PACKAGE=" + package);
|
||||
Console.WriteLine("SETUP_ZIP=" + zip);
|
||||
Console.WriteLine("SETUP_BASE64=" + zip + ".b64.txt");
|
||||
Console.WriteLine("SETUP_SHA256=" + zip + ".sha256.txt");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine("Packaging fehlgeschlagen: " + ex.Message);
|
||||
Console.Error.WriteLine("Packaging failed: " + ex);
|
||||
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)
|
||||
{
|
||||
var encoded = Convert.ToBase64String(File.ReadAllBytes(source));
|
||||
using (var writer = new StreamWriter(target, false, new UTF8Encoding(false)))
|
||||
var builder = new StringBuilder(encoded.Length + encoded.Length / 64 + 2);
|
||||
for (var offset = 0; offset < encoded.Length; offset += 64)
|
||||
{
|
||||
for (var offset = 0; offset < encoded.Length; offset += 120)
|
||||
writer.WriteLine(encoded.Substring(offset, Math.Min(120, encoded.Length - offset)));
|
||||
builder.Append(encoded, offset, Math.Min(64, encoded.Length - offset));
|
||||
builder.Append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
File.WriteAllText(target, builder.ToString(), new UTF8Encoding(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,42 @@
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{38E61630-11AB-4F8D-B421-F8A88831C4BD}</ProjectGuid>
|
||||
<ProjectGuid>{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>BizTalkPlatformManagementTool.Setup</RootNamespace>
|
||||
<AssemblyName>BizTalkPlatformManagementTool.Setup</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel>
|
||||
<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>
|
||||
<DebugType>pdbonly</DebugType><Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel>
|
||||
<DocumentationFile>bin\Release\BizTalkPlatformManagementTool.Setup.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" /><Reference Include="System.Core" /><Reference Include="System.Drawing" /><Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Security" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="InstallerEngine.cs" /><Compile Include="MainForm.cs" /><Compile Include="Program.cs" /><None Include="app.manifest" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj"><Project>{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}</Project><Name>BizTalkPlatformManagementTool</Name><ReferenceOutputAssembly>false</ReferenceOutputAssembly></ProjectReference>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="InstallerEngine.cs" />
|
||||
<Compile Include="MainForm.cs" />
|
||||
<Compile Include="PackageManifest.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="SetupOperationLog.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup><None Include="app.manifest" /></ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,148 +1,204 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
/// <summary>
|
||||
/// Stellt die Bedienoberfläche für Installation, Update, Deinstallation und Diagnosezugriff bereit.
|
||||
/// </summary>
|
||||
internal sealed class MainForm : Form
|
||||
{
|
||||
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>Ausführende Installer-Engine.</summary>
|
||||
private readonly InstallerEngine engine;
|
||||
|
||||
public MainForm(InstallerEngine engine)
|
||||
/// <summary>Gibt an, ob das Fenster direkt zur Deinstallation geöffnet wurde.</summary>
|
||||
private readonly bool uninstallMode;
|
||||
|
||||
/// <summary>Sichtbare Fortschritts- und Diagnoseausgabe.</summary>
|
||||
private readonly TextBox output = new TextBox();
|
||||
|
||||
/// <summary>Auswahl für die optionale maschinenweite Desktop-Verknüpfung.</summary>
|
||||
private readonly CheckBox desktopShortcut = new CheckBox();
|
||||
|
||||
/// <summary>Schaltfläche für Installation oder Update.</summary>
|
||||
private readonly Button installButton = new Button();
|
||||
|
||||
/// <summary>Schaltfläche für die Deinstallation.</summary>
|
||||
private readonly Button uninstallButton = new Button();
|
||||
|
||||
/// <summary>Verhindert parallele Aktionen und Schließen während einer Operation.</summary>
|
||||
private bool busy;
|
||||
|
||||
/// <summary>
|
||||
/// Initialisiert das Setup-Fenster für den normalen oder direkten Deinstallationsmodus.
|
||||
/// </summary>
|
||||
/// <param name="engine">Die ausführende Installer-Engine.</param>
|
||||
/// <param name="uninstallMode"><c>true</c>, wenn das Setup über den Uninstall-Eintrag gestartet wurde.</param>
|
||||
public MainForm(InstallerEngine engine, bool uninstallMode)
|
||||
{
|
||||
_engine = engine;
|
||||
Text = "BizTalk Platform Management Tool – Setup";
|
||||
ClientSize = new Size(680, 500);
|
||||
MinimumSize = new Size(696, 539);
|
||||
this.engine = engine;
|
||||
this.uninstallMode = uninstallMode;
|
||||
Text = "BizTalk Platform Management Tool Setup";
|
||||
Width = 780;
|
||||
Height = 520;
|
||||
MinimumSize = new Size(680, 420);
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
AutoScaleMode = AutoScaleMode.Dpi;
|
||||
Font = new Font("Segoe UI", 9F);
|
||||
BackColor = Color.White;
|
||||
BuildUi();
|
||||
FormClosing += OnFormClosing;
|
||||
}
|
||||
|
||||
/// <summary>Erstellt und verdrahtet die vollständige Setup-Oberfläche.</summary>
|
||||
private void BuildUi()
|
||||
{
|
||||
var header = new Panel { Dock = DockStyle.Top, Height = 112, BackColor = Color.FromArgb(28, 67, 102) };
|
||||
header.Controls.Add(new Label { Text = "BizTalk Platform Management Tool", Location = new Point(24, 20), Size = new Size(620, 34), ForeColor = Color.White, Font = new Font(Font.FontFamily, 17F, FontStyle.Bold) });
|
||||
header.Controls.Add(new Label { Text = "Installieren oder sicher auf eine neue Version aktualisieren", Location = new Point(26, 62), Size = new Size(620, 25), ForeColor = Color.FromArgb(220, 232, 242) });
|
||||
Controls.Add(header);
|
||||
var root = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(16), RowCount = 5, ColumnCount = 1 };
|
||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
|
||||
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
|
||||
root.Controls.Add(new Label
|
||||
{
|
||||
Text = _engine.IsInstalled
|
||||
? "Eine vorhandene Installation wurde erkannt. Arbeitsdaten bleiben beim Update erhalten."
|
||||
: "Bereit zur Installation. Setup muss aus dem vollständig entpackten Paket gestartet werden.",
|
||||
Dock = DockStyle.Fill,
|
||||
TextAlign = ContentAlignment.MiddleLeft
|
||||
}, 0, 0);
|
||||
body.SetColumnSpan(body.GetControlFromPosition(0, 0), 2);
|
||||
AutoSize = true,
|
||||
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
|
||||
Text = "BizTalk Platform Management Tool 2.3.4"
|
||||
});
|
||||
root.Controls.Add(new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Padding = new Padding(0, 8, 0, 8),
|
||||
Text = "Transaktionaler Installer mit SHA-256-Pruefung, Staging-Self-Test und automatischem Rollback.\r\nZiel: " + engine.InstallDirectory
|
||||
});
|
||||
|
||||
_desktop.Text = "Desktop-Verknüpfung erstellen";
|
||||
_desktop.Checked = true;
|
||||
_desktop.Dock = DockStyle.Fill;
|
||||
body.Controls.Add(_desktop, 0, 1);
|
||||
body.SetColumnSpan(_desktop, 2);
|
||||
_keepData.Text = "Wartungszustände und Logs bei Deinstallation behalten";
|
||||
_keepData.Checked = true;
|
||||
_keepData.Dock = DockStyle.Fill;
|
||||
body.Controls.Add(_keepData, 0, 2);
|
||||
body.SetColumnSpan(_keepData, 2);
|
||||
desktopShortcut.Text = "Optionale Desktop-Verknuepfung fuer alle Benutzer erstellen";
|
||||
desktopShortcut.Checked = false;
|
||||
desktopShortcut.AutoSize = true;
|
||||
desktopShortcut.Enabled = !uninstallMode;
|
||||
root.Controls.Add(desktopShortcut);
|
||||
|
||||
ConfigureButton(_install, "Installieren / aktualisieren", Color.FromArgb(31, 111, 181));
|
||||
ConfigureButton(_uninstall, "Deinstallieren", Color.FromArgb(120, 55, 55));
|
||||
_install.Click += async delegate { await InstallAsync(); };
|
||||
_uninstall.Click += async delegate { await UninstallAsync(); };
|
||||
body.Controls.Add(_install, 0, 3);
|
||||
body.Controls.Add(_uninstall, 1, 3);
|
||||
output.Multiline = true;
|
||||
output.ReadOnly = true;
|
||||
output.ScrollBars = ScrollBars.Vertical;
|
||||
output.Dock = DockStyle.Fill;
|
||||
output.Font = new Font(FontFamily.GenericMonospace, 9);
|
||||
root.Controls.Add(output);
|
||||
|
||||
_status.Dock = DockStyle.Fill;
|
||||
_status.Multiline = true;
|
||||
_status.ReadOnly = true;
|
||||
_status.ScrollBars = ScrollBars.Vertical;
|
||||
_status.BackColor = Color.FromArgb(247, 249, 252);
|
||||
_status.BorderStyle = BorderStyle.FixedSingle;
|
||||
body.Controls.Add(_status, 0, 4);
|
||||
body.SetColumnSpan(_status, 2);
|
||||
_uninstall.Enabled = _engine.IsInstalled;
|
||||
var buttons = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft };
|
||||
var closeButton = new Button { Text = "Schliessen", AutoSize = true };
|
||||
closeButton.Click += (sender, args) => Close();
|
||||
var logsButton = new Button { Text = "Diagnoselogs oeffnen", AutoSize = true };
|
||||
logsButton.Click += OpenLogs;
|
||||
installButton.Text = engine.IsInstalled ? "Update installieren" : "Installieren";
|
||||
installButton.AutoSize = true;
|
||||
installButton.Enabled = engine.HasInstallPayload && !uninstallMode;
|
||||
installButton.Click += (sender, args) => Run(false);
|
||||
uninstallButton.Text = "Deinstallieren";
|
||||
uninstallButton.AutoSize = true;
|
||||
uninstallButton.Enabled = engine.IsInstalled;
|
||||
uninstallButton.Click += (sender, args) => Run(true);
|
||||
buttons.Controls.Add(closeButton);
|
||||
buttons.Controls.Add(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.");
|
||||
}
|
||||
|
||||
private async Task InstallAsync()
|
||||
/// <summary>Öffnet das zuletzt verwendete Diagnoseverzeichnis im Windows Explorer.</summary>
|
||||
/// <param name="sender">Die auslösende Schaltfläche.</param>
|
||||
/// <param name="e">Die Ereignisargumente.</param>
|
||||
private void OpenLogs(object sender, EventArgs e)
|
||||
{
|
||||
SetBusy(true);
|
||||
try
|
||||
{
|
||||
await Task.Run(() => _engine.Install(_desktop.Checked, Report));
|
||||
Report("Installation erfolgreich abgeschlossen.");
|
||||
MessageBox.Show(this, "Installation/Update erfolgreich abgeschlossen.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
Directory.CreateDirectory(engine.InstallerLogDirectory);
|
||||
Process.Start("explorer.exe", engine.InstallerLogDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Report("FEHLER: " + ex.Message);
|
||||
MessageBox.Show(this, ex.Message, "Installation fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
MessageBox.Show(this, "Diagnoseordner konnte nicht geoeffnet werden: " + ex.Message + "\n\n" + engine.InstallerLogDirectory, "Diagnoselogs", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
finally { SetBusy(false); }
|
||||
}
|
||||
|
||||
private async Task UninstallAsync()
|
||||
/// <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 (MessageBox.Show(this, "BizTalk Platform Management Tool wirklich deinstallieren?", Text, MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes) return;
|
||||
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);
|
||||
try
|
||||
// Dateisystem-, Registry- und Self-Test-Operationen dürfen die WinForms-Nachrichtenpumpe nicht blockieren.
|
||||
Task.Run(() =>
|
||||
{
|
||||
await Task.Run(() => _engine.Uninstall(_keepData.Checked, Report));
|
||||
MessageBox.Show(this, "Deinstallation abgeschlossen.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
try
|
||||
{
|
||||
if (uninstall) engine.Uninstall(Append);
|
||||
else engine.Install(createDesktopShortcut, Append);
|
||||
Append(uninstall ? "FERTIG: Deinstallation erfolgreich." : "FERTIG: Installation/Update erfolgreich.");
|
||||
Invoke(new Action(() =>
|
||||
{
|
||||
installButton.Text = engine.IsInstalled ? "Update installieren" : "Installieren";
|
||||
uninstallButton.Enabled = engine.IsInstalled;
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Append("FEHLER: " + ex);
|
||||
Invoke(new Action(() => MessageBox.Show(this, ex.Message, "Setup fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetBusy(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Fügt der sichtbaren Ausgabe threadsicher eine zeitgestempelte Nachricht hinzu.</summary>
|
||||
/// <param name="message">Die anzuzeigende Nachricht.</param>
|
||||
private void Append(string message)
|
||||
{
|
||||
if (IsDisposed || Disposing) return;
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Report("FEHLER: " + ex.Message);
|
||||
MessageBox.Show(this, ex.Message, "Deinstallation fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
try { BeginInvoke(new Action<string>(Append), message); } catch (InvalidOperationException) { }
|
||||
return;
|
||||
}
|
||||
finally { SetBusy(false); }
|
||||
output.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message + Environment.NewLine);
|
||||
}
|
||||
|
||||
private void Report(string message)
|
||||
/// <summary>Schaltet Steuerelemente und Wartecursor threadsicher in oder aus dem Arbeitszustand.</summary>
|
||||
/// <param name="value"><c>true</c>, solange eine Setup-Operation läuft.</param>
|
||||
private void SetBusy(bool value)
|
||||
{
|
||||
if (InvokeRequired) { BeginInvoke(new Action<string>(Report), message); return; }
|
||||
_status.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message + Environment.NewLine);
|
||||
if (InvokeRequired)
|
||||
{
|
||||
try { BeginInvoke(new Action<bool>(SetBusy), value); } catch (InvalidOperationException) { }
|
||||
return;
|
||||
}
|
||||
busy = value;
|
||||
installButton.Enabled = !value && engine.HasInstallPayload && !uninstallMode;
|
||||
uninstallButton.Enabled = !value && engine.IsInstalled;
|
||||
desktopShortcut.Enabled = !value && !uninstallMode;
|
||||
UseWaitCursor = value;
|
||||
}
|
||||
|
||||
private void SetBusy(bool busy)
|
||||
/// <summary>Verhindert das Schließen des Fensters während einer laufenden Setup-Operation.</summary>
|
||||
/// <param name="sender">Das zu schließende Setup-Fenster.</param>
|
||||
/// <param name="e">Die abbrechbaren Argumente des Schließereignisses.</param>
|
||||
private void OnFormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
if (InvokeRequired) { BeginInvoke(new Action<bool>(SetBusy), busy); return; }
|
||||
_install.Enabled = !busy;
|
||||
_uninstall.Enabled = !busy && _engine.IsInstalled;
|
||||
_desktop.Enabled = !busy;
|
||||
_keepData.Enabled = !busy;
|
||||
UseWaitCursor = busy;
|
||||
}
|
||||
|
||||
private static void ConfigureButton(Button button, string text, Color color)
|
||||
{
|
||||
button.Text = text;
|
||||
button.Dock = DockStyle.Fill;
|
||||
button.Margin = new Padding(4, 7, 4, 7);
|
||||
button.FlatStyle = FlatStyle.Flat;
|
||||
button.FlatAppearance.BorderSize = 0;
|
||||
button.BackColor = color;
|
||||
button.ForeColor = Color.White;
|
||||
button.Font = new Font("Segoe UI", 9F, FontStyle.Bold);
|
||||
if (!busy) return;
|
||||
e.Cancel = true;
|
||||
MessageBox.Show(this, "Das Setup arbeitet noch. Bitte warten Sie bis zum Abschluss.", "Setup laeuft", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
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,14 +3,20 @@ using System.Windows.Forms;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Setup
|
||||
{
|
||||
/// <summary>Enthält den Einstiegspunkt des administrativen Windows-Setups.</summary>
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>Startet die Setup-Oberfläche im Installations- oder Deinstallationsmodus.</summary>
|
||||
/// <param name="args">Befehlszeilenargumente; <c>--uninstall</c> aktiviert die Deinstallation.</param>
|
||||
/// <returns>Null nach regulärem Schließen der Oberfläche.</returns>
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new MainForm(new InstallerEngine(AppDomain.CurrentDomain.BaseDirectory)));
|
||||
var uninstallMode = args != null && args.Length == 1 && string.Equals(args[0], "--uninstall", StringComparison.OrdinalIgnoreCase);
|
||||
Application.Run(new MainForm(new InstallerEngine(AppDomain.CurrentDomain.BaseDirectory), uninstallMode));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("BizTalk Platform Management Tool Setup")]
|
||||
[assembly: AssemblyDescription("Transactional installer and updater for BizTalk Platform Management Tool")]
|
||||
[assembly: AssemblyCompany("BEW")]
|
||||
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
|
||||
[assembly: AssemblyVersion("2.3.4.0")]
|
||||
[assembly: AssemblyFileVersion("2.3.4.0")]
|
||||
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
||||
@@ -0,0 +1,265 @@
|
||||
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,11 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="BizTalkPlatformManagementTool.Setup" />
|
||||
<assemblyIdentity version="2.3.4.0" name="BizTalkPlatformManagementTool.Setup" />
|
||||
<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>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application><supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /></application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<appSettings>
|
||||
<!-- Semicolon-delimited optional override. Normal BizTalk 2020 and ScheduledTask 7.x folders are auto-discovered. -->
|
||||
<add key="AdapterAssemblySearchPaths" value="%ProgramFiles(x86)%\Microsoft BizTalk Server 2020;%ProgramFiles(x86)%\BizTalk ScheduledTask Adapter 7.0.2" />
|
||||
</appSettings>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
|
||||
</startup>
|
||||
|
||||
@@ -34,33 +34,43 @@
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
<DocumentationFile>bin\Release\BizTalkPlatformManagementTool.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Management" />
|
||||
<Reference Include="System.Runtime.Serialization" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="RuntimeSelfTest.cs" />
|
||||
<Compile Include="Models\ArtifactStates.cs" />
|
||||
<Compile Include="Models\BizTalkSnapshot.cs" />
|
||||
<Compile Include="Models\DiffModels.cs" />
|
||||
<Compile Include="Models\OperationModels.cs" />
|
||||
<Compile Include="Services\BizTalkWmiClient.cs" />
|
||||
<Compile Include="Services\AppPaths.cs" />
|
||||
<Compile Include="Services\BizTalkApplicationResolver.cs" />
|
||||
<Compile Include="Services\AdapterAssemblyResolver.cs" />
|
||||
<Compile Include="Services\CsvWriter.cs" />
|
||||
<Compile Include="Services\HtmlReportWriter.cs" />
|
||||
<Compile Include="Services\JsonFileStore.cs" />
|
||||
<Compile Include="Services\OperationLogger.cs" />
|
||||
<Compile Include="Services\ExceptionDiagnostics.cs" />
|
||||
<Compile Include="Services\OperationPlanExecutor.cs" />
|
||||
<Compile Include="Services\SnapshotComparer.cs" />
|
||||
<Compile Include="Services\SnapshotValidator.cs" />
|
||||
<Compile Include="Services\SnapshotStore.cs" />
|
||||
<Compile Include="Services\BizTalkOperationService.cs" />
|
||||
<Compile Include="Ui\ShutdownOperatorPrompts.cs" />
|
||||
<Compile Include="Ui\MainForm.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,58 +1,123 @@
|
||||
namespace BizTalkPlatformManagementTool.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains BizTalk WMI state constants and display helpers used by snapshots,
|
||||
/// plans and reports.
|
||||
/// </summary>
|
||||
public static class ArtifactStates
|
||||
{
|
||||
/// <summary>
|
||||
/// WMI status value for a bound send port.
|
||||
/// </summary>
|
||||
public const int SendPortBound = 1;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a stopped send port.
|
||||
/// </summary>
|
||||
public const int SendPortStopped = 2;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a started send port.
|
||||
/// </summary>
|
||||
public const int SendPortStarted = 3;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for an unbound orchestration.
|
||||
/// </summary>
|
||||
public const int OrchestrationUnbound = 1;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a bound orchestration.
|
||||
/// </summary>
|
||||
public const int OrchestrationBound = 2;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a stopped orchestration.
|
||||
/// </summary>
|
||||
public const int OrchestrationStopped = 3;
|
||||
|
||||
/// <summary>
|
||||
/// WMI status value for a started orchestration.
|
||||
/// </summary>
|
||||
public const int OrchestrationStarted = 4;
|
||||
|
||||
/// <summary>
|
||||
/// WMI service state value for a stopped host instance.
|
||||
/// </summary>
|
||||
public const int HostStopped = 1;
|
||||
|
||||
/// <summary>
|
||||
/// WMI service state value for a host instance that is starting.
|
||||
/// </summary>
|
||||
public const int HostStartPending = 2;
|
||||
|
||||
/// <summary>
|
||||
/// WMI service state value for a host instance that is stopping.
|
||||
/// </summary>
|
||||
public const int HostStopPending = 3;
|
||||
|
||||
/// <summary>
|
||||
/// WMI service state value for a started host instance.
|
||||
/// </summary>
|
||||
public const int HostStarted = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a receive location enabled flag into the text used in reports.
|
||||
/// </summary>
|
||||
/// <param name="enabled">True when the receive location is enabled.</param>
|
||||
/// <returns>A display value for the receive location state.</returns>
|
||||
public static string FormatReceiveLocation(bool enabled)
|
||||
{
|
||||
return enabled ? "Aktiviert" : "Deaktiviert";
|
||||
return enabled ? "Enabled" : "Disabled";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an MSBTS_SendPort.Status value into a display string.
|
||||
/// </summary>
|
||||
/// <param name="status">The raw WMI send port status value.</param>
|
||||
/// <returns>A known status name or an Unknown value with the raw code.</returns>
|
||||
public static string FormatSendPort(int status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case SendPortStarted: return "Gestartet";
|
||||
case SendPortStopped: return "Gestoppt";
|
||||
case SendPortBound: return "Gebunden";
|
||||
default: return "Unbekannt (" + status + ")";
|
||||
case SendPortStarted: return "Started";
|
||||
case SendPortStopped: return "Stopped";
|
||||
case SendPortBound: return "Bound";
|
||||
default: return "Unknown (" + 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)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case OrchestrationStarted: return "Gestartet";
|
||||
case OrchestrationStopped: return "Gestoppt";
|
||||
case OrchestrationBound: return "Gebunden";
|
||||
case OrchestrationUnbound: return "Nicht gebunden";
|
||||
default: return "Unbekannt (" + status + ")";
|
||||
case OrchestrationStarted: return "Started";
|
||||
case OrchestrationStopped: return "Stopped";
|
||||
case OrchestrationBound: return "Bound";
|
||||
case OrchestrationUnbound: return "Unbound";
|
||||
default: return "Unknown (" + 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)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case HostStarted: return "Gestartet";
|
||||
case HostStartPending: return "Wird gestartet";
|
||||
case HostStopPending: return "Wird gestoppt";
|
||||
case HostStopped: return "Gestoppt";
|
||||
default: return "Unbekannt (" + state + ")";
|
||||
case HostStarted: return "Started";
|
||||
case HostStartPending: return "StartPending";
|
||||
case HostStopPending: return "StopPending";
|
||||
case HostStopped: return "Stopped";
|
||||
default: return "Unknown (" + state + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,34 +3,62 @@ using System.Runtime.Serialization;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents one captured BizTalk runtime state including application artifacts
|
||||
/// and host instances.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class BizTalkSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new snapshot with empty application and host instance collections.
|
||||
/// </summary>
|
||||
public BizTalkSnapshot()
|
||||
{
|
||||
Applications = new List<ApplicationSnapshot>();
|
||||
HostInstances = new List<HostInstanceState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tool version that created the snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string ToolVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local timestamp when the snapshot was created.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk server name used for the WMI connection.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk application snapshots captured from WMI.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public List<ApplicationSnapshot> Applications { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the host instances captured from the BizTalk group.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public List<HostInstanceState> HostInstances { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Groups the captured artifact states for one BizTalk application.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class ApplicationSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an application snapshot with empty artifact collections.
|
||||
/// </summary>
|
||||
public ApplicationSnapshot()
|
||||
{
|
||||
ReceiveLocations = new List<ReceiveLocationState>();
|
||||
@@ -38,100 +66,169 @@ namespace BizTalkPlatformManagementTool.Models
|
||||
Orchestrations = new List<OrchestrationState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk application name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive locations that belong to the application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public List<ReceiveLocationState> ReceiveLocations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the send ports that belong to the application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public List<SendPortState> SendPorts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the orchestrations that belong to the application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public List<OrchestrationState> Orchestrations { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the relevant WMI state for a BizTalk receive location.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class ReceiveLocationState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive location name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parent receive port name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string ReceivePortName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the receive location is enabled.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive adapter name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string AdapterName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive location transport address.
|
||||
/// </summary>
|
||||
[DataMember(Order = 6)]
|
||||
public string Address { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the relevant WMI state for a BizTalk send port.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class SendPortState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the send port name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw MSBTS_SendPort.Status value.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the primary transport adapter type.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string PrimaryTransportType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the primary transport address.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string PrimaryTransportAddress { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the relevant WMI state for a BizTalk orchestration.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class OrchestrationState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the orchestration name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw MSBTS_Orchestration.OrchestrationStatus value.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public int OrchestrationStatus { get; set; }
|
||||
|
||||
[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]
|
||||
public sealed class HostInstanceState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the host instance name used as the WMI key.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string InstanceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk host name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string HostName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server on which the host instance runs.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw MSBTS_HostInstance.ServiceState value.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public int RawState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted host instance state.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string StateText { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,56 +3,104 @@ using System.Runtime.Serialization;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains all differences detected between two BizTalk snapshots.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class SnapshotDiff
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new diff with empty artifact and host instance collections.
|
||||
/// </summary>
|
||||
public SnapshotDiff()
|
||||
{
|
||||
ArtifactDifferences = new List<ArtifactDiffEntry>();
|
||||
HostInstanceDifferences = new List<HostInstanceDiffEntry>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets state differences for receive locations, send ports and orchestrations.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public List<ArtifactDiffEntry> ArtifactDifferences { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets state differences for host instances.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public List<HostInstanceDiffEntry> HostInstanceDifferences { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a before/after state change for one BizTalk application artifact.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class ArtifactDiffEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the artifact category, such as SendPort or ReceiveLocation.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string ArtifactType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the artifact name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted state in the before snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string Before { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted state in the after snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string After { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a before/after state change for one BizTalk host instance.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class HostInstanceDiffEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the host instance name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string InstanceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk host name.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string HostName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server on which the host instance runs.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted state in the before snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string Before { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted state in the after snapshot.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string After { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,98 +3,402 @@ using System.Runtime.Serialization;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the supported operation plan modes.
|
||||
/// </summary>
|
||||
public enum OperationMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Plan mode for stopping BizTalk runtime artifacts before maintenance.
|
||||
/// </summary>
|
||||
Shutdown,
|
||||
Restore
|
||||
|
||||
/// <summary>
|
||||
/// Plan mode for returning BizTalk runtime artifacts to a captured state.
|
||||
/// </summary>
|
||||
Restore,
|
||||
|
||||
/// <summary>
|
||||
/// Recovery mode that uses an existing snapshot without creating or
|
||||
/// overwriting a new before snapshot.
|
||||
/// </summary>
|
||||
EmergencyRestore
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the artifact categories that can appear in an operation plan.
|
||||
/// </summary>
|
||||
public enum OperationStepKind
|
||||
{
|
||||
/// <summary>
|
||||
/// A receive location step.
|
||||
/// </summary>
|
||||
ReceiveLocation,
|
||||
|
||||
/// <summary>
|
||||
/// An operator confirmation boundary between shutdown phases.
|
||||
/// </summary>
|
||||
OperatorCheckpoint,
|
||||
|
||||
/// <summary>
|
||||
/// A send port step.
|
||||
/// </summary>
|
||||
SendPort,
|
||||
|
||||
/// <summary>
|
||||
/// An orchestration step.
|
||||
/// </summary>
|
||||
Orchestration,
|
||||
|
||||
/// <summary>
|
||||
/// A host instance step.
|
||||
/// </summary>
|
||||
HostInstance,
|
||||
|
||||
/// <summary>
|
||||
/// A Windows service prerequisite such as Enterprise Single Sign-On.
|
||||
/// </summary>
|
||||
WindowsService,
|
||||
|
||||
/// <summary>
|
||||
/// An informational step that is intentionally not executed.
|
||||
/// </summary>
|
||||
Note
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the ordered shutdown or restore plan written before any
|
||||
/// runtime-changing operation is executed.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class OperationPlan
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new operation plan with an empty step collection.
|
||||
/// </summary>
|
||||
public OperationPlan()
|
||||
{
|
||||
Steps = new List<OperationStep>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the plan mode as a serialized string.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local timestamp when the plan was created.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target server for server-scoped plan steps.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ordered operation steps.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public List<OperationStep> Steps { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes one executable or informational step in a shutdown or restore plan.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class OperationStep
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the artifact kind for this step.
|
||||
/// </summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Kind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the owning BizTalk application, when applicable.
|
||||
/// </summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the artifact or host instance name displayed to the user.
|
||||
/// </summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server associated with the step, when server scoped.
|
||||
/// </summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the human-readable action description.
|
||||
/// </summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WMI class used to resolve the runtime object.
|
||||
/// </summary>
|
||||
[DataMember(Order = 6)]
|
||||
public string WmiClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WMI key property used to locate the runtime object.
|
||||
/// </summary>
|
||||
[DataMember(Order = 7)]
|
||||
public string KeyProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WMI key value used to locate the runtime object.
|
||||
/// </summary>
|
||||
[DataMember(Order = 8)]
|
||||
public string KeyValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WMI method or service pseudo-method to execute.
|
||||
/// </summary>
|
||||
[DataMember(Order = 9)]
|
||||
public string MethodName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the numeric arguments passed to the WMI method.
|
||||
/// </summary>
|
||||
[DataMember(Order = 10)]
|
||||
public int[] Arguments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw WMI state expected after the method completes.
|
||||
/// </summary>
|
||||
[DataMember(Order = 11)]
|
||||
public int? TargetState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the step should be executed.
|
||||
/// </summary>
|
||||
[DataMember(Order = 12)]
|
||||
public bool Execute { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an operator-facing warning for skipped or risky steps.
|
||||
/// </summary>
|
||||
[DataMember(Order = 13)]
|
||||
public string Warning { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the receive adapter name used for dependency preflight.</summary>
|
||||
[DataMember(Order = 14, EmitDefaultValue = false)]
|
||||
public string QualifierProperty { get; set; }
|
||||
public string AdapterName { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the receive transport address used to identify scheduler URIs.</summary>
|
||||
[DataMember(Order = 15, EmitDefaultValue = false)]
|
||||
public string QualifierValue { get; set; }
|
||||
public string Address { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains user-selected runtime options for snapshot, shutdown and restore actions.
|
||||
/// </summary>
|
||||
public sealed class OperationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the BizTalk server used for WMI operations.
|
||||
/// </summary>
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the directory where snapshots, plans, reports and diffs are written.
|
||||
/// </summary>
|
||||
public string OutputDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state file used as restore input.
|
||||
/// </summary>
|
||||
public string StateFile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether operations should only be logged.
|
||||
/// </summary>
|
||||
public bool DryRun { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of seconds to wait for a target runtime state.
|
||||
/// </summary>
|
||||
public int WaitTimeoutSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of seconds between WMI polling attempts.
|
||||
/// </summary>
|
||||
public int PollIntervalSeconds { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the outcome recorded for one attempted operation-plan step.
|
||||
/// </summary>
|
||||
public static class OperationStepOutcomes
|
||||
{
|
||||
/// <summary>The step was executed and reached its target state.</summary>
|
||||
public const string Succeeded = "Succeeded";
|
||||
|
||||
/// <summary>The runtime object was already in the requested target state.</summary>
|
||||
public const string AlreadySatisfied = "AlreadySatisfied";
|
||||
|
||||
/// <summary>The step was intentionally not executable.</summary>
|
||||
public const string Skipped = "Skipped";
|
||||
|
||||
/// <summary>The step was shown without mutation because dry-run was enabled.</summary>
|
||||
public const string DryRun = "DryRun";
|
||||
|
||||
/// <summary>The step failed, while subsequent independent steps were still attempted.</summary>
|
||||
public const string Failed = "Failed";
|
||||
|
||||
/// <summary>The operator explicitly confirmed a non-mutating checkpoint.</summary>
|
||||
public const string Confirmed = "Confirmed";
|
||||
|
||||
/// <summary>The step was not reached because the operator stopped at a checkpoint.</summary>
|
||||
public const string NotExecuted = "NotExecuted";
|
||||
|
||||
/// <summary>The operator declined to continue at a checkpoint.</summary>
|
||||
public const string Declined = "Declined";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one durable step result from a shutdown, restore or emergency restore.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class OperationStepResult
|
||||
{
|
||||
/// <summary>Gets or sets the one-based plan-step number.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public int Index { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the artifact kind.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Kind { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the owning BizTalk application.</summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Application { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the artifact, host instance or service name.</summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the requested action.</summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string Action { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the stable outcome value.</summary>
|
||||
[DataMember(Order = 6)]
|
||||
public string Outcome { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the local start timestamp.</summary>
|
||||
[DataMember(Order = 7)]
|
||||
public string StartedAt { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the local completion timestamp.</summary>
|
||||
[DataMember(Order = 8)]
|
||||
public string FinishedAt { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the complete operator-facing exception chain.</summary>
|
||||
[DataMember(Order = 9)]
|
||||
public string Error { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Durable summary of a best-effort plan execution. A failed step never
|
||||
/// prevents later independent steps from being attempted.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class OperationExecutionReport
|
||||
{
|
||||
/// <summary>Initializes an empty report.</summary>
|
||||
public OperationExecutionReport()
|
||||
{
|
||||
Steps = new List<OperationStepResult>();
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the shutdown or restore mode.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Mode { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the target server.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>Gets or sets whether the execution was a dry-run.</summary>
|
||||
[DataMember(Order = 3)]
|
||||
public bool DryRun { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the local execution start timestamp.</summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string StartedAt { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the local execution completion timestamp.</summary>
|
||||
[DataMember(Order = 5)]
|
||||
public string FinishedAt { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the number of successfully executed steps.</summary>
|
||||
[DataMember(Order = 6)]
|
||||
public int SucceededCount { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the number of steps already at their target state.</summary>
|
||||
[DataMember(Order = 7)]
|
||||
public int AlreadySatisfiedCount { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the number of intentionally skipped steps.</summary>
|
||||
[DataMember(Order = 8)]
|
||||
public int SkippedCount { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the number of dry-run-only steps.</summary>
|
||||
[DataMember(Order = 9)]
|
||||
public int DryRunCount { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the number of failed steps.</summary>
|
||||
[DataMember(Order = 10)]
|
||||
public int FailedCount { get; set; }
|
||||
|
||||
/// <summary>Gets or sets an error that prevented runtime initialization.</summary>
|
||||
[DataMember(Order = 11)]
|
||||
public string InitializationError { get; set; }
|
||||
|
||||
/// <summary>Gets or sets an error encountered while creating the post-operation snapshot.</summary>
|
||||
[DataMember(Order = 12)]
|
||||
public string PostSnapshotError { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the ordered per-step results.</summary>
|
||||
[DataMember(Order = 13)]
|
||||
public List<OperationStepResult> Steps { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the last durable operator-checkpoint decision.</summary>
|
||||
[DataMember(Order = 14, EmitDefaultValue = false)]
|
||||
public string CheckpointDecision { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the local timestamp of the checkpoint decision.</summary>
|
||||
[DataMember(Order = 15, EmitDefaultValue = false)]
|
||||
public string CheckpointAt { get; set; }
|
||||
|
||||
/// <summary>Gets or sets whether the operator stopped the remaining shutdown phases.</summary>
|
||||
[DataMember(Order = 16)]
|
||||
public bool OperatorStopped { get; set; }
|
||||
|
||||
/// <summary>Gets or sets how many remaining plan rows were not reached.</summary>
|
||||
[DataMember(Order = 17)]
|
||||
public int NotExecutedCount { get; set; }
|
||||
|
||||
/// <summary>Gets whether operator attention is required.</summary>
|
||||
public bool HasFailures
|
||||
{
|
||||
get { return FailedCount > 0 || !string.IsNullOrWhiteSpace(InitializationError) || !string.IsNullOrWhiteSpace(PostSnapshotError); }
|
||||
}
|
||||
|
||||
/// <summary>Gets whether failure or an intentional operator stop needs review.</summary>
|
||||
public bool RequiresOperatorReview
|
||||
{
|
||||
get { return HasFailures || OperatorStopped; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,70 @@
|
||||
using System;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using BizTalkPlatformManagementTool.Ui;
|
||||
|
||||
namespace BizTalkPlatformManagementTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the WinForms application entry point and startup guard checks.
|
||||
/// </summary>
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts the application after verifying that BizTalk WMI operations can run elevated.
|
||||
/// </summary>
|
||||
/// <param name="args">Befehlszeilenargumente; <c>--self-test</c> startet die WMI-freie Installerprüfung.</param>
|
||||
/// <returns>Null bei erfolgreichem Abschluss, andernfalls ein prozessgeeigneter Fehlercode.</returns>
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
// Der Self-Test muss ohne Administratorprüfung und ohne WinForms-Oberfläche laufen,
|
||||
// damit der Installer ihn bereits im isolierten Staging-Verzeichnis ausführen kann.
|
||||
if (args != null && args.Length == 1 && string.Equals(args[0], "--self-test", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return RuntimeSelfTest.Run();
|
||||
}
|
||||
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
|
||||
Application.ThreadException += delegate(object sender, System.Threading.ThreadExceptionEventArgs args)
|
||||
{
|
||||
MessageBox.Show("Unerwarteter Fehler: " + args.Exception.Message, "BizTalk Platform Management Tool", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
};
|
||||
|
||||
if (!IsRunningAsAdministrator())
|
||||
{
|
||||
MessageBox.Show(
|
||||
"Das BizTalk Platform Management Tool benötigt Administratorrechte.\r\n\r\n" +
|
||||
"Bitte starten Sie die Anwendung über ‚Als Administrator ausführen‘ erneut. " +
|
||||
"Ohne erhöhte Rechte sind BizTalk-WMI-Operationen nicht zuverlässig möglich.",
|
||||
"Administratorrechte erforderlich",
|
||||
"BizTalk Platform Management Tool must be started with administrator rights.\r\n\r\n" +
|
||||
"Please close the application and start it again with 'Run as administrator'. " +
|
||||
"Without elevated rights, BizTalk WMI operations in root\\MicrosoftBizTalkServer will fail.",
|
||||
"Administrator Rights Required",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
Application.Run(new MainForm());
|
||||
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());
|
||||
GC.KeepAlive(mutex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the current Windows identity is a local administrator.
|
||||
/// </summary>
|
||||
/// <returns>True when the process is elevated as administrator; otherwise false.</returns>
|
||||
private static bool IsRunningAsAdministrator()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("BizTalk Platform Management Tool")]
|
||||
[assembly: AssemblyDescription("Kontrollierte BizTalk-Wartung mit Zustandswiederherstellung")]
|
||||
[assembly: AssemblyCompany("JR IT Services")]
|
||||
[assembly: AssemblyDescription("Controlled BizTalk Server maintenance snapshots, plans and runtime operations")]
|
||||
[assembly: AssemblyCompany("BEW")]
|
||||
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2026")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("DA63FBDA-4B7F-4D93-B63E-652F26A94796")]
|
||||
[assembly: AssemblyVersion("2.1.0.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.0.0")]
|
||||
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
|
||||
[assembly: AssemblyVersion("2.3.4.0")]
|
||||
[assembly: AssemblyFileVersion("2.3.4.0")]
|
||||
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
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);
|
||||
|
||||
var runtimeLogDirectory = Path.Combine(directory, "runtime-logs");
|
||||
var runtimeLogger = new OperationLogger(null, runtimeLogDirectory);
|
||||
runtimeLogger.Info("runtime log self-test marker");
|
||||
if (!runtimeLogger.IsFileLoggingAvailable
|
||||
|| !File.Exists(runtimeLogger.LogFilePath)
|
||||
|| !runtimeLogger.ReadRecentEntries(10).Any(x => x.Message == "runtime log self-test marker"))
|
||||
{
|
||||
throw new InvalidOperationException("Runtime log write/read self-test failed.");
|
||||
}
|
||||
|
||||
var operationService = new BizTalkOperationService(null);
|
||||
var emergencyPlan = operationService.CreateEmergencyRestorePlan(loaded, loaded.Server);
|
||||
if (emergencyPlan.Steps.Count == 0 || emergencyPlan.Steps[0].Kind != "WindowsService" || emergencyPlan.Steps[0].Name != "ENTSSO")
|
||||
{
|
||||
throw new InvalidOperationException("Emergency restore self-test did not place ENTSSO first.");
|
||||
}
|
||||
var dryRunReport = operationService.ExecutePlan(emergencyPlan, new OperationOptions
|
||||
{
|
||||
Server = loaded.Server,
|
||||
OutputDirectory = directory,
|
||||
StateFile = snapshotPath,
|
||||
DryRun = true,
|
||||
WaitTimeoutSeconds = 30,
|
||||
PollIntervalSeconds = 1
|
||||
});
|
||||
if (dryRunReport.HasFailures || dryRunReport.DryRunCount != emergencyPlan.Steps.Count(x => x.Execute))
|
||||
{
|
||||
throw new InvalidOperationException("Emergency restore dry-run self-test returned an unexpected result.");
|
||||
}
|
||||
operationService.SaveExecutionReport(directory, "emergency-result.json", dryRunReport);
|
||||
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,395 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves custom-adapter and BizTalk support assemblies from installed product
|
||||
/// directories for the lifetime of one real operation-plan execution.
|
||||
/// </summary>
|
||||
internal sealed class AdapterAssemblyResolver : IDisposable
|
||||
{
|
||||
/// <summary>The dependency whose absence breaks ScheduledTask Enable/Disable validation.</summary>
|
||||
internal const string SchedulerAssemblyName = "Microsoft.BizTalk.Scheduler";
|
||||
|
||||
/// <summary>Logger receiving all search-path and load diagnostics.</summary>
|
||||
private readonly OperationLogger _logger;
|
||||
|
||||
/// <summary>Trusted directories searched after normal CLR resolution has failed.</summary>
|
||||
private readonly List<string> _searchDirectories;
|
||||
|
||||
/// <summary>Prevents duplicate event registration and unregisters safely.</summary>
|
||||
private bool _attached;
|
||||
|
||||
/// <summary>Prevents repeated ScheduledTask dependency preflight in one plan.</summary>
|
||||
private bool _schedulerPrepared;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes and attaches the process-local resolver.
|
||||
/// </summary>
|
||||
/// <param name="logger">The operation logger used for support diagnostics.</param>
|
||||
public AdapterAssemblyResolver(OperationLogger logger)
|
||||
: this(logger, DiscoverSearchDirectories())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a resolver with explicit directories for regression tests.</summary>
|
||||
/// <param name="logger">Optional logger.</param>
|
||||
/// <param name="searchDirectories">Directories that may contain dependencies.</param>
|
||||
internal AdapterAssemblyResolver(OperationLogger logger, IEnumerable<string> searchDirectories)
|
||||
{
|
||||
_logger = logger;
|
||||
_searchDirectories = NormalizeDirectories(searchDirectories).ToList();
|
||||
AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
|
||||
_attached = true;
|
||||
}
|
||||
|
||||
/// <summary>Gets the immutable ordered dependency search path.</summary>
|
||||
internal IList<string> SearchDirectories
|
||||
{
|
||||
get { return _searchDirectories.AsReadOnly(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Preloads the BizTalk Scheduler dependency before WMI validates a Scheduler
|
||||
/// receive location and emits actionable diagnostics when it cannot be found.
|
||||
/// </summary>
|
||||
public void PrepareScheduledTaskAdapter()
|
||||
{
|
||||
if (_schedulerPrepared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_schedulerPrepared = true;
|
||||
|
||||
Info("ScheduledTask adapter preflight. ProcessBitness=" + (Environment.Is64BitProcess ? "64" : "32")
|
||||
+ "; SearchDirectories=" + string.Join(" | ", _searchDirectories.ToArray()));
|
||||
|
||||
var alreadyLoaded = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.FirstOrDefault(x => string.Equals(x.GetName().Name, SchedulerAssemblyName, StringComparison.OrdinalIgnoreCase));
|
||||
if (alreadyLoaded != null)
|
||||
{
|
||||
Success("ScheduledTask dependency already loaded: " + DescribeAssembly(alreadyLoaded));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Normal CLR/GAC resolution remains authoritative. If it fails, the attached
|
||||
// resolver gets the same request and may satisfy it from an installed product path.
|
||||
var normallyResolved = Assembly.Load(new AssemblyName(SchedulerAssemblyName));
|
||||
Success("ScheduledTask dependency resolved through CLR/GAC: " + DescribeAssembly(normallyResolved));
|
||||
return;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// The explicit path diagnostic below reports every searched installation directory.
|
||||
}
|
||||
catch (FileLoadException ex)
|
||||
{
|
||||
Warning("ScheduledTask dependency CLR/GAC resolution found an incompatible or unloadable assembly. " + ExceptionDiagnostics.Format(ex));
|
||||
}
|
||||
|
||||
var path = FindCandidateFile(SchedulerAssemblyName, null, _searchDirectories);
|
||||
if (path == null)
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
"ScheduledTask receive-location control requires Microsoft.BizTalk.Scheduler.dll. "
|
||||
+ "The assembly was not found in the BizTalk installation directory, the ScheduledTask Adapter directory, "
|
||||
+ "or configured AdapterAssemblySearchPaths. Search directories: "
|
||||
+ string.Join(" | ", _searchDirectories.ToArray())
|
||||
+ ". Install the matching BizTalk Scheduler assembly in the GAC or add its existing directory to "
|
||||
+ "BizTalkPlatformManagementTool.exe.config; do not copy an assembly from another BizTalk version.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.LoadFrom(path);
|
||||
Success("ScheduledTask dependency loaded process-locally: " + DescribeAssembly(assembly));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"ScheduledTask dependency was found but could not be loaded from '" + path + "'. "
|
||||
+ ExceptionDiagnostics.Format(ex), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Unregisters the process-local resolver.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_attached)
|
||||
{
|
||||
AppDomain.CurrentDomain.AssemblyResolve -= ResolveAssembly;
|
||||
_attached = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Handles unresolved CLR assembly requests using identity-checked files.</summary>
|
||||
private Assembly ResolveAssembly(object sender, ResolveEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var requested = new AssemblyName(args.Name);
|
||||
var loaded = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => AssemblyIdentityMatches(requested, x.GetName()));
|
||||
if (loaded != null)
|
||||
{
|
||||
return loaded;
|
||||
}
|
||||
|
||||
var path = FindCandidateFile(requested.Name, requested, _searchDirectories);
|
||||
if (path == null)
|
||||
{
|
||||
Warning("Assembly resolution failed. Requested=" + args.Name
|
||||
+ "; RequestingAssembly=" + (args.RequestingAssembly == null ? "(unknown)" : args.RequestingAssembly.FullName)
|
||||
+ "; SearchDirectories=" + string.Join(" | ", _searchDirectories.ToArray()));
|
||||
return null;
|
||||
}
|
||||
|
||||
var assembly = Assembly.LoadFrom(path);
|
||||
Success("Resolved adapter dependency process-locally. Requested=" + args.Name
|
||||
+ "; Loaded=" + DescribeAssembly(assembly));
|
||||
return assembly;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Warning("Assembly resolution raised an error. Requested=" + args.Name + "; " + ExceptionDiagnostics.Format(ex));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an identity-compatible DLL without loading it into the current AppDomain.
|
||||
/// </summary>
|
||||
/// <param name="simpleName">Requested simple assembly name.</param>
|
||||
/// <param name="requested">Full requested identity, or null to accept any installed version.</param>
|
||||
/// <param name="directories">Trusted directories to search.</param>
|
||||
/// <returns>The matching DLL path, or null.</returns>
|
||||
internal static string FindCandidateFile(string simpleName, AssemblyName requested, IEnumerable<string> directories)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(simpleName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
foreach (var directory in NormalizeDirectories(directories))
|
||||
{
|
||||
var candidate = Path.Combine(directory, simpleName + ".dll");
|
||||
if (!File.Exists(candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
var actual = AssemblyName.GetAssemblyName(candidate);
|
||||
if (requested == null
|
||||
? string.Equals(actual.Name, simpleName, StringComparison.OrdinalIgnoreCase)
|
||||
: AssemblyIdentityMatches(requested, actual))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A native, corrupt or unrelated file is never loaded merely because its file name matches.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Compares simple name, requested version, culture and public key token.</summary>
|
||||
private static bool AssemblyIdentityMatches(AssemblyName requested, AssemblyName actual)
|
||||
{
|
||||
if (requested == null || actual == null
|
||||
|| !string.Equals(requested.Name, actual.Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (requested.Version != null && actual.Version != requested.Version)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(requested.CultureName)
|
||||
&& !string.Equals(requested.CultureName, actual.CultureName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var expectedToken = requested.GetPublicKeyToken();
|
||||
var actualToken = actual.GetPublicKeyToken();
|
||||
return expectedToken == null || expectedToken.Length == 0 || TokensEqual(expectedToken, actualToken);
|
||||
}
|
||||
|
||||
/// <summary>Compares strong-name public key tokens.</summary>
|
||||
private static bool TokensEqual(byte[] left, byte[] right)
|
||||
{
|
||||
if (left == null || right == null || left.Length != right.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < left.Length; i++)
|
||||
{
|
||||
if (left[i] != right[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Discovers BizTalk, ScheduledTask and explicitly configured directories.</summary>
|
||||
internal static IEnumerable<string> DiscoverSearchDirectories()
|
||||
{
|
||||
var result = new List<string>();
|
||||
AddConfiguredDirectories(result);
|
||||
AddRegistryDirectories(result);
|
||||
|
||||
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
|
||||
AddMatchingDirectories(result, programFilesX86, "Microsoft BizTalk Server*");
|
||||
AddMatchingDirectories(result, programFilesX86, "BizTalk ScheduledTask Adapter*");
|
||||
AddMatchingDirectories(result, programFilesX86, "Biztalk ScheduledTask Adapter*");
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Adds semicolon-delimited paths from App.config after environment expansion.</summary>
|
||||
private static void AddConfiguredDirectories(ICollection<string> result)
|
||||
{
|
||||
try
|
||||
{
|
||||
var configured = ConfigurationManager.AppSettings["AdapterAssemblySearchPaths"];
|
||||
if (string.IsNullOrWhiteSpace(configured))
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var value in configured.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
result.Add(Environment.ExpandEnvironmentVariables(value.Trim()));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Registry and conventional installation directories remain available.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds known assembly-path values from both registry views.</summary>
|
||||
private static void AddRegistryDirectories(ICollection<string> result)
|
||||
{
|
||||
var views = new[] { RegistryView.Registry32, RegistryView.Registry64 };
|
||||
foreach (var view in views)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
|
||||
{
|
||||
AddRegistryTreeDirectories(baseKey, @"SOFTWARE\Microsoft\BizTalk Server\3.0", result, 0);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Registry discovery is best-effort and never blocks normal BizTalk adapters.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reads installation and assembly directories from a shallow BizTalk registry tree.</summary>
|
||||
private static void AddRegistryTreeDirectories(RegistryKey baseKey, string subKeyName, ICollection<string> result, int depth)
|
||||
{
|
||||
if (depth > 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
using (var key = baseKey.OpenSubKey(subKeyName, false))
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var valueName in key.GetValueNames())
|
||||
{
|
||||
var value = key.GetValue(valueName) as string;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
value = Environment.ExpandEnvironmentVariables(value.Trim().Trim('"'));
|
||||
if (value.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.Add(Path.GetDirectoryName(value));
|
||||
}
|
||||
else if (Directory.Exists(value))
|
||||
{
|
||||
result.Add(value);
|
||||
}
|
||||
}
|
||||
foreach (var child in key.GetSubKeyNames())
|
||||
{
|
||||
AddRegistryTreeDirectories(baseKey, subKeyName + "\\" + child, result, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds directories matching one non-recursive product-folder pattern.</summary>
|
||||
private static void AddMatchingDirectories(ICollection<string> result, string parent, string pattern)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(parent))
|
||||
{
|
||||
foreach (var directory in Directory.GetDirectories(parent, pattern, SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
result.Add(directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A denied optional discovery path is reported later if Scheduler preflight is required.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Normalizes, de-duplicates and filters existing directories.</summary>
|
||||
private static IEnumerable<string> NormalizeDirectories(IEnumerable<string> directories)
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var value in directories ?? Enumerable.Empty<string>())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string fullPath;
|
||||
try
|
||||
{
|
||||
fullPath = Path.GetFullPath(value.Trim().Trim('"')).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (Directory.Exists(fullPath) && seen.Add(fullPath))
|
||||
{
|
||||
yield return fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Builds a diagnostic assembly description.</summary>
|
||||
private static string DescribeAssembly(Assembly assembly)
|
||||
{
|
||||
return assembly.FullName + "; Location=" + assembly.Location;
|
||||
}
|
||||
|
||||
/// <summary>Writes informational diagnostics when a logger is available.</summary>
|
||||
private void Info(string message) { if (_logger != null) _logger.Info(message); }
|
||||
|
||||
/// <summary>Writes warning diagnostics when a logger is available.</summary>
|
||||
private void Warning(string message) { if (_logger != null) _logger.Warning(message); }
|
||||
|
||||
/// <summary>Writes successful dependency diagnostics when a logger is available.</summary>
|
||||
private void Success(string message) { if (_logger != null) _logger.Success(message); }
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SqlClient;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Management;
|
||||
using System.Reflection;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves runtime artifacts to their owning BizTalk applications by reading the
|
||||
/// ExplorerOM application hierarchy without a compile-time BizTalk dependency.
|
||||
/// </summary>
|
||||
internal sealed class BizTalkApplicationResolver
|
||||
{
|
||||
/// <summary>The strong-name token used by Microsoft BizTalk product assemblies.</summary>
|
||||
private static readonly byte[] BizTalkPublicKeyToken = { 0x31, 0xbf, 0x38, 0x56, 0xad, 0x36, 0x4e, 0x35 };
|
||||
|
||||
/// <summary>Maps group-unique send-port names to application names.</summary>
|
||||
private readonly ArtifactApplicationMap _sendPorts = new ArtifactApplicationMap();
|
||||
|
||||
/// <summary>Maps receive-port names to application names.</summary>
|
||||
private readonly ArtifactApplicationMap _receivePorts = new ArtifactApplicationMap();
|
||||
|
||||
/// <summary>Maps receive-location names to application names.</summary>
|
||||
private readonly ArtifactApplicationMap _receiveLocations = new ArtifactApplicationMap();
|
||||
|
||||
/// <summary>Maps orchestration names and supported aliases to application names.</summary>
|
||||
private readonly ArtifactApplicationMap _orchestrations = new ArtifactApplicationMap();
|
||||
|
||||
/// <summary>Initializes an empty resolver, primarily for deterministic regression tests.</summary>
|
||||
internal BizTalkApplicationResolver()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the BizTalk Management database location through WMI and builds an
|
||||
/// application/artifact index through the locally installed ExplorerOM assembly.
|
||||
/// </summary>
|
||||
/// <param name="client">Connected BizTalk WMI client.</param>
|
||||
/// <param name="logger">Optional operation logger for support diagnostics.</param>
|
||||
/// <returns>A resolver populated from the BizTalk application catalog.</returns>
|
||||
internal static BizTalkApplicationResolver Load(BizTalkWmiClient client, OperationLogger logger)
|
||||
{
|
||||
if (client == null)
|
||||
{
|
||||
throw new ArgumentNullException("client");
|
||||
}
|
||||
|
||||
string databaseServer;
|
||||
string databaseName;
|
||||
ReadManagementDatabase(client, out databaseServer, out databaseName);
|
||||
|
||||
var assembly = LoadExplorerOmAssembly();
|
||||
var catalogType = assembly.GetType("Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer", true, false);
|
||||
var catalog = Activator.CreateInstance(catalogType);
|
||||
try
|
||||
{
|
||||
var connection = new SqlConnectionStringBuilder
|
||||
{
|
||||
DataSource = databaseServer,
|
||||
InitialCatalog = databaseName,
|
||||
IntegratedSecurity = true,
|
||||
ConnectTimeout = 15,
|
||||
ApplicationName = "BizTalkPlatformManagementTool"
|
||||
};
|
||||
SetProperty(catalog, "ConnectionString", connection.ConnectionString);
|
||||
|
||||
var resolver = new BizTalkApplicationResolver();
|
||||
resolver.IndexApplications(GetProperty(catalog, "Applications"));
|
||||
Info(logger, "BizTalk application catalog loaded through ExplorerOM. "
|
||||
+ "Applications=" + resolver.ApplicationCount
|
||||
+ "; SendPorts=" + resolver._sendPorts.Count
|
||||
+ "; ReceivePorts=" + resolver._receivePorts.Count
|
||||
+ "; ReceiveLocations=" + resolver._receiveLocations.Count
|
||||
+ "; Orchestrations=" + resolver._orchestrations.Count
|
||||
+ "; ManagementDatabase=" + databaseServer + "\\" + databaseName + ".");
|
||||
return resolver;
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"BizTalk ExplorerOM could not read the application catalog from "
|
||||
+ databaseServer + "\\" + databaseName + ". "
|
||||
+ ExceptionDiagnostics.Format(ex.InnerException ?? ex), ex.InnerException ?? ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
var disposable = catalog as IDisposable;
|
||||
if (disposable != null)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the number of distinct application names represented in the index.</summary>
|
||||
internal int ApplicationCount
|
||||
{
|
||||
get
|
||||
{
|
||||
return _sendPorts.ApplicationNames
|
||||
.Concat(_receivePorts.ApplicationNames)
|
||||
.Concat(_receiveLocations.ApplicationNames)
|
||||
.Concat(_orchestrations.ApplicationNames)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Count();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds a send-port ownership record to the index.</summary>
|
||||
/// <param name="application">Owning BizTalk application.</param>
|
||||
/// <param name="name">Send-port name.</param>
|
||||
internal void AddSendPort(string application, string name)
|
||||
{
|
||||
_sendPorts.Add(application, name);
|
||||
}
|
||||
|
||||
/// <summary>Adds a receive-port ownership record to the index.</summary>
|
||||
/// <param name="application">Owning BizTalk application.</param>
|
||||
/// <param name="name">Receive-port name.</param>
|
||||
internal void AddReceivePort(string application, string name)
|
||||
{
|
||||
_receivePorts.Add(application, name);
|
||||
}
|
||||
|
||||
/// <summary>Adds a receive-location ownership record to the index.</summary>
|
||||
/// <param name="application">Owning BizTalk application.</param>
|
||||
/// <param name="name">Receive-location name.</param>
|
||||
internal void AddReceiveLocation(string application, string name)
|
||||
{
|
||||
_receiveLocations.Add(application, name);
|
||||
}
|
||||
|
||||
/// <summary>Adds an orchestration ownership record to the index.</summary>
|
||||
/// <param name="application">Owning BizTalk application.</param>
|
||||
/// <param name="name">Orchestration name or alias.</param>
|
||||
internal void AddOrchestration(string application, string name)
|
||||
{
|
||||
_orchestrations.Add(application, name);
|
||||
}
|
||||
|
||||
/// <summary>Resolves a send port to its owning application.</summary>
|
||||
/// <param name="name">Send-port name.</param>
|
||||
/// <returns>The application name, or null when absent or ambiguous.</returns>
|
||||
internal string ResolveSendPort(string name)
|
||||
{
|
||||
return _sendPorts.Resolve(name);
|
||||
}
|
||||
|
||||
/// <summary>Resolves a receive location, with its receive port as a stable fallback.</summary>
|
||||
/// <param name="locationName">Receive-location name.</param>
|
||||
/// <param name="receivePortName">Parent receive-port name.</param>
|
||||
/// <returns>The application name, or null when absent or ambiguous.</returns>
|
||||
internal string ResolveReceiveLocation(string locationName, string receivePortName)
|
||||
{
|
||||
return _receiveLocations.Resolve(locationName) ?? _receivePorts.Resolve(receivePortName);
|
||||
}
|
||||
|
||||
/// <summary>Resolves an orchestration to its owning application.</summary>
|
||||
/// <param name="name">WMI orchestration name.</param>
|
||||
/// <returns>The application name, or null when absent or ambiguous.</returns>
|
||||
internal string ResolveOrchestration(string name)
|
||||
{
|
||||
return _orchestrations.Resolve(name);
|
||||
}
|
||||
|
||||
/// <summary>Enumerates all applications and their runtime artifact collections.</summary>
|
||||
/// <param name="applications">ExplorerOM application collection.</param>
|
||||
private void IndexApplications(object applications)
|
||||
{
|
||||
foreach (var application in Enumerate(applications, "Applications"))
|
||||
{
|
||||
var applicationName = ReadString(application, "Name");
|
||||
if (string.IsNullOrWhiteSpace(applicationName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var sendPort in Enumerate(GetProperty(application, "SendPorts"), "Application.SendPorts"))
|
||||
{
|
||||
AddSendPort(applicationName, ReadString(sendPort, "Name"));
|
||||
}
|
||||
|
||||
foreach (var receivePort in Enumerate(GetProperty(application, "ReceivePorts"), "Application.ReceivePorts"))
|
||||
{
|
||||
AddReceivePort(applicationName, ReadString(receivePort, "Name"));
|
||||
foreach (var location in Enumerate(GetProperty(receivePort, "ReceiveLocations"), "ReceivePort.ReceiveLocations"))
|
||||
{
|
||||
AddReceiveLocation(applicationName, ReadString(location, "Name"));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var orchestration in Enumerate(GetProperty(application, "Orchestrations"), "Application.Orchestrations"))
|
||||
{
|
||||
AddOrchestration(applicationName, ReadString(orchestration, "FullName"));
|
||||
AddOrchestration(applicationName, ReadString(orchestration, "AssemblyQualifiedName"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reads the BizTalk Management database endpoint from MSBTS_GroupSetting.</summary>
|
||||
/// <param name="client">Connected WMI client.</param>
|
||||
/// <param name="server">Resolved SQL Server name.</param>
|
||||
/// <param name="database">Resolved management database name.</param>
|
||||
private static void ReadManagementDatabase(BizTalkWmiClient client, out string server, out string database)
|
||||
{
|
||||
var settings = client.Query("MSBTS_GroupSetting");
|
||||
try
|
||||
{
|
||||
if (settings.Count != 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Expected exactly one MSBTS_GroupSetting instance but found " + settings.Count + ".");
|
||||
}
|
||||
server = BizTalkWmiClient.SafeGetString(settings[0], "MgmtDbServerName", null);
|
||||
database = BizTalkWmiClient.SafeGetString(settings[0], "MgmtDbName", null);
|
||||
if (string.IsNullOrWhiteSpace(server) || string.IsNullOrWhiteSpace(database))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"MSBTS_GroupSetting did not expose MgmtDbServerName and MgmtDbName.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (ManagementObject setting in settings)
|
||||
{
|
||||
setting.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Loads an identity-verified ExplorerOM assembly from the CLR/GAC or installed BizTalk folders.</summary>
|
||||
/// <returns>The loaded Microsoft.BizTalk.ExplorerOM assembly.</returns>
|
||||
private static Assembly LoadExplorerOmAssembly()
|
||||
{
|
||||
var requested = new AssemblyName("Microsoft.BizTalk.ExplorerOM");
|
||||
requested.SetPublicKeyToken(BizTalkPublicKeyToken);
|
||||
|
||||
var loaded = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.FirstOrDefault(x => string.Equals(x.GetName().Name, requested.Name, StringComparison.OrdinalIgnoreCase));
|
||||
if (loaded != null)
|
||||
{
|
||||
RequireMicrosoftIdentity(loaded);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
var explorerType = Type.GetType(
|
||||
"Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer, Microsoft.BizTalk.ExplorerOM",
|
||||
false);
|
||||
if (explorerType != null)
|
||||
{
|
||||
RequireMicrosoftIdentity(explorerType.Assembly);
|
||||
return explorerType.Assembly;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Assembly.Load(requested);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// The installed-directory fallback below keeps non-GAC BizTalk admin installations usable.
|
||||
}
|
||||
catch (FileLoadException)
|
||||
{
|
||||
// The fallback still enforces the Microsoft strong-name identity before loading a file.
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
// ExplorerOM versions vary across supported BizTalk installations. This GAC-only
|
||||
// compatibility API is followed by an explicit Microsoft public-key-token check.
|
||||
var partial = Assembly.LoadWithPartialName(requested.Name);
|
||||
#pragma warning restore 618
|
||||
if (partial != null)
|
||||
{
|
||||
RequireMicrosoftIdentity(partial);
|
||||
return partial;
|
||||
}
|
||||
|
||||
var path = AdapterAssemblyResolver.FindCandidateFile(
|
||||
requested.Name,
|
||||
requested,
|
||||
AdapterAssemblyResolver.DiscoverSearchDirectories());
|
||||
if (path == null)
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
"Microsoft.BizTalk.ExplorerOM.dll was not found through CLR/GAC resolution or in the configured/installed BizTalk directories. "
|
||||
+ "The application association cannot be derived from the runtime WMI classes alone.");
|
||||
}
|
||||
var fromPath = Assembly.LoadFrom(path);
|
||||
RequireMicrosoftIdentity(fromPath);
|
||||
return fromPath;
|
||||
}
|
||||
|
||||
/// <summary>Rejects a same-named assembly that is not signed with the BizTalk product token.</summary>
|
||||
/// <param name="assembly">Loaded ExplorerOM candidate.</param>
|
||||
private static void RequireMicrosoftIdentity(Assembly assembly)
|
||||
{
|
||||
var actual = assembly == null ? null : assembly.GetName().GetPublicKeyToken();
|
||||
if (actual == null || actual.Length != BizTalkPublicKeyToken.Length)
|
||||
{
|
||||
throw new FileLoadException("Microsoft.BizTalk.ExplorerOM has no valid BizTalk product public key token.");
|
||||
}
|
||||
for (var index = 0; index < actual.Length; index++)
|
||||
{
|
||||
if (actual[index] != BizTalkPublicKeyToken[index])
|
||||
{
|
||||
throw new FileLoadException("Microsoft.BizTalk.ExplorerOM has an unexpected public key token.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets a required public instance property through reflection.</summary>
|
||||
/// <param name="instance">Object that owns the property.</param>
|
||||
/// <param name="name">Property name.</param>
|
||||
/// <returns>The property value.</returns>
|
||||
private static object GetProperty(object instance, string name)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
throw new InvalidOperationException("ExplorerOM object was null while reading property '" + name + "'.");
|
||||
}
|
||||
var property = instance.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public);
|
||||
if (property == null)
|
||||
{
|
||||
throw new MissingMemberException(instance.GetType().FullName, name);
|
||||
}
|
||||
return property.GetValue(instance, null);
|
||||
}
|
||||
|
||||
/// <summary>Sets a required public instance property through reflection.</summary>
|
||||
/// <param name="instance">Object that owns the property.</param>
|
||||
/// <param name="name">Property name.</param>
|
||||
/// <param name="value">Value to assign.</param>
|
||||
private static void SetProperty(object instance, string name, object value)
|
||||
{
|
||||
var property = instance.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public);
|
||||
if (property == null || !property.CanWrite)
|
||||
{
|
||||
throw new MissingMemberException(instance.GetType().FullName, name);
|
||||
}
|
||||
property.SetValue(instance, value, null);
|
||||
}
|
||||
|
||||
/// <summary>Reads an optional string property from one ExplorerOM object.</summary>
|
||||
/// <param name="instance">ExplorerOM object.</param>
|
||||
/// <param name="name">Property name.</param>
|
||||
/// <returns>The trimmed string value, or null.</returns>
|
||||
private static string ReadString(object instance, string name)
|
||||
{
|
||||
var property = instance.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public);
|
||||
if (property == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var value = property.GetValue(instance, null) as string;
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
|
||||
/// <summary>Validates and enumerates one ExplorerOM collection.</summary>
|
||||
/// <param name="value">Collection object.</param>
|
||||
/// <param name="description">Property path used in diagnostics.</param>
|
||||
/// <returns>The collection as an enumerable sequence.</returns>
|
||||
private static IEnumerable Enumerate(object value, string description)
|
||||
{
|
||||
var enumerable = value as IEnumerable;
|
||||
if (enumerable == null)
|
||||
{
|
||||
throw new InvalidOperationException("ExplorerOM property '" + description + "' is not enumerable.");
|
||||
}
|
||||
return enumerable;
|
||||
}
|
||||
|
||||
/// <summary>Writes an informational message when logging is available.</summary>
|
||||
/// <param name="logger">Optional logger.</param>
|
||||
/// <param name="message">Message text.</param>
|
||||
private static void Info(OperationLogger logger, string message)
|
||||
{
|
||||
if (logger != null)
|
||||
{
|
||||
logger.Info(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stores one artifact-type mapping and rejects cross-application ambiguity.</summary>
|
||||
private sealed class ArtifactApplicationMap
|
||||
{
|
||||
/// <summary>Unambiguous name/application mappings.</summary>
|
||||
private readonly Dictionary<string, string> _applications = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Names observed under more than one application.</summary>
|
||||
private readonly HashSet<string> _ambiguous = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Gets the number of unambiguous artifact names.</summary>
|
||||
internal int Count { get { return _applications.Count; } }
|
||||
|
||||
/// <summary>Gets the represented application names.</summary>
|
||||
internal IEnumerable<string> ApplicationNames { get { return _applications.Values; } }
|
||||
|
||||
/// <summary>Adds one mapping unless the artifact name is blank or becomes ambiguous.</summary>
|
||||
/// <param name="application">Application name.</param>
|
||||
/// <param name="artifact">Artifact name.</param>
|
||||
internal void Add(string application, string artifact)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(application) || string.IsNullOrWhiteSpace(artifact))
|
||||
{
|
||||
return;
|
||||
}
|
||||
application = application.Trim();
|
||||
artifact = artifact.Trim();
|
||||
if (_ambiguous.Contains(artifact))
|
||||
{
|
||||
return;
|
||||
}
|
||||
string existing;
|
||||
if (_applications.TryGetValue(artifact, out existing)
|
||||
&& !string.Equals(existing, application, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_applications.Remove(artifact);
|
||||
_ambiguous.Add(artifact);
|
||||
return;
|
||||
}
|
||||
_applications[artifact] = application;
|
||||
}
|
||||
|
||||
/// <summary>Resolves one unambiguous artifact name.</summary>
|
||||
/// <param name="artifact">Artifact name.</param>
|
||||
/// <returns>The application name, or null.</returns>
|
||||
internal string Resolve(string artifact)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(artifact) || _ambiguous.Contains(artifact.Trim()))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string application;
|
||||
return _applications.TryGetValue(artifact.Trim(), out application) ? application : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,24 +6,53 @@ using System.Threading;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides the WMI access layer for BizTalk Server objects in root\MicrosoftBizTalkServer.
|
||||
/// </summary>
|
||||
public sealed class BizTalkWmiClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// BizTalk WMI namespace used for all platform management queries.
|
||||
/// </summary>
|
||||
private const string NamespacePath = "root\\MicrosoftBizTalkServer";
|
||||
|
||||
/// <summary>
|
||||
/// Target server for the WMI connection.
|
||||
/// </summary>
|
||||
private readonly string _server;
|
||||
|
||||
/// <summary>
|
||||
/// Logger used for WMI diagnostics and operation traces.
|
||||
/// </summary>
|
||||
private readonly OperationLogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Connected WMI management scope, created lazily or by Connect.
|
||||
/// </summary>
|
||||
private ManagementScope _scope;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new WMI client for the specified server.
|
||||
/// </summary>
|
||||
/// <param name="server">The target server name, or an empty value to use the local machine.</param>
|
||||
/// <param name="logger">The operation logger used for diagnostics and trace output.</param>
|
||||
public BizTalkWmiClient(string server, OperationLogger logger)
|
||||
{
|
||||
_server = string.IsNullOrWhiteSpace(server) ? Environment.MachineName : server.Trim();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the normalized target server name used by this client.
|
||||
/// </summary>
|
||||
public string Server
|
||||
{
|
||||
get { return _server; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the BizTalk WMI namespace on the target server.
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
var path = "\\\\" + _server + "\\" + NamespacePath;
|
||||
@@ -33,11 +62,22 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
_logger.Success("WMI connection established.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a broad WMI query for all instances of the requested BizTalk class.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <returns>The matching WMI objects. The caller owns the returned objects.</returns>
|
||||
public List<ManagementObject> Query(string className)
|
||||
{
|
||||
return Query(className, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a broad WMI query and optionally logs the query text.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <param name="logQuery">True to write the query to the operation log.</param>
|
||||
/// <returns>The matching WMI objects. The caller owns the returned objects.</returns>
|
||||
private List<ManagementObject> Query(string className, bool logQuery)
|
||||
{
|
||||
EnsureConnected();
|
||||
@@ -63,23 +103,38 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
catch (ManagementException ex)
|
||||
{
|
||||
foreach (var item in result)
|
||||
{
|
||||
item.Dispose();
|
||||
}
|
||||
throw new InvalidOperationException("WMI query failed. Query: " + queryText + ". WMI error: " + ex.Message, ex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds one WMI object by comparing a property value client-side.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <param name="propertyName">The property used as the lookup key.</param>
|
||||
/// <param name="value">The expected property value.</param>
|
||||
/// <returns>The matching object, or null when no object matches.</returns>
|
||||
public ManagementObject FindByProperty(string className, string propertyName, string value)
|
||||
{
|
||||
return FindByProperties(className, propertyName, value, null, null, true);
|
||||
return FindByProperty(className, propertyName, value, true);
|
||||
}
|
||||
|
||||
public ManagementObject FindByProperties(string className, string propertyName, string value, string qualifierProperty, string qualifierValue)
|
||||
{
|
||||
return FindByProperties(className, propertyName, value, qualifierProperty, qualifierValue, true);
|
||||
}
|
||||
|
||||
private ManagementObject FindByProperties(string className, string propertyName, string value, string qualifierProperty, string qualifierValue, bool logLookup)
|
||||
/// <summary>
|
||||
/// Finds one WMI object using SELECT * plus a client-side filter so special
|
||||
/// characters in BizTalk names cannot break a WQL WHERE clause.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <param name="propertyName">The property used as the lookup key.</param>
|
||||
/// <param name="value">The expected property value.</param>
|
||||
/// <param name="logLookup">True to write the lookup details to the operation log.</param>
|
||||
/// <returns>The matching object, or null when no object matches.</returns>
|
||||
private ManagementObject FindByProperty(string className, string propertyName, string value, bool logLookup)
|
||||
{
|
||||
EnsureConnected();
|
||||
|
||||
@@ -89,14 +144,6 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
"{0} = \"{1}\"",
|
||||
propertyName,
|
||||
value ?? string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(qualifierProperty))
|
||||
{
|
||||
filter += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" AND {0} = \"{1}\"",
|
||||
qualifierProperty,
|
||||
qualifierValue ?? string.Empty);
|
||||
}
|
||||
if (logLookup)
|
||||
{
|
||||
_logger.Info("Resolving WMI object: " + queryText + " with client-side filter " + filter);
|
||||
@@ -104,37 +151,28 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
|
||||
try
|
||||
{
|
||||
// Bewusst keine WQL-WHERE-Klausel: BizTalk-Namen können Zeichen enthalten,
|
||||
// die sonst eine fehlerhafte oder anders interpretierte Query erzeugen.
|
||||
ManagementObject match = null;
|
||||
var items = Query(className, false);
|
||||
var matches = new List<ManagementObject>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (MatchesProperty(item, className, propertyName, value)
|
||||
&& (string.IsNullOrWhiteSpace(qualifierProperty)
|
||||
|| MatchesProperty(item, className, qualifierProperty, qualifierValue)))
|
||||
if (MatchesProperty(item, className, propertyName, value))
|
||||
{
|
||||
matches.Add(item);
|
||||
match = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!matches.Contains(item))
|
||||
if (!object.ReferenceEquals(item, match))
|
||||
{
|
||||
item.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
return match;
|
||||
}
|
||||
catch (ManagementException ex)
|
||||
{
|
||||
@@ -142,6 +180,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a WMI method and validates that the method returned success.
|
||||
/// </summary>
|
||||
/// <param name="instance">The WMI object on which the method should be called.</param>
|
||||
/// <param name="methodName">The method name to invoke.</param>
|
||||
/// <param name="arguments">Optional method arguments.</param>
|
||||
/// <returns>The WMI return code.</returns>
|
||||
public uint InvokeMethod(ManagementObject instance, string methodName, params object[] arguments)
|
||||
{
|
||||
if (instance == null)
|
||||
@@ -162,23 +207,44 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
throw new InvalidOperationException("WMI method failed. Class: " + instance.Path.ClassName + ", method: " + methodToCall + ", object: " + SafeObjectName(instance) + ". WMI error: " + ex.Message, ex);
|
||||
}
|
||||
|
||||
var returnCode = ExtractReturnCode(result);
|
||||
if (returnCode != 0)
|
||||
var output = result as ManagementBaseObject;
|
||||
try
|
||||
{
|
||||
throw new InvalidOperationException("WMI method returned an error. Class: " + instance.Path.ClassName + ", method: " + methodToCall + ", object: " + SafeObjectName(instance) + ", ReturnValue: " + returnCode.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
var returnCode = ExtractReturnCode(result);
|
||||
if (returnCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException("WMI method returned an error. Class: " + instance.Path.ClassName + ", method: " + methodToCall + ", object: " + SafeObjectName(instance) + ", ReturnValue: " + returnCode.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return returnCode;
|
||||
return returnCode;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (output != null)
|
||||
{
|
||||
output.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void WaitForState(string className, string keyProperty, string keyValue, string qualifierProperty, string qualifierValue, Func<ManagementObject, bool> isReached, string description, int timeoutSeconds, int pollIntervalSeconds)
|
||||
/// <summary>
|
||||
/// Polls one WMI object until it reaches the expected state or times out.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name to query.</param>
|
||||
/// <param name="keyProperty">The WMI key property used to find the object.</param>
|
||||
/// <param name="keyValue">The WMI key value used to find the object.</param>
|
||||
/// <param name="isReached">Predicate that returns true when the state is reached.</param>
|
||||
/// <param name="description">Human-readable state description used in logs and errors.</param>
|
||||
/// <param name="timeoutSeconds">Maximum number of seconds to wait.</param>
|
||||
/// <param name="pollIntervalSeconds">Number of seconds between polling attempts.</param>
|
||||
public void WaitForState(string className, string keyProperty, string keyValue, Func<ManagementObject, bool> isReached, string description, int timeoutSeconds, int pollIntervalSeconds)
|
||||
{
|
||||
var deadline = DateTime.UtcNow.AddSeconds(Math.Max(1, timeoutSeconds));
|
||||
var delay = Math.Max(1, pollIntervalSeconds);
|
||||
|
||||
while (DateTime.UtcNow <= deadline)
|
||||
{
|
||||
using (var current = FindByProperties(className, keyProperty, keyValue, qualifierProperty, qualifierValue, false))
|
||||
using (var current = FindByProperty(className, keyProperty, keyValue, false))
|
||||
{
|
||||
if (current != null && isReached(current))
|
||||
{
|
||||
@@ -187,12 +253,25 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
Thread.Sleep(TimeSpan.FromSeconds(delay));
|
||||
var remaining = deadline - DateTime.UtcNow;
|
||||
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 + "]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a WMI property as a string without failing on missing or invalid properties.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object or output parameter object.</param>
|
||||
/// <param name="propertyName">The property to read.</param>
|
||||
/// <param name="fallback">The value returned when the property cannot be read.</param>
|
||||
/// <returns>The property value or the fallback value.</returns>
|
||||
public static string SafeGetString(ManagementBaseObject item, string propertyName, string fallback)
|
||||
{
|
||||
try
|
||||
@@ -211,6 +290,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a WMI property as an integer without failing on missing or invalid properties.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object or output parameter object.</param>
|
||||
/// <param name="propertyName">The property to read.</param>
|
||||
/// <param name="fallback">The value returned when the property cannot be read.</param>
|
||||
/// <returns>The property value or the fallback value.</returns>
|
||||
public static int SafeGetInt32(ManagementBaseObject item, string propertyName, int fallback)
|
||||
{
|
||||
try
|
||||
@@ -229,6 +315,13 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a WMI property as a Boolean without failing on missing or invalid properties.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object or output parameter object.</param>
|
||||
/// <param name="propertyName">The property to read.</param>
|
||||
/// <param name="fallback">The value returned when the property cannot be read.</param>
|
||||
/// <returns>The property value or the fallback value.</returns>
|
||||
public static bool SafeGetBoolean(ManagementBaseObject item, string propertyName, bool fallback)
|
||||
{
|
||||
try
|
||||
@@ -247,6 +340,9 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the management scope is connected before a WMI operation runs.
|
||||
/// </summary>
|
||||
private void EnsureConnected()
|
||||
{
|
||||
if (_scope == null || !_scope.IsConnected)
|
||||
@@ -255,6 +351,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a WMI object exposes a property, using case-insensitive comparison.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object to inspect.</param>
|
||||
/// <param name="propertyName">The property name to find.</param>
|
||||
/// <returns>True when the property exists; otherwise false.</returns>
|
||||
private static bool HasProperty(ManagementBaseObject item, string propertyName)
|
||||
{
|
||||
foreach (PropertyData property in item.Properties)
|
||||
@@ -268,6 +370,14 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a WMI object matches a requested key value.
|
||||
/// </summary>
|
||||
/// <param name="item">The WMI object to inspect.</param>
|
||||
/// <param name="className">The WMI class name of the object.</param>
|
||||
/// <param name="propertyName">The requested key property.</param>
|
||||
/// <param name="expectedValue">The expected key value.</param>
|
||||
/// <returns>True when the object matches the requested value.</returns>
|
||||
private static bool MatchesProperty(ManagementBaseObject item, string className, string propertyName, string expectedValue)
|
||||
{
|
||||
foreach (var candidate in CandidatePropertyNames(className, propertyName))
|
||||
@@ -287,6 +397,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the primary and compatibility property names for a WMI lookup.
|
||||
/// </summary>
|
||||
/// <param name="className">The WMI class name being queried.</param>
|
||||
/// <param name="propertyName">The requested key property.</param>
|
||||
/// <returns>The candidate property names to inspect.</returns>
|
||||
private static IEnumerable<string> CandidatePropertyNames(string className, string propertyName)
|
||||
{
|
||||
yield return propertyName;
|
||||
@@ -298,6 +414,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a readable object name for diagnostics without letting WMI metadata failures escape.
|
||||
/// </summary>
|
||||
/// <param name="instance">The WMI object being described.</param>
|
||||
/// <returns>A relative WMI path or fallback object name.</returns>
|
||||
private static string SafeObjectName(ManagementObject instance)
|
||||
{
|
||||
try
|
||||
@@ -310,6 +431,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the exact method casing exposed by the WMI class.
|
||||
/// </summary>
|
||||
/// <param name="instance">The WMI object whose class should be inspected.</param>
|
||||
/// <param name="requestedName">The requested method name.</param>
|
||||
/// <returns>The method name as exposed by WMI.</returns>
|
||||
private static string ResolveMethodName(ManagementObject instance, string requestedName)
|
||||
{
|
||||
using (var managementClass = new ManagementClass(instance.Scope, new ManagementPath(instance.Path.ClassName), null))
|
||||
@@ -326,6 +453,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
throw new MissingMethodException(instance.Path.ClassName, requestedName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the WMI return code from either a scalar return value or output parameters.
|
||||
/// </summary>
|
||||
/// <param name="result">The object returned by ManagementObject.InvokeMethod.</param>
|
||||
/// <returns>The numeric WMI return code.</returns>
|
||||
private static uint ExtractReturnCode(object result)
|
||||
{
|
||||
if (result == null)
|
||||
@@ -342,8 +474,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return Convert.ToUInt32(result, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases resources owned by this client.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_scope = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,16 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes snapshot and diff data to CSV files for review outside the GUI.
|
||||
/// </summary>
|
||||
public static class CsvWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes application artifact states from a snapshot to a CSV file.
|
||||
/// </summary>
|
||||
/// <param name="path">The target CSV file path.</param>
|
||||
/// <param name="snapshot">The snapshot whose artifact states should be exported.</param>
|
||||
public static void WriteSnapshotArtifacts(string path, BizTalkSnapshot snapshot)
|
||||
{
|
||||
var lines = new List<string> { "Application,Type,Name,Status" };
|
||||
@@ -28,6 +36,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
File.WriteAllLines(path, lines, Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes host instance states from a snapshot to a CSV file.
|
||||
/// </summary>
|
||||
/// <param name="path">The target CSV file path.</param>
|
||||
/// <param name="snapshot">The snapshot whose host instance states should be exported.</param>
|
||||
public static void WriteSnapshotHosts(string path, BizTalkSnapshot snapshot)
|
||||
{
|
||||
var lines = new List<string> { "InstanceName,HostName,Server,State" };
|
||||
@@ -38,6 +51,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
File.WriteAllLines(path, lines, Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes snapshot differences to a CSV file.
|
||||
/// </summary>
|
||||
/// <param name="path">The target CSV file path.</param>
|
||||
/// <param name="diff">The diff model to export.</param>
|
||||
public static void WriteDiff(string path, SnapshotDiff diff)
|
||||
{
|
||||
var lines = new List<string> { "Scope,Application,Type,Name,Server,Before,After" };
|
||||
@@ -52,6 +70,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
File.WriteAllLines(path, lines, Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds one CSV row from already ordered field values.
|
||||
/// </summary>
|
||||
/// <param name="values">The values that should be escaped and joined.</param>
|
||||
/// <returns>A single CSV row.</returns>
|
||||
private static string Row(params string[] values)
|
||||
{
|
||||
var escaped = new string[values.Length];
|
||||
@@ -62,9 +85,19 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return string.Join(",", escaped);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes a CSV field when it contains separators, quotes or line breaks.
|
||||
/// </summary>
|
||||
/// <param name="value">The raw field value.</param>
|
||||
/// <returns>The CSV-safe field value.</returns>
|
||||
private static string Escape(string value)
|
||||
{
|
||||
value = value ?? string.Empty;
|
||||
if (value.Length > 0 && (value[0] == '=' || value[0] == '+' || value[0] == '-' || value[0] == '@' || value[0] == '\t'))
|
||||
{
|
||||
// Tabellenkalkulationen dürfen exportierte Namen nicht als Formel ausführen.
|
||||
value = "'" + value;
|
||||
}
|
||||
if (value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) < 0)
|
||||
{
|
||||
return value;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates support-ready, single-line exception diagnostics for the GUI, durable
|
||||
/// execution reports and rolling log files.
|
||||
/// </summary>
|
||||
internal static class ExceptionDiagnostics
|
||||
{
|
||||
/// <summary>
|
||||
/// Formats the complete exception chain including type, HRESULT, fusion details
|
||||
/// and stack trace without embedding physical line breaks in the daily log.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception to format.</param>
|
||||
/// <returns>A detailed single-line diagnostic string.</returns>
|
||||
public static string Format(Exception exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
return "Unknown operation error.";
|
||||
}
|
||||
|
||||
var parts = new List<string>();
|
||||
var current = exception;
|
||||
var depth = 0;
|
||||
while (current != null)
|
||||
{
|
||||
var label = depth == 0 ? "Exception" : "InnerException[" + depth.ToString(CultureInfo.InvariantCulture) + "]";
|
||||
var part = label
|
||||
+ " Type=" + current.GetType().FullName
|
||||
+ "; HResult=0x" + current.HResult.ToString("X8", CultureInfo.InvariantCulture)
|
||||
+ "; Message=" + Flatten(current.Message);
|
||||
|
||||
var fileNotFound = current as FileNotFoundException;
|
||||
if (fileNotFound != null && !string.IsNullOrWhiteSpace(fileNotFound.FusionLog))
|
||||
{
|
||||
part += "; FusionLog=" + Flatten(fileNotFound.FusionLog);
|
||||
}
|
||||
var fileLoad = current as FileLoadException;
|
||||
if (fileLoad != null && !string.IsNullOrWhiteSpace(fileLoad.FusionLog))
|
||||
{
|
||||
part += "; FusionLog=" + Flatten(fileLoad.FusionLog);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(current.StackTrace))
|
||||
{
|
||||
part += "; StackTrace=" + Flatten(current.StackTrace);
|
||||
}
|
||||
|
||||
parts.Add(part);
|
||||
current = current.InnerException;
|
||||
depth++;
|
||||
}
|
||||
return string.Join(" | ", parts.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>Replaces physical control characters with readable escape sequences.</summary>
|
||||
/// <param name="value">The text to flatten.</param>
|
||||
/// <returns>Single-line text suitable for one durable log record.</returns>
|
||||
private static string Flatten(string value)
|
||||
{
|
||||
return (value ?? string.Empty)
|
||||
.Replace("\r", "\\r")
|
||||
.Replace("\n", "\\n")
|
||||
.Replace("\t", "\\t");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,16 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes HTML reports for BizTalk snapshots and snapshot differences.
|
||||
/// </summary>
|
||||
public static class HtmlReportWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes a complete HTML snapshot report.
|
||||
/// </summary>
|
||||
/// <param name="path">The target HTML file path.</param>
|
||||
/// <param name="snapshot">The snapshot to render.</param>
|
||||
public static void WriteSnapshot(string path, BizTalkSnapshot snapshot)
|
||||
{
|
||||
var html = new StringBuilder();
|
||||
@@ -50,6 +58,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
System.IO.File.WriteAllText(path, html.ToString(), Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a complete HTML diff report.
|
||||
/// </summary>
|
||||
/// <param name="path">The target HTML file path.</param>
|
||||
/// <param name="diff">The diff model to render.</param>
|
||||
public static void WriteDiff(string path, SnapshotDiff diff)
|
||||
{
|
||||
var html = new StringBuilder();
|
||||
@@ -74,12 +87,26 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
System.IO.File.WriteAllText(path, html.ToString(), Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one artifact status row to a report table.
|
||||
/// </summary>
|
||||
/// <param name="html">The report builder receiving the row markup.</param>
|
||||
/// <param name="name">The artifact name.</param>
|
||||
/// <param name="status">The formatted artifact status.</param>
|
||||
/// <param name="ok">True when the row should use the positive status style.</param>
|
||||
/// <param name="detail1">The first detail column value.</param>
|
||||
/// <param name="detail2">The second detail column value.</param>
|
||||
private static void StatusRow(StringBuilder html, string name, string status, bool ok, string detail1, string detail2)
|
||||
{
|
||||
html.Append("<tr><td>").Append(Encode(name)).Append("</td><td class='").Append(ok ? "ok" : "bad").Append("'>").Append(Encode(status))
|
||||
.Append("</td><td>").Append(Encode(detail1)).Append("</td><td>").Append(Encode(detail2)).Append("</td></tr>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the common document header, style block and title.
|
||||
/// </summary>
|
||||
/// <param name="html">The report builder receiving the header markup.</param>
|
||||
/// <param name="title">The document title and main heading.</param>
|
||||
private static void Header(StringBuilder html, string title)
|
||||
{
|
||||
html.Append("<!doctype html><html><head><meta charset='utf-8'><title>").Append(Encode(title)).Append("</title><style>")
|
||||
@@ -87,13 +114,23 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
.Append("</style></head><body><h1>").Append(Encode(title)).Append("</h1>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the common HTML document footer.
|
||||
/// </summary>
|
||||
/// <param name="html">The report builder receiving the footer markup.</param>
|
||||
private static void Footer(StringBuilder html)
|
||||
{
|
||||
html.Append("</body></html>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTML-encodes report values and treats null values as empty text.
|
||||
/// </summary>
|
||||
/// <param name="value">The raw value to encode.</param>
|
||||
/// <returns>An HTML-safe value.</returns>
|
||||
private static string Encode(string value)
|
||||
{
|
||||
// Alle aus BizTalk gelesenen Werte werden vor der Aufnahme in HTML neutralisiert.
|
||||
return WebUtility.HtmlEncode(value ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Json;
|
||||
@@ -5,57 +6,51 @@ using System.Text;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Persists DataContract models as JSON with repository-defined encoding rules.
|
||||
/// </summary>
|
||||
public static class JsonFileStore
|
||||
{
|
||||
private const long MaximumJsonBytes = 16L * 1024L * 1024L;
|
||||
/// <summary>
|
||||
/// UTF-8 encoding instance that writes JSON without a byte order mark.
|
||||
/// </summary>
|
||||
private static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a value to a UTF-8 JSON file without a byte order mark.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The model type to serialize.</typeparam>
|
||||
/// <param name="path">The target JSON file path.</param>
|
||||
/// <param name="value">The value to serialize.</param>
|
||||
public static void Save<T>(string path, T value)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path)));
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentException("A JSON target path is required.", "path");
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
Directory.CreateDirectory(directory);
|
||||
var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
|
||||
{
|
||||
UseSimpleDictionaryFormat = true
|
||||
});
|
||||
|
||||
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);
|
||||
var payload = stream.ToArray();
|
||||
if (payload.LongLength > MaximumJsonBytes)
|
||||
{
|
||||
throw new InvalidDataException("Die JSON-Ausgabe überschreitet die Sicherheitsgrenze von 16 MiB.");
|
||||
}
|
||||
|
||||
using (var file = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough))
|
||||
{
|
||||
file.Write(payload, 0, payload.Length);
|
||||
file.Flush(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
File.Replace(temporaryPath, fullPath, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Move(temporaryPath, fullPath);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(temporaryPath))
|
||||
{
|
||||
File.Delete(temporaryPath);
|
||||
}
|
||||
serializer.WriteObject(stream, value);
|
||||
var json = Utf8NoBom.GetString(stream.ToArray());
|
||||
WriteAtomically(fullPath, json);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a JSON file into the requested DataContract model type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The model type to deserialize.</typeparam>
|
||||
/// <param name="path">The JSON file path to load.</param>
|
||||
/// <returns>The deserialized model.</returns>
|
||||
public static T Load<T>(string path)
|
||||
{
|
||||
var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
|
||||
@@ -63,16 +58,6 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
UseSimpleDictionaryFormat = true
|
||||
});
|
||||
|
||||
var information = new FileInfo(path);
|
||||
if (!information.Exists)
|
||||
{
|
||||
throw new FileNotFoundException("Die JSON-Datei wurde nicht gefunden.", path);
|
||||
}
|
||||
if (information.Length == 0 || information.Length > MaximumJsonBytes)
|
||||
{
|
||||
throw new InvalidDataException("Die JSON-Datei ist leer oder überschreitet die Sicherheitsgrenze von 16 MiB: " + path);
|
||||
}
|
||||
|
||||
var json = NormalizeJson(File.ReadAllText(path, Encoding.UTF8));
|
||||
using (var stream = new MemoryStream(Utf8NoBom.GetBytes(json)))
|
||||
{
|
||||
@@ -82,11 +67,16 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
catch (SerializationException ex)
|
||||
{
|
||||
throw new InvalidDataException("Die JSON-Datei '" + path + "' konnte nicht geladen werden. " + DescribeJsonStart(json), ex);
|
||||
throw new InvalidDataException("Failed to load JSON file '" + path + "'. " + 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)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
@@ -107,21 +97,110 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return json;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a short diagnostic message for a JSON deserialization failure.
|
||||
/// </summary>
|
||||
/// <param name="json">The normalized JSON text that failed to deserialize.</param>
|
||||
/// <returns>A diagnostic suffix describing the beginning of the JSON content.</returns>
|
||||
private static string DescribeJsonStart(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return "Die Datei ist leer oder enthält nur Leerzeichen.";
|
||||
return "The file is empty or contains only whitespace.";
|
||||
}
|
||||
|
||||
var trimmed = json.TrimStart();
|
||||
var first = trimmed.Length == 0 ? '\0' : trimmed[0];
|
||||
if (first != '{' && first != '[')
|
||||
{
|
||||
return "Das erste JSON-Zeichen ist '" + first + "' statt '{' oder '['.";
|
||||
return "The first JSON character is '" + first + "' instead of '{' or '['.";
|
||||
}
|
||||
|
||||
return "Die Datei beginnt mit gültiger JSON-Syntax, passt aber nicht zum erwarteten Datenmodell.";
|
||||
return "The file starts with valid JSON syntax but could not be deserialized into the expected model.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a file through a same-directory temporary file so an interrupted
|
||||
/// save cannot leave a truncated snapshot or operation plan behind.
|
||||
/// </summary>
|
||||
/// <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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +1,258 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the severity used for visible and file-based operation log entries.
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Informational progress or diagnostic entry.
|
||||
/// </summary>
|
||||
Info,
|
||||
|
||||
/// <summary>
|
||||
/// Non-fatal condition that needs operator attention.
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// Failed operation or exception entry.
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// Successful operation entry.
|
||||
/// </summary>
|
||||
Success
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one operation log entry displayed in the GUI and written to disk.
|
||||
/// </summary>
|
||||
public sealed class LogEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the local time when the entry was created.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the entry severity.
|
||||
/// </summary>
|
||||
public LogLevel Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the operator-facing message.
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes operation log entries to a daily rolling file and an optional UI sink.
|
||||
/// </summary>
|
||||
public sealed class OperationLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Prefix used for daily log files written to the resolved log directory.
|
||||
/// </summary>
|
||||
private const string LogFilePrefix = "BizTalkPlatformManagementTool-";
|
||||
|
||||
/// <summary>
|
||||
/// File extension used for operation log files.
|
||||
/// </summary>
|
||||
private const string LogFileExtension = ".log";
|
||||
private const int RetentionDays = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Number of calendar days retained, including the current day.
|
||||
/// </summary>
|
||||
internal const int RetentionDays = 30;
|
||||
|
||||
/// <summary>Maximum number of historical records restored into the GUI by default.</summary>
|
||||
public const int DefaultGridHistoryLimit = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide lock that serializes log file appends.
|
||||
/// </summary>
|
||||
private static readonly object FileLock = new object();
|
||||
private static int _cleanupDone;
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback for forwarding entries to the UI.
|
||||
/// </summary>
|
||||
private readonly Action<LogEntry> _sink;
|
||||
private readonly string _logDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Ordered local directories available for primary and fallback logging.
|
||||
/// </summary>
|
||||
private readonly List<string> _candidateDirectories;
|
||||
|
||||
/// <summary>Index of the currently active writable directory, or minus one.</summary>
|
||||
private int _activeDirectoryIndex;
|
||||
|
||||
/// <summary>Directory where daily log files are currently written.</summary>
|
||||
private string _logDirectory;
|
||||
|
||||
/// <summary>Latest actionable storage warning for the operator.</summary>
|
||||
private string _storageWarning;
|
||||
|
||||
/// <summary>Last storage notice already forwarded to the UI sink.</summary>
|
||||
private string _reportedStorageNotice;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a logger that verifies ProgramData and ordered local fallback directories.
|
||||
/// </summary>
|
||||
/// <param name="sink">Optional callback that receives entries for display.</param>
|
||||
public OperationLogger(Action<LogEntry> sink)
|
||||
: this(sink, BuildLogDirectoryCandidates())
|
||||
{
|
||||
_sink = sink;
|
||||
_logDirectory = ResolveLogDirectory();
|
||||
CleanupOldLogs();
|
||||
}
|
||||
|
||||
/// <summary>Initializes a logger with an explicit directory for regression tests.</summary>
|
||||
/// <param name="sink">Optional callback that receives new entries.</param>
|
||||
/// <param name="logDirectory">Directory used for plain and compressed logs.</param>
|
||||
internal OperationLogger(Action<LogEntry> sink, string logDirectory)
|
||||
: this(sink, new[] { logDirectory })
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a logger with ordered primary and fallback directories.</summary>
|
||||
/// <param name="sink">Optional callback that receives new entries.</param>
|
||||
/// <param name="candidateDirectories">Ordered local directories to verify.</param>
|
||||
internal OperationLogger(Action<LogEntry> sink, IEnumerable<string> candidateDirectories)
|
||||
{
|
||||
_sink = sink;
|
||||
_candidateDirectories = NormalizeDirectories(candidateDirectories).ToList();
|
||||
_activeDirectoryIndex = -1;
|
||||
SelectInitialLogDirectory();
|
||||
if (_logDirectory != null)
|
||||
{
|
||||
MaintainLogs(DateTime.Now.Date);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the directory containing active and compressed runtime logs.</summary>
|
||||
public string LogDirectory
|
||||
{
|
||||
get { lock (FileLock) { return _logDirectory ?? string.Empty; } }
|
||||
}
|
||||
|
||||
/// <summary>Gets whether a daily runtime log can currently be written.</summary>
|
||||
public bool IsFileLoggingAvailable
|
||||
{
|
||||
get { lock (FileLock) { return _activeDirectoryIndex >= 0 && !string.IsNullOrWhiteSpace(_logDirectory); } }
|
||||
}
|
||||
|
||||
/// <summary>Gets the latest fallback or total-storage-failure diagnostic.</summary>
|
||||
public string StorageWarning
|
||||
{
|
||||
get { lock (FileLock) { return _storageWarning; } }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the daily log file for the current date.
|
||||
/// </summary>
|
||||
public string LogFilePath
|
||||
{
|
||||
get
|
||||
{
|
||||
return Path.Combine(_logDirectory, LogFilePrefix + DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + LogFileExtension);
|
||||
lock (FileLock)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(_logDirectory)
|
||||
? string.Empty
|
||||
: BuildLogFilePath(_logDirectory, DateTime.Now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads retained plain and GZip-compressed records for restoring the operation grid.
|
||||
/// Malformed legacy lines are ignored without affecting current logging.
|
||||
/// </summary>
|
||||
/// <param name="maximumEntries">Maximum newest entries to return.</param>
|
||||
/// <returns>Chronologically ordered retained log entries.</returns>
|
||||
public IList<LogEntry> ReadRecentEntries(int maximumEntries)
|
||||
{
|
||||
if (maximumEntries <= 0)
|
||||
{
|
||||
return new List<LogEntry>();
|
||||
}
|
||||
|
||||
var entries = new List<LogEntry>();
|
||||
var directory = LogDirectory;
|
||||
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
|
||||
{
|
||||
return entries;
|
||||
}
|
||||
try
|
||||
{
|
||||
var files = Directory.GetFiles(directory, LogFilePrefix + "*" + LogFileExtension + "*")
|
||||
.Where(IsSupportedLogFile)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
foreach (var file in files)
|
||||
{
|
||||
ReadEntries(file, entries);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Historical display is optional; current operations and file logging continue.
|
||||
}
|
||||
|
||||
return entries
|
||||
.OrderBy(x => x.Timestamp)
|
||||
.Skip(Math.Max(0, entries.Count - maximumEntries))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an informational entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public void Info(string message)
|
||||
{
|
||||
Write(LogLevel.Info, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a warning entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public void Warning(string message)
|
||||
{
|
||||
Write(LogLevel.Warning, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an error entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public void Error(string message)
|
||||
{
|
||||
Write(LogLevel.Error, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a success entry.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public void Success(string message)
|
||||
{
|
||||
Write(LogLevel.Success, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a log entry and sends it to both output targets.
|
||||
/// </summary>
|
||||
/// <param name="level">The entry severity.</param>
|
||||
/// <param name="message">The message to log.</param>
|
||||
private void Write(LogLevel level, string message)
|
||||
{
|
||||
var entry = new LogEntry
|
||||
@@ -75,87 +262,397 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
Message = message
|
||||
};
|
||||
|
||||
WriteToFile(entry);
|
||||
var storageNotice = WriteToFile(entry);
|
||||
DeliverToSink(entry);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(storageNotice))
|
||||
{
|
||||
var warning = new LogEntry
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
Level = LogLevel.Warning,
|
||||
Message = storageNotice
|
||||
};
|
||||
WriteToFile(warning);
|
||||
DeliverToSink(warning);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Forwards one record to the optional GUI without affecting runtime work.</summary>
|
||||
/// <param name="entry">The record to display.</param>
|
||||
private void DeliverToSink(LogEntry entry)
|
||||
{
|
||||
if (_sink == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_sink(entry);
|
||||
}
|
||||
|
||||
private void WriteToFile(LogEntry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
var line = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"[{0:yyyy-MM-dd HH:mm:ss}][{1}] {2}{3}",
|
||||
entry.Timestamp,
|
||||
entry.Level.ToString().ToUpperInvariant(),
|
||||
entry.Message,
|
||||
Environment.NewLine);
|
||||
|
||||
lock (FileLock)
|
||||
{
|
||||
File.AppendAllText(LogFilePath, line);
|
||||
}
|
||||
_sink(entry);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Logging must never interrupt BizTalk operations.
|
||||
// Auch die optionale GUI-Weiterleitung ist nur ein Diagnosekanal. Ein Fehler
|
||||
// dort darf insbesondere nicht die Exception-Behandlung eines BizTalk-Schritts
|
||||
// erneut unterbrechen und dadurch alle späteren Planschritte verhindern.
|
||||
WriteToFile(new LogEntry
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
Level = LogLevel.Warning,
|
||||
Message = "Log display sink failed; runtime operation continues. Error: " + ex.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupOldLogs()
|
||||
/// <summary>
|
||||
/// Appends one entry to the current daily log file.
|
||||
/// </summary>
|
||||
/// <param name="entry">The entry to write.</param>
|
||||
/// <returns>A new operator-visible storage notice, or null.</returns>
|
||||
private string WriteToFile(LogEntry entry)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _cleanupDone, 1) == 1)
|
||||
var line = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"[{0:yyyy-MM-dd HH:mm:ss}][{1}] {2}{3}",
|
||||
entry.Timestamp,
|
||||
entry.Level.ToString().ToUpperInvariant(),
|
||||
(entry.Message ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n"),
|
||||
Environment.NewLine);
|
||||
|
||||
lock (FileLock)
|
||||
{
|
||||
var failures = new List<string>();
|
||||
var previousDirectory = _logDirectory;
|
||||
var startIndex = _activeDirectoryIndex >= 0 ? _activeDirectoryIndex : 0;
|
||||
for (var index = startIndex; index < _candidateDirectories.Count; index++)
|
||||
{
|
||||
var candidate = _candidateDirectories[index];
|
||||
string error;
|
||||
if (!TryAppend(candidate, line, out error))
|
||||
{
|
||||
failures.Add(candidate + " => " + error);
|
||||
continue;
|
||||
}
|
||||
|
||||
_activeDirectoryIndex = index;
|
||||
_logDirectory = candidate;
|
||||
if (!string.Equals(previousDirectory, candidate, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_storageWarning = "Runtime log path switched to writable fallback '" + candidate
|
||||
+ "'. Failed path(s): " + string.Join(" | ", failures.ToArray());
|
||||
return TakeUnreportedStorageNotice(_storageWarning);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_activeDirectoryIndex = -1;
|
||||
_logDirectory = null;
|
||||
_storageWarning = "RUNTIME FILE LOGGING UNAVAILABLE. No candidate directory accepted an append. Attempted: "
|
||||
+ string.Join(" | ", failures.ToArray()) + ". Operations continue and remain visible in the grid, but no durable runtime log is being written.";
|
||||
return TakeUnreportedStorageNotice(_storageWarning);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns a storage notice only once per distinct failure state.</summary>
|
||||
/// <param name="notice">The current diagnostic.</param>
|
||||
/// <returns>The notice when it has not been reported before; otherwise null.</returns>
|
||||
private string TakeUnreportedStorageNotice(string notice)
|
||||
{
|
||||
if (string.Equals(_reportedStorageNotice, notice, StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
_reportedStorageNotice = notice;
|
||||
return notice;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compresses completed daily logs and removes all records outside retention.
|
||||
/// </summary>
|
||||
/// <param name="today">The current local date.</param>
|
||||
internal void MaintainLogs(DateTime today)
|
||||
{
|
||||
var directory = LogDirectory;
|
||||
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var cutoff = DateTime.Now.Date.AddDays(-(RetentionDays - 1));
|
||||
foreach (var file in Directory.GetFiles(_logDirectory, LogFilePrefix + "*" + LogFileExtension))
|
||||
var cutoff = today.Date.AddDays(-(RetentionDays - 1));
|
||||
foreach (var file in Directory.GetFiles(directory, LogFilePrefix + "*" + LogFileExtension + "*"))
|
||||
{
|
||||
var lastWrite = File.GetLastWriteTime(file);
|
||||
if (lastWrite.Date < cutoff)
|
||||
DateTime fileDate;
|
||||
if (!TryGetLogDate(file, out fileDate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (fileDate < cutoff)
|
||||
{
|
||||
File.Delete(file);
|
||||
continue;
|
||||
}
|
||||
if (fileDate < today.Date && file.EndsWith(LogFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Compress(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Log retention cleanup is best-effort.
|
||||
// Die Aufbewahrungsbereinigung ist bestmöglich und beeinflusst den aktuellen Lauf nicht.
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveLogDirectory()
|
||||
/// <summary>Compresses a completed log atomically and removes the plain source afterward.</summary>
|
||||
/// <param name="source">The completed plain log.</param>
|
||||
private static void Compress(string source)
|
||||
{
|
||||
var candidates = new[]
|
||||
var target = source + ".gz";
|
||||
if (File.Exists(target))
|
||||
{
|
||||
AppPaths.LogDirectory,
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), AppPaths.ProductName, "Logs"),
|
||||
AppDomain.CurrentDomain.BaseDirectory
|
||||
};
|
||||
return;
|
||||
}
|
||||
var temporary = target + ".tmp." + Guid.NewGuid().ToString("N");
|
||||
try
|
||||
{
|
||||
using (var input = File.OpenRead(source))
|
||||
using (var output = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||
using (var gzip = new GZipStream(output, CompressionLevel.Optimal))
|
||||
{
|
||||
input.CopyTo(gzip);
|
||||
}
|
||||
File.Move(temporary, target);
|
||||
File.Delete(source);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(temporary))
|
||||
{
|
||||
File.Delete(temporary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
/// <summary>Reads one supported plain or compressed file into an entry collection.</summary>
|
||||
private static void ReadEntries(string path, ICollection<LogEntry> entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
using (var payload = path.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)
|
||||
? (Stream)new GZipStream(file, CompressionMode.Decompress)
|
||||
: file)
|
||||
using (var reader = new StreamReader(payload, Encoding.UTF8, true))
|
||||
{
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
LogEntry entry;
|
||||
if (TryParse(line, out entry))
|
||||
{
|
||||
entries.Add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// One unreadable archive must not hide all other retained history.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Parses the stable daily log line format.</summary>
|
||||
/// <param name="line">One physical line.</param>
|
||||
/// <param name="entry">Parsed entry when successful.</param>
|
||||
/// <returns>True for a valid record.</returns>
|
||||
internal static bool TryParse(string line, out LogEntry entry)
|
||||
{
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(line) || line.Length < 29 || line[0] != '[' || line[21] != '[')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var timestampEnd = line.IndexOf(']');
|
||||
var levelEnd = line.IndexOf(']', timestampEnd + 1);
|
||||
DateTime timestamp;
|
||||
LogLevel level;
|
||||
if (timestampEnd != 20 || levelEnd < 0
|
||||
|| !DateTime.TryParseExact(line.Substring(1, 19), "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out timestamp)
|
||||
|| !Enum.TryParse(line.Substring(timestampEnd + 2, levelEnd - timestampEnd - 2), true, out level))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var messageStart = levelEnd + 1;
|
||||
if (messageStart < line.Length && line[messageStart] == ' ')
|
||||
{
|
||||
messageStart++;
|
||||
}
|
||||
entry = new LogEntry { Timestamp = timestamp, Level = level, Message = line.Substring(messageStart) };
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Checks the supported exact file suffixes.</summary>
|
||||
private static bool IsSupportedLogFile(string path)
|
||||
{
|
||||
return path.EndsWith(LogFileExtension, StringComparison.OrdinalIgnoreCase)
|
||||
|| path.EndsWith(LogFileExtension + ".gz", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Extracts the calendar date from a stable log file name.</summary>
|
||||
private static bool TryGetLogDate(string path, out DateTime date)
|
||||
{
|
||||
var name = Path.GetFileName(path);
|
||||
var suffixLength = name.EndsWith(LogFileExtension + ".gz", StringComparison.OrdinalIgnoreCase)
|
||||
? (LogFileExtension + ".gz").Length
|
||||
: name.EndsWith(LogFileExtension, StringComparison.OrdinalIgnoreCase) ? LogFileExtension.Length : 0;
|
||||
if (suffixLength == 0 || !name.StartsWith(LogFilePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
date = default(DateTime);
|
||||
return false;
|
||||
}
|
||||
var value = name.Substring(LogFilePrefix.Length, name.Length - LogFilePrefix.Length - suffixLength);
|
||||
return DateTime.TryParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out date);
|
||||
}
|
||||
|
||||
/// <summary>Selects the first directory that passes a real create/write/delete probe.</summary>
|
||||
private void SelectInitialLogDirectory()
|
||||
{
|
||||
var failures = new List<string>();
|
||||
for (var index = 0; index < _candidateDirectories.Count; index++)
|
||||
{
|
||||
string error;
|
||||
if (!TryVerifyWritable(_candidateDirectories[index], out error))
|
||||
{
|
||||
failures.Add(_candidateDirectories[index] + " => " + error);
|
||||
continue;
|
||||
}
|
||||
_activeDirectoryIndex = index;
|
||||
_logDirectory = _candidateDirectories[index];
|
||||
if (index > 0)
|
||||
{
|
||||
_storageWarning = "Primary runtime log path is not writable. Using verified fallback '"
|
||||
+ _logDirectory + "'. Failed path(s): " + string.Join(" | ", failures.ToArray());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_storageWarning = "RUNTIME FILE LOGGING UNAVAILABLE. No candidate directory passed the startup write probe. Attempted: "
|
||||
+ (failures.Count == 0 ? "(none)" : string.Join(" | ", failures.ToArray()))
|
||||
+ ". Operations remain visible in the grid, but no durable runtime log is being written.";
|
||||
}
|
||||
|
||||
/// <summary>Builds primary and local fallback directories in deterministic order.</summary>
|
||||
/// <returns>ProgramData, LocalAppData, executable-local and Temp candidates.</returns>
|
||||
private static IEnumerable<string> BuildLogDirectoryCandidates()
|
||||
{
|
||||
yield return Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"BizTalkPlatformManagementTool",
|
||||
"Logs");
|
||||
yield return Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"BizTalkPlatformManagementTool",
|
||||
"Logs");
|
||||
yield return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
|
||||
yield return Path.Combine(Path.GetTempPath(), "BizTalkPlatformManagementTool", "Logs");
|
||||
}
|
||||
|
||||
/// <summary>Normalizes and de-duplicates candidate paths without requiring existence.</summary>
|
||||
/// <param name="directories">Raw ordered paths.</param>
|
||||
/// <returns>Safe absolute unique paths.</returns>
|
||||
private static IEnumerable<string> NormalizeDirectories(IEnumerable<string> directories)
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var directory in directories ?? Enumerable.Empty<string>())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string fullPath;
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(candidate);
|
||||
return candidate;
|
||||
fullPath = Path.GetFullPath(directory.Trim()).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Das Logging darf die Bedienoberflaeche nicht am Start hindern.
|
||||
continue;
|
||||
}
|
||||
if (seen.Add(fullPath))
|
||||
{
|
||||
yield return fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AppDomain.CurrentDomain.BaseDirectory;
|
||||
/// <summary>Performs a real write probe and removes the temporary probe file.</summary>
|
||||
/// <param name="directory">Candidate directory.</param>
|
||||
/// <param name="error">Detailed failure when the probe fails.</param>
|
||||
/// <returns>True only after bytes were created and flushed successfully.</returns>
|
||||
private static bool TryVerifyWritable(string directory, out string error)
|
||||
{
|
||||
var probe = Path.Combine(directory, ".runtime-log-write-probe-" + Guid.NewGuid().ToString("N") + ".tmp");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
using (var stream = new FileStream(probe, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
stream.WriteByte(0x42);
|
||||
stream.Flush();
|
||||
}
|
||||
File.Delete(probe);
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.GetType().Name + ": " + ex.Message;
|
||||
try
|
||||
{
|
||||
if (File.Exists(probe))
|
||||
{
|
||||
File.Delete(probe);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Probe cleanup is best-effort and the original write error remains authoritative.
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Appends one physical line to a candidate daily log.</summary>
|
||||
/// <param name="directory">Candidate directory.</param>
|
||||
/// <param name="line">Serialized log line.</param>
|
||||
/// <param name="error">Detailed append failure.</param>
|
||||
/// <returns>True after the append completed.</returns>
|
||||
private static bool TryAppend(string directory, string line, out string error)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
File.AppendAllText(BuildLogFilePath(directory, DateTime.Now), line, new UTF8Encoding(false));
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.GetType().Name + ": " + ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Builds the stable daily file path for a directory and local date.</summary>
|
||||
/// <param name="directory">Active directory.</param>
|
||||
/// <param name="timestamp">Timestamp whose local date selects the file.</param>
|
||||
/// <returns>Full daily log path.</returns>
|
||||
private static string BuildLogFilePath(string directory, DateTime timestamp)
|
||||
{
|
||||
return Path.Combine(
|
||||
directory,
|
||||
LogFilePrefix + timestamp.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + LogFileExtension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
using System;
|
||||
using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the successful outcomes returned by the runtime-specific execution layer.
|
||||
/// </summary>
|
||||
internal enum RuntimeStepOutcome
|
||||
{
|
||||
/// <summary>The requested method was executed and verified.</summary>
|
||||
Succeeded,
|
||||
|
||||
/// <summary>The object was already in the requested target state.</summary>
|
||||
AlreadySatisfied
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Abstracts one state-aware runtime step so best-effort orchestration can be
|
||||
/// regression-tested without a BizTalk WMI provider.
|
||||
/// </summary>
|
||||
internal interface IOperationStepRuntime
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes one step or reports that its target state already exists.
|
||||
/// </summary>
|
||||
/// <param name="step">The operation-plan step.</param>
|
||||
/// <param name="options">Timeout and polling options.</param>
|
||||
/// <returns>The successful runtime outcome.</returns>
|
||||
RuntimeStepOutcome Execute(OperationStep step, OperationOptions options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests an operator decision at a non-mutating boundary in an operation plan.
|
||||
/// </summary>
|
||||
/// <param name="checkpoint">The checkpoint plan row.</param>
|
||||
/// <param name="progress">The durable execution progress before the checkpoint.</param>
|
||||
/// <returns>True to continue with later phases; false to stop safely.</returns>
|
||||
internal delegate bool OperationCheckpointHandler(OperationStep checkpoint, OperationExecutionReport progress);
|
||||
|
||||
/// <summary>
|
||||
/// Runs every independent plan step, records durable outcomes and deliberately
|
||||
/// continues after isolated failures.
|
||||
/// </summary>
|
||||
internal sealed class OperationPlanExecutor
|
||||
{
|
||||
/// <summary>Logger used for detailed progress and failure diagnostics.</summary>
|
||||
private readonly OperationLogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new best-effort plan executor.
|
||||
/// </summary>
|
||||
/// <param name="logger">The operation logger, or null for isolated tests.</param>
|
||||
public OperationPlanExecutor(OperationLogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes or simulates every plan step and returns a complete report.
|
||||
/// </summary>
|
||||
/// <param name="plan">The validated operation plan.</param>
|
||||
/// <param name="options">The execution options.</param>
|
||||
/// <param name="runtime">The runtime implementation; optional only during dry-run.</param>
|
||||
/// <returns>A complete per-step execution report.</returns>
|
||||
public OperationExecutionReport Execute(OperationPlan plan, OperationOptions options, IOperationStepRuntime runtime)
|
||||
{
|
||||
return Execute(plan, options, runtime, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes or simulates every plan step with an optional operator checkpoint handler.
|
||||
/// </summary>
|
||||
/// <param name="plan">The validated operation plan.</param>
|
||||
/// <param name="options">The execution options.</param>
|
||||
/// <param name="runtime">The runtime implementation; optional only during dry-run.</param>
|
||||
/// <param name="checkpointHandler">UI-independent operator decision callback.</param>
|
||||
/// <returns>A complete per-step execution report, including unreached rows.</returns>
|
||||
internal OperationExecutionReport Execute(
|
||||
OperationPlan plan,
|
||||
OperationOptions options,
|
||||
IOperationStepRuntime runtime,
|
||||
OperationCheckpointHandler checkpointHandler)
|
||||
{
|
||||
if (plan == null || options == null)
|
||||
{
|
||||
throw new ArgumentNullException(plan == null ? "plan" : "options");
|
||||
}
|
||||
if (!options.DryRun && runtime == null)
|
||||
{
|
||||
throw new ArgumentNullException("runtime");
|
||||
}
|
||||
|
||||
var report = NewReport(plan, options);
|
||||
for (var index = 0; index < plan.Steps.Count; index++)
|
||||
{
|
||||
var step = plan.Steps[index];
|
||||
var stepResult = NewStepResult(index + 1, step);
|
||||
var stopAfterCurrentStep = false;
|
||||
try
|
||||
{
|
||||
if (!step.Execute)
|
||||
{
|
||||
stepResult.Outcome = OperationStepOutcomes.Skipped;
|
||||
report.SkippedCount++;
|
||||
Warning(step.Action + (string.IsNullOrWhiteSpace(step.Warning) ? string.Empty : " " + step.Warning));
|
||||
}
|
||||
else if (options.DryRun)
|
||||
{
|
||||
stepResult.Outcome = OperationStepOutcomes.DryRun;
|
||||
report.DryRunCount++;
|
||||
Info("DRY RUN: " + DescribeStep(step));
|
||||
}
|
||||
else if (IsOperatorCheckpoint(step))
|
||||
{
|
||||
Info("Operator checkpoint reached after the receive-location phase: " + step.Action);
|
||||
report.CheckpointAt = DateTimeOffset.Now.ToString("o");
|
||||
if (checkpointHandler == null)
|
||||
{
|
||||
throw new InvalidOperationException("No operator checkpoint handler is available. Shutdown stops safely before later runtime phases.");
|
||||
}
|
||||
|
||||
if (checkpointHandler(step, report))
|
||||
{
|
||||
report.CheckpointDecision = "Continue";
|
||||
stepResult.Outcome = OperationStepOutcomes.Confirmed;
|
||||
Success("Operator confirmed that the BizTalk environment is drained. Continuing with orchestrations, send ports and host instances.");
|
||||
}
|
||||
else
|
||||
{
|
||||
report.CheckpointDecision = "Stop";
|
||||
report.OperatorStopped = true;
|
||||
stepResult.Outcome = OperationStepOutcomes.Declined;
|
||||
stopAfterCurrentStep = true;
|
||||
Warning("Operator stopped the shutdown safely at the drain checkpoint. No later shutdown phase will be executed.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Info("Executing step " + (index + 1) + "/" + plan.Steps.Count + ": " + DescribeStep(step));
|
||||
var outcome = runtime.Execute(step, options);
|
||||
if (outcome == RuntimeStepOutcome.AlreadySatisfied)
|
||||
{
|
||||
stepResult.Outcome = OperationStepOutcomes.AlreadySatisfied;
|
||||
report.AlreadySatisfiedCount++;
|
||||
Info("Already in target state: " + DescribeStep(step));
|
||||
}
|
||||
else
|
||||
{
|
||||
stepResult.Outcome = OperationStepOutcomes.Succeeded;
|
||||
report.SucceededCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stepResult.Outcome = OperationStepOutcomes.Failed;
|
||||
stepResult.Error = FormatException(ex);
|
||||
report.FailedCount++;
|
||||
if (IsOperatorCheckpoint(step))
|
||||
{
|
||||
report.CheckpointDecision = "Error";
|
||||
report.CheckpointAt = report.CheckpointAt ?? DateTimeOffset.Now.ToString("o");
|
||||
report.OperatorStopped = true;
|
||||
stopAfterCurrentStep = true;
|
||||
Error("OPERATOR CHECKPOINT FAILED CLOSED; no later shutdown phase will be executed. Error: " + stepResult.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
Error("STEP FAILED; continuing with remaining independent steps: " + DescribeStep(step) + ". Error: " + stepResult.Error);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
stepResult.FinishedAt = DateTimeOffset.Now.ToString("o");
|
||||
report.Steps.Add(stepResult);
|
||||
}
|
||||
|
||||
if (stopAfterCurrentStep)
|
||||
{
|
||||
AddNotExecutedSteps(plan, index + 1, report);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
report.FinishedAt = DateTimeOffset.Now.ToString("o");
|
||||
var summary = "Plan execution summary: succeeded=" + report.SucceededCount
|
||||
+ ", already_satisfied=" + report.AlreadySatisfiedCount
|
||||
+ ", skipped=" + report.SkippedCount
|
||||
+ ", dry_run=" + report.DryRunCount
|
||||
+ ", failed=" + report.FailedCount
|
||||
+ ", not_executed=" + report.NotExecutedCount
|
||||
+ ", checkpoint=" + (report.CheckpointDecision ?? "not_required") + ".";
|
||||
if (report.OperatorStopped && report.FailedCount == 0)
|
||||
{
|
||||
Warning(summary + " Shutdown stopped safely by operator; review the partial-state snapshot and report.");
|
||||
}
|
||||
else if (report.FailedCount == 0)
|
||||
{
|
||||
Success(summary);
|
||||
}
|
||||
else
|
||||
{
|
||||
Error(summary + " Operator review is required.");
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
/// <summary>Checks whether a plan row is the non-mutating operator boundary.</summary>
|
||||
/// <param name="step">The plan row.</param>
|
||||
/// <returns>True for the stable operator-checkpoint kind.</returns>
|
||||
private static bool IsOperatorCheckpoint(OperationStep step)
|
||||
{
|
||||
return step != null && string.Equals(step.Kind, OperationStepKind.OperatorCheckpoint.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Adds auditable results for all rows intentionally not reached after a stop.</summary>
|
||||
/// <param name="plan">The source plan.</param>
|
||||
/// <param name="startIndex">The zero-based first unreached row.</param>
|
||||
/// <param name="report">The report receiving the unreached rows.</param>
|
||||
private static void AddNotExecutedSteps(OperationPlan plan, int startIndex, OperationExecutionReport report)
|
||||
{
|
||||
for (var index = startIndex; index < plan.Steps.Count; index++)
|
||||
{
|
||||
var result = NewStepResult(index + 1, plan.Steps[index]);
|
||||
result.Outcome = OperationStepOutcomes.NotExecuted;
|
||||
result.Error = "Not reached because the operator stopped at the inbound-drain checkpoint.";
|
||||
result.FinishedAt = DateTimeOffset.Now.ToString("o");
|
||||
report.Steps.Add(result);
|
||||
report.NotExecutedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates the common execution-report header.</summary>
|
||||
/// <param name="plan">The source plan.</param>
|
||||
/// <param name="options">The execution options.</param>
|
||||
/// <returns>An initialized report.</returns>
|
||||
private static OperationExecutionReport NewReport(OperationPlan plan, OperationOptions options)
|
||||
{
|
||||
return new OperationExecutionReport
|
||||
{
|
||||
Mode = plan.Mode,
|
||||
Server = plan.Server,
|
||||
DryRun = options.DryRun,
|
||||
StartedAt = DateTimeOffset.Now.ToString("o")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Creates one report row from a plan step.</summary>
|
||||
/// <param name="index">The one-based step index.</param>
|
||||
/// <param name="step">The source step.</param>
|
||||
/// <returns>An initialized step result.</returns>
|
||||
private static OperationStepResult NewStepResult(int index, OperationStep step)
|
||||
{
|
||||
return new OperationStepResult
|
||||
{
|
||||
Index = index,
|
||||
Kind = step == null ? string.Empty : step.Kind,
|
||||
Application = step == null ? string.Empty : step.Application,
|
||||
Name = step == null ? string.Empty : step.Name,
|
||||
Action = step == null ? string.Empty : step.Action,
|
||||
StartedAt = DateTimeOffset.Now.ToString("o")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Builds a stable operator-facing step description.</summary>
|
||||
/// <param name="step">The plan step.</param>
|
||||
/// <returns>The compact description.</returns>
|
||||
private static string DescribeStep(OperationStep step)
|
||||
{
|
||||
if (step == null)
|
||||
{
|
||||
return "<unknown step>";
|
||||
}
|
||||
return step.Action + " '" + step.Name + "'"
|
||||
+ " [kind=" + step.Kind
|
||||
+ ", class=" + step.WmiClass
|
||||
+ ", key=" + step.KeyProperty + "=" + (step.KeyValue ?? string.Empty)
|
||||
+ ", method=" + step.MethodName
|
||||
+ (string.IsNullOrWhiteSpace(step.AdapterName) ? string.Empty : ", adapter=" + step.AdapterName)
|
||||
+ (string.IsNullOrWhiteSpace(step.Address) ? string.Empty : ", address=" + step.Address)
|
||||
+ (string.IsNullOrWhiteSpace(step.Server) ? string.Empty : ", server=" + step.Server)
|
||||
+ "]";
|
||||
}
|
||||
|
||||
/// <summary>Formats the complete unique exception-message chain.</summary>
|
||||
/// <param name="exception">The exception to format.</param>
|
||||
/// <returns>A single diagnostic message.</returns>
|
||||
internal static string FormatException(Exception exception)
|
||||
{
|
||||
return ExceptionDiagnostics.Format(exception);
|
||||
}
|
||||
|
||||
/// <summary>Writes an informational message when a logger is available.</summary>
|
||||
private void Info(string message) { if (_logger != null) _logger.Info(message); }
|
||||
|
||||
/// <summary>Writes a warning when a logger is available.</summary>
|
||||
private void Warning(string message) { if (_logger != null) _logger.Warning(message); }
|
||||
|
||||
/// <summary>Writes an error when a logger is available.</summary>
|
||||
private void Error(string message) { if (_logger != null) _logger.Error(message); }
|
||||
|
||||
/// <summary>Writes a success message when a logger is available.</summary>
|
||||
private void Success(string message) { if (_logger != null) _logger.Success(message); }
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,26 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Compares two BizTalk snapshots and returns the state differences that matter
|
||||
/// after a maintenance window.
|
||||
/// </summary>
|
||||
public static class SnapshotComparer
|
||||
{
|
||||
/// <summary>
|
||||
/// Compares all supported artifact and host instance states.
|
||||
/// </summary>
|
||||
/// <param name="before">The snapshot captured before maintenance.</param>
|
||||
/// <param name="after">The snapshot captured after maintenance.</param>
|
||||
/// <returns>A diff containing changed, new and missing artifacts.</returns>
|
||||
public static SnapshotDiff Compare(BizTalkSnapshot before, BizTalkSnapshot after)
|
||||
{
|
||||
if (before == null) throw new ArgumentNullException("before");
|
||||
if (after == null) throw new ArgumentNullException("after");
|
||||
SnapshotValidator.Validate(before);
|
||||
SnapshotValidator.Validate(after);
|
||||
if (!SnapshotValidator.ServerNamesEqual(before.Server, after.Server))
|
||||
{
|
||||
throw new InvalidOperationException("Snapshots from different servers cannot be compared: '" + before.Server + "' and '" + after.Server + "'.");
|
||||
}
|
||||
var diff = new SnapshotDiff();
|
||||
|
||||
CompareReceiveLocations(diff, FlattenReceiveLocations(before), FlattenReceiveLocations(after));
|
||||
@@ -20,6 +34,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
return diff;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds receive location differences to the shared diff model.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entries.</param>
|
||||
/// <param name="before">Receive locations keyed by name from the before snapshot.</param>
|
||||
/// <param name="after">Receive locations keyed by name from the after snapshot.</param>
|
||||
private static void CompareReceiveLocations(SnapshotDiff diff, Dictionary<string, ReceiveLocationState> before, Dictionary<string, ReceiveLocationState> after)
|
||||
{
|
||||
foreach (var pair in after)
|
||||
@@ -44,6 +64,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds send port differences to the shared diff model.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entries.</param>
|
||||
/// <param name="before">Send ports keyed by name from the before snapshot.</param>
|
||||
/// <param name="after">Send ports keyed by name from the after snapshot.</param>
|
||||
private static void CompareSendPorts(SnapshotDiff diff, Dictionary<string, SendPortState> before, Dictionary<string, SendPortState> after)
|
||||
{
|
||||
foreach (var pair in after)
|
||||
@@ -68,6 +94,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds orchestration differences to the shared diff model.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entries.</param>
|
||||
/// <param name="before">Orchestrations keyed by name from the before snapshot.</param>
|
||||
/// <param name="after">Orchestrations keyed by name from the after snapshot.</param>
|
||||
private static void CompareOrchestrations(SnapshotDiff diff, Dictionary<string, OrchestrationState> before, Dictionary<string, OrchestrationState> after)
|
||||
{
|
||||
foreach (var pair in after)
|
||||
@@ -92,6 +124,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds host instance differences to the shared diff model.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entries.</param>
|
||||
/// <param name="before">Host instances from the before snapshot.</param>
|
||||
/// <param name="after">Host instances from the after snapshot.</param>
|
||||
private static void CompareHostInstances(SnapshotDiff diff, List<HostInstanceState> before, List<HostInstanceState> after)
|
||||
{
|
||||
var beforeMap = new Dictionary<string, HostInstanceState>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -99,11 +137,12 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
|
||||
foreach (var item in before)
|
||||
{
|
||||
beforeMap[Key(item.Server, item.InstanceName)] = item;
|
||||
// Hostname allein ist gruppenweit nicht eindeutig; der Server gehört zur Identität.
|
||||
beforeMap[SnapshotValidator.ArtifactKey(item.Server, item.InstanceName)] = item;
|
||||
}
|
||||
foreach (var item in after)
|
||||
{
|
||||
afterMap[Key(item.Server, item.InstanceName)] = item;
|
||||
afterMap[SnapshotValidator.ArtifactKey(item.Server, item.InstanceName)] = item;
|
||||
}
|
||||
|
||||
foreach (var pair in afterMap)
|
||||
@@ -128,6 +167,15 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds one artifact diff entry.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff model receiving the entry.</param>
|
||||
/// <param name="application">The BizTalk application name.</param>
|
||||
/// <param name="type">The artifact type.</param>
|
||||
/// <param name="name">The artifact name.</param>
|
||||
/// <param name="before">The formatted before state.</param>
|
||||
/// <param name="after">The formatted after state.</param>
|
||||
private static void AddArtifact(SnapshotDiff diff, string application, string type, string name, string before, string after)
|
||||
{
|
||||
diff.ArtifactDifferences.Add(new ArtifactDiffEntry
|
||||
@@ -140,6 +188,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flattens receive locations from all applications into a case-insensitive name map.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The snapshot to flatten.</param>
|
||||
/// <returns>A map keyed by receive location name.</returns>
|
||||
private static Dictionary<string, ReceiveLocationState> FlattenReceiveLocations(BizTalkSnapshot snapshot)
|
||||
{
|
||||
var map = new Dictionary<string, ReceiveLocationState>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -147,12 +200,17 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
foreach (var item in app.ReceiveLocations)
|
||||
{
|
||||
map[Key(item.Application, item.ReceivePortName, item.Name)] = item;
|
||||
map[SnapshotValidator.ArtifactKey(app.Application, item.Name)] = item;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flattens send ports from all applications into a case-insensitive name map.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The snapshot to flatten.</param>
|
||||
/// <returns>A map keyed by send port name.</returns>
|
||||
private static Dictionary<string, SendPortState> FlattenSendPorts(BizTalkSnapshot snapshot)
|
||||
{
|
||||
var map = new Dictionary<string, SendPortState>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -160,12 +218,17 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
foreach (var item in app.SendPorts)
|
||||
{
|
||||
map[Key(item.Application, item.Name)] = item;
|
||||
map[SnapshotValidator.ArtifactKey(app.Application, item.Name)] = item;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flattens orchestrations from all applications into a case-insensitive name map.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The snapshot to flatten.</param>
|
||||
/// <returns>A map keyed by orchestration name.</returns>
|
||||
private static Dictionary<string, OrchestrationState> FlattenOrchestrations(BizTalkSnapshot snapshot)
|
||||
{
|
||||
var map = new Dictionary<string, OrchestrationState>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -173,15 +236,10 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
foreach (var item in app.Orchestrations)
|
||||
{
|
||||
map[Key(item.Application, item.Name, item.AssemblyName, item.AssemblyVersion, item.AssemblyCulture, item.AssemblyPublicKeyToken)] = item;
|
||||
map[SnapshotValidator.ArtifactKey(app.Application, item.Name)] = item;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static string Key(params string[] parts)
|
||||
{
|
||||
return string.Join("|", Array.ConvertAll(parts, value => (value ?? string.Empty).Trim().ToUpperInvariant()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,16 @@ using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves snapshot and diff models together with their sidecar report formats.
|
||||
/// </summary>
|
||||
public static class SnapshotStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves a snapshot as JSON and creates CSV, host CSV and HTML sidecars.
|
||||
/// </summary>
|
||||
/// <param name="jsonPath">The primary JSON output path.</param>
|
||||
/// <param name="snapshot">The snapshot to persist.</param>
|
||||
public static void SaveSnapshotSet(string jsonPath, BizTalkSnapshot snapshot)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath)));
|
||||
@@ -14,6 +22,11 @@ namespace BizTalkPlatformManagementTool.Services
|
||||
HtmlReportWriter.WriteSnapshot(jsonPath + ".html", snapshot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a diff as JSON and creates CSV and HTML sidecars.
|
||||
/// </summary>
|
||||
/// <param name="jsonPath">The primary JSON output path.</param>
|
||||
/// <param name="diff">The diff model to persist.</param>
|
||||
public static void SaveDiffSet(string jsonPath, SnapshotDiff diff)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath)));
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
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
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using BizTalkPlatformManagementTool.Models;
|
||||
|
||||
namespace BizTalkPlatformManagementTool.Ui
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds stable, testable operator guidance for shutdown plan review, dry-run
|
||||
/// simulation and the real inbound-drain checkpoint.
|
||||
/// </summary>
|
||||
internal static class ShutdownOperatorPrompts
|
||||
{
|
||||
/// <summary>Title of the non-mutating dry-run explanation.</summary>
|
||||
public const string DryRunTitle = "Shutdown Dry Run - No Runtime Changes";
|
||||
|
||||
/// <summary>Title of the saved-plan confirmation.</summary>
|
||||
public const string PreparedPlanTitle = "Confirm Prepared BizTalk Plan";
|
||||
|
||||
/// <summary>Title of the real inbound-drain decision.</summary>
|
||||
public const string InboundDrainTitle = "ACTION REQUIRED - Confirm BizTalk Inbound Drain";
|
||||
|
||||
/// <summary>Status displayed while the real shutdown waits for drain verification.</summary>
|
||||
public const string WaitingForInboundDrainStatus = "ACTION REQUIRED: Shutdown paused. Verify inbound drain and answer the owned confirmation dialog.";
|
||||
|
||||
/// <summary>Builds the explicit explanation shown for a shutdown dry-run.</summary>
|
||||
/// <param name="plan">The prepared shutdown plan.</param>
|
||||
/// <param name="planPath">The durable path of the prepared plan.</param>
|
||||
/// <returns>Operator-facing dry-run guidance.</returns>
|
||||
public static string BuildDryRunNotice(OperationPlan plan, string planPath)
|
||||
{
|
||||
if (plan == null)
|
||||
{
|
||||
throw new ArgumentNullException("plan");
|
||||
}
|
||||
|
||||
var checkpointCount = CountCheckpoints(plan);
|
||||
return "DRY RUN is enabled. This run only simulates the saved shutdown plan.\n\n"
|
||||
+ "No receive location is disabled and no runtime state is changed. Therefore the real inbound-drain decision dialog will not open during this run.\n\n"
|
||||
+ (checkpointCount == 0
|
||||
? "This plan has no later shutdown phase and therefore needs no drain checkpoint.\n\n"
|
||||
: "The plan contains the inbound-drain checkpoint. It will require an explicit Yes/No decision when the same plan is prepared with Dry run disabled.\n\n")
|
||||
+ "Prepared plan:\n" + planPath;
|
||||
}
|
||||
|
||||
/// <summary>Builds the confirmation for a fully prepared real operation.</summary>
|
||||
/// <param name="actionName">The operation name.</param>
|
||||
/// <param name="plan">The prepared operation plan.</param>
|
||||
/// <param name="server">The selected target server.</param>
|
||||
/// <param name="planPath">The durable path of the prepared plan.</param>
|
||||
/// <returns>Operator-facing prepared-plan confirmation.</returns>
|
||||
public static string BuildPreparedPlanConfirmation(string actionName, OperationPlan plan, string server, string planPath)
|
||||
{
|
||||
if (plan == null)
|
||||
{
|
||||
throw new ArgumentNullException("plan");
|
||||
}
|
||||
|
||||
var executableSteps = plan.Steps.Count(x => x.Execute && !IsCheckpoint(x));
|
||||
var checkpointCount = CountCheckpoints(plan);
|
||||
return actionName + " will execute " + executableSteps + " step(s) on server '" + server + "'.\n\n"
|
||||
+ (checkpointCount == 0
|
||||
? string.Empty
|
||||
: "The saved plan contains an operator drain checkpoint after all receive locations. The real shutdown will visibly pause there for a second, explicit decision.\n\n")
|
||||
+ "The exact plan was saved to:\n" + planPath + "\n\nContinue now?";
|
||||
}
|
||||
|
||||
/// <summary>Builds the real inbound-drain decision from completed receive-location results.</summary>
|
||||
/// <param name="progress">The execution results already reached.</param>
|
||||
/// <returns>Operator-facing drain verification guidance.</returns>
|
||||
public static string BuildInboundDrainConfirmation(OperationExecutionReport progress)
|
||||
{
|
||||
if (progress == null)
|
||||
{
|
||||
throw new ArgumentNullException("progress");
|
||||
}
|
||||
|
||||
var receiveResults = progress.Steps
|
||||
.Where(x => string.Equals(x.Kind, OperationStepKind.ReceiveLocation.ToString(), StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
var succeeded = receiveResults.Count(x => x.Outcome == OperationStepOutcomes.Succeeded);
|
||||
var alreadyDisabled = receiveResults.Count(x => x.Outcome == OperationStepOutcomes.AlreadySatisfied);
|
||||
var failed = receiveResults.Count(x => x.Outcome == OperationStepOutcomes.Failed);
|
||||
var failureWarning = failed == 0
|
||||
? string.Empty
|
||||
: "\nWARNING: " + failed + " receive location(s) failed. Review the red Operation Log entries before continuing.\n";
|
||||
|
||||
return "The receive-location shutdown phase has finished. The shutdown is PAUSED and no later phase can run until you decide.\n\n"
|
||||
+ "Disabled successfully: " + succeeded + "\n"
|
||||
+ "Already disabled: " + alreadyDisabled + "\n"
|
||||
+ "Failed: " + failed + "\n"
|
||||
+ failureWarning + "\n"
|
||||
+ "Keep this dialog open while you verify in BizTalk Group Hub and your enterprise monitoring that no new inbound work arrives and all in-flight service instances/messages have drained.\n\n"
|
||||
+ "Continue with orchestrations, send ports and host instances?\n\n"
|
||||
+ "Yes = continue shutdown. No or closing this dialog = stop safely and persist all remaining steps as NotExecuted.";
|
||||
}
|
||||
|
||||
/// <summary>Counts executable drain checkpoints in a plan.</summary>
|
||||
private static int CountCheckpoints(OperationPlan plan)
|
||||
{
|
||||
return plan.Steps.Count(x => x.Execute && IsCheckpoint(x));
|
||||
}
|
||||
|
||||
/// <summary>Checks whether a plan row is the stable operator checkpoint kind.</summary>
|
||||
private static bool IsCheckpoint(OperationStep step)
|
||||
{
|
||||
return step != null && string.Equals(step.Kind, OperationStepKind.OperatorCheckpoint.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="BizTalkPlatformManagementTool.app" />
|
||||
<assemblyIdentity version="2.3.4.0" name="BizTalkPlatformManagementTool" />
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
@@ -10,7 +10,6 @@
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{4f476546-937d-4f22-9fc8-4b416b2f8f9f}" />
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
+7
-37
@@ -1,44 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{B2AD340B-859F-413B-8811-7FBA4BD2BC02}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>BizTalkPlatformManagementTool.Tests</RootNamespace>
|
||||
<AssemblyName>BizTalkPlatformManagementTool.Tests</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup><Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration><Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform><ProjectGuid>{318F4307-F62C-47C9-9B90-F0C9BF2F812A}</ProjectGuid><OutputType>Exe</OutputType><RootNamespace>BizTalkPlatformManagementTool.Tests</RootNamespace><AssemblyName>BizTalkPlatformManagementTool.Tests</AssemblyName><TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion><FileAlignment>512</FileAlignment><Deterministic>true</Deterministic></PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "><DebugSymbols>true</DebugSymbols><DebugType>full</DebugType><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath><DefineConstants>DEBUG;TRACE</DefineConstants><WarningLevel>4</WarningLevel></PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "><DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><WarningLevel>4</WarningLevel><DocumentationFile>bin\Release\BizTalkPlatformManagementTool.Tests.xml</DocumentationFile></PropertyGroup>
|
||||
<ItemGroup><Reference Include="System" /><Reference Include="System.Core" /></ItemGroup>
|
||||
<ItemGroup><Compile Include="Program.cs" /></ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Program.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj">
|
||||
<Project>{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}</Project>
|
||||
<Name>BizTalkPlatformManagementTool</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj"><Project>{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}</Project><Name>BizTalkPlatformManagementTool</Name></ProjectReference>
|
||||
<ProjectReference Include="..\..\src\BizTalkPlatformManagementTool.Setup\BizTalkPlatformManagementTool.Setup.csproj"><Project>{675B68A9-BD80-46A5-B8C5-3B11B0B374E2}</Project><Name>BizTalkPlatformManagementTool.Setup</Name></ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user