Compare commits
5
Commits
3b4a621cd3
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b1c14b892 | ||
|
|
61b5b7e452 | ||
|
|
b18adab26d | ||
|
|
fe3c84a27f | ||
|
|
999f69bcb9 |
+12
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Projekt und Sicherheitsziel
|
## 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.2.4.
|
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
|
## Installerinvarianten
|
||||||
|
|
||||||
@@ -27,14 +27,25 @@ Die zentrale Implementierung liegt in `src/BizTalkPlatformManagementTool.Setup/I
|
|||||||
- Teilfehler werden vollständig pro Schritt persistiert; der Gesamtlauf bleibt sichtbar fehlgeschlagen und darf nicht als Erfolg ausgegeben werden.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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 ü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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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
|
## Versionierung
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,75 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [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.
|
||||||
|
|
||||||
|
## [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.
|
||||||
|
|
||||||
|
### 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.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
|
## [2.2.4] - 2026-08-24
|
||||||
### Fixed
|
### Fixed
|
||||||
- Permission, Windows Script Host, validation and rollback errors affecting the optional all-users desktop shortcut no longer abort installation, update or uninstallation.
|
- Permission, Windows Script Host, validation and rollback errors affecting the optional all-users desktop shortcut no longer abort installation, update or uninstallation.
|
||||||
|
|||||||
+43
-10
@@ -1,6 +1,6 @@
|
|||||||
# BizTalk Platform Management Tool Dokumentation
|
# BizTalk Platform Management Tool Dokumentation
|
||||||
|
|
||||||
**Stand:** 2026-08-19
|
**Stand:** 2026-08-26
|
||||||
**Implementierung:** C# WinForms, .NET Framework 4.6.1
|
**Implementierung:** C# WinForms, .NET Framework 4.6.1
|
||||||
**Archivierte PowerShell-Version:** `archive/powershell/BizTalkPlatformManagementTool.ps1`
|
**Archivierte PowerShell-Version:** `archive/powershell/BizTalkPlatformManagementTool.ps1`
|
||||||
|
|
||||||
@@ -20,6 +20,9 @@ Das BizTalk Platform Management Tool unterstützt kontrollierte Wartungsfenster
|
|||||||
- Projekt: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj`
|
- Projekt: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj`
|
||||||
- UI: `src/BizTalkPlatformManagementTool/Ui/MainForm.cs`
|
- UI: `src/BizTalkPlatformManagementTool/Ui/MainForm.cs`
|
||||||
- WMI-Zugriff: `src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.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`
|
- Operationslogik: `src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs`
|
||||||
- Snapshot-Validierung: `src/BizTalkPlatformManagementTool/Services/SnapshotValidator.cs`
|
- Snapshot-Validierung: `src/BizTalkPlatformManagementTool/Services/SnapshotValidator.cs`
|
||||||
- Installer: `src/BizTalkPlatformManagementTool.Setup`
|
- Installer: `src/BizTalkPlatformManagementTool.Setup`
|
||||||
@@ -35,6 +38,7 @@ Deutsche Inline-Kommentare stehen gezielt an Stellen, deren Zweck nicht allein a
|
|||||||
|
|
||||||
- sichere Shutdown-/Restore-Reihenfolge und Schutz gebundener Orchestrierungen,
|
- sichere Shutdown-/Restore-Reihenfolge und Schutz gebundener Orchestrierungen,
|
||||||
- WMI-Auflösung über breite Abfrage mit clientseitigem Filter,
|
- WMI-Auflösung über breite Abfrage mit clientseitigem Filter,
|
||||||
|
- read-only Anwendungszuordnung über die ExplorerOM-Anwendungshierarchie,
|
||||||
- atomare JSON-Ersetzung auf demselben Volume,
|
- atomare JSON-Ersetzung auf demselben Volume,
|
||||||
- Neutralisierung formelartiger CSV-Werte,
|
- Neutralisierung formelartiger CSV-Werte,
|
||||||
- Persistenz eines Operationsplans vor der Benutzerbestätigung,
|
- Persistenz eines Operationsplans vor der Benutzerbestätigung,
|
||||||
@@ -53,21 +57,26 @@ Selbsterklärende Zuweisungen und reine UI-Konstruktion werden nicht zeilenweise
|
|||||||
6. Mit **Snapshot Before** die Datei `before.json` erzeugen.
|
6. Mit **Snapshot Before** die Datei `before.json` erzeugen.
|
||||||
7. Mit **Shutdown** den Plan `shutdown-plan.json` erzeugen und prüfen.
|
7. Mit **Shutdown** den Plan `shutdown-plan.json` erzeugen und prüfen.
|
||||||
8. **Dry run** erst deaktivieren, wenn der Plan korrekt ist.
|
8. **Dry run** erst deaktivieren, wenn der Plan korrekt ist.
|
||||||
9. Nach der Wartung mit **Restore** aus `before.json` wiederherstellen.
|
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. Mit **Snapshot After** und **Compare** die Umgebung validieren.
|
10. Nach der Wartung mit **Restore** aus `before.json` wiederherstellen.
|
||||||
|
11. Mit **Snapshot After** und **Compare** die Umgebung validieren.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des letzten Snapshots und zeigt `Started`, `Stopped`, `Partial` oder `Unknown`. **Clear** leert die sichtbaren Ergebnis- und Log-Grids, loescht aber keine Dateien. **Close** beendet die Anwendung.
|
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.
|
||||||
|
|
||||||
## Sicherheitsdesign
|
## Sicherheitsdesign
|
||||||
|
|
||||||
- Dry-run ist standardmäßig aktiviert.
|
- 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.
|
- 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.
|
- 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.
|
- Jede Operation schreibt Einträge in das sichtbare Operation Log.
|
||||||
- Zusätzlich wird unter `%ProgramData%\BizTalkPlatformManagementTool\Logs` eine tägliche Logdatei `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben. Nur wenn ProgramData nicht verfügbar ist, wird auf das EXE-Verzeichnis zurückgefallen.
|
- 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.
|
||||||
- Logdateien werden rollierend für den aktuellen Tag plus vier vorherige Tage vorgehalten.
|
- 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.
|
- Der Kopfbereich zeigt den zuletzt erkannten Umgebungsstatus aus den Host Instances.
|
||||||
- Operationspläne werden vor Laufzeitänderungen gespeichert.
|
- Operationspläne werden vor Laufzeitänderungen gespeichert.
|
||||||
- Jeder ausführbare Schritt prüft vor der Mutation, ob sein Sollzustand bereits erreicht ist.
|
- Jeder ausführbare Schritt prüft vor der Mutation, ob sein Sollzustand bereits erreicht ist.
|
||||||
@@ -75,9 +84,11 @@ Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des
|
|||||||
- Ein Teilfehler bleibt im GUI und im Ergebnisreport ausdrücklich fehlgeschlagen und wird nicht als Gesamterfolg ausgegeben.
|
- 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.
|
- Der Nachher-Snapshot wird auch nach Einzelfehlern separat versucht; ein Snapshotfehler wird im Ergebnisreport gesichert.
|
||||||
- WMI-Methodenrückgaben werden geprüft.
|
- 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.
|
- Wartezeiten nutzen konfigurierbare Timeout- und Polling-Werte.
|
||||||
- Host Instances auf anderen Servern werden übersprungen und als Warnung protokolliert.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- Die EXE enthält zusätzlich einen WMI-freien `--self-test`, den der Installer vor und nach der Aktivierung ausführt.
|
||||||
@@ -85,9 +96,12 @@ Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des
|
|||||||
## Shutdown-Reihenfolge
|
## Shutdown-Reihenfolge
|
||||||
|
|
||||||
1. Receive Locations deaktivieren, die im Snapshot aktiviert waren.
|
1. Receive Locations deaktivieren, die im Snapshot aktiviert waren.
|
||||||
2. Orchestrations stoppen, die gestartet waren.
|
2. Operator-Checkpoint: eingehenden Verkehr, Group Hub, laufende Service Instances und Enterprise-Monitoring prüfen; erst nach dem Leerfahren fortsetzen.
|
||||||
3. Send Ports stoppen, die gestartet waren.
|
3. Orchestrations stoppen, die gestartet waren.
|
||||||
4. Host Instances stoppen, die auf dem ausgewählten Server gestartet waren.
|
4. Send Ports stoppen, die gestartet waren.
|
||||||
|
5. Host Instances stoppen, die auf dem ausgewählten Server gestartet waren.
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
## Restore-Reihenfolge
|
## Restore-Reihenfolge
|
||||||
|
|
||||||
@@ -120,6 +134,18 @@ Der Best-effort-Ansatz bedeutet nicht, dass Fehler ignoriert werden: Jeder Teilf
|
|||||||
- Orchestrations mit Status `Bound` bleiben unverändert und werden zur manuellen Prüfung markiert, weil ein blindes Unenlist zu `Unbound` führen kann.
|
- 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.
|
- Receive Locations werden zuletzt aktiviert, damit eingehender Verkehr erst nach Host Instances und abhängigen Artefakten wieder aufgenommen wird.
|
||||||
|
|
||||||
|
## ScheduledTask-Adapter
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Der Resolver sucht nur in vorhandenen, vertrauenswürdigen Installationsverzeichnissen und akzeptiert eine DLL erst nach Prüfung der Assemblyidentität. Er berücksichtigt:
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Ausgabedateien
|
## Ausgabedateien
|
||||||
|
|
||||||
- Snapshots: `before.json`, `after.json`
|
- Snapshots: `before.json`, `after.json`
|
||||||
@@ -129,13 +155,19 @@ Der Best-effort-Ansatz bedeutet nicht, dass Fehler ignoriert werden: Jeder Teilf
|
|||||||
- Emergency-Dateien: `emergency-source-before-*`, `emergency-restore-plan-*`, `emergency-restore-result-*`, `emergency-restore-after-*`
|
- Emergency-Dateien: `emergency-source-before-*`, `emergency-restore-plan-*`, `emergency-restore-result-*`, `emergency-restore-after-*`
|
||||||
- Diff: `diff.json`, `diff.csv`, `diff.html`
|
- Diff: `diff.json`, `diff.csv`, `diff.html`
|
||||||
- Snapshot-Reports: `*.csv`, `*.hosts.csv`, `*.html`
|
- Snapshot-Reports: `*.csv`, `*.hosts.csv`, `*.html`
|
||||||
- Laufzeitlogs: `%ProgramData%\BizTalkPlatformManagementTool\Logs\BizTalkPlatformManagementTool-yyyy-MM-dd.log`
|
- 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`
|
- Installerlogs: `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs\setup-*.log`
|
||||||
|
|
||||||
## Fehleranalyse
|
## Fehleranalyse
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
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.
|
||||||
@@ -177,6 +209,7 @@ Das äußere ZIP erhält zusätzlich eine SHA-256-Datei und eine Certutil-kompat
|
|||||||
|
|
||||||
- Atomare JSON-Aktualisierung und BOM-Kompatibilität.
|
- Atomare JSON-Aktualisierung und BOM-Kompatibilität.
|
||||||
- Anwendungsbezogene Diff-Identität bei gleichnamigen Artefakten.
|
- Anwendungsbezogene Diff-Identität bei gleichnamigen Artefakten.
|
||||||
|
- ExplorerOM-basierte Artefaktzuordnung einschließlich Receive-Port-Fallback und Ambiguitätsschutz.
|
||||||
- Restore-Servergrenze und sichere Restore-Reihenfolge.
|
- Restore-Servergrenze und sichere Restore-Reihenfolge.
|
||||||
- Neutralisierung formelartiger CSV-Werte.
|
- Neutralisierung formelartiger CSV-Werte.
|
||||||
- Erkennung manipulierter Payload-Dateien.
|
- Erkennung manipulierter Payload-Dateien.
|
||||||
|
|||||||
+39
-2
@@ -15,6 +15,8 @@ Das Release erzeugt folgende Dateien unter `artifacts`:
|
|||||||
- `BizTalkPlatformManagementTool-Setup.zip.b64.txt`: Certutil-kompatible Base64-Übertragung
|
- `BizTalkPlatformManagementTool-Setup.zip.b64.txt`: Certutil-kompatible Base64-Übertragung
|
||||||
- `BizTalkPlatformManagementTool-Setup.zip.sha256.txt`: SHA-256 des ZIP-Archivs
|
- `BizTalkPlatformManagementTool-Setup.zip.sha256.txt`: SHA-256 des ZIP-Archivs
|
||||||
- `BizTalkPlatformManagementTool-Setup.zip`: direkt entpackbares Installationspaket
|
- `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
|
||||||
|
|
||||||
Auf dem Zielsystem wird die TXT-Datei so rekonstruiert und geprüft:
|
Auf dem Zielsystem wird die TXT-Datei so rekonstruiert und geprüft:
|
||||||
|
|
||||||
@@ -38,11 +40,44 @@ Der Hash aus `certutil` muss exakt dem Wert in der SHA-256-Datei entsprechen. Da
|
|||||||
Installationsziele:
|
Installationsziele:
|
||||||
|
|
||||||
- Programm: `%ProgramFiles%\BizTalkPlatformManagementTool`
|
- Programm: `%ProgramFiles%\BizTalkPlatformManagementTool`
|
||||||
- Laufzeitlogs: `%ProgramData%\BizTalkPlatformManagementTool\Logs`
|
- Laufzeitlogs primär: `%ProgramData%\BizTalkPlatformManagementTool\Logs`
|
||||||
- Installerlogs: `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs`
|
- Installerlogs: `%ProgramData%\BizTalkPlatformManagementTool\InstallerLogs`
|
||||||
- Uninstaller: `%ProgramData%\BizTalkPlatformManagementTool\Setup\Uninstall.exe`
|
- Uninstaller: `%ProgramData%\BizTalkPlatformManagementTool\Setup\Uninstall.exe`
|
||||||
- Startmenü: `BizTalk Platform Management Tool`
|
- 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
|
## Update und Rollback
|
||||||
|
|
||||||
Der Installer verändert eine bestehende Installation erst nach erfolgreicher Paketprüfung:
|
Der Installer verändert eine bestehende Installation erst nach erfolgreicher Paketprüfung:
|
||||||
@@ -99,7 +134,9 @@ Ein erfolgreicher Installer-Self-Test bestätigt Paket, Programmstart und lokale
|
|||||||
|
|
||||||
## Emergency Restore nach einem Teilabbruch
|
## Emergency Restore nach einem Teilabbruch
|
||||||
|
|
||||||
Version 2.2.4 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.
|
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.
|
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.
|
2. Anwendung als Administrator starten und denselben Zielserver wählen, der im Snapshot gespeichert ist.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -22,14 +22,20 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
|
|||||||
- Host instance handling for the selected BizTalk server
|
- Host instance handling for the selected BizTalk server
|
||||||
- Dry-run mode enabled by default
|
- Dry-run mode enabled by default
|
||||||
- WMI access through `root\MicrosoftBizTalkServer`
|
- 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
|
- Startup check for administrator rights
|
||||||
- Detailed operation logging in the GUI and daily rolling log files under ProgramData
|
- 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
|
- 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
|
- 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
|
- Durable per-step result reports even when a shutdown or restore completes only partially
|
||||||
- Environment status indicator based on host instance state
|
- Environment status indicator based on host instance state
|
||||||
- Clear and Close actions in the main toolbar
|
- Clear and Close actions in the main toolbar
|
||||||
- No compile-time dependency on BizTalk ExplorerOM assemblies
|
- 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
|
- 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
|
- 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
|
- Opt-in all-users desktop shortcut whose ACL, WSH, validation or rollback failures remain visible but cannot roll back the core installation
|
||||||
@@ -44,12 +50,13 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
|
|||||||
4. Click **Snapshot Before**.
|
4. Click **Snapshot Before**.
|
||||||
5. Click **Shutdown** and review `shutdown-plan.json`.
|
5. Click **Shutdown** and review `shutdown-plan.json`.
|
||||||
6. Disable **Dry run** only when the plan is correct.
|
6. Disable **Dry run** only when the plan is correct.
|
||||||
7. After maintenance, click **Restore** using the saved `before.json`.
|
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. Click **Snapshot After** and **Compare**.
|
8. After maintenance, click **Restore** using the saved `before.json`.
|
||||||
|
9. Click **Snapshot After** and **Compare**.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
The environment indicator shows `Started`, `Stopped`, `Partial` or `Unknown` from the most recent snapshot. `Clear` removes the visible status and operation log grids; it does not delete files.
|
The 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.
|
||||||
|
|
||||||
The application requests administrator rights through its UAC manifest and checks them again during startup. Only one GUI instance can run per Windows session.
|
The application requests administrator rights through its UAC manifest and checks them again during startup. Only one GUI instance can run per Windows session.
|
||||||
|
|
||||||
@@ -59,6 +66,7 @@ Before a real shutdown or restore, the exact fresh plan is saved and a second di
|
|||||||
|
|
||||||
Shutdown:
|
Shutdown:
|
||||||
- Disable receive locations that were enabled.
|
- 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 orchestrations that were started.
|
||||||
- Stop send ports that were started.
|
- Stop send ports that were started.
|
||||||
- Stop host instances that were started on the selected server.
|
- Stop host instances that were started on the selected server.
|
||||||
@@ -71,6 +79,10 @@ Restore:
|
|||||||
|
|
||||||
Orchestrations that were `Bound` are deliberately left unchanged during restore to avoid accidentally making them `Unbound`.
|
Orchestrations that were `Bound` are deliberately left unchanged during restore to avoid accidentally making them `Unbound`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
|
|
||||||
- `before.json`, `after.json`
|
- `before.json`, `after.json`
|
||||||
@@ -80,16 +92,30 @@ Orchestrations that were `Bound` are deliberately left unchanged during restore
|
|||||||
- Timestamped `emergency-source-before-*`, `emergency-restore-plan-*`, `emergency-restore-result-*`, `emergency-restore-after-*` and `emergency-restore-diff-*` files
|
- Timestamped `emergency-source-before-*`, `emergency-restore-plan-*`, `emergency-restore-result-*`, `emergency-restore-after-*` and `emergency-restore-diff-*` files
|
||||||
- `diff.json`, `diff.csv`, `diff.html`
|
- `diff.json`, `diff.csv`, `diff.html`
|
||||||
- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html`
|
- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html`
|
||||||
- Runtime logs under `%ProgramData%\BizTalkPlatformManagementTool\Logs`
|
- Runtime logs primarily under `%ProgramData%\BizTalkPlatformManagementTool\Logs`
|
||||||
|
|
||||||
Log files are retained for the current day plus the previous four days. Older `BizTalkPlatformManagementTool-*.log` files are removed on startup. If ProgramData is unexpectedly unavailable, logging falls back to the executable directory.
|
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.
|
||||||
|
|
||||||
|
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
|
## 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.
|
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.
|
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.
|
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.
|
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.
|
||||||
@@ -119,5 +145,10 @@ Targeted German inline comments explain non-obvious operational decisions such a
|
|||||||
- [PROD activation incident analysis and 2.2.3 fix](docs/PROD-Installer-Aktivierungsfehler-2026-08-24.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)
|
- [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)
|
- [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)
|
- [AI maintainer handoff](AI-README.md)
|
||||||
- [References](REFERENCES.md)
|
- [References](REFERENCES.md)
|
||||||
|
|||||||
@@ -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.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_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.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.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
|
- MSBTS_SendPort.Enlist (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-enlist-method-wmi
|
||||||
@@ -14,6 +19,12 @@
|
|||||||
- MSBTS_ReceiveLocation (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi
|
- MSBTS_ReceiveLocation (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi
|
||||||
- MSBTS_ReceiveLocation.Enable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-enable-method-wmi
|
- MSBTS_ReceiveLocation.Enable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-enable-method-wmi
|
||||||
- MSBTS_ReceiveLocation.Disable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-disable-method-wmi
|
- MSBTS_ReceiveLocation.Disable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-disable-method-wmi
|
||||||
|
- 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 (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-wmi
|
||||||
- MSBTS_HostInstance.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-start-method-wmi
|
- MSBTS_HostInstance.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-start-method-wmi
|
||||||
- MSBTS_HostInstance.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-stop-method-wmi
|
- MSBTS_HostInstance.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-stop-method-wmi
|
||||||
|
- 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,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,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.
|
||||||
@@ -23,7 +23,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
|||||||
private const string ProductName = "BizTalk Platform Management Tool";
|
private const string ProductName = "BizTalk Platform Management Tool";
|
||||||
|
|
||||||
/// <summary>Aktuelle Produktversion des Installers und Uninstall-Eintrags.</summary>
|
/// <summary>Aktuelle Produktversion des Installers und Uninstall-Eintrags.</summary>
|
||||||
private const string ProductVersion = "2.2.4";
|
private const string ProductVersion = "2.3.4";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen.
|
/// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen.
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ namespace BizTalkPlatformManagementTool.Setup
|
|||||||
{
|
{
|
||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
|
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
|
||||||
Text = "BizTalk Platform Management Tool 2.2.4"
|
Text = "BizTalk Platform Management Tool 2.3.4"
|
||||||
});
|
});
|
||||||
root.Controls.Add(new Label
|
root.Controls.Add(new Label
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ using System.Runtime.InteropServices;
|
|||||||
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
|
||||||
[assembly: ComVisible(false)]
|
[assembly: ComVisible(false)]
|
||||||
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
|
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
|
||||||
[assembly: AssemblyVersion("2.2.4.0")]
|
[assembly: AssemblyVersion("2.3.4.0")]
|
||||||
[assembly: AssemblyFileVersion("2.2.4.0")]
|
[assembly: AssemblyFileVersion("2.3.4.0")]
|
||||||
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
<assemblyIdentity version="2.2.4.0" name="BizTalkPlatformManagementTool.Setup" />
|
<assemblyIdentity version="2.3.4.0" name="BizTalkPlatformManagementTool.Setup" />
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
|
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
|
||||||
</trustInfo>
|
</trustInfo>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<configuration>
|
<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>
|
<startup>
|
||||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
|
||||||
</startup>
|
</startup>
|
||||||
|
|||||||
@@ -39,10 +39,12 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Configuration" />
|
||||||
<Reference Include="System.Data" />
|
<Reference Include="System.Data" />
|
||||||
<Reference Include="System.Drawing" />
|
<Reference Include="System.Drawing" />
|
||||||
<Reference Include="System.Management" />
|
<Reference Include="System.Management" />
|
||||||
<Reference Include="System.Runtime.Serialization" />
|
<Reference Include="System.Runtime.Serialization" />
|
||||||
|
<Reference Include="System.IO.Compression" />
|
||||||
<Reference Include="System.ServiceProcess" />
|
<Reference Include="System.ServiceProcess" />
|
||||||
<Reference Include="System.Windows.Forms" />
|
<Reference Include="System.Windows.Forms" />
|
||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
@@ -56,15 +58,19 @@
|
|||||||
<Compile Include="Models\DiffModels.cs" />
|
<Compile Include="Models\DiffModels.cs" />
|
||||||
<Compile Include="Models\OperationModels.cs" />
|
<Compile Include="Models\OperationModels.cs" />
|
||||||
<Compile Include="Services\BizTalkWmiClient.cs" />
|
<Compile Include="Services\BizTalkWmiClient.cs" />
|
||||||
|
<Compile Include="Services\BizTalkApplicationResolver.cs" />
|
||||||
|
<Compile Include="Services\AdapterAssemblyResolver.cs" />
|
||||||
<Compile Include="Services\CsvWriter.cs" />
|
<Compile Include="Services\CsvWriter.cs" />
|
||||||
<Compile Include="Services\HtmlReportWriter.cs" />
|
<Compile Include="Services\HtmlReportWriter.cs" />
|
||||||
<Compile Include="Services\JsonFileStore.cs" />
|
<Compile Include="Services\JsonFileStore.cs" />
|
||||||
<Compile Include="Services\OperationLogger.cs" />
|
<Compile Include="Services\OperationLogger.cs" />
|
||||||
|
<Compile Include="Services\ExceptionDiagnostics.cs" />
|
||||||
<Compile Include="Services\OperationPlanExecutor.cs" />
|
<Compile Include="Services\OperationPlanExecutor.cs" />
|
||||||
<Compile Include="Services\SnapshotComparer.cs" />
|
<Compile Include="Services\SnapshotComparer.cs" />
|
||||||
<Compile Include="Services\SnapshotValidator.cs" />
|
<Compile Include="Services\SnapshotValidator.cs" />
|
||||||
<Compile Include="Services\SnapshotStore.cs" />
|
<Compile Include="Services\SnapshotStore.cs" />
|
||||||
<Compile Include="Services\BizTalkOperationService.cs" />
|
<Compile Include="Services\BizTalkOperationService.cs" />
|
||||||
|
<Compile Include="Ui\ShutdownOperatorPrompts.cs" />
|
||||||
<Compile Include="Ui\MainForm.cs" />
|
<Compile Include="Ui\MainForm.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ namespace BizTalkPlatformManagementTool.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
ReceiveLocation,
|
ReceiveLocation,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An operator confirmation boundary between shutdown phases.
|
||||||
|
/// </summary>
|
||||||
|
OperatorCheckpoint,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A send port step.
|
/// A send port step.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -184,6 +189,14 @@ namespace BizTalkPlatformManagementTool.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[DataMember(Order = 13)]
|
[DataMember(Order = 13)]
|
||||||
public string Warning { get; set; }
|
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 AdapterName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the receive transport address used to identify scheduler URIs.</summary>
|
||||||
|
[DataMember(Order = 15, EmitDefaultValue = false)]
|
||||||
|
public string Address { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -241,6 +254,15 @@ namespace BizTalkPlatformManagementTool.Models
|
|||||||
|
|
||||||
/// <summary>The step failed, while subsequent independent steps were still attempted.</summary>
|
/// <summary>The step failed, while subsequent independent steps were still attempted.</summary>
|
||||||
public const string Failed = "Failed";
|
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>
|
/// <summary>
|
||||||
@@ -351,10 +373,32 @@ namespace BizTalkPlatformManagementTool.Models
|
|||||||
[DataMember(Order = 13)]
|
[DataMember(Order = 13)]
|
||||||
public List<OperationStepResult> Steps { get; set; }
|
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>
|
/// <summary>Gets whether operator attention is required.</summary>
|
||||||
public bool HasFailures
|
public bool HasFailures
|
||||||
{
|
{
|
||||||
get { return FailedCount > 0 || !string.IsNullOrWhiteSpace(InitializationError) || !string.IsNullOrWhiteSpace(PostSnapshotError); }
|
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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ using System.Runtime.InteropServices;
|
|||||||
[assembly: AssemblyCopyright("Copyright © 2026")]
|
[assembly: AssemblyCopyright("Copyright © 2026")]
|
||||||
[assembly: ComVisible(false)]
|
[assembly: ComVisible(false)]
|
||||||
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
|
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
|
||||||
[assembly: AssemblyVersion("2.2.4.0")]
|
[assembly: AssemblyVersion("2.3.4.0")]
|
||||||
[assembly: AssemblyFileVersion("2.2.4.0")]
|
[assembly: AssemblyFileVersion("2.3.4.0")]
|
||||||
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ namespace BizTalkPlatformManagementTool
|
|||||||
SnapshotStore.SaveSnapshotSet(Path.Combine(directory, "before.json"), loaded);
|
SnapshotStore.SaveSnapshotSet(Path.Combine(directory, "before.json"), loaded);
|
||||||
SnapshotStore.SaveDiffSet(Path.Combine(directory, "diff.json"), diff);
|
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 operationService = new BizTalkOperationService(null);
|
||||||
var emergencyPlan = operationService.CreateEmergencyRestorePlan(loaded, loaded.Server);
|
var emergencyPlan = operationService.CreateEmergencyRestorePlan(loaded, loaded.Server);
|
||||||
if (emergencyPlan.Steps.Count == 0 || emergencyPlan.Steps[0].Kind != "WindowsService" || emergencyPlan.Steps[0].Name != "ENTSSO")
|
if (emergencyPlan.Steps.Count == 0 || emergencyPlan.Steps[0].Kind != "WindowsService" || emergencyPlan.Steps[0].Name != "ENTSSO")
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,10 +16,10 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Current tool version written into generated snapshots.
|
/// Current tool version written into generated snapshots.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Version = "2.2.4-net461";
|
public const string Version = "2.3.4-net461";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fallback application name used when WMI does not expose an application property.
|
/// Fallback application name used when neither WMI nor the ExplorerOM catalog can resolve ownership.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const string UnknownApplication = "(Unknown Application)";
|
private const string UnknownApplication = "(Unknown Application)";
|
||||||
|
|
||||||
@@ -48,7 +48,17 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
var ports = client.Query("MSBTS_SendPort");
|
var ports = client.Query("MSBTS_SendPort");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.Success("WMI/CIM diagnostic succeeded. Send ports visible: " + ports.Count);
|
var applications = TryLoadApplicationResolver(client);
|
||||||
|
var resolved = applications == null
|
||||||
|
? 0
|
||||||
|
: ports.Count(x => !string.IsNullOrWhiteSpace(applications.ResolveSendPort(BizTalkWmiClient.SafeGetString(x, "Name", null))));
|
||||||
|
_logger.Success("WMI/CIM diagnostic succeeded. Send ports visible: " + ports.Count
|
||||||
|
+ "; application associations resolved: " + resolved + ".");
|
||||||
|
if (ports.Count > 0 && resolved != ports.Count)
|
||||||
|
{
|
||||||
|
_logger.Warning("Application association is incomplete. Unresolved send ports: "
|
||||||
|
+ (ports.Count - resolved) + ". Check the preceding ExplorerOM and BizTalk Management database diagnostics.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -74,18 +84,21 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
};
|
};
|
||||||
|
|
||||||
var apps = new Dictionary<string, ApplicationSnapshot>(StringComparer.OrdinalIgnoreCase);
|
var apps = new Dictionary<string, ApplicationSnapshot>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var applicationResolver = TryLoadApplicationResolver(client);
|
||||||
var receiveLocations = client.Query("MSBTS_ReceiveLocation");
|
var receiveLocations = client.Query("MSBTS_ReceiveLocation");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var item in receiveLocations)
|
foreach (var item in receiveLocations)
|
||||||
{
|
{
|
||||||
var appName = GetApplicationName(item);
|
var name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty);
|
||||||
|
var receivePortName = BizTalkWmiClient.SafeGetString(item, "ReceivePortName", string.Empty);
|
||||||
|
var appName = GetApplicationName(item, applicationResolver, "ReceiveLocation", name, receivePortName);
|
||||||
var app = GetApplication(apps, appName);
|
var app = GetApplication(apps, appName);
|
||||||
app.ReceiveLocations.Add(new ReceiveLocationState
|
app.ReceiveLocations.Add(new ReceiveLocationState
|
||||||
{
|
{
|
||||||
Application = app.Application,
|
Application = app.Application,
|
||||||
Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty),
|
Name = name,
|
||||||
ReceivePortName = BizTalkWmiClient.SafeGetString(item, "ReceivePortName", string.Empty),
|
ReceivePortName = receivePortName,
|
||||||
Enabled = !BizTalkWmiClient.SafeGetBoolean(item, "IsDisabled", true),
|
Enabled = !BizTalkWmiClient.SafeGetBoolean(item, "IsDisabled", true),
|
||||||
AdapterName = BizTalkWmiClient.SafeGetString(item, "AdapterName", string.Empty),
|
AdapterName = BizTalkWmiClient.SafeGetString(item, "AdapterName", string.Empty),
|
||||||
Address = BizTalkWmiClient.SafeGetString(item, "InboundTransportURL", string.Empty)
|
Address = BizTalkWmiClient.SafeGetString(item, "InboundTransportURL", string.Empty)
|
||||||
@@ -102,12 +115,13 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
foreach (var item in sendPorts)
|
foreach (var item in sendPorts)
|
||||||
{
|
{
|
||||||
var appName = GetApplicationName(item);
|
var name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty);
|
||||||
|
var appName = GetApplicationName(item, applicationResolver, "SendPort", name, null);
|
||||||
var app = GetApplication(apps, appName);
|
var app = GetApplication(apps, appName);
|
||||||
app.SendPorts.Add(new SendPortState
|
app.SendPorts.Add(new SendPortState
|
||||||
{
|
{
|
||||||
Application = app.Application,
|
Application = app.Application,
|
||||||
Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty),
|
Name = name,
|
||||||
Status = BizTalkWmiClient.SafeGetInt32(item, "Status", 0),
|
Status = BizTalkWmiClient.SafeGetInt32(item, "Status", 0),
|
||||||
PrimaryTransportType = BizTalkWmiClient.SafeGetString(item, "PTTransportType", string.Empty),
|
PrimaryTransportType = BizTalkWmiClient.SafeGetString(item, "PTTransportType", string.Empty),
|
||||||
PrimaryTransportAddress = BizTalkWmiClient.SafeGetString(item, "PTAddress", string.Empty)
|
PrimaryTransportAddress = BizTalkWmiClient.SafeGetString(item, "PTAddress", string.Empty)
|
||||||
@@ -124,12 +138,13 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
foreach (var item in orchestrations)
|
foreach (var item in orchestrations)
|
||||||
{
|
{
|
||||||
var appName = GetApplicationName(item);
|
var name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty);
|
||||||
|
var appName = GetApplicationName(item, applicationResolver, "Orchestration", name, null);
|
||||||
var app = GetApplication(apps, appName);
|
var app = GetApplication(apps, appName);
|
||||||
app.Orchestrations.Add(new OrchestrationState
|
app.Orchestrations.Add(new OrchestrationState
|
||||||
{
|
{
|
||||||
Application = app.Application,
|
Application = app.Application,
|
||||||
Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty),
|
Name = name,
|
||||||
OrchestrationStatus = BizTalkWmiClient.SafeGetInt32(item, "OrchestrationStatus", 0)
|
OrchestrationStatus = BizTalkWmiClient.SafeGetInt32(item, "OrchestrationStatus", 0)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -170,6 +185,14 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
snapshot.HostInstances = snapshot.HostInstances.OrderBy(x => x.Server).ThenBy(x => x.InstanceName).ToList();
|
snapshot.HostInstances = snapshot.HostInstances.OrderBy(x => x.Server).ThenBy(x => x.InstanceName).ToList();
|
||||||
|
|
||||||
SnapshotValidator.Validate(snapshot);
|
SnapshotValidator.Validate(snapshot);
|
||||||
|
ApplicationSnapshot unresolved;
|
||||||
|
if (apps.TryGetValue(UnknownApplication, out unresolved) && _logger != null)
|
||||||
|
{
|
||||||
|
_logger.Warning("Snapshot contains unresolved BizTalk application associations. ReceiveLocations="
|
||||||
|
+ unresolved.ReceiveLocations.Count + "; SendPorts=" + unresolved.SendPorts.Count
|
||||||
|
+ "; Orchestrations=" + unresolved.Orchestrations.Count
|
||||||
|
+ ". Review ExplorerOM and BizTalk Management database diagnostics before plan approval.");
|
||||||
|
}
|
||||||
_logger.Success("Snapshot created. Applications: " + snapshot.Applications.Count + ", host instances: " + snapshot.HostInstances.Count);
|
_logger.Success("Snapshot created. Applications: " + snapshot.Applications.Count + ", host instances: " + snapshot.HostInstances.Count);
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
@@ -192,10 +215,25 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
foreach (var item in app.ReceiveLocations.Where(x => x.Enabled))
|
foreach (var item in app.ReceiveLocations.Where(x => x.Enabled))
|
||||||
{
|
{
|
||||||
plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, null));
|
var step = Step("ReceiveLocation", app.Application, item.Name, null, "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, null);
|
||||||
|
SetReceiveTransport(step, item);
|
||||||
|
plan.Steps.Add(step);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (HasShutdownWorkAfterInboundPhase(snapshot, server))
|
||||||
|
{
|
||||||
|
plan.Steps.Add(new OperationStep
|
||||||
|
{
|
||||||
|
Kind = OperationStepKind.OperatorCheckpoint.ToString(),
|
||||||
|
Application = string.Empty,
|
||||||
|
Name = "Inbound drain verification",
|
||||||
|
Action = "Confirm that in-flight BizTalk processing has drained before continuing shutdown",
|
||||||
|
Execute = true,
|
||||||
|
Warning = "The operator must verify Group Hub/runtime monitoring. No automatic empty-state claim is made."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var app in snapshot.Applications)
|
foreach (var app in snapshot.Applications)
|
||||||
{
|
{
|
||||||
foreach (var item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted))
|
foreach (var item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted))
|
||||||
@@ -306,7 +344,9 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
foreach (var item in app.ReceiveLocations)
|
foreach (var item in app.ReceiveLocations)
|
||||||
{
|
{
|
||||||
plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, item.Enabled ? "Enable receive location" : "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, item.Enabled ? "Enable" : "Disable", null, null));
|
var step = Step("ReceiveLocation", app.Application, item.Name, null, item.Enabled ? "Enable receive location" : "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, item.Enabled ? "Enable" : "Disable", null, null);
|
||||||
|
SetReceiveTransport(step, item);
|
||||||
|
plan.Steps.Add(step);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,6 +385,21 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
/// <param name="plan">The ordered plan to execute.</param>
|
/// <param name="plan">The ordered plan to execute.</param>
|
||||||
/// <param name="options">The runtime options controlling server, dry-run and wait behavior.</param>
|
/// <param name="options">The runtime options controlling server, dry-run and wait behavior.</param>
|
||||||
public OperationExecutionReport ExecutePlan(OperationPlan plan, OperationOptions options)
|
public OperationExecutionReport ExecutePlan(OperationPlan plan, OperationOptions options)
|
||||||
|
{
|
||||||
|
return ExecutePlan(plan, options, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a plan with an optional fail-closed operator checkpoint callback.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plan">The ordered plan to execute.</param>
|
||||||
|
/// <param name="options">The runtime execution options.</param>
|
||||||
|
/// <param name="checkpointHandler">Operator callback used by real shutdown plans.</param>
|
||||||
|
/// <returns>The complete execution report.</returns>
|
||||||
|
internal OperationExecutionReport ExecutePlan(
|
||||||
|
OperationPlan plan,
|
||||||
|
OperationOptions options,
|
||||||
|
OperationCheckpointHandler checkpointHandler)
|
||||||
{
|
{
|
||||||
if (plan == null || options == null)
|
if (plan == null || options == null)
|
||||||
{
|
{
|
||||||
@@ -358,14 +413,14 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
if (options.DryRun)
|
if (options.DryRun)
|
||||||
{
|
{
|
||||||
// Dry-run benötigt absichtlich weder WMI-Verbindung noch Servicezugriff.
|
// Dry-run benötigt absichtlich weder WMI-Verbindung noch Servicezugriff.
|
||||||
return executor.Execute(plan, options, null);
|
return executor.Execute(plan, options, null, checkpointHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var runtime = new WmiOperationStepRuntime(options.Server, _logger))
|
using (var runtime = new WmiOperationStepRuntime(options.Server, _logger))
|
||||||
{
|
{
|
||||||
return executor.Execute(plan, options, runtime);
|
return executor.Execute(plan, options, runtime, checkpointHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -387,6 +442,19 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Checks whether a shutdown has a runtime phase after receive locations.</summary>
|
||||||
|
/// <param name="snapshot">The validated source snapshot.</param>
|
||||||
|
/// <param name="server">The selected host-instance server.</param>
|
||||||
|
/// <returns>True when orchestration, send-port or host-instance work follows.</returns>
|
||||||
|
private static bool HasShutdownWorkAfterInboundPhase(BizTalkSnapshot snapshot, string server)
|
||||||
|
{
|
||||||
|
return snapshot.Applications.Any(app =>
|
||||||
|
app.Orchestrations.Any(item => item.OrchestrationStatus == ArtifactStates.OrchestrationStarted)
|
||||||
|
|| app.SendPorts.Any(item => item.Status == ArtifactStates.SendPortStarted))
|
||||||
|
|| snapshot.HostInstances.Any(item => item.RawState == ArtifactStates.HostStarted
|
||||||
|
&& (string.IsNullOrWhiteSpace(item.Server) || SnapshotValidator.ServerNamesEqual(item.Server, server)));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves a snapshot and its report sidecars.
|
/// Saves a snapshot and its report sidecars.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -465,6 +533,9 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
/// <summary>Operation logger.</summary>
|
/// <summary>Operation logger.</summary>
|
||||||
private readonly OperationLogger _logger;
|
private readonly OperationLogger _logger;
|
||||||
|
|
||||||
|
/// <summary>Process-local resolver for adapter validation dependencies.</summary>
|
||||||
|
private readonly AdapterAssemblyResolver _assemblyResolver;
|
||||||
|
|
||||||
/// <summary>Lazily connected BizTalk WMI client.</summary>
|
/// <summary>Lazily connected BizTalk WMI client.</summary>
|
||||||
private BizTalkWmiClient _client;
|
private BizTalkWmiClient _client;
|
||||||
|
|
||||||
@@ -475,6 +546,7 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
_server = string.IsNullOrWhiteSpace(server) ? Environment.MachineName : server.Trim();
|
_server = string.IsNullOrWhiteSpace(server) ? Environment.MachineName : server.Trim();
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_assemblyResolver = new AdapterAssemblyResolver(logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Executes one Windows-service or BizTalk-WMI step state-aware.</summary>
|
/// <summary>Executes one Windows-service or BizTalk-WMI step state-aware.</summary>
|
||||||
@@ -499,6 +571,10 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
return RuntimeStepOutcome.AlreadySatisfied;
|
return RuntimeStepOutcome.AlreadySatisfied;
|
||||||
}
|
}
|
||||||
|
if (IsScheduledTaskReceiveLocation(step))
|
||||||
|
{
|
||||||
|
_assemblyResolver.PrepareScheduledTaskAdapter();
|
||||||
|
}
|
||||||
ExecuteWmiStep(instance, step, options);
|
ExecuteWmiStep(instance, step, options);
|
||||||
return RuntimeStepOutcome.Succeeded;
|
return RuntimeStepOutcome.Succeeded;
|
||||||
}
|
}
|
||||||
@@ -514,6 +590,20 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
_client.Dispose();
|
_client.Dispose();
|
||||||
_client = null;
|
_client = null;
|
||||||
}
|
}
|
||||||
|
_assemblyResolver.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Detects the third-party ScheduledTask adapter from captured plan metadata.</summary>
|
||||||
|
/// <param name="step">The receive-location step.</param>
|
||||||
|
/// <returns>True when Scheduler dependency preflight is required.</returns>
|
||||||
|
private static bool IsScheduledTaskReceiveLocation(OperationStep step)
|
||||||
|
{
|
||||||
|
return step != null
|
||||||
|
&& string.Equals(step.Kind, "ReceiveLocation", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& ((!string.IsNullOrWhiteSpace(step.Address)
|
||||||
|
&& step.Address.TrimStart().StartsWith("scheduler:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| (!string.IsNullOrWhiteSpace(step.AdapterName)
|
||||||
|
&& step.AdapterName.IndexOf("schedul", StringComparison.OrdinalIgnoreCase) >= 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -735,6 +825,15 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Copies receive transport metadata into a durable operation step.</summary>
|
||||||
|
/// <param name="step">The plan step to enrich.</param>
|
||||||
|
/// <param name="receiveLocation">The captured receive location.</param>
|
||||||
|
private static void SetReceiveTransport(OperationStep step, ReceiveLocationState receiveLocation)
|
||||||
|
{
|
||||||
|
step.AdapterName = receiveLocation == null ? null : receiveLocation.AdapterName;
|
||||||
|
step.Address = receiveLocation == null ? null : receiveLocation.Address;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts integer method arguments into the object array required by WMI.
|
/// Converts integer method arguments into the object array required by WMI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -773,6 +872,8 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
+ ", class=" + step.WmiClass
|
+ ", class=" + step.WmiClass
|
||||||
+ ", key=" + step.KeyProperty + "=" + (step.KeyValue ?? string.Empty)
|
+ ", key=" + step.KeyProperty + "=" + (step.KeyValue ?? string.Empty)
|
||||||
+ ", method=" + step.MethodName
|
+ ", 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)
|
+ (string.IsNullOrWhiteSpace(step.Server) ? string.Empty : ", server=" + step.Server)
|
||||||
+ "]";
|
+ "]";
|
||||||
}
|
}
|
||||||
@@ -795,24 +896,75 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads the application name from the available WMI properties.
|
/// Reads a directly exposed application name or resolves it through the
|
||||||
|
/// ExplorerOM application hierarchy because the runtime WMI classes do not
|
||||||
|
/// define a reliable application property.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="item">The WMI object being mapped into a snapshot item.</param>
|
/// <param name="item">The WMI object being mapped into a snapshot item.</param>
|
||||||
|
/// <param name="resolver">Optional ExplorerOM application resolver.</param>
|
||||||
|
/// <param name="kind">Artifact kind used to select the catalog index.</param>
|
||||||
|
/// <param name="name">Artifact name.</param>
|
||||||
|
/// <param name="parentName">Optional parent receive-port name.</param>
|
||||||
/// <returns>The discovered application name or a stable fallback.</returns>
|
/// <returns>The discovered application name or a stable fallback.</returns>
|
||||||
private static string GetApplicationName(ManagementObject item)
|
private static string GetApplicationName(ManagementObject item, BizTalkApplicationResolver resolver, string kind, string name, string parentName)
|
||||||
{
|
{
|
||||||
var names = new[] { "ApplicationName", "Application", "BizTalkApplication" };
|
var names = new[] { "ApplicationName", "Application", "BizTalkApplication" };
|
||||||
foreach (var name in names)
|
foreach (var propertyName in names)
|
||||||
{
|
{
|
||||||
var value = BizTalkWmiClient.SafeGetString(item, name, null);
|
var value = BizTalkWmiClient.SafeGetString(item, propertyName, null);
|
||||||
if (!string.IsNullOrWhiteSpace(value))
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resolver != null)
|
||||||
|
{
|
||||||
|
string application = null;
|
||||||
|
if (string.Equals(kind, "SendPort", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
application = resolver.ResolveSendPort(name);
|
||||||
|
}
|
||||||
|
else if (string.Equals(kind, "ReceiveLocation", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
application = resolver.ResolveReceiveLocation(name, parentName);
|
||||||
|
}
|
||||||
|
else if (string.Equals(kind, "Orchestration", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
application = resolver.ResolveOrchestration(name);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(application))
|
||||||
|
{
|
||||||
|
return application;
|
||||||
|
}
|
||||||
|
}
|
||||||
return UnknownApplication;
|
return UnknownApplication;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the optional ExplorerOM application index while preserving WMI-only
|
||||||
|
/// snapshot availability and emitting an actionable warning on failure.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">Connected BizTalk WMI client.</param>
|
||||||
|
/// <returns>The populated resolver, or null when the catalog could not be read.</returns>
|
||||||
|
private BizTalkApplicationResolver TryLoadApplicationResolver(BizTalkWmiClient client)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return BizTalkApplicationResolver.Load(client, _logger);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (_logger != null)
|
||||||
|
{
|
||||||
|
_logger.Warning("BizTalk application association could not be loaded. Runtime state collection remains available, "
|
||||||
|
+ "but affected artifacts will be grouped under " + UnknownApplication + ". "
|
||||||
|
+ ExceptionDiagnostics.Format(ex));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gibt alle von einer WMI-Abfrage übernommenen Objekte deterministisch frei.
|
/// Gibt alle von einer WMI-Abfrage übernommenen Objekte deterministisch frei.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Services
|
namespace BizTalkPlatformManagementTool.Services
|
||||||
{
|
{
|
||||||
@@ -68,39 +71,88 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
private const string LogFileExtension = ".log";
|
private const string LogFileExtension = ".log";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of daily log files retained, including the current day.
|
/// Number of calendar days retained, including the current day.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int RetentionDays = 5;
|
internal const int RetentionDays = 30;
|
||||||
|
|
||||||
|
/// <summary>Maximum number of historical records restored into the GUI by default.</summary>
|
||||||
|
public const int DefaultGridHistoryLimit = 10000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process-wide lock that serializes log file appends.
|
/// Process-wide lock that serializes log file appends.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly object FileLock = new object();
|
private static readonly object FileLock = new object();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process-wide flag that ensures log cleanup runs only once.
|
|
||||||
/// </summary>
|
|
||||||
private static int _cleanupDone;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional callback for forwarding entries to the UI.
|
/// Optional callback for forwarding entries to the UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Action<LogEntry> _sink;
|
private readonly Action<LogEntry> _sink;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Directory where daily log files are written.
|
/// Ordered local directories available for primary and fallback logging.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly string _logDirectory;
|
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>
|
/// <summary>
|
||||||
/// Initializes a new logger that writes beside the executable.
|
/// Initializes a logger that verifies ProgramData and ordered local fallback directories.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sink">Optional callback that receives entries for display.</param>
|
/// <param name="sink">Optional callback that receives entries for display.</param>
|
||||||
public OperationLogger(Action<LogEntry> sink)
|
public OperationLogger(Action<LogEntry> sink)
|
||||||
|
: this(sink, BuildLogDirectoryCandidates())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
_sink = sink;
|
||||||
_logDirectory = ResolveLogDirectory();
|
_candidateDirectories = NormalizeDirectories(candidateDirectories).ToList();
|
||||||
CleanupOldLogs();
|
_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>
|
/// <summary>
|
||||||
@@ -110,9 +162,55 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
get
|
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>
|
/// <summary>
|
||||||
/// Writes an informational entry.
|
/// Writes an informational entry.
|
||||||
@@ -164,13 +262,30 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
Message = message
|
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)
|
if (_sink == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_sink(entry);
|
_sink(entry);
|
||||||
@@ -193,48 +308,93 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
/// Appends one entry to the current daily log file.
|
/// Appends one entry to the current daily log file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="entry">The entry to write.</param>
|
/// <param name="entry">The entry to write.</param>
|
||||||
private void WriteToFile(LogEntry entry)
|
/// <returns>A new operator-visible storage notice, or null.</returns>
|
||||||
{
|
private string WriteToFile(LogEntry entry)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var line = string.Format(
|
var line = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"[{0:yyyy-MM-dd HH:mm:ss}][{1}] {2}{3}",
|
"[{0:yyyy-MM-dd HH:mm:ss}][{1}] {2}{3}",
|
||||||
entry.Timestamp,
|
entry.Timestamp,
|
||||||
entry.Level.ToString().ToUpperInvariant(),
|
entry.Level.ToString().ToUpperInvariant(),
|
||||||
entry.Message,
|
(entry.Message ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n"),
|
||||||
Environment.NewLine);
|
Environment.NewLine);
|
||||||
|
|
||||||
lock (FileLock)
|
lock (FileLock)
|
||||||
{
|
{
|
||||||
File.AppendAllText(LogFilePath, line);
|
var failures = new List<string>();
|
||||||
}
|
var previousDirectory = _logDirectory;
|
||||||
}
|
var startIndex = _activeDirectoryIndex >= 0 ? _activeDirectoryIndex : 0;
|
||||||
catch
|
for (var index = startIndex; index < _candidateDirectories.Count; index++)
|
||||||
{
|
{
|
||||||
// Ein Logfehler darf niemals eine fachliche BizTalk-Operation abbrechen.
|
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>
|
/// <summary>
|
||||||
/// Deletes log files older than the configured retention window.
|
/// Compresses completed daily logs and removes all records outside retention.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void CleanupOldLogs()
|
/// <param name="today">The current local date.</param>
|
||||||
|
internal void MaintainLogs(DateTime today)
|
||||||
{
|
{
|
||||||
if (Interlocked.Exchange(ref _cleanupDone, 1) == 1)
|
var directory = LogDirectory;
|
||||||
|
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var cutoff = DateTime.Now.Date.AddDays(-(RetentionDays - 1));
|
var cutoff = today.Date.AddDays(-(RetentionDays - 1));
|
||||||
foreach (var file in Directory.GetFiles(_logDirectory, LogFilePrefix + "*" + LogFileExtension))
|
foreach (var file in Directory.GetFiles(directory, LogFilePrefix + "*" + LogFileExtension + "*"))
|
||||||
{
|
{
|
||||||
var lastWrite = File.GetLastWriteTime(file);
|
DateTime fileDate;
|
||||||
if (lastWrite.Date < cutoff)
|
if (!TryGetLogDate(file, out fileDate))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (fileDate < cutoff)
|
||||||
{
|
{
|
||||||
File.Delete(file);
|
File.Delete(file);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (fileDate < today.Date && file.EndsWith(LogFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Compress(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,23 +404,255 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Compresses a completed log atomically and removes the plain source afterward.</summary>
|
||||||
/// Ermittelt das bevorzugte maschinenweite Logverzeichnis mit Rückfall auf das EXE-Verzeichnis.
|
/// <param name="source">The completed plain log.</param>
|
||||||
/// </summary>
|
private static void Compress(string source)
|
||||||
/// <returns>Ein verwendbares Verzeichnis für die täglichen Laufzeitlogs.</returns>
|
|
||||||
private static string ResolveLogDirectory()
|
|
||||||
{
|
{
|
||||||
var commonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
var target = source + ".gz";
|
||||||
var preferred = Path.Combine(commonData, "BizTalkPlatformManagementTool", "Logs");
|
if (File.Exists(target))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var temporary = target + ".tmp." + Guid.NewGuid().ToString("N");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(preferred);
|
using (var input = File.OpenRead(source))
|
||||||
return preferred;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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
|
catch
|
||||||
{
|
{
|
||||||
return AppDomain.CurrentDomain.BaseDirectory;
|
// 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
|
||||||
|
{
|
||||||
|
fullPath = Path.GetFullPath(directory.Trim()).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (seen.Add(fullPath))
|
||||||
|
{
|
||||||
|
yield return fullPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
RuntimeStepOutcome Execute(OperationStep step, OperationOptions options);
|
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>
|
/// <summary>
|
||||||
/// Runs every independent plan step, records durable outcomes and deliberately
|
/// Runs every independent plan step, records durable outcomes and deliberately
|
||||||
/// continues after isolated failures.
|
/// continues after isolated failures.
|
||||||
@@ -56,6 +64,23 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
/// <param name="runtime">The runtime implementation; optional only during dry-run.</param>
|
/// <param name="runtime">The runtime implementation; optional only during dry-run.</param>
|
||||||
/// <returns>A complete per-step execution report.</returns>
|
/// <returns>A complete per-step execution report.</returns>
|
||||||
public OperationExecutionReport Execute(OperationPlan plan, OperationOptions options, IOperationStepRuntime runtime)
|
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)
|
if (plan == null || options == null)
|
||||||
{
|
{
|
||||||
@@ -71,6 +96,7 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
{
|
{
|
||||||
var step = plan.Steps[index];
|
var step = plan.Steps[index];
|
||||||
var stepResult = NewStepResult(index + 1, step);
|
var stepResult = NewStepResult(index + 1, step);
|
||||||
|
var stopAfterCurrentStep = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!step.Execute)
|
if (!step.Execute)
|
||||||
@@ -85,6 +111,30 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
report.DryRunCount++;
|
report.DryRunCount++;
|
||||||
Info("DRY RUN: " + DescribeStep(step));
|
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
|
else
|
||||||
{
|
{
|
||||||
Info("Executing step " + (index + 1) + "/" + plan.Steps.Count + ": " + DescribeStep(step));
|
Info("Executing step " + (index + 1) + "/" + plan.Steps.Count + ": " + DescribeStep(step));
|
||||||
@@ -107,13 +157,30 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
stepResult.Outcome = OperationStepOutcomes.Failed;
|
stepResult.Outcome = OperationStepOutcomes.Failed;
|
||||||
stepResult.Error = FormatException(ex);
|
stepResult.Error = FormatException(ex);
|
||||||
report.FailedCount++;
|
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);
|
Error("STEP FAILED; continuing with remaining independent steps: " + DescribeStep(step) + ". Error: " + stepResult.Error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
stepResult.FinishedAt = DateTimeOffset.Now.ToString("o");
|
stepResult.FinishedAt = DateTimeOffset.Now.ToString("o");
|
||||||
report.Steps.Add(stepResult);
|
report.Steps.Add(stepResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stopAfterCurrentStep)
|
||||||
|
{
|
||||||
|
AddNotExecutedSteps(plan, index + 1, report);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
report.FinishedAt = DateTimeOffset.Now.ToString("o");
|
report.FinishedAt = DateTimeOffset.Now.ToString("o");
|
||||||
@@ -121,8 +188,14 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
+ ", already_satisfied=" + report.AlreadySatisfiedCount
|
+ ", already_satisfied=" + report.AlreadySatisfiedCount
|
||||||
+ ", skipped=" + report.SkippedCount
|
+ ", skipped=" + report.SkippedCount
|
||||||
+ ", dry_run=" + report.DryRunCount
|
+ ", dry_run=" + report.DryRunCount
|
||||||
+ ", failed=" + report.FailedCount + ".";
|
+ ", failed=" + report.FailedCount
|
||||||
if (report.FailedCount == 0)
|
+ ", 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);
|
Success(summary);
|
||||||
}
|
}
|
||||||
@@ -133,6 +206,31 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
return report;
|
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>
|
/// <summary>Creates the common execution-report header.</summary>
|
||||||
/// <param name="plan">The source plan.</param>
|
/// <param name="plan">The source plan.</param>
|
||||||
/// <param name="options">The execution options.</param>
|
/// <param name="options">The execution options.</param>
|
||||||
@@ -179,6 +277,8 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
+ ", class=" + step.WmiClass
|
+ ", class=" + step.WmiClass
|
||||||
+ ", key=" + step.KeyProperty + "=" + (step.KeyValue ?? string.Empty)
|
+ ", key=" + step.KeyProperty + "=" + (step.KeyValue ?? string.Empty)
|
||||||
+ ", method=" + step.MethodName
|
+ ", 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)
|
+ (string.IsNullOrWhiteSpace(step.Server) ? string.Empty : ", server=" + step.Server)
|
||||||
+ "]";
|
+ "]";
|
||||||
}
|
}
|
||||||
@@ -188,21 +288,7 @@ namespace BizTalkPlatformManagementTool.Services
|
|||||||
/// <returns>A single diagnostic message.</returns>
|
/// <returns>A single diagnostic message.</returns>
|
||||||
internal static string FormatException(Exception exception)
|
internal static string FormatException(Exception exception)
|
||||||
{
|
{
|
||||||
if (exception == null)
|
return ExceptionDiagnostics.Format(exception);
|
||||||
{
|
|
||||||
return "Unknown operation error.";
|
|
||||||
}
|
|
||||||
var message = exception.Message;
|
|
||||||
var inner = exception.InnerException;
|
|
||||||
while (inner != null)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(inner.Message) && message.IndexOf(inner.Message, StringComparison.OrdinalIgnoreCase) < 0)
|
|
||||||
{
|
|
||||||
message += " Inner error: " + inner.Message;
|
|
||||||
}
|
|
||||||
inner = inner.InnerException;
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Writes an informational message when a logger is available.</summary>
|
/// <summary>Writes an informational message when a logger is available.</summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -125,6 +126,9 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private Button _clearButton;
|
private Button _clearButton;
|
||||||
|
|
||||||
|
/// <summary>Button that opens the persistent runtime log directory.</summary>
|
||||||
|
private Button _openLogsButton;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Button that closes the application.
|
/// Button that closes the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -150,7 +154,19 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
_service = new BizTalkOperationService(_logger);
|
_service = new BizTalkOperationService(_logger);
|
||||||
BuildUi();
|
BuildUi();
|
||||||
FormClosing += MainFormClosing;
|
FormClosing += MainFormClosing;
|
||||||
_logger.Info("Log file: " + _logger.LogFilePath);
|
LoadLogHistory();
|
||||||
|
if (!string.IsNullOrWhiteSpace(_logger.StorageWarning))
|
||||||
|
{
|
||||||
|
_logger.Warning(_logger.StorageWarning);
|
||||||
|
}
|
||||||
|
if (_logger.IsFileLoggingAvailable)
|
||||||
|
{
|
||||||
|
_logger.Success("Runtime log storage verified by startup append. Active file: " + _logger.LogFilePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Error("Runtime file logging is unavailable. Use the Operation Log grid and resolve the storage warning before a real maintenance operation.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -222,6 +238,8 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
_dryRunCheckBox.Dock = DockStyle.Fill;
|
_dryRunCheckBox.Dock = DockStyle.Fill;
|
||||||
_dryRunCheckBox.Margin = new Padding(8, 7, 8, 7);
|
_dryRunCheckBox.Margin = new Padding(8, 7, 8, 7);
|
||||||
_dryRunCheckBox.TextAlign = ContentAlignment.MiddleLeft;
|
_dryRunCheckBox.TextAlign = ContentAlignment.MiddleLeft;
|
||||||
|
_dryRunCheckBox.CheckedChanged += DryRunCheckBoxCheckedChanged;
|
||||||
|
UpdateDryRunPresentation();
|
||||||
|
|
||||||
var browseButton = new Button { Text = "Browse...", Dock = DockStyle.Fill, Margin = new Padding(8, 5, 8, 5) };
|
var browseButton = new Button { Text = "Browse...", Dock = DockStyle.Fill, Margin = new Padding(8, 5, 8, 5) };
|
||||||
browseButton.Click += BrowseButtonClick;
|
browseButton.Click += BrowseButtonClick;
|
||||||
@@ -272,6 +290,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
_emergencyRestoreButton = ActionButton("Emergency Restore", EmergencyRestoreClick);
|
_emergencyRestoreButton = ActionButton("Emergency Restore", EmergencyRestoreClick);
|
||||||
_emergencyRestoreButton.Width = 142;
|
_emergencyRestoreButton.Width = 142;
|
||||||
_clearButton = ActionButton("Clear", ClearClick);
|
_clearButton = ActionButton("Clear", ClearClick);
|
||||||
|
_openLogsButton = ActionButton("Log Folder", OpenLogsClick);
|
||||||
_closeButton = ActionButton("Close", CloseClick);
|
_closeButton = ActionButton("Close", CloseClick);
|
||||||
|
|
||||||
panel.Controls.Add(_diagnoseButton);
|
panel.Controls.Add(_diagnoseButton);
|
||||||
@@ -283,6 +302,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
panel.Controls.Add(_validateStateButton);
|
panel.Controls.Add(_validateStateButton);
|
||||||
panel.Controls.Add(_emergencyRestoreButton);
|
panel.Controls.Add(_emergencyRestoreButton);
|
||||||
panel.Controls.Add(_clearButton);
|
panel.Controls.Add(_clearButton);
|
||||||
|
panel.Controls.Add(_openLogsButton);
|
||||||
panel.Controls.Add(_closeButton);
|
panel.Controls.Add(_closeButton);
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
@@ -306,7 +326,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
statusPage.Controls.Add(_statusGrid);
|
statusPage.Controls.Add(_statusGrid);
|
||||||
|
|
||||||
ConfigureGrid(_logGrid);
|
ConfigureGrid(_logGrid);
|
||||||
_logGrid.Columns.Add("Time", "Time");
|
_logGrid.Columns.Add("Timestamp", "Timestamp");
|
||||||
_logGrid.Columns.Add("Level", "Level");
|
_logGrid.Columns.Add("Level", "Level");
|
||||||
_logGrid.Columns.Add("Message", "Message");
|
_logGrid.Columns.Add("Message", "Message");
|
||||||
_logGrid.Columns[2].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
|
_logGrid.Columns[2].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
|
||||||
@@ -386,7 +406,9 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
private void ShutdownClick(object sender, EventArgs e)
|
private void ShutdownClick(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var options = GetOptions();
|
var options = GetOptions();
|
||||||
RunAsync("Preparing shutdown...", () =>
|
RunAsync(options.DryRun
|
||||||
|
? "DRY RUN: Preparing shutdown plan; no runtime state will be changed."
|
||||||
|
: "REAL SHUTDOWN: Preparing snapshot and saved plan.", () =>
|
||||||
{
|
{
|
||||||
var snapshot = _service.CreateSnapshot(options.Server);
|
var snapshot = _service.CreateSnapshot(options.Server);
|
||||||
_service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot);
|
_service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot);
|
||||||
@@ -394,12 +416,16 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
// Der exakte, frisch erzeugte Plan wird vor Bestätigung und jeder Laufzeitänderung gespeichert.
|
// Der exakte, frisch erzeugte Plan wird vor Bestätigung und jeder Laufzeitänderung gespeichert.
|
||||||
var planPath = _service.SavePlan(options.OutputDirectory, "shutdown-plan.json", plan);
|
var planPath = _service.SavePlan(options.OutputDirectory, "shutdown-plan.json", plan);
|
||||||
ShowPlan(plan);
|
ShowPlan(plan);
|
||||||
if (!options.DryRun && !ConfirmPreparedPlan("Shutdown", plan, options.Server, planPath))
|
if (options.DryRun)
|
||||||
|
{
|
||||||
|
ShowShutdownDryRunNotice(plan, planPath);
|
||||||
|
}
|
||||||
|
else if (!ConfirmPreparedPlan("Shutdown", plan, options.Server, planPath))
|
||||||
{
|
{
|
||||||
_logger.Warning("Shutdown cancelled after plan review. No runtime state was changed.");
|
_logger.Warning("Shutdown cancelled after plan review. No runtime state was changed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var report = _service.ExecutePlan(plan, options);
|
var report = _service.ExecutePlan(plan, options, ConfirmInboundDrainCheckpoint);
|
||||||
CapturePostOperationSnapshot(options, report, "shutdown-after.json");
|
CapturePostOperationSnapshot(options, report, "shutdown-after.json");
|
||||||
var reportPath = _service.SaveExecutionReport(options.OutputDirectory, "shutdown-result.json", report);
|
var reportPath = _service.SaveExecutionReport(options.OutputDirectory, "shutdown-result.json", report);
|
||||||
ShowExecutionReport(report);
|
ShowExecutionReport(report);
|
||||||
@@ -559,8 +585,14 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
if (report != null && report.HasFailures)
|
if (report != null && report.HasFailures)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Plan completed with failures, but all remaining independent steps were attempted. " +
|
"Plan requires operator review. Failed steps: " + report.FailedCount
|
||||||
"Failed steps: " + report.FailedCount + ". Review: " + reportPath);
|
+ "; later steps not executed: " + report.NotExecutedCount + ". Review: " + reportPath);
|
||||||
|
}
|
||||||
|
if (report != null && report.OperatorStopped)
|
||||||
|
{
|
||||||
|
throw new OperationCanceledException(
|
||||||
|
"Shutdown stopped safely at the inbound-drain checkpoint. Later steps not executed: "
|
||||||
|
+ report.NotExecutedCount + ". Review: " + reportPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,6 +609,26 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
_statusLabel.Text = "Ready.";
|
_statusLabel.Text = "Ready.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Opens the persistent local runtime log directory in Windows Explorer.</summary>
|
||||||
|
/// <param name="sender">The control that raised the event.</param>
|
||||||
|
/// <param name="e">The event arguments.</param>
|
||||||
|
private void OpenLogsClick(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_logger.IsFileLoggingAvailable || string.IsNullOrWhiteSpace(_logger.LogDirectory))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(_logger.StorageWarning ?? "No writable runtime log directory is active.");
|
||||||
|
}
|
||||||
|
Directory.CreateDirectory(_logger.LogDirectory);
|
||||||
|
Process.Start("explorer.exe", _logger.LogDirectory);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error("Could not open runtime log directory. " + ExceptionDiagnostics.Format(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the Close button click.
|
/// Handles the Close button click.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -603,6 +655,11 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
_logger.Success("Operation completed.");
|
_logger.Success("Operation completed.");
|
||||||
SetBusy(false, "Ready.");
|
SetBusy(false, "Ready.");
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException ex)
|
||||||
|
{
|
||||||
|
_logger.Warning(ex.Message);
|
||||||
|
SetBusy(false, "Stopped safely: " + ex.Message);
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(FormatException(ex));
|
_logger.Error(FormatException(ex));
|
||||||
@@ -657,11 +714,80 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
var confirmed = false;
|
var confirmed = false;
|
||||||
Action showConfirmation = () =>
|
Action showConfirmation = () =>
|
||||||
{
|
{
|
||||||
var executableSteps = plan.Steps.Count(x => x.Execute);
|
_statusLabel.Text = "ACTION REQUIRED: Review the saved " + actionName.ToLowerInvariant() + " plan and answer the owned confirmation dialog.";
|
||||||
|
Activate();
|
||||||
|
BringToFront();
|
||||||
var result = MessageBox.Show(
|
var result = MessageBox.Show(
|
||||||
actionName + " will execute " + executableSteps + " step(s) on server '" + server + "'.\n\n"
|
this,
|
||||||
+ "The exact plan was saved to:\n" + planPath + "\n\nContinue now?",
|
ShutdownOperatorPrompts.BuildPreparedPlanConfirmation(actionName, plan, server, planPath),
|
||||||
"Confirm Prepared BizTalk Plan",
|
ShutdownOperatorPrompts.PreparedPlanTitle,
|
||||||
|
MessageBoxButtons.YesNo,
|
||||||
|
MessageBoxIcon.Warning,
|
||||||
|
MessageBoxDefaultButton.Button2);
|
||||||
|
confirmed = result == DialogResult.Yes;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (InvokeRequired)
|
||||||
|
{
|
||||||
|
Invoke(showConfirmation);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
showConfirmation();
|
||||||
|
}
|
||||||
|
return confirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Makes the dry-run boundary explicit: the prepared checkpoint is visible in
|
||||||
|
/// the plan, but no receive location changes and no real drain decision occurs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plan">The prepared shutdown plan.</param>
|
||||||
|
/// <param name="planPath">The durable path of the prepared plan.</param>
|
||||||
|
private void ShowShutdownDryRunNotice(OperationPlan plan, string planPath)
|
||||||
|
{
|
||||||
|
Action showNotice = () =>
|
||||||
|
{
|
||||||
|
_statusLabel.Text = "DRY RUN: Plan prepared. No runtime changes and no real drain decision dialog.";
|
||||||
|
Activate();
|
||||||
|
BringToFront();
|
||||||
|
MessageBox.Show(
|
||||||
|
this,
|
||||||
|
ShutdownOperatorPrompts.BuildDryRunNotice(plan, planPath),
|
||||||
|
ShutdownOperatorPrompts.DryRunTitle,
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Information);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (InvokeRequired)
|
||||||
|
{
|
||||||
|
Invoke(showNotice);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
showNotice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pauses a real shutdown after the receive-location phase until the operator
|
||||||
|
/// confirms that the enterprise environment has drained.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="checkpoint">The persisted checkpoint plan row.</param>
|
||||||
|
/// <param name="progress">The execution results already reached.</param>
|
||||||
|
/// <returns>True only after an explicit Yes decision.</returns>
|
||||||
|
private bool ConfirmInboundDrainCheckpoint(OperationStep checkpoint, OperationExecutionReport progress)
|
||||||
|
{
|
||||||
|
var confirmed = false;
|
||||||
|
Action showConfirmation = () =>
|
||||||
|
{
|
||||||
|
_statusLabel.Text = ShutdownOperatorPrompts.WaitingForInboundDrainStatus;
|
||||||
|
Activate();
|
||||||
|
BringToFront();
|
||||||
|
var result = MessageBox.Show(
|
||||||
|
this,
|
||||||
|
ShutdownOperatorPrompts.BuildInboundDrainConfirmation(progress),
|
||||||
|
ShutdownOperatorPrompts.InboundDrainTitle,
|
||||||
MessageBoxButtons.YesNo,
|
MessageBoxButtons.YesNo,
|
||||||
MessageBoxIcon.Warning,
|
MessageBoxIcon.Warning,
|
||||||
MessageBoxDefaultButton.Button2);
|
MessageBoxDefaultButton.Button2);
|
||||||
@@ -693,8 +819,12 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
var confirmed = false;
|
var confirmed = false;
|
||||||
Action showConfirmation = () =>
|
Action showConfirmation = () =>
|
||||||
{
|
{
|
||||||
|
_statusLabel.Text = "ACTION REQUIRED: Review the emergency restore plan and answer the owned confirmation dialog.";
|
||||||
|
Activate();
|
||||||
|
BringToFront();
|
||||||
var executableSteps = plan.Steps.Count(x => x.Execute);
|
var executableSteps = plan.Steps.Count(x => x.Execute);
|
||||||
var result = MessageBox.Show(
|
var result = MessageBox.Show(
|
||||||
|
this,
|
||||||
"EMERGENCY RESTORE will reconcile " + executableSteps + " step(s) on server '" + server + "'.\n\n"
|
"EMERGENCY RESTORE will reconcile " + executableSteps + " step(s) on server '" + server + "'.\n\n"
|
||||||
+ "Source snapshot (will not be overwritten):\n" + sourcePath + "\n\n"
|
+ "Source snapshot (will not be overwritten):\n" + sourcePath + "\n\n"
|
||||||
+ "Enterprise Single Sign-On will be ensured Running first. Already-correct states are skipped; isolated failures are recorded and later steps continue.\n\n"
|
+ "Enterprise Single Sign-On will be ensured Running first. Already-correct states are skipped; isolated failures are recorded and later steps continue.\n\n"
|
||||||
@@ -816,7 +946,38 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
{
|
{
|
||||||
InvokeIfRequired(() =>
|
InvokeIfRequired(() =>
|
||||||
{
|
{
|
||||||
var index = _logGrid.Rows.Add(entry.Timestamp.ToString("HH:mm:ss"), entry.Level.ToString(), entry.Message);
|
AddLogRow(entry);
|
||||||
|
_logGrid.FirstDisplayedScrollingRowIndex = Math.Max(0, _logGrid.Rows.Count - 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Restores retained plain and compressed log records into the grid at startup.</summary>
|
||||||
|
private void LoadLogHistory()
|
||||||
|
{
|
||||||
|
var entries = _logger.ReadRecentEntries(OperationLogger.DefaultGridHistoryLimit);
|
||||||
|
_logGrid.SuspendLayout();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
AddLogRow(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_logGrid.ResumeLayout();
|
||||||
|
}
|
||||||
|
if (_logGrid.Rows.Count > 0)
|
||||||
|
{
|
||||||
|
_logGrid.FirstDisplayedScrollingRowIndex = _logGrid.Rows.Count - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Adds and colors one retained or live log row.</summary>
|
||||||
|
/// <param name="entry">The log entry to display.</param>
|
||||||
|
private void AddLogRow(LogEntry entry)
|
||||||
|
{
|
||||||
|
var index = _logGrid.Rows.Add(entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"), entry.Level.ToString(), entry.Message);
|
||||||
var row = _logGrid.Rows[index];
|
var row = _logGrid.Rows[index];
|
||||||
if (entry.Level == LogLevel.Error)
|
if (entry.Level == LogLevel.Error)
|
||||||
{
|
{
|
||||||
@@ -830,8 +991,6 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
{
|
{
|
||||||
row.DefaultCellStyle.ForeColor = Color.DarkGreen;
|
row.DefaultCellStyle.ForeColor = Color.DarkGreen;
|
||||||
}
|
}
|
||||||
_logGrid.FirstDisplayedScrollingRowIndex = Math.Max(0, _logGrid.Rows.Count - 1);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -853,11 +1012,45 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
_validateStateButton.Enabled = !busy;
|
_validateStateButton.Enabled = !busy;
|
||||||
_emergencyRestoreButton.Enabled = !busy;
|
_emergencyRestoreButton.Enabled = !busy;
|
||||||
_clearButton.Enabled = !busy;
|
_clearButton.Enabled = !busy;
|
||||||
|
_openLogsButton.Enabled = !busy;
|
||||||
_closeButton.Enabled = !busy;
|
_closeButton.Enabled = !busy;
|
||||||
_statusLabel.Text = status;
|
_serverTextBox.Enabled = !busy;
|
||||||
|
_outputTextBox.Enabled = !busy;
|
||||||
|
_stateFileTextBox.Enabled = !busy;
|
||||||
|
_timeoutInput.Enabled = !busy;
|
||||||
|
_pollInput.Enabled = !busy;
|
||||||
|
_dryRunCheckBox.Enabled = !busy;
|
||||||
|
_statusLabel.Text = !busy && string.Equals(status, "Ready.", StringComparison.Ordinal)
|
||||||
|
? "Ready. " + (_dryRunCheckBox.Checked
|
||||||
|
? "DRY RUN is enabled; shutdown will not change runtime state or request a drain decision."
|
||||||
|
: "REAL EXECUTION is enabled; shutdown will pause for the owned drain decision.")
|
||||||
|
: status;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Updates the prominent execution-mode warning when Dry run changes.</summary>
|
||||||
|
/// <param name="sender">The checkbox that raised the event.</param>
|
||||||
|
/// <param name="e">The event arguments.</param>
|
||||||
|
private void DryRunCheckBoxCheckedChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
UpdateDryRunPresentation();
|
||||||
|
if (!_isBusy)
|
||||||
|
{
|
||||||
|
_statusLabel.Text = _dryRunCheckBox.Checked
|
||||||
|
? "Ready. DRY RUN is enabled; shutdown will not change runtime state or request a drain decision."
|
||||||
|
: "Ready. REAL EXECUTION is enabled; shutdown will pause for the owned drain decision.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Uses explicit text and color instead of relying on a small check mark.</summary>
|
||||||
|
private void UpdateDryRunPresentation()
|
||||||
|
{
|
||||||
|
_dryRunCheckBox.Text = _dryRunCheckBox.Checked
|
||||||
|
? "DRY RUN - no changes / no drain dialog"
|
||||||
|
: "REAL EXECUTION - drain dialog required";
|
||||||
|
_dryRunCheckBox.BackColor = _dryRunCheckBox.Checked ? Color.LightGoldenrodYellow : Color.MistyRose;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the output directory Browse button click.
|
/// Handles the output directory Browse button click.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1071,19 +1264,7 @@ namespace BizTalkPlatformManagementTool.Ui
|
|||||||
return "Operation failed.";
|
return "Operation failed.";
|
||||||
}
|
}
|
||||||
|
|
||||||
var message = ex.Message;
|
return ExceptionDiagnostics.Format(ex);
|
||||||
var inner = ex.InnerException;
|
|
||||||
while (inner != null)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(inner.Message) && message.IndexOf(inner.Message, StringComparison.OrdinalIgnoreCase) < 0)
|
|
||||||
{
|
|
||||||
message += " Inner error: " + inner.Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
inner = inner.InnerException;
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
<assemblyIdentity version="2.2.4.0" name="BizTalkPlatformManagementTool" />
|
<assemblyIdentity version="2.3.4.0" name="BizTalkPlatformManagementTool" />
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
<security>
|
<security>
|
||||||
<requestedPrivileges>
|
<requestedPrivileges>
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using BizTalkPlatformManagementTool.Models;
|
using BizTalkPlatformManagementTool.Models;
|
||||||
using BizTalkPlatformManagementTool.Services;
|
using BizTalkPlatformManagementTool.Services;
|
||||||
using BizTalkPlatformManagementTool.Setup;
|
using BizTalkPlatformManagementTool.Setup;
|
||||||
|
using BizTalkPlatformManagementTool.Ui;
|
||||||
|
|
||||||
namespace BizTalkPlatformManagementTool.Tests
|
namespace BizTalkPlatformManagementTool.Tests
|
||||||
{
|
{
|
||||||
@@ -27,7 +29,12 @@ namespace BizTalkPlatformManagementTool.Tests
|
|||||||
Run("Legacy213SnapshotIsAcceptedForRecovery", Legacy213SnapshotIsAcceptedForRecovery);
|
Run("Legacy213SnapshotIsAcceptedForRecovery", Legacy213SnapshotIsAcceptedForRecovery);
|
||||||
Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder);
|
Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder);
|
||||||
Run("ShutdownPlanUsesGlobalSafeOrder", ShutdownPlanUsesGlobalSafeOrder);
|
Run("ShutdownPlanUsesGlobalSafeOrder", ShutdownPlanUsesGlobalSafeOrder);
|
||||||
|
Run("ShutdownPlanRequiresCheckpointBeforeLaterWorkWithoutReceiveLocations", ShutdownPlanRequiresCheckpointBeforeLaterWorkWithoutReceiveLocations);
|
||||||
Run("ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure", ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure);
|
Run("ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure", ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure);
|
||||||
|
Run("ShutdownCheckpointDeclineStopsLaterPhases", ShutdownCheckpointDeclineStopsLaterPhases);
|
||||||
|
Run("ShutdownCheckpointMissingHandlerFailsClosed", ShutdownCheckpointMissingHandlerFailsClosed);
|
||||||
|
Run("ShutdownCheckpointDryRunDoesNotPrompt", ShutdownCheckpointDryRunDoesNotPrompt);
|
||||||
|
Run("ShutdownOperatorPromptsDistinguishDryRunAndRealDecision", ShutdownOperatorPromptsDistinguishDryRunAndRealDecision);
|
||||||
Run("EmergencyRestorePlanStartsSsoFirst", EmergencyRestorePlanStartsSsoFirst);
|
Run("EmergencyRestorePlanStartsSsoFirst", EmergencyRestorePlanStartsSsoFirst);
|
||||||
Run("HostInstancePlanAcceptsShortAndFqdnServer", HostInstancePlanAcceptsShortAndFqdnServer);
|
Run("HostInstancePlanAcceptsShortAndFqdnServer", HostInstancePlanAcceptsShortAndFqdnServer);
|
||||||
Run("PlanExecutionContinuesAfterSchedulerFailure", PlanExecutionContinuesAfterSchedulerFailure);
|
Run("PlanExecutionContinuesAfterSchedulerFailure", PlanExecutionContinuesAfterSchedulerFailure);
|
||||||
@@ -35,6 +42,14 @@ namespace BizTalkPlatformManagementTool.Tests
|
|||||||
Run("LoggerSinkFailureCannotAbortPlanExecution", LoggerSinkFailureCannotAbortPlanExecution);
|
Run("LoggerSinkFailureCannotAbortPlanExecution", LoggerSinkFailureCannotAbortPlanExecution);
|
||||||
Run("PlanExecutionSkipsAlreadySatisfiedState", PlanExecutionSkipsAlreadySatisfiedState);
|
Run("PlanExecutionSkipsAlreadySatisfiedState", PlanExecutionSkipsAlreadySatisfiedState);
|
||||||
Run("ExecutionReportRoundTripPreservesFailure", ExecutionReportRoundTripPreservesFailure);
|
Run("ExecutionReportRoundTripPreservesFailure", ExecutionReportRoundTripPreservesFailure);
|
||||||
|
Run("ScheduledReceivePlanCarriesAdapterMetadata", ScheduledReceivePlanCarriesAdapterMetadata);
|
||||||
|
Run("ApplicationResolverMapsArtifactsAndRejectsAmbiguity", ApplicationResolverMapsArtifactsAndRejectsAmbiguity);
|
||||||
|
Run("AdapterAssemblyResolverRequiresMatchingIdentity", AdapterAssemblyResolverRequiresMatchingIdentity);
|
||||||
|
Run("OperationLogPersistsCompressesAndRetainsThirtyDays", OperationLogPersistsCompressesAndRetainsThirtyDays);
|
||||||
|
Run("OperationLogUsesVerifiedStartupFallback", OperationLogUsesVerifiedStartupFallback);
|
||||||
|
Run("OperationLogFailsOverAfterAppendFailure", OperationLogFailsOverAfterAppendFailure);
|
||||||
|
Run("OperationLogSurfacesTotalStorageFailure", OperationLogSurfacesTotalStorageFailure);
|
||||||
|
Run("ExceptionDiagnosticsContainSupportContext", ExceptionDiagnosticsContainSupportContext);
|
||||||
Run("CsvNeutralizesFormulaValues", CsvNeutralizesFormulaValues);
|
Run("CsvNeutralizesFormulaValues", CsvNeutralizesFormulaValues);
|
||||||
Run("PackageManifestRejectsTampering", PackageManifestRejectsTampering);
|
Run("PackageManifestRejectsTampering", PackageManifestRejectsTampering);
|
||||||
Run("PackageManifestRejectsUndeclaredAndTraversalFiles", PackageManifestRejectsUndeclaredAndTraversalFiles);
|
Run("PackageManifestRejectsUndeclaredAndTraversalFiles", PackageManifestRejectsUndeclaredAndTraversalFiles);
|
||||||
@@ -159,16 +174,198 @@ namespace BizTalkPlatformManagementTool.Tests
|
|||||||
|
|
||||||
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
||||||
var lastReceiveLocation = plan.Steps.FindLastIndex(x => x.Kind == "ReceiveLocation");
|
var lastReceiveLocation = plan.Steps.FindLastIndex(x => x.Kind == "ReceiveLocation");
|
||||||
|
var checkpoint = plan.Steps.FindIndex(x => x.Kind == OperationStepKind.OperatorCheckpoint.ToString());
|
||||||
var firstOrchestration = plan.Steps.FindIndex(x => x.Kind == "Orchestration");
|
var firstOrchestration = plan.Steps.FindIndex(x => x.Kind == "Orchestration");
|
||||||
var lastOrchestration = plan.Steps.FindLastIndex(x => x.Kind == "Orchestration");
|
var lastOrchestration = plan.Steps.FindLastIndex(x => x.Kind == "Orchestration");
|
||||||
var firstSendPort = plan.Steps.FindIndex(x => x.Kind == "SendPort");
|
var firstSendPort = plan.Steps.FindIndex(x => x.Kind == "SendPort");
|
||||||
var lastSendPort = plan.Steps.FindLastIndex(x => x.Kind == "SendPort");
|
var lastSendPort = plan.Steps.FindLastIndex(x => x.Kind == "SendPort");
|
||||||
var firstHost = plan.Steps.FindIndex(x => x.Kind == "HostInstance");
|
var firstHost = plan.Steps.FindIndex(x => x.Kind == "HostInstance");
|
||||||
Assert(lastReceiveLocation < firstOrchestration, "receive locations were not globally first");
|
Assert(lastReceiveLocation < checkpoint && checkpoint < firstOrchestration, "operator checkpoint was not placed directly after the global receive-location phase");
|
||||||
Assert(lastOrchestration < firstSendPort, "orchestrations were not globally before send ports");
|
Assert(lastOrchestration < firstSendPort, "orchestrations were not globally before send ports");
|
||||||
Assert(lastSendPort < firstHost, "host instances were not globally last");
|
Assert(lastSendPort < firstHost, "host instances were not globally last");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft den Checkpoint auch dann, wenn keine aktive Receive Location als Planzeile existiert.</summary>
|
||||||
|
private static void ShutdownPlanRequiresCheckpointBeforeLaterWorkWithoutReceiveLocations()
|
||||||
|
{
|
||||||
|
var snapshot = Snapshot("APP", "SEND", ArtifactStates.SendPortStarted);
|
||||||
|
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
||||||
|
var checkpoint = plan.Steps.FindIndex(x => x.Kind == OperationStepKind.OperatorCheckpoint.ToString());
|
||||||
|
var sendPort = plan.Steps.FindIndex(x => x.Kind == OperationStepKind.SendPort.ToString());
|
||||||
|
|
||||||
|
Assert(checkpoint >= 0, "later shutdown work did not create an inbound-drain checkpoint");
|
||||||
|
Assert(checkpoint < sendPort, "checkpoint was not placed before later shutdown work");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft, dass Scheduler-Erkennung auch nach Planpersistenz möglich bleibt.</summary>
|
||||||
|
private static void ScheduledReceivePlanCarriesAdapterMetadata()
|
||||||
|
{
|
||||||
|
var snapshot = Snapshot("APP", "SP", ArtifactStates.SendPortStarted);
|
||||||
|
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState
|
||||||
|
{
|
||||||
|
Application = "APP",
|
||||||
|
Name = "RV_PMP_Trigger_Schedule",
|
||||||
|
Enabled = true,
|
||||||
|
AdapterName = "Schedule",
|
||||||
|
Address = "scheduler://PMP-trigger"
|
||||||
|
});
|
||||||
|
var service = new BizTalkOperationService(null);
|
||||||
|
var shutdown = service.CreateShutdownPlan(snapshot, snapshot.Server);
|
||||||
|
var step = shutdown.Steps.Single(x => x.Kind == "ReceiveLocation");
|
||||||
|
Assert(step.AdapterName == "Schedule", "adapter name missing from shutdown plan");
|
||||||
|
Assert(step.Address == "scheduler://PMP-trigger", "scheduler URI missing from shutdown plan");
|
||||||
|
|
||||||
|
InTemp(directory =>
|
||||||
|
{
|
||||||
|
var path = Path.Combine(directory, "plan.json");
|
||||||
|
JsonFileStore.Save(path, shutdown);
|
||||||
|
var loaded = JsonFileStore.Load<OperationPlan>(path);
|
||||||
|
Assert(loaded.Steps.Single(x => x.Kind == "ReceiveLocation").Address.StartsWith("scheduler:", StringComparison.OrdinalIgnoreCase), "scheduler metadata did not survive serialization");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft die ExplorerOM-basierte Zuordnung aller Laufzeitartefakte und den Ambiguitätsschutz.</summary>
|
||||||
|
private static void ApplicationResolverMapsArtifactsAndRejectsAmbiguity()
|
||||||
|
{
|
||||||
|
var resolver = new BizTalkApplicationResolver();
|
||||||
|
resolver.AddSendPort("APP-A", "SP-A");
|
||||||
|
resolver.AddReceivePort("APP-B", "RP-B");
|
||||||
|
resolver.AddReceiveLocation("APP-B", "RL-B");
|
||||||
|
resolver.AddOrchestration("APP-C", "Company.Process.Order");
|
||||||
|
|
||||||
|
Assert(resolver.ApplicationCount == 3, "application count did not de-duplicate artifact owners");
|
||||||
|
Assert(resolver.ResolveSendPort("sp-a") == "APP-A", "send port was not resolved case-insensitively");
|
||||||
|
Assert(resolver.ResolveReceiveLocation("RL-B", null) == "APP-B", "receive location was not resolved");
|
||||||
|
Assert(resolver.ResolveReceiveLocation("missing", "rp-b") == "APP-B", "receive-port fallback was not used");
|
||||||
|
Assert(resolver.ResolveOrchestration("company.process.order") == "APP-C", "orchestration was not resolved");
|
||||||
|
|
||||||
|
resolver.AddSendPort("APP-D", "SP-A");
|
||||||
|
Assert(resolver.ResolveSendPort("SP-A") == null, "cross-application duplicate was not rejected as ambiguous");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft Dateinamen- und vollständige Assemblyidentität vor prozesslokalem Laden.</summary>
|
||||||
|
private static void AdapterAssemblyResolverRequiresMatchingIdentity()
|
||||||
|
{
|
||||||
|
InTemp(directory =>
|
||||||
|
{
|
||||||
|
var source = typeof(BizTalkOperationService).Assembly.Location;
|
||||||
|
var identity = AssemblyName.GetAssemblyName(source);
|
||||||
|
var candidate = Path.Combine(directory, identity.Name + ".dll");
|
||||||
|
File.Copy(source, candidate);
|
||||||
|
Assert(AdapterAssemblyResolver.FindCandidateFile(identity.Name, identity, new[] { directory }) == candidate, "matching assembly identity was not found");
|
||||||
|
|
||||||
|
var wrongVersion = new AssemblyName(identity.FullName);
|
||||||
|
wrongVersion.Version = new Version(identity.Version.Major + 1, 0, 0, 0);
|
||||||
|
Assert(AdapterAssemblyResolver.FindCandidateFile(identity.Name, wrongVersion, new[] { directory }) == null, "wrong assembly version was accepted");
|
||||||
|
|
||||||
|
File.Copy(source, Path.Combine(directory, AdapterAssemblyResolver.SchedulerAssemblyName + ".dll"));
|
||||||
|
Assert(AdapterAssemblyResolver.FindCandidateFile(AdapterAssemblyResolver.SchedulerAssemblyName, null, new[] { directory }) == null, "file-name-only scheduler impostor was accepted");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft Neustart-Rehydration, Vortagskompression und die 30-Tage-Grenze.</summary>
|
||||||
|
private static void OperationLogPersistsCompressesAndRetainsThirtyDays()
|
||||||
|
{
|
||||||
|
InTemp(directory =>
|
||||||
|
{
|
||||||
|
var today = DateTime.Today;
|
||||||
|
var yesterday = Path.Combine(directory, "BizTalkPlatformManagementTool-" + today.AddDays(-1).ToString("yyyy-MM-dd") + ".log");
|
||||||
|
var expired = Path.Combine(directory, "BizTalkPlatformManagementTool-" + today.AddDays(-30).ToString("yyyy-MM-dd") + ".log");
|
||||||
|
File.WriteAllText(yesterday, "[" + today.AddDays(-1).ToString("yyyy-MM-dd") + " 08:00:00][WARNING] retained warning\r\n", Encoding.UTF8);
|
||||||
|
File.WriteAllText(expired, "[" + today.AddDays(-30).ToString("yyyy-MM-dd") + " 08:00:00][INFO] expired\r\n", Encoding.UTF8);
|
||||||
|
|
||||||
|
var logger = new OperationLogger(null, directory);
|
||||||
|
Assert(File.Exists(yesterday + ".gz") && !File.Exists(yesterday), "completed daily log was not compressed");
|
||||||
|
Assert(!File.Exists(expired), "log outside 30-day retention was not deleted");
|
||||||
|
logger.Error("durable failure marker");
|
||||||
|
|
||||||
|
var restarted = new OperationLogger(null, directory);
|
||||||
|
var entries = restarted.ReadRecentEntries(100);
|
||||||
|
Assert(entries.Any(x => x.Level == LogLevel.Warning && x.Message == "retained warning"), "compressed history was not restored");
|
||||||
|
Assert(entries.Any(x => x.Level == LogLevel.Error && x.Message == "durable failure marker"), "current history was not restored after restart");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft, dass erst ein real beschreibbarer Pfad als aktiv gilt.</summary>
|
||||||
|
private static void OperationLogUsesVerifiedStartupFallback()
|
||||||
|
{
|
||||||
|
InTemp(directory =>
|
||||||
|
{
|
||||||
|
var blocked = Path.Combine(directory, "blocked-primary");
|
||||||
|
var fallback = Path.Combine(directory, "writable-fallback");
|
||||||
|
File.WriteAllText(blocked, "not a directory");
|
||||||
|
|
||||||
|
var logger = new OperationLogger(null, new[] { blocked, fallback });
|
||||||
|
Assert(logger.IsFileLoggingAvailable, "writable fallback was not activated");
|
||||||
|
Assert(string.Equals(logger.LogDirectory, fallback, StringComparison.OrdinalIgnoreCase), "wrong fallback directory selected");
|
||||||
|
Assert(logger.StorageWarning.Contains("Primary runtime log path is not writable"), "startup fallback warning missing");
|
||||||
|
logger.Info("verified fallback marker");
|
||||||
|
Assert(File.Exists(logger.LogFilePath), "fallback log file was not created");
|
||||||
|
Assert(File.ReadAllText(logger.LogFilePath).Contains("verified fallback marker"), "fallback append was not durable");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft automatisches Failover, wenn der aktive Pfad später ausfällt.</summary>
|
||||||
|
private static void OperationLogFailsOverAfterAppendFailure()
|
||||||
|
{
|
||||||
|
InTemp(directory =>
|
||||||
|
{
|
||||||
|
var primary = Path.Combine(directory, "primary");
|
||||||
|
var fallback = Path.Combine(directory, "fallback");
|
||||||
|
var visible = new List<LogEntry>();
|
||||||
|
var logger = new OperationLogger(visible.Add, new[] { primary, fallback });
|
||||||
|
Assert(string.Equals(logger.LogDirectory, primary, StringComparison.OrdinalIgnoreCase), "primary directory was not initially selected");
|
||||||
|
|
||||||
|
Directory.Delete(primary, true);
|
||||||
|
File.WriteAllText(primary, "now blocked");
|
||||||
|
logger.Error("runtime failover marker");
|
||||||
|
|
||||||
|
Assert(string.Equals(logger.LogDirectory, fallback, StringComparison.OrdinalIgnoreCase), "append failure did not switch to fallback");
|
||||||
|
Assert(File.ReadAllText(logger.LogFilePath).Contains("runtime failover marker"), "failed primary record was not written to fallback");
|
||||||
|
Assert(visible.Any(x => x.Level == LogLevel.Warning && x.Message.Contains("Runtime log path switched")), "runtime fallback was not visible in the sink");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft eine sichtbare Warnung, wenn kein Dateipfad beschreibbar ist.</summary>
|
||||||
|
private static void OperationLogSurfacesTotalStorageFailure()
|
||||||
|
{
|
||||||
|
InTemp(directory =>
|
||||||
|
{
|
||||||
|
var blockedOne = Path.Combine(directory, "blocked-one");
|
||||||
|
var blockedTwo = Path.Combine(directory, "blocked-two");
|
||||||
|
File.WriteAllText(blockedOne, "not a directory");
|
||||||
|
File.WriteAllText(blockedTwo, "not a directory");
|
||||||
|
var visible = new List<LogEntry>();
|
||||||
|
var logger = new OperationLogger(visible.Add, new[] { blockedOne, blockedTwo });
|
||||||
|
Assert(!logger.IsFileLoggingAvailable, "logger reported unavailable candidates as writable");
|
||||||
|
Assert(logger.StorageWarning.Contains("RUNTIME FILE LOGGING UNAVAILABLE"), "total startup failure diagnostic missing");
|
||||||
|
|
||||||
|
logger.Error("grid-only marker");
|
||||||
|
Assert(visible.Any(x => x.Message == "grid-only marker"), "business entry disappeared with file logging failure");
|
||||||
|
Assert(visible.Any(x => x.Level == LogLevel.Warning && x.Message.Contains("RUNTIME FILE LOGGING UNAVAILABLE")), "file logging failure was not visible in the grid sink");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft Typ, HRESULT, innere Ausnahme und Stacktrace für Supportfälle.</summary>
|
||||||
|
private static void ExceptionDiagnosticsContainSupportContext()
|
||||||
|
{
|
||||||
|
Exception failure;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("outer detail", new FileNotFoundException("scheduler dependency missing"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failure = ex;
|
||||||
|
}
|
||||||
|
var diagnostic = ExceptionDiagnostics.Format(failure);
|
||||||
|
Assert(diagnostic.Contains("Type=System.InvalidOperationException"), "exception type missing");
|
||||||
|
Assert(diagnostic.Contains("HResult=0x"), "HRESULT missing");
|
||||||
|
Assert(diagnostic.Contains("InnerException[1]"), "inner exception index missing");
|
||||||
|
Assert(diagnostic.Contains("scheduler dependency missing"), "inner message missing");
|
||||||
|
Assert(diagnostic.Contains("StackTrace="), "stack trace missing");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Prüft den vollständigen Shutdown-Fortgang von Receive Location bis Host Instance nach einem frühen Fehler.</summary>
|
/// <summary>Prüft den vollständigen Shutdown-Fortgang von Receive Location bis Host Instance nach einem frühen Fehler.</summary>
|
||||||
private static void ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure()
|
private static void ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure()
|
||||||
{
|
{
|
||||||
@@ -179,10 +376,92 @@ namespace BizTalkPlatformManagementTool.Tests
|
|||||||
|
|
||||||
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
||||||
var runtime = new FakeOperationStepRuntime { FailingName = "RV_PMP_Trigger_Schedule" };
|
var runtime = new FakeOperationStepRuntime { FailingName = "RV_PMP_Trigger_Schedule" };
|
||||||
var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(false), runtime);
|
var checkpointSawReceiveFailure = false;
|
||||||
|
var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(false), runtime, (checkpoint, progress) =>
|
||||||
|
{
|
||||||
|
checkpointSawReceiveFailure = progress.FailedCount == 1;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
Assert(runtime.Calls.SequenceEqual(new[] { "RV_PMP_Trigger_Schedule", "ORCHESTRATION", "SEND", "HOST:SERVER" }), "shutdown did not continue through every later artifact category");
|
Assert(runtime.Calls.SequenceEqual(new[] { "RV_PMP_Trigger_Schedule", "ORCHESTRATION", "SEND", "HOST:SERVER" }), "shutdown did not continue through every later artifact category");
|
||||||
Assert(report.FailedCount == 1 && report.SucceededCount == 3, "cross-category shutdown outcome is incomplete");
|
Assert(report.FailedCount == 1 && report.SucceededCount == 3, "cross-category shutdown outcome is incomplete");
|
||||||
|
Assert(checkpointSawReceiveFailure, "checkpoint did not receive the completed receive-location failure summary");
|
||||||
|
Assert(report.CheckpointDecision == "Continue" && report.Steps.Single(x => x.Kind == OperationStepKind.OperatorCheckpoint.ToString()).Outcome == OperationStepOutcomes.Confirmed, "confirmed drain checkpoint was not persisted");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft, dass Nein am Drain-Checkpoint keine spätere Shutdown-Mutation zulässt.</summary>
|
||||||
|
private static void ShutdownCheckpointDeclineStopsLaterPhases()
|
||||||
|
{
|
||||||
|
var snapshot = Snapshot("APP", "SEND", ArtifactStates.SendPortStarted);
|
||||||
|
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RL", Enabled = true });
|
||||||
|
snapshot.Applications[0].Orchestrations.Add(new OrchestrationState { Application = "APP", Name = "ORCHESTRATION", OrchestrationStatus = ArtifactStates.OrchestrationStarted });
|
||||||
|
snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HOST:SERVER", HostName = "HOST", Server = snapshot.Server, RawState = ArtifactStates.HostStarted });
|
||||||
|
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
||||||
|
var runtime = new FakeOperationStepRuntime();
|
||||||
|
|
||||||
|
var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(false), runtime, (checkpoint, progress) => false);
|
||||||
|
|
||||||
|
Assert(runtime.Calls.SequenceEqual(new[] { "RL" }), "a later shutdown phase executed after checkpoint decline");
|
||||||
|
Assert(report.OperatorStopped && report.CheckpointDecision == "Stop", "operator stop decision was not persisted");
|
||||||
|
Assert(report.NotExecutedCount == 3, "not-executed downstream count is wrong");
|
||||||
|
Assert(report.Steps.SkipWhile(x => x.Kind != OperationStepKind.OperatorCheckpoint.ToString()).Skip(1).All(x => x.Outcome == OperationStepOutcomes.NotExecuted), "downstream rows were not marked NotExecuted");
|
||||||
|
Assert(report.RequiresOperatorReview && !report.HasFailures, "safe operator stop was incorrectly classified as a runtime failure");
|
||||||
|
InTemp(directory =>
|
||||||
|
{
|
||||||
|
var path = Path.Combine(directory, "shutdown-result.json");
|
||||||
|
JsonFileStore.Save(path, report);
|
||||||
|
var loaded = JsonFileStore.Load<OperationExecutionReport>(path);
|
||||||
|
Assert(loaded.OperatorStopped && loaded.CheckpointDecision == "Stop" && loaded.NotExecutedCount == 3, "serialized report lost checkpoint decision evidence");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft den sicheren Stopp, wenn der echte Lauf keinen Dialog-Handler besitzt.</summary>
|
||||||
|
private static void ShutdownCheckpointMissingHandlerFailsClosed()
|
||||||
|
{
|
||||||
|
var snapshot = Snapshot("APP", "SEND", ArtifactStates.SendPortStarted);
|
||||||
|
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RL", Enabled = true });
|
||||||
|
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
||||||
|
var runtime = new FakeOperationStepRuntime();
|
||||||
|
|
||||||
|
var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(false), runtime);
|
||||||
|
|
||||||
|
Assert(runtime.Calls.SequenceEqual(new[] { "RL" }), "missing checkpoint handler did not fail closed");
|
||||||
|
Assert(report.OperatorStopped && report.CheckpointDecision == "Error" && report.FailedCount == 1, "checkpoint handler failure was not durable");
|
||||||
|
Assert(report.NotExecutedCount == 1 && report.Steps.Last().Outcome == OperationStepOutcomes.NotExecuted, "later send port was not protected after checkpoint error");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft, dass Dry-run den Checkpoint zeigt, aber keine Bedienerentscheidung anfordert.</summary>
|
||||||
|
private static void ShutdownCheckpointDryRunDoesNotPrompt()
|
||||||
|
{
|
||||||
|
var snapshot = Snapshot("APP", "SEND", ArtifactStates.SendPortStarted);
|
||||||
|
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RL", Enabled = true });
|
||||||
|
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
||||||
|
var callbackCalls = 0;
|
||||||
|
|
||||||
|
var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(true), null, (checkpoint, progress) => { callbackCalls++; return false; });
|
||||||
|
|
||||||
|
Assert(callbackCalls == 0, "dry-run requested a real operator checkpoint decision");
|
||||||
|
Assert(report.DryRunCount == plan.Steps.Count(x => x.Execute), "dry-run did not display every executable plan row");
|
||||||
|
Assert(!report.OperatorStopped && string.IsNullOrWhiteSpace(report.CheckpointDecision), "dry-run persisted a real checkpoint decision");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft, dass Dry-run-Hinweis und echter Entscheidungsdialog nicht verwechselt werden können.</summary>
|
||||||
|
private static void ShutdownOperatorPromptsDistinguishDryRunAndRealDecision()
|
||||||
|
{
|
||||||
|
var snapshot = Snapshot("APP", "SEND", ArtifactStates.SendPortStarted);
|
||||||
|
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RL", Enabled = true });
|
||||||
|
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
||||||
|
var dryRun = ShutdownOperatorPrompts.BuildDryRunNotice(plan, @"C:\evidence\shutdown-plan.json");
|
||||||
|
var prepared = ShutdownOperatorPrompts.BuildPreparedPlanConfirmation("Shutdown", plan, snapshot.Server, @"C:\evidence\shutdown-plan.json");
|
||||||
|
var progress = new OperationExecutionReport();
|
||||||
|
progress.Steps.Add(new OperationStepResult { Kind = OperationStepKind.ReceiveLocation.ToString(), Outcome = OperationStepOutcomes.Succeeded });
|
||||||
|
progress.Steps.Add(new OperationStepResult { Kind = OperationStepKind.ReceiveLocation.ToString(), Outcome = OperationStepOutcomes.Failed });
|
||||||
|
var checkpoint = ShutdownOperatorPrompts.BuildInboundDrainConfirmation(progress);
|
||||||
|
|
||||||
|
Assert(dryRun.Contains("No receive location is disabled") && dryRun.Contains("will not open"), "dry-run notice does not explain the missing real dialog");
|
||||||
|
Assert(prepared.Contains("second, explicit decision"), "real-plan confirmation does not announce the later checkpoint dialog");
|
||||||
|
Assert(checkpoint.Contains("shutdown is PAUSED") && checkpoint.Contains("Disabled successfully: 1") && checkpoint.Contains("Failed: 1"), "checkpoint decision does not expose its blocking state and receive results");
|
||||||
|
Assert(checkpoint.Contains("closing this dialog = stop safely"), "checkpoint close behavior is not explicit");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Prüft ENTSSO als erste Voraussetzung des Emergency Restore.</summary>
|
/// <summary>Prüft ENTSSO als erste Voraussetzung des Emergency Restore.</summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user