diff --git a/AI-README.md b/AI-README.md
index e13f2c6..2b05582 100644
--- a/AI-README.md
+++ b/AI-README.md
@@ -2,7 +2,7 @@
## Projekt und Sicherheitsziel
-Das Repository enthält ein .NET-Framework-4.6.1-WinForms-Tool für kontrollierte BizTalk-2020-Wartungsoperationen. Änderungen müssen Dry-run, explizite Freigabe realer Aktionen, sichere Reihenfolgen und wiederherstellbare Installergrenzen erhalten. Die aktuelle Produktversion ist 2.3.1.
+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.2.
## Installerinvarianten
@@ -28,6 +28,9 @@ Die zentrale Implementierung liegt in `src/BizTalkPlatformManagementTool.Setup/I
- 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.
- Shutdown-Kategorien gelten global über alle Anwendungen: Receive Locations, Orchestrations, Send Ports, Host Instances.
+- Zwischen der globalen Receive-Location-Phase und allen späteren Shutdown-Kategorien liegt ein persistierter Operator-Checkpoint. Ein realer Lauf darf nur nach explizitem Continue fortsetzen; Stop oder Handlerfehler markiert alle späteren Zeilen als `NotExecuted` und arbeitet fail-closed.
+- Dry-run zeigt den Checkpoint ohne Callback. Der Ergebnisreport muss Entscheidung, Zeitpunkt, OperatorStopped und NotExecutedCount dauerhaft enthalten.
+- Ein BizTalk-Application-Stop wird nicht zusätzlich ausgeführt: `Partially Started` ist während des Drains erwartbar, während Application-Stop-Optionen eine breitere, teils destruktive Semantik als das reine Setzen eines Statuswerts besitzen.
- Restore-Kategorien gelten global über alle Anwendungen: Host Instances, Send Ports, Orchestrations, Receive Locations.
- Emergency Restore überschreibt niemals die Eingabe-`before.json`, erzeugt eine timestamp-basierte Kopie und stellt `ENTSSO` vor Host Instances sicher.
- Emergency Restore muss mit genau einer validen `before.json` funktionieren; Dateien eines vorherigen fehlgeschlagenen Laufs dürfen keine Voraussetzung sein.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc58435..d578302 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,20 @@
# Changelog
+## [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.
diff --git a/Dokumentation.md b/Dokumentation.md
index 7d47d0c..2f9e5ef 100644
--- a/Dokumentation.md
+++ b/Dokumentation.md
@@ -55,8 +55,9 @@ Selbsterklärende Zuweisungen und reine UI-Konstruktion werden nicht zeilenweise
6. Mit **Snapshot Before** die Datei `before.json` erzeugen.
7. Mit **Shutdown** den Plan `shutdown-plan.json` erzeugen und prüfen.
8. **Dry run** erst deaktivieren, wenn der Plan korrekt ist.
-9. Nach der Wartung mit **Restore** aus `before.json` wiederherstellen.
-10. Mit **Snapshot After** und **Compare** die Umgebung validieren.
+9. Beim echten Shutdown nach den Receive Locations den Drain-Dialog geöffnet lassen, Group Hub und Enterprise-Monitoring prüfen und erst bei leer gelaufener Verarbeitung **Yes** wählen.
+10. Nach der Wartung mit **Restore** aus `before.json` wiederherstellen.
+11. Mit **Snapshot After** und **Compare** die Umgebung validieren.
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.
@@ -67,6 +68,8 @@ Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des
- Dry-run ist standardmäßig aktiviert.
- Beim Start wird geprüft, ob die Anwendung mit Administratorrechten läuft. Ohne erhöhte Rechte wird eine Fehlermeldung angezeigt und die Anwendung beendet.
- Echte Shutdown-/Restore-Aktionen verlangen erst nach Erzeugung und Speicherung des frischen Plans eine zusätzliche Bestätigung mit Zielserver, Plandatei und exakter Zahl ausführbarer Schritte.
+- Ein echter Shutdown hält nach der globalen Receive-Location-Phase an einem im Plan gespeicherten Operator-Checkpoint. Nur ein explizites **Yes** setzt Orchestrierungen, Send Ports und Host Instances fort.
+- **No** oder ein Fehler des Bestätigungsdialogs arbeitet fail-closed: spätere Planzeilen werden ohne Mutation als `NotExecuted` gespeichert; Nachher-Snapshot und Ergebnisreport werden trotzdem versucht.
- Jede Operation schreibt Einträge in das sichtbare Operation Log.
- Primär wird unter `%ProgramData%\BizTalkPlatformManagementTool\Logs` eine tägliche Logdatei `BizTalkPlatformManagementTool-yyyy-MM-dd.log` geschrieben. Jeder Kandidat muss einen echten Create/Write/Flush/Delete-Test bestehen. Die Fallbackreihenfolge ist LocalAppData, `Logs` unter dem EXE-Verzeichnis und Temp.
- Jeder GUI-Start hängt einen Prüfdatensatz an die aktive Tagesdatei an. **Log Folder** öffnet diesen tatsächlich beschreibbaren Pfad; ein Fallback oder der vollständige Ausfall des Dateiloggings wird mit Pfaden und Exception im Grid angezeigt.
@@ -90,9 +93,12 @@ Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des
## Shutdown-Reihenfolge
1. Receive Locations deaktivieren, die im Snapshot aktiviert waren.
-2. Orchestrations stoppen, die gestartet waren.
-3. Send Ports stoppen, die gestartet waren.
-4. Host Instances stoppen, die auf dem ausgewählten Server gestartet waren.
+2. Operator-Checkpoint: eingehenden Verkehr, Group Hub, laufende Service Instances und Enterprise-Monitoring prüfen; erst nach dem Leerfahren fortsetzen.
+3. Orchestrations stoppen, die gestartet waren.
+4. Send Ports stoppen, die gestartet waren.
+5. Host Instances stoppen, die auf dem ausgewählten Server gestartet waren.
+
+`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
diff --git a/Installation.md b/Installation.md
index 91f9aae..83418cf 100644
--- a/Installation.md
+++ b/Installation.md
@@ -15,8 +15,8 @@ Das Release erzeugt folgende Dateien unter `artifacts`:
- `BizTalkPlatformManagementTool-Setup.zip.b64.txt`: Certutil-kompatible Base64-Übertragung
- `BizTalkPlatformManagementTool-Setup.zip.sha256.txt`: SHA-256 des ZIP-Archivs
- `BizTalkPlatformManagementTool-Setup.zip`: direkt entpackbares Installationspaket
-- `BizTalkPlatformManagementTool-2.3.1-source.zip.b64.txt`: vollständiger Quellstand als Base64-TXT
-- `BizTalkPlatformManagementTool-2.3.1-source.zip.sha256.txt`: SHA-256 des Quellarchivs
+- `BizTalkPlatformManagementTool-2.3.2-source.zip.b64.txt`: vollständiger Quellstand als Base64-TXT
+- `BizTalkPlatformManagementTool-2.3.2-source.zip.sha256.txt`: SHA-256 des Quellarchivs
Auf dem Zielsystem wird die TXT-Datei so rekonstruiert und geprüft:
@@ -60,6 +60,18 @@ Danach wird bei jedem Programmstart die Zeile `Runtime log storage verified by s
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.
+## Shutdown-Drain-Checkpoint ab 2.3.2
+
+Ein echter Shutdown pausiert nach allen Receive Locations und vor Orchestrierungen, Send Ports und Host Instances. Der Dialog zeigt erfolgreiche, bereits deaktivierte und fehlgeschlagene Receive Locations. Er darf geöffnet bleiben, während Group Hub, laufende Service Instances, MessageBox-/Queue-Metriken und das Enterprise-Monitoring geprüft werden.
+
+- **Yes** protokolliert `CheckpointDecision=Continue` und setzt den gespeicherten Plan fort.
+- **No** beendet den Lauf sicher; alle späteren Zeilen werden als `NotExecuted` gespeichert.
+- Ein fehlender oder fehlerhafter Dialog stoppt ebenfalls fail-closed und wird als Fehler protokolliert.
+- Dry-run zeigt den Checkpoint im Plan/Grid, fordert aber keine echte Entscheidung an.
+- Nach einem Stopp werden `shutdown-after.json` und `shutdown-result.json` soweit möglich trotzdem geschrieben.
+
+Der Dialog behauptet nicht automatisch, die Umgebung sei leer. Diese fachliche Entscheidung bleibt bewusst beim Operator und den vorhandenen Enterprise-Monitoringquellen. `Partially Started` in der BizTalk-Konsole ist direkt nach dem Abschalten der Receive Locations erwartbar. Es wird kein pauschaler Application-Stop ergänzt, da dieser je nach Modus weitere Artefaktzustände, Subscriptions, Policies oder laufende Instanzen verändert.
+
## Update und Rollback
Der Installer verändert eine bestehende Installation erst nach erfolgreicher Paketprüfung:
diff --git a/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.txt b/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.txt
index 9e1913b..e1bc7ba 100644
--- a/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.txt
+++ b/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.txt
@@ -1,6 +1,6 @@
PROD-RUNBOOK: SCHEDULEDTASK-STEUERUNG UND PERSISTENTES LAUFZEITLOGGING
Stand: 2026-08-26
-Zielversion: 2.3.1
+Zielversion: 2.3.2
Adapter: BizTalk ScheduledTask Adapter 7.0.2
Adapterpfad: C:\Program Files (x86)\BizTalk ScheduledTask Adapter 7.0.2
@@ -13,8 +13,8 @@ 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.1 als Administrator installieren/aktualisieren.
-3. setup_version=2.3.1.0 und erfolgreichen Ziel-Self-Test pruefen.
+2. Setup 2.3.2 als Administrator installieren/aktualisieren.
+3. setup_version=2.3.2.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.
@@ -34,9 +34,10 @@ FUNKTIONSTEST
Calling MSBTS_ReceiveLocation.Disable
Reached: Disable receive location completed
6. shutdown-result.json: Succeeded oder AlreadySatisfied.
-7. Restore zuerst Dry run, dann real.
-8. Calling MSBTS_ReceiveLocation.Enable und Reached pruefen.
-9. Snapshot After und Compare; Sollzustand muss wiederhergestellt sein.
+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.
@@ -73,7 +74,7 @@ ABNAHME
- 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.1 bzw. 2.3.1-net461.
+- Setup/Tool/Reports zeigen 2.3.2 bzw. 2.3.2-net461.
Detailfassung:
docs\PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md
diff --git a/PROD-Shutdown-Drain-Checkpoint-2.3.2.txt b/PROD-Shutdown-Drain-Checkpoint-2.3.2.txt
new file mode 100644
index 0000000..138c88f
--- /dev/null
+++ b/PROD-Shutdown-Drain-Checkpoint-2.3.2.txt
@@ -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
diff --git a/README.md b/README.md
index 1b1a95f..76477dd 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
- 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
- Best-effort plan execution: one isolated WMI failure is recorded while remaining independent steps continue
- Idempotent execution that skips artifacts already in the requested target state
- Durable per-step result reports even when a shutdown or restore completes only partially
@@ -47,8 +48,9 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
4. Click **Snapshot Before**.
5. Click **Shutdown** and review `shutdown-plan.json`.
6. Disable **Dry run** only when the plan is correct.
-7. After maintenance, click **Restore** using the saved `before.json`.
-8. Click **Snapshot After** and **Compare**.
+7. During a real shutdown, keep the drain dialog open until Group Hub and enterprise monitoring show that in-flight processing has emptied; choose **Yes** only then.
+8. After maintenance, click **Restore** using the saved `before.json`.
+9. Click **Snapshot After** and **Compare**.
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.
@@ -62,6 +64,7 @@ Before a real shutdown or restore, the exact fresh plan is saved and a second di
Shutdown:
- Disable receive locations that were enabled.
+- Pause at the persisted inbound-drain checkpoint and require an explicit operator decision.
- Stop orchestrations that were started.
- Stop send ports that were started.
- Stop host instances that were started on the selected server.
@@ -74,6 +77,10 @@ Restore:
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, **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.
+
+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
- `before.json`, `after.json`
@@ -97,6 +104,8 @@ The Operation Log shows the WMI class, key property, key value and method for re
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).
@@ -134,5 +143,6 @@ Targeted German inline comments explain non-obvious operational decisions such a
- [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)
- [AI maintainer handoff](AI-README.md)
- [References](REFERENCES.md)
diff --git a/REFERENCES.md b/REFERENCES.md
index ff35ad7..8eb1497 100644
--- a/REFERENCES.md
+++ b/REFERENCES.md
@@ -20,3 +20,6 @@
- MSBTS_HostInstance (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-wmi
- MSBTS_HostInstance.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-start-method-wmi
- MSBTS_HostInstance.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-stop-method-wmi
+- 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
diff --git a/docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md b/docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md
index fec6574..6ac9fb9 100644
--- a/docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md
+++ b/docs/PROD-ScheduledTask-und-Laufzeitlogging-2026-08-26.md
@@ -2,7 +2,7 @@
**Stand:** 2026-08-26
-**Zielversion:** 2.3.1
+**Zielversion:** 2.3.2
**Betroffener Adapter:** BizTalk ScheduledTask Adapter 7.0.2
@@ -17,8 +17,8 @@ Version 2.3.1 erkennt ScheduledTask-Schritte an Adaptername oder `scheduler:`-UR
## Vorbereitung
1. Vorhandene `before.json`, Ergebnisdateien und Laufzeitlogs außerhalb des Installationsordners sichern.
-2. Setup 2.3.1 als Administrator installieren beziehungsweise aktualisieren.
-3. Im Setup-Log `setup_version=2.3.1.0` und erfolgreichen Ziel-Self-Test prüfen.
+2. Setup 2.3.2 als Administrator installieren beziehungsweise aktualisieren.
+3. Im Setup-Log `setup_version=2.3.2.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.
@@ -37,9 +37,10 @@ Version 2.3.1 erkennt ScheduledTask-Schritte an Adaptername oder `scheduler:`-UR
- `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. Nach der Wartung **Restore** zunächst im Dry-run, danach real ausführen.
-9. Entsprechend `Calling MSBTS_ReceiveLocation.Enable`, den erreichten Zustand und `Succeeded`/`AlreadySatisfied` prüfen.
-10. **Snapshot After** und **Compare** ausführen; die Receive Location muss dem gespeicherten Sollzustand entsprechen.
+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
@@ -77,6 +78,6 @@ Der Fehlerdatensatz enthält Exceptiontyp, HRESULT, innere Ausnahmen, vorhandene
- 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.1 beziehungsweise `2.3.1-net461`.
+- Setup, Tool und Ergebnisdateien melden Version 2.3.2 beziehungsweise `2.3.2-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.
diff --git a/docs/PROD-Shutdown-Drain-Checkpoint-2026-08-26.md b/docs/PROD-Shutdown-Drain-Checkpoint-2026-08-26.md
new file mode 100644
index 0000000..bdce273
--- /dev/null
+++ b/docs/PROD-Shutdown-Drain-Checkpoint-2026-08-26.md
@@ -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.
diff --git a/src/BizTalkPlatformManagementTool.Setup/InstallerEngine.cs b/src/BizTalkPlatformManagementTool.Setup/InstallerEngine.cs
index 40fc551..aa7cb72 100644
--- a/src/BizTalkPlatformManagementTool.Setup/InstallerEngine.cs
+++ b/src/BizTalkPlatformManagementTool.Setup/InstallerEngine.cs
@@ -23,7 +23,7 @@ namespace BizTalkPlatformManagementTool.Setup
private const string ProductName = "BizTalk Platform Management Tool";
/// Aktuelle Produktversion des Installers und Uninstall-Eintrags.
- private const string ProductVersion = "2.3.1";
+ private const string ProductVersion = "2.3.2";
///
/// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen.
diff --git a/src/BizTalkPlatformManagementTool.Setup/MainForm.cs b/src/BizTalkPlatformManagementTool.Setup/MainForm.cs
index f2989cf..534ab45 100644
--- a/src/BizTalkPlatformManagementTool.Setup/MainForm.cs
+++ b/src/BizTalkPlatformManagementTool.Setup/MainForm.cs
@@ -65,7 +65,7 @@ namespace BizTalkPlatformManagementTool.Setup
{
AutoSize = true,
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
- Text = "BizTalk Platform Management Tool 2.3.1"
+ Text = "BizTalk Platform Management Tool 2.3.2"
});
root.Controls.Add(new Label
{
diff --git a/src/BizTalkPlatformManagementTool.Setup/Properties/AssemblyInfo.cs b/src/BizTalkPlatformManagementTool.Setup/Properties/AssemblyInfo.cs
index 98d30af..fec2322 100644
--- a/src/BizTalkPlatformManagementTool.Setup/Properties/AssemblyInfo.cs
+++ b/src/BizTalkPlatformManagementTool.Setup/Properties/AssemblyInfo.cs
@@ -8,6 +8,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyProduct("BizTalk Platform Management Tool")]
[assembly: ComVisible(false)]
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
-[assembly: AssemblyVersion("2.3.1.0")]
-[assembly: AssemblyFileVersion("2.3.1.0")]
+[assembly: AssemblyVersion("2.3.2.0")]
+[assembly: AssemblyFileVersion("2.3.2.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
diff --git a/src/BizTalkPlatformManagementTool.Setup/app.manifest b/src/BizTalkPlatformManagementTool.Setup/app.manifest
index e114ec1..83a87d7 100644
--- a/src/BizTalkPlatformManagementTool.Setup/app.manifest
+++ b/src/BizTalkPlatformManagementTool.Setup/app.manifest
@@ -1,6 +1,6 @@
-
+
diff --git a/src/BizTalkPlatformManagementTool/Models/OperationModels.cs b/src/BizTalkPlatformManagementTool/Models/OperationModels.cs
index 5963362..1474db3 100644
--- a/src/BizTalkPlatformManagementTool/Models/OperationModels.cs
+++ b/src/BizTalkPlatformManagementTool/Models/OperationModels.cs
@@ -35,6 +35,11 @@ namespace BizTalkPlatformManagementTool.Models
///
ReceiveLocation,
+ ///
+ /// An operator confirmation boundary between shutdown phases.
+ ///
+ OperatorCheckpoint,
+
///
/// A send port step.
///
@@ -249,6 +254,15 @@ namespace BizTalkPlatformManagementTool.Models
/// The step failed, while subsequent independent steps were still attempted.
public const string Failed = "Failed";
+
+ /// The operator explicitly confirmed a non-mutating checkpoint.
+ public const string Confirmed = "Confirmed";
+
+ /// The step was not reached because the operator stopped at a checkpoint.
+ public const string NotExecuted = "NotExecuted";
+
+ /// The operator declined to continue at a checkpoint.
+ public const string Declined = "Declined";
}
///
@@ -359,10 +373,32 @@ namespace BizTalkPlatformManagementTool.Models
[DataMember(Order = 13)]
public List Steps { get; set; }
+ /// Gets or sets the last durable operator-checkpoint decision.
+ [DataMember(Order = 14, EmitDefaultValue = false)]
+ public string CheckpointDecision { get; set; }
+
+ /// Gets or sets the local timestamp of the checkpoint decision.
+ [DataMember(Order = 15, EmitDefaultValue = false)]
+ public string CheckpointAt { get; set; }
+
+ /// Gets or sets whether the operator stopped the remaining shutdown phases.
+ [DataMember(Order = 16)]
+ public bool OperatorStopped { get; set; }
+
+ /// Gets or sets how many remaining plan rows were not reached.
+ [DataMember(Order = 17)]
+ public int NotExecutedCount { get; set; }
+
/// Gets whether operator attention is required.
public bool HasFailures
{
get { return FailedCount > 0 || !string.IsNullOrWhiteSpace(InitializationError) || !string.IsNullOrWhiteSpace(PostSnapshotError); }
}
+
+ /// Gets whether failure or an intentional operator stop needs review.
+ public bool RequiresOperatorReview
+ {
+ get { return HasFailures || OperatorStopped; }
+ }
}
}
diff --git a/src/BizTalkPlatformManagementTool/Properties/AssemblyInfo.cs b/src/BizTalkPlatformManagementTool/Properties/AssemblyInfo.cs
index e77903e..ff5a59b 100644
--- a/src/BizTalkPlatformManagementTool/Properties/AssemblyInfo.cs
+++ b/src/BizTalkPlatformManagementTool/Properties/AssemblyInfo.cs
@@ -9,6 +9,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: ComVisible(false)]
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
-[assembly: AssemblyVersion("2.3.1.0")]
-[assembly: AssemblyFileVersion("2.3.1.0")]
+[assembly: AssemblyVersion("2.3.2.0")]
+[assembly: AssemblyFileVersion("2.3.2.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
diff --git a/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs b/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs
index f39b3fa..3539da0 100644
--- a/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs
+++ b/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs
@@ -16,7 +16,7 @@ namespace BizTalkPlatformManagementTool.Services
///
/// Current tool version written into generated snapshots.
///
- public const string Version = "2.3.1-net461";
+ public const string Version = "2.3.2-net461";
///
/// Fallback application name used when WMI does not expose an application property.
@@ -198,6 +198,19 @@ namespace BizTalkPlatformManagementTool.Services
}
}
+ 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 item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted))
@@ -349,6 +362,21 @@ namespace BizTalkPlatformManagementTool.Services
/// The ordered plan to execute.
/// The runtime options controlling server, dry-run and wait behavior.
public OperationExecutionReport ExecutePlan(OperationPlan plan, OperationOptions options)
+ {
+ return ExecutePlan(plan, options, null);
+ }
+
+ ///
+ /// Executes a plan with an optional fail-closed operator checkpoint callback.
+ ///
+ /// The ordered plan to execute.
+ /// The runtime execution options.
+ /// Operator callback used by real shutdown plans.
+ /// The complete execution report.
+ internal OperationExecutionReport ExecutePlan(
+ OperationPlan plan,
+ OperationOptions options,
+ OperationCheckpointHandler checkpointHandler)
{
if (plan == null || options == null)
{
@@ -362,14 +390,14 @@ namespace BizTalkPlatformManagementTool.Services
if (options.DryRun)
{
// Dry-run benötigt absichtlich weder WMI-Verbindung noch Servicezugriff.
- return executor.Execute(plan, options, null);
+ return executor.Execute(plan, options, null, checkpointHandler);
}
try
{
using (var runtime = new WmiOperationStepRuntime(options.Server, _logger))
{
- return executor.Execute(plan, options, runtime);
+ return executor.Execute(plan, options, runtime, checkpointHandler);
}
}
catch (Exception ex)
@@ -391,6 +419,19 @@ namespace BizTalkPlatformManagementTool.Services
}
}
+ /// Checks whether a shutdown has a runtime phase after receive locations.
+ /// The validated source snapshot.
+ /// The selected host-instance server.
+ /// True when orchestration, send-port or host-instance work follows.
+ 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)));
+ }
+
///
/// Saves a snapshot and its report sidecars.
///
diff --git a/src/BizTalkPlatformManagementTool/Services/OperationPlanExecutor.cs b/src/BizTalkPlatformManagementTool/Services/OperationPlanExecutor.cs
index 2f1d665..f72e48a 100644
--- a/src/BizTalkPlatformManagementTool/Services/OperationPlanExecutor.cs
+++ b/src/BizTalkPlatformManagementTool/Services/OperationPlanExecutor.cs
@@ -30,6 +30,14 @@ namespace BizTalkPlatformManagementTool.Services
RuntimeStepOutcome Execute(OperationStep step, OperationOptions options);
}
+ ///
+ /// Requests an operator decision at a non-mutating boundary in an operation plan.
+ ///
+ /// The checkpoint plan row.
+ /// The durable execution progress before the checkpoint.
+ /// True to continue with later phases; false to stop safely.
+ internal delegate bool OperationCheckpointHandler(OperationStep checkpoint, OperationExecutionReport progress);
+
///
/// Runs every independent plan step, records durable outcomes and deliberately
/// continues after isolated failures.
@@ -56,6 +64,23 @@ namespace BizTalkPlatformManagementTool.Services
/// The runtime implementation; optional only during dry-run.
/// A complete per-step execution report.
public OperationExecutionReport Execute(OperationPlan plan, OperationOptions options, IOperationStepRuntime runtime)
+ {
+ return Execute(plan, options, runtime, null);
+ }
+
+ ///
+ /// Executes or simulates every plan step with an optional operator checkpoint handler.
+ ///
+ /// The validated operation plan.
+ /// The execution options.
+ /// The runtime implementation; optional only during dry-run.
+ /// UI-independent operator decision callback.
+ /// A complete per-step execution report, including unreached rows.
+ internal OperationExecutionReport Execute(
+ OperationPlan plan,
+ OperationOptions options,
+ IOperationStepRuntime runtime,
+ OperationCheckpointHandler checkpointHandler)
{
if (plan == null || options == null)
{
@@ -71,6 +96,7 @@ namespace BizTalkPlatformManagementTool.Services
{
var step = plan.Steps[index];
var stepResult = NewStepResult(index + 1, step);
+ var stopAfterCurrentStep = false;
try
{
if (!step.Execute)
@@ -85,6 +111,30 @@ namespace BizTalkPlatformManagementTool.Services
report.DryRunCount++;
Info("DRY RUN: " + DescribeStep(step));
}
+ else if (IsOperatorCheckpoint(step))
+ {
+ Info("Operator checkpoint reached after the receive-location phase: " + step.Action);
+ report.CheckpointAt = DateTimeOffset.Now.ToString("o");
+ if (checkpointHandler == null)
+ {
+ throw new InvalidOperationException("No operator checkpoint handler is available. Shutdown stops safely before later runtime phases.");
+ }
+
+ if (checkpointHandler(step, report))
+ {
+ report.CheckpointDecision = "Continue";
+ stepResult.Outcome = OperationStepOutcomes.Confirmed;
+ Success("Operator confirmed that the BizTalk environment is drained. Continuing with orchestrations, send ports and host instances.");
+ }
+ else
+ {
+ report.CheckpointDecision = "Stop";
+ report.OperatorStopped = true;
+ stepResult.Outcome = OperationStepOutcomes.Declined;
+ stopAfterCurrentStep = true;
+ Warning("Operator stopped the shutdown safely at the drain checkpoint. No later shutdown phase will be executed.");
+ }
+ }
else
{
Info("Executing step " + (index + 1) + "/" + plan.Steps.Count + ": " + DescribeStep(step));
@@ -107,13 +157,30 @@ namespace BizTalkPlatformManagementTool.Services
stepResult.Outcome = OperationStepOutcomes.Failed;
stepResult.Error = FormatException(ex);
report.FailedCount++;
- Error("STEP FAILED; continuing with remaining independent steps: " + DescribeStep(step) + ". Error: " + stepResult.Error);
+ if (IsOperatorCheckpoint(step))
+ {
+ report.CheckpointDecision = "Error";
+ report.CheckpointAt = report.CheckpointAt ?? DateTimeOffset.Now.ToString("o");
+ report.OperatorStopped = true;
+ stopAfterCurrentStep = true;
+ Error("OPERATOR CHECKPOINT FAILED CLOSED; no later shutdown phase will be executed. Error: " + stepResult.Error);
+ }
+ else
+ {
+ Error("STEP FAILED; continuing with remaining independent steps: " + DescribeStep(step) + ". Error: " + stepResult.Error);
+ }
}
finally
{
stepResult.FinishedAt = DateTimeOffset.Now.ToString("o");
report.Steps.Add(stepResult);
}
+
+ if (stopAfterCurrentStep)
+ {
+ AddNotExecutedSteps(plan, index + 1, report);
+ break;
+ }
}
report.FinishedAt = DateTimeOffset.Now.ToString("o");
@@ -121,8 +188,14 @@ namespace BizTalkPlatformManagementTool.Services
+ ", already_satisfied=" + report.AlreadySatisfiedCount
+ ", skipped=" + report.SkippedCount
+ ", dry_run=" + report.DryRunCount
- + ", failed=" + report.FailedCount + ".";
- if (report.FailedCount == 0)
+ + ", failed=" + report.FailedCount
+ + ", not_executed=" + report.NotExecutedCount
+ + ", checkpoint=" + (report.CheckpointDecision ?? "not_required") + ".";
+ if (report.OperatorStopped && report.FailedCount == 0)
+ {
+ Warning(summary + " Shutdown stopped safely by operator; review the partial-state snapshot and report.");
+ }
+ else if (report.FailedCount == 0)
{
Success(summary);
}
@@ -133,6 +206,31 @@ namespace BizTalkPlatformManagementTool.Services
return report;
}
+ /// Checks whether a plan row is the non-mutating operator boundary.
+ /// The plan row.
+ /// True for the stable operator-checkpoint kind.
+ private static bool IsOperatorCheckpoint(OperationStep step)
+ {
+ return step != null && string.Equals(step.Kind, OperationStepKind.OperatorCheckpoint.ToString(), StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// Adds auditable results for all rows intentionally not reached after a stop.
+ /// The source plan.
+ /// The zero-based first unreached row.
+ /// The report receiving the unreached rows.
+ 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++;
+ }
+ }
+
/// Creates the common execution-report header.
/// The source plan.
/// The execution options.
diff --git a/src/BizTalkPlatformManagementTool/Ui/MainForm.cs b/src/BizTalkPlatformManagementTool/Ui/MainForm.cs
index baaa847..dc2768d 100644
--- a/src/BizTalkPlatformManagementTool/Ui/MainForm.cs
+++ b/src/BizTalkPlatformManagementTool/Ui/MainForm.cs
@@ -417,7 +417,7 @@ namespace BizTalkPlatformManagementTool.Ui
_logger.Warning("Shutdown cancelled after plan review. No runtime state was changed.");
return;
}
- var report = _service.ExecutePlan(plan, options);
+ var report = _service.ExecutePlan(plan, options, ConfirmInboundDrainCheckpoint);
CapturePostOperationSnapshot(options, report, "shutdown-after.json");
var reportPath = _service.SaveExecutionReport(options.OutputDirectory, "shutdown-result.json", report);
ShowExecutionReport(report);
@@ -577,8 +577,14 @@ namespace BizTalkPlatformManagementTool.Ui
if (report != null && report.HasFailures)
{
throw new InvalidOperationException(
- "Plan completed with failures, but all remaining independent steps were attempted. " +
- "Failed steps: " + report.FailedCount + ". Review: " + reportPath);
+ "Plan requires operator review. Failed steps: " + report.FailedCount
+ + "; 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);
}
}
@@ -641,6 +647,11 @@ namespace BizTalkPlatformManagementTool.Ui
_logger.Success("Operation completed.");
SetBusy(false, "Ready.");
}
+ catch (OperationCanceledException ex)
+ {
+ _logger.Warning(ex.Message);
+ SetBusy(false, "Stopped safely: " + ex.Message);
+ }
catch (Exception ex)
{
_logger.Error(FormatException(ex));
@@ -695,9 +706,11 @@ namespace BizTalkPlatformManagementTool.Ui
var confirmed = false;
Action showConfirmation = () =>
{
- var executableSteps = plan.Steps.Count(x => x.Execute);
+ var executableSteps = plan.Steps.Count(x => x.Execute && !string.Equals(x.Kind, OperationStepKind.OperatorCheckpoint.ToString(), StringComparison.OrdinalIgnoreCase));
+ var checkpointCount = plan.Steps.Count(x => x.Execute && string.Equals(x.Kind, OperationStepKind.OperatorCheckpoint.ToString(), StringComparison.OrdinalIgnoreCase));
var result = MessageBox.Show(
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.\n\n")
+ "The exact plan was saved to:\n" + planPath + "\n\nContinue now?",
"Confirm Prepared BizTalk Plan",
MessageBoxButtons.YesNo,
@@ -717,6 +730,54 @@ namespace BizTalkPlatformManagementTool.Ui
return confirmed;
}
+ ///
+ /// Pauses a real shutdown after the receive-location phase until the operator
+ /// confirms that the enterprise environment has drained.
+ ///
+ /// The persisted checkpoint plan row.
+ /// The execution results already reached.
+ /// True only after an explicit Yes decision.
+ private bool ConfirmInboundDrainCheckpoint(OperationStep checkpoint, OperationExecutionReport progress)
+ {
+ var confirmed = false;
+ Action showConfirmation = () =>
+ {
+ 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";
+ var result = MessageBox.Show(
+ "The receive-location shutdown phase has finished.\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 = stop safely and persist all remaining steps as NotExecuted.",
+ "Confirm BizTalk Inbound Drain",
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Warning,
+ MessageBoxDefaultButton.Button2);
+ confirmed = result == DialogResult.Yes;
+ };
+
+ if (InvokeRequired)
+ {
+ Invoke(showConfirmation);
+ }
+ else
+ {
+ showConfirmation();
+ }
+ return confirmed;
+ }
+
///
/// Confirms the stronger emergency-recovery contract, including the preserved
/// source snapshot and automatic Enterprise SSO prerequisite.
diff --git a/src/BizTalkPlatformManagementTool/app.manifest b/src/BizTalkPlatformManagementTool/app.manifest
index b90625a..39a45d1 100644
--- a/src/BizTalkPlatformManagementTool/app.manifest
+++ b/src/BizTalkPlatformManagementTool/app.manifest
@@ -1,6 +1,6 @@
-
+
diff --git a/tests/BizTalkPlatformManagementTool.Tests/Program.cs b/tests/BizTalkPlatformManagementTool.Tests/Program.cs
index b588dbe..0535075 100644
--- a/tests/BizTalkPlatformManagementTool.Tests/Program.cs
+++ b/tests/BizTalkPlatformManagementTool.Tests/Program.cs
@@ -29,6 +29,9 @@ namespace BizTalkPlatformManagementTool.Tests
Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder);
Run("ShutdownPlanUsesGlobalSafeOrder", ShutdownPlanUsesGlobalSafeOrder);
Run("ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure", ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure);
+ Run("ShutdownCheckpointDeclineStopsLaterPhases", ShutdownCheckpointDeclineStopsLaterPhases);
+ Run("ShutdownCheckpointMissingHandlerFailsClosed", ShutdownCheckpointMissingHandlerFailsClosed);
+ Run("ShutdownCheckpointDryRunDoesNotPrompt", ShutdownCheckpointDryRunDoesNotPrompt);
Run("EmergencyRestorePlanStartsSsoFirst", EmergencyRestorePlanStartsSsoFirst);
Run("HostInstancePlanAcceptsShortAndFqdnServer", HostInstancePlanAcceptsShortAndFqdnServer);
Run("PlanExecutionContinuesAfterSchedulerFailure", PlanExecutionContinuesAfterSchedulerFailure);
@@ -167,12 +170,13 @@ namespace BizTalkPlatformManagementTool.Tests
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
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 lastOrchestration = plan.Steps.FindLastIndex(x => x.Kind == "Orchestration");
var firstSendPort = plan.Steps.FindIndex(x => x.Kind == "SendPort");
var lastSendPort = plan.Steps.FindLastIndex(x => x.Kind == "SendPort");
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(lastSendPort < firstHost, "host instances were not globally last");
}
@@ -337,10 +341,73 @@ namespace BizTalkPlatformManagementTool.Tests
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
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(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");
+ }
+
+ /// Prüft, dass Nein am Drain-Checkpoint keine spätere Shutdown-Mutation zulässt.
+ 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(path);
+ Assert(loaded.OperatorStopped && loaded.CheckpointDecision == "Stop" && loaded.NotExecutedCount == 3, "serialized report lost checkpoint decision evidence");
+ });
+ }
+
+ /// Prüft den sicheren Stopp, wenn der echte Lauf keinen Dialog-Handler besitzt.
+ 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");
+ }
+
+ /// Prüft, dass Dry-run den Checkpoint zeigt, aber keine Bedienerentscheidung anfordert.
+ 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");
}
/// Prüft ENTSSO als erste Voraussetzung des Emergency Restore.