Make shutdown checkpoint dialogs unmistakable

This commit is contained in:
2026-08-27 20:17:16 +02:00
parent 61b5b7e452
commit 8b1c14b892
19 changed files with 385 additions and 48 deletions
+2 -1
View File
@@ -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.3.
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
@@ -37,6 +37,7 @@ Die zentrale Implementierung liegt in `src/BizTalkPlatformManagementTool.Setup/I
- Emergency Restore überschreibt niemals die Eingabe-`before.json`, erzeugt eine timestamp-basierte Kopie und stellt `ENTSSO` vor Host Instances sicher.
- Emergency Restore muss mit genau einer validen `before.json` funktionieren; Dateien eines vorherigen fehlgeschlagenen Laufs dürfen keine Voraussetzung sein.
- Ein älterer kompatibler Snapshot, insbesondere aus 2.1.3, darf nicht allein anhand seines `ToolVersion`-Werts abgelehnt werden.
- Alle laufzeitrelevanten Bestätigungsdialoge müssen mit dem Hauptformular als Owner angezeigt werden. Vor dem echten Drain-Checkpoint muss der Status sichtbar `ACTION REQUIRED`/pausiert melden; Dry-run muss ausdrücklich erklären, dass keine Runtime-Änderung und keine echte Drain-Entscheidung erfolgt.
- Der echte Emergency Restore erzeugt nach Möglichkeit automatisch einen timestamp-basierten Soll/Ist-Diff aus Recovery-Quelle und Nachher-Snapshot.
- Die zentrale best-effort Orchestrierung liegt in `OperationPlanExecutor`; die WMI-/Service-Zustandsprüfung bleibt im produktiven Runtime-Adapter.
- Datei-Logging und optionale GUI-Logweiterleitung sind Diagnosekanäle und dürfen niemals einen fachlichen Planschritt oder dessen Fehlerbehandlung unterbrechen.
+14
View File
@@ -1,6 +1,20 @@
# 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.
+2 -2
View File
@@ -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.3-source.zip.b64.txt`: vollständiger Quellstand als Base64-TXT
- `BizTalkPlatformManagementTool-2.3.3-source.zip.sha256.txt`: SHA-256 des Quellarchivs
- `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:
@@ -1,6 +1,6 @@
PROD-RUNBOOK: SCHEDULEDTASK-STEUERUNG UND PERSISTENTES LAUFZEITLOGGING
Stand: 2026-08-26
Zielversion: 2.3.3
Zielversion: 2.3.4
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.3 als Administrator installieren/aktualisieren.
3. setup_version=2.3.3.0 und erfolgreichen Ziel-Self-Test pruefen.
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.
@@ -74,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.3 bzw. 2.3.3-net461.
- 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.
+3 -1
View File
@@ -29,6 +29,7 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
- Thirty-day runtime-log retention with GZip compression for completed daily logs
- Process-local ScheduledTask adapter dependency resolution for `scheduler:` receive locations without modifying the GAC
- Fail-closed operator drain checkpoint after all receive locations and before later shutdown phases
- Owner-bound shutdown confirmations with an explicit paused/action-required status and an unambiguous Dry-run notice
- Best-effort plan execution: one isolated WMI failure is recorded while remaining independent steps continue
- Idempotent execution that skips artifacts already in the requested target state
- Durable per-step result reports even when a shutdown or restore completes only partially
@@ -78,7 +79,7 @@ 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.
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.
@@ -148,5 +149,6 @@ Targeted German inline comments explain non-obvious operational decisions such a
- [PROD runtime-log storage validation and 2.3.1 fix](docs/PROD-Laufzeitlog-Ablage-2026-08-26.md)
- [PROD shutdown drain checkpoint and application-status analysis](docs/PROD-Shutdown-Drain-Checkpoint-2026-08-26.md)
- [PROD application-association analysis and 2.3.3 validation](docs/PROD-Application-Zuordnung-2026-08-27.md)
- [PROD shutdown-dialog visibility analysis and 2.3.4 validation](docs/PROD-Shutdown-Dialog-Sichtbarkeit-2026-08-27.md)
- [AI maintainer handoff](AI-README.md)
- [References](REFERENCES.md)
@@ -2,7 +2,7 @@
**Stand:** 2026-08-26
**Zielversion:** 2.3.3
**Zielversion:** 2.3.4
**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.3 als Administrator installieren beziehungsweise aktualisieren.
3. Im Setup-Log `setup_version=2.3.3.0` und erfolgreichen Ziel-Self-Test prüfen.
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.
@@ -78,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.3 beziehungsweise `2.3.3-net461`.
- 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.
@@ -23,7 +23,7 @@ namespace BizTalkPlatformManagementTool.Setup
private const string ProductName = "BizTalk Platform Management Tool";
/// <summary>Aktuelle Produktversion des Installers und Uninstall-Eintrags.</summary>
private const string ProductVersion = "2.3.3";
private const string ProductVersion = "2.3.4";
/// <summary>
/// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen.
@@ -65,7 +65,7 @@ namespace BizTalkPlatformManagementTool.Setup
{
AutoSize = true,
Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
Text = "BizTalk Platform Management Tool 2.3.3"
Text = "BizTalk Platform Management Tool 2.3.4"
});
root.Controls.Add(new Label
{
@@ -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.3.0")]
[assembly: AssemblyFileVersion("2.3.3.0")]
[assembly: AssemblyVersion("2.3.4.0")]
[assembly: AssemblyFileVersion("2.3.4.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="2.3.3.0" name="BizTalkPlatformManagementTool.Setup" />
<assemblyIdentity version="2.3.4.0" name="BizTalkPlatformManagementTool.Setup" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
</trustInfo>
@@ -70,6 +70,7 @@
<Compile Include="Services\SnapshotValidator.cs" />
<Compile Include="Services\SnapshotStore.cs" />
<Compile Include="Services\BizTalkOperationService.cs" />
<Compile Include="Ui\ShutdownOperatorPrompts.cs" />
<Compile Include="Ui\MainForm.cs" />
</ItemGroup>
<ItemGroup>
@@ -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.3.0")]
[assembly: AssemblyFileVersion("2.3.3.0")]
[assembly: AssemblyVersion("2.3.4.0")]
[assembly: AssemblyFileVersion("2.3.4.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
@@ -16,7 +16,7 @@ namespace BizTalkPlatformManagementTool.Services
/// <summary>
/// Current tool version written into generated snapshots.
/// </summary>
public const string Version = "2.3.3-net461";
public const string Version = "2.3.4-net461";
/// <summary>
/// Fallback application name used when neither WMI nor the ExplorerOM catalog can resolve ownership.
@@ -238,6 +238,8 @@ namespace BizTalkPlatformManagementTool.Ui
_dryRunCheckBox.Dock = DockStyle.Fill;
_dryRunCheckBox.Margin = new Padding(8, 7, 8, 7);
_dryRunCheckBox.TextAlign = ContentAlignment.MiddleLeft;
_dryRunCheckBox.CheckedChanged += DryRunCheckBoxCheckedChanged;
UpdateDryRunPresentation();
var browseButton = new Button { Text = "Browse...", Dock = DockStyle.Fill, Margin = new Padding(8, 5, 8, 5) };
browseButton.Click += BrowseButtonClick;
@@ -404,7 +406,9 @@ namespace BizTalkPlatformManagementTool.Ui
private void ShutdownClick(object sender, EventArgs e)
{
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);
_service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot);
@@ -412,7 +416,11 @@ namespace BizTalkPlatformManagementTool.Ui
// Der exakte, frisch erzeugte Plan wird vor Bestätigung und jeder Laufzeitänderung gespeichert.
var planPath = _service.SavePlan(options.OutputDirectory, "shutdown-plan.json", 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.");
return;
@@ -706,13 +714,13 @@ namespace BizTalkPlatformManagementTool.Ui
var confirmed = false;
Action showConfirmation = () =>
{
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));
_statusLabel.Text = "ACTION REQUIRED: Review the saved " + actionName.ToLowerInvariant() + " plan and answer the owned confirmation dialog.";
Activate();
BringToFront();
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",
this,
ShutdownOperatorPrompts.BuildPreparedPlanConfirmation(actionName, plan, server, planPath),
ShutdownOperatorPrompts.PreparedPlanTitle,
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
@@ -730,6 +738,37 @@ namespace BizTalkPlatformManagementTool.Ui
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.
@@ -742,25 +781,13 @@ namespace BizTalkPlatformManagementTool.Ui
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";
_statusLabel.Text = ShutdownOperatorPrompts.WaitingForInboundDrainStatus;
Activate();
BringToFront();
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",
this,
ShutdownOperatorPrompts.BuildInboundDrainConfirmation(progress),
ShutdownOperatorPrompts.InboundDrainTitle,
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
@@ -792,8 +819,12 @@ namespace BizTalkPlatformManagementTool.Ui
var confirmed = false;
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 result = MessageBox.Show(
this,
"EMERGENCY RESTORE will reconcile " + executableSteps + " step(s) on server '" + server + "'.\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"
@@ -983,10 +1014,43 @@ namespace BizTalkPlatformManagementTool.Ui
_clearButton.Enabled = !busy;
_openLogsButton.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>
/// Handles the output directory Browse button click.
/// </summary>
@@ -0,0 +1,109 @@
using System;
using System.Linq;
using BizTalkPlatformManagementTool.Models;
namespace BizTalkPlatformManagementTool.Ui
{
/// <summary>
/// Builds stable, testable operator guidance for shutdown plan review, dry-run
/// simulation and the real inbound-drain checkpoint.
/// </summary>
internal static class ShutdownOperatorPrompts
{
/// <summary>Title of the non-mutating dry-run explanation.</summary>
public const string DryRunTitle = "Shutdown Dry Run - No Runtime Changes";
/// <summary>Title of the saved-plan confirmation.</summary>
public const string PreparedPlanTitle = "Confirm Prepared BizTalk Plan";
/// <summary>Title of the real inbound-drain decision.</summary>
public const string InboundDrainTitle = "ACTION REQUIRED - Confirm BizTalk Inbound Drain";
/// <summary>Status displayed while the real shutdown waits for drain verification.</summary>
public const string WaitingForInboundDrainStatus = "ACTION REQUIRED: Shutdown paused. Verify inbound drain and answer the owned confirmation dialog.";
/// <summary>Builds the explicit explanation shown for a shutdown dry-run.</summary>
/// <param name="plan">The prepared shutdown plan.</param>
/// <param name="planPath">The durable path of the prepared plan.</param>
/// <returns>Operator-facing dry-run guidance.</returns>
public static string BuildDryRunNotice(OperationPlan plan, string planPath)
{
if (plan == null)
{
throw new ArgumentNullException("plan");
}
var checkpointCount = CountCheckpoints(plan);
return "DRY RUN is enabled. This run only simulates the saved shutdown plan.\n\n"
+ "No receive location is disabled and no runtime state is changed. Therefore the real inbound-drain decision dialog will not open during this run.\n\n"
+ (checkpointCount == 0
? "This plan has no later shutdown phase and therefore needs no drain checkpoint.\n\n"
: "The plan contains the inbound-drain checkpoint. It will require an explicit Yes/No decision when the same plan is prepared with Dry run disabled.\n\n")
+ "Prepared plan:\n" + planPath;
}
/// <summary>Builds the confirmation for a fully prepared real operation.</summary>
/// <param name="actionName">The operation name.</param>
/// <param name="plan">The prepared operation plan.</param>
/// <param name="server">The selected target server.</param>
/// <param name="planPath">The durable path of the prepared plan.</param>
/// <returns>Operator-facing prepared-plan confirmation.</returns>
public static string BuildPreparedPlanConfirmation(string actionName, OperationPlan plan, string server, string planPath)
{
if (plan == null)
{
throw new ArgumentNullException("plan");
}
var executableSteps = plan.Steps.Count(x => x.Execute && !IsCheckpoint(x));
var checkpointCount = CountCheckpoints(plan);
return actionName + " will execute " + executableSteps + " step(s) on server '" + server + "'.\n\n"
+ (checkpointCount == 0
? string.Empty
: "The saved plan contains an operator drain checkpoint after all receive locations. The real shutdown will visibly pause there for a second, explicit decision.\n\n")
+ "The exact plan was saved to:\n" + planPath + "\n\nContinue now?";
}
/// <summary>Builds the real inbound-drain decision from completed receive-location results.</summary>
/// <param name="progress">The execution results already reached.</param>
/// <returns>Operator-facing drain verification guidance.</returns>
public static string BuildInboundDrainConfirmation(OperationExecutionReport progress)
{
if (progress == null)
{
throw new ArgumentNullException("progress");
}
var receiveResults = progress.Steps
.Where(x => string.Equals(x.Kind, OperationStepKind.ReceiveLocation.ToString(), StringComparison.OrdinalIgnoreCase))
.ToList();
var succeeded = receiveResults.Count(x => x.Outcome == OperationStepOutcomes.Succeeded);
var alreadyDisabled = receiveResults.Count(x => x.Outcome == OperationStepOutcomes.AlreadySatisfied);
var failed = receiveResults.Count(x => x.Outcome == OperationStepOutcomes.Failed);
var failureWarning = failed == 0
? string.Empty
: "\nWARNING: " + failed + " receive location(s) failed. Review the red Operation Log entries before continuing.\n";
return "The receive-location shutdown phase has finished. The shutdown is PAUSED and no later phase can run until you decide.\n\n"
+ "Disabled successfully: " + succeeded + "\n"
+ "Already disabled: " + alreadyDisabled + "\n"
+ "Failed: " + failed + "\n"
+ failureWarning + "\n"
+ "Keep this dialog open while you verify in BizTalk Group Hub and your enterprise monitoring that no new inbound work arrives and all in-flight service instances/messages have drained.\n\n"
+ "Continue with orchestrations, send ports and host instances?\n\n"
+ "Yes = continue shutdown. No or closing this dialog = stop safely and persist all remaining steps as NotExecuted.";
}
/// <summary>Counts executable drain checkpoints in a plan.</summary>
private static int CountCheckpoints(OperationPlan plan)
{
return plan.Steps.Count(x => x.Execute && IsCheckpoint(x));
}
/// <summary>Checks whether a plan row is the stable operator checkpoint kind.</summary>
private static bool IsCheckpoint(OperationStep step)
{
return step != null && string.Equals(step.Kind, OperationStepKind.OperatorCheckpoint.ToString(), StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="2.3.3.0" name="BizTalkPlatformManagementTool" />
<assemblyIdentity version="2.3.4.0" name="BizTalkPlatformManagementTool" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
@@ -7,6 +7,7 @@ using System.Text;
using BizTalkPlatformManagementTool.Models;
using BizTalkPlatformManagementTool.Services;
using BizTalkPlatformManagementTool.Setup;
using BizTalkPlatformManagementTool.Ui;
namespace BizTalkPlatformManagementTool.Tests
{
@@ -28,10 +29,12 @@ namespace BizTalkPlatformManagementTool.Tests
Run("Legacy213SnapshotIsAcceptedForRecovery", Legacy213SnapshotIsAcceptedForRecovery);
Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder);
Run("ShutdownPlanUsesGlobalSafeOrder", ShutdownPlanUsesGlobalSafeOrder);
Run("ShutdownPlanRequiresCheckpointBeforeLaterWorkWithoutReceiveLocations", ShutdownPlanRequiresCheckpointBeforeLaterWorkWithoutReceiveLocations);
Run("ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure", ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure);
Run("ShutdownCheckpointDeclineStopsLaterPhases", ShutdownCheckpointDeclineStopsLaterPhases);
Run("ShutdownCheckpointMissingHandlerFailsClosed", ShutdownCheckpointMissingHandlerFailsClosed);
Run("ShutdownCheckpointDryRunDoesNotPrompt", ShutdownCheckpointDryRunDoesNotPrompt);
Run("ShutdownOperatorPromptsDistinguishDryRunAndRealDecision", ShutdownOperatorPromptsDistinguishDryRunAndRealDecision);
Run("EmergencyRestorePlanStartsSsoFirst", EmergencyRestorePlanStartsSsoFirst);
Run("HostInstancePlanAcceptsShortAndFqdnServer", HostInstancePlanAcceptsShortAndFqdnServer);
Run("PlanExecutionContinuesAfterSchedulerFailure", PlanExecutionContinuesAfterSchedulerFailure);
@@ -182,6 +185,18 @@ namespace BizTalkPlatformManagementTool.Tests
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()
{
@@ -430,6 +445,25 @@ namespace BizTalkPlatformManagementTool.Tests
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>
private static void EmergencyRestorePlanStartsSsoFirst()
{