Add resilient emergency restore for partial BizTalk operations

This commit is contained in:
2026-08-19 17:45:20 +02:00
parent 08626197be
commit 9ce7e8d45a
19 changed files with 1221 additions and 120 deletions
@@ -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.1.3";
private const string ProductVersion = "2.2.0";
/// <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.1.3"
Text = "BizTalk Platform Management Tool 2.2.0"
});
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.1.3.0")]
[assembly: AssemblyFileVersion("2.1.3.0")]
[assembly: AssemblyVersion("2.2.0.0")]
[assembly: AssemblyFileVersion("2.2.0.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.1.3.0" name="BizTalkPlatformManagementTool.Setup" />
<assemblyIdentity version="2.2.0.0" name="BizTalkPlatformManagementTool.Setup" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
</trustInfo>
@@ -43,6 +43,7 @@
<Reference Include="System.Drawing" />
<Reference Include="System.Management" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
</ItemGroup>
@@ -59,6 +60,7 @@
<Compile Include="Services\HtmlReportWriter.cs" />
<Compile Include="Services\JsonFileStore.cs" />
<Compile Include="Services\OperationLogger.cs" />
<Compile Include="Services\OperationPlanExecutor.cs" />
<Compile Include="Services\SnapshotComparer.cs" />
<Compile Include="Services\SnapshotValidator.cs" />
<Compile Include="Services\SnapshotStore.cs" />
@@ -16,7 +16,13 @@ namespace BizTalkPlatformManagementTool.Models
/// <summary>
/// Plan mode for returning BizTalk runtime artifacts to a captured state.
/// </summary>
Restore
Restore,
/// <summary>
/// Recovery mode that uses an existing snapshot without creating or
/// overwriting a new before snapshot.
/// </summary>
EmergencyRestore
}
/// <summary>
@@ -44,6 +50,11 @@ namespace BizTalkPlatformManagementTool.Models
/// </summary>
HostInstance,
/// <summary>
/// A Windows service prerequisite such as Enterprise Single Sign-On.
/// </summary>
WindowsService,
/// <summary>
/// An informational step that is intentionally not executed.
/// </summary>
@@ -210,4 +221,140 @@ namespace BizTalkPlatformManagementTool.Models
/// </summary>
public int PollIntervalSeconds { get; set; }
}
/// <summary>
/// Defines the outcome recorded for one attempted operation-plan step.
/// </summary>
public static class OperationStepOutcomes
{
/// <summary>The step was executed and reached its target state.</summary>
public const string Succeeded = "Succeeded";
/// <summary>The runtime object was already in the requested target state.</summary>
public const string AlreadySatisfied = "AlreadySatisfied";
/// <summary>The step was intentionally not executable.</summary>
public const string Skipped = "Skipped";
/// <summary>The step was shown without mutation because dry-run was enabled.</summary>
public const string DryRun = "DryRun";
/// <summary>The step failed, while subsequent independent steps were still attempted.</summary>
public const string Failed = "Failed";
}
/// <summary>
/// Represents one durable step result from a shutdown, restore or emergency restore.
/// </summary>
[DataContract]
public sealed class OperationStepResult
{
/// <summary>Gets or sets the one-based plan-step number.</summary>
[DataMember(Order = 1)]
public int Index { get; set; }
/// <summary>Gets or sets the artifact kind.</summary>
[DataMember(Order = 2)]
public string Kind { get; set; }
/// <summary>Gets or sets the owning BizTalk application.</summary>
[DataMember(Order = 3)]
public string Application { get; set; }
/// <summary>Gets or sets the artifact, host instance or service name.</summary>
[DataMember(Order = 4)]
public string Name { get; set; }
/// <summary>Gets or sets the requested action.</summary>
[DataMember(Order = 5)]
public string Action { get; set; }
/// <summary>Gets or sets the stable outcome value.</summary>
[DataMember(Order = 6)]
public string Outcome { get; set; }
/// <summary>Gets or sets the local start timestamp.</summary>
[DataMember(Order = 7)]
public string StartedAt { get; set; }
/// <summary>Gets or sets the local completion timestamp.</summary>
[DataMember(Order = 8)]
public string FinishedAt { get; set; }
/// <summary>Gets or sets the complete operator-facing exception chain.</summary>
[DataMember(Order = 9)]
public string Error { get; set; }
}
/// <summary>
/// Durable summary of a best-effort plan execution. A failed step never
/// prevents later independent steps from being attempted.
/// </summary>
[DataContract]
public sealed class OperationExecutionReport
{
/// <summary>Initializes an empty report.</summary>
public OperationExecutionReport()
{
Steps = new List<OperationStepResult>();
}
/// <summary>Gets or sets the shutdown or restore mode.</summary>
[DataMember(Order = 1)]
public string Mode { get; set; }
/// <summary>Gets or sets the target server.</summary>
[DataMember(Order = 2)]
public string Server { get; set; }
/// <summary>Gets or sets whether the execution was a dry-run.</summary>
[DataMember(Order = 3)]
public bool DryRun { get; set; }
/// <summary>Gets or sets the local execution start timestamp.</summary>
[DataMember(Order = 4)]
public string StartedAt { get; set; }
/// <summary>Gets or sets the local execution completion timestamp.</summary>
[DataMember(Order = 5)]
public string FinishedAt { get; set; }
/// <summary>Gets or sets the number of successfully executed steps.</summary>
[DataMember(Order = 6)]
public int SucceededCount { get; set; }
/// <summary>Gets or sets the number of steps already at their target state.</summary>
[DataMember(Order = 7)]
public int AlreadySatisfiedCount { get; set; }
/// <summary>Gets or sets the number of intentionally skipped steps.</summary>
[DataMember(Order = 8)]
public int SkippedCount { get; set; }
/// <summary>Gets or sets the number of dry-run-only steps.</summary>
[DataMember(Order = 9)]
public int DryRunCount { get; set; }
/// <summary>Gets or sets the number of failed steps.</summary>
[DataMember(Order = 10)]
public int FailedCount { get; set; }
/// <summary>Gets or sets an error that prevented runtime initialization.</summary>
[DataMember(Order = 11)]
public string InitializationError { get; set; }
/// <summary>Gets or sets an error encountered while creating the post-operation snapshot.</summary>
[DataMember(Order = 12)]
public string PostSnapshotError { get; set; }
/// <summary>Gets or sets the ordered per-step results.</summary>
[DataMember(Order = 13)]
public List<OperationStepResult> Steps { get; set; }
/// <summary>Gets whether operator attention is required.</summary>
public bool HasFailures
{
get { return FailedCount > 0 || !string.IsNullOrWhiteSpace(InitializationError) || !string.IsNullOrWhiteSpace(PostSnapshotError); }
}
}
}
@@ -1,4 +1,5 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("BizTalk Platform Management Tool")]
@@ -8,5 +9,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: ComVisible(false)]
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
[assembly: AssemblyVersion("2.1.3.0")]
[assembly: AssemblyFileVersion("2.1.3.0")]
[assembly: AssemblyVersion("2.2.0.0")]
[assembly: AssemblyFileVersion("2.2.0.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Linq;
using BizTalkPlatformManagementTool.Models;
using BizTalkPlatformManagementTool.Services;
@@ -35,6 +36,27 @@ namespace BizTalkPlatformManagementTool
SnapshotStore.SaveSnapshotSet(Path.Combine(directory, "before.json"), loaded);
SnapshotStore.SaveDiffSet(Path.Combine(directory, "diff.json"), diff);
var operationService = new BizTalkOperationService(null);
var emergencyPlan = operationService.CreateEmergencyRestorePlan(loaded, loaded.Server);
if (emergencyPlan.Steps.Count == 0 || emergencyPlan.Steps[0].Kind != "WindowsService" || emergencyPlan.Steps[0].Name != "ENTSSO")
{
throw new InvalidOperationException("Emergency restore self-test did not place ENTSSO first.");
}
var dryRunReport = operationService.ExecutePlan(emergencyPlan, new OperationOptions
{
Server = loaded.Server,
OutputDirectory = directory,
StateFile = snapshotPath,
DryRun = true,
WaitTimeoutSeconds = 30,
PollIntervalSeconds = 1
});
if (dryRunReport.HasFailures || dryRunReport.DryRunCount != emergencyPlan.Steps.Count(x => x.Execute))
{
throw new InvalidOperationException("Emergency restore dry-run self-test returned an unexpected result.");
}
operationService.SaveExecutionReport(directory, "emergency-result.json", dryRunReport);
Console.WriteLine("SELF_TEST_OK version=" + BizTalkOperationService.Version);
return 0;
}
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management;
using System.ServiceProcess;
using BizTalkPlatformManagementTool.Models;
namespace BizTalkPlatformManagementTool.Services
@@ -15,7 +16,7 @@ namespace BizTalkPlatformManagementTool.Services
/// <summary>
/// Current tool version written into generated snapshots.
/// </summary>
public const string Version = "2.1.3-net461";
public const string Version = "2.2.0-net461";
/// <summary>
/// Fallback application name used when WMI does not expose an application property.
@@ -193,12 +194,18 @@ namespace BizTalkPlatformManagementTool.Services
{
plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, null));
}
}
foreach (var app in snapshot.Applications)
{
foreach (var item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted))
{
plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Stop orchestration", "MSBTS_Orchestration", "Name", item.Name, "Stop", new[] { 1, 1 }, ArtifactStates.OrchestrationStopped));
}
}
foreach (var app in snapshot.Applications)
{
foreach (var item in app.SendPorts.Where(x => x.Status == ArtifactStates.SendPortStarted))
{
plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Stop send port", "MSBTS_SendPort", "Name", item.Name, "Stop", null, ArtifactStates.SendPortStopped));
@@ -208,7 +215,7 @@ namespace BizTalkPlatformManagementTool.Services
foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted))
{
var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Stop host instance", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Stop", null, ArtifactStates.HostStopped);
if (!string.IsNullOrEmpty(item.Server) && !string.Equals(item.Server, server, StringComparison.OrdinalIgnoreCase))
if (!string.IsNullOrEmpty(item.Server) && !SnapshotValidator.ServerNamesEqual(item.Server, server))
{
step.Execute = false;
step.Warning = "Host instance belongs to server '" + item.Server + "'. Run this action on that server.";
@@ -235,7 +242,7 @@ namespace BizTalkPlatformManagementTool.Services
foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted))
{
var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Start host instance", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Start", null, ArtifactStates.HostStarted);
if (!string.IsNullOrEmpty(item.Server) && !string.Equals(item.Server, server, StringComparison.OrdinalIgnoreCase))
if (!string.IsNullOrEmpty(item.Server) && !SnapshotValidator.ServerNamesEqual(item.Server, server))
{
step.Execute = false;
step.Warning = "Host instance belongs to server '" + item.Server + "'. Run this action on that server.";
@@ -260,7 +267,10 @@ namespace BizTalkPlatformManagementTool.Services
plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Ensure send port is bound", "MSBTS_SendPort", "Name", item.Name, "UnEnlist", null, ArtifactStates.SendPortBound));
}
}
}
foreach (var app in snapshot.Applications)
{
foreach (var item in app.Orchestrations)
{
if (item.OrchestrationStatus == ArtifactStates.OrchestrationStarted)
@@ -290,7 +300,10 @@ namespace BizTalkPlatformManagementTool.Services
plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Unenlist orchestration", "MSBTS_Orchestration", "Name", item.Name, "UnenlistService", new[] { 1 }, ArtifactStates.OrchestrationUnbound));
}
}
}
foreach (var app in snapshot.Applications)
{
foreach (var item in app.ReceiveLocations)
{
plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, item.Enabled ? "Enable receive location" : "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, item.Enabled ? "Enable" : "Disable", null, null));
@@ -300,12 +313,38 @@ namespace BizTalkPlatformManagementTool.Services
return plan;
}
/// <summary>
/// Creates a recovery plan from an existing snapshot without taking or
/// overwriting a new before snapshot. Enterprise SSO is added as the first prerequisite.
/// </summary>
/// <param name="snapshot">The preserved pre-maintenance state.</param>
/// <param name="server">The selected target server.</param>
/// <returns>An ordered, state-aware emergency restore plan.</returns>
public OperationPlan CreateEmergencyRestorePlan(BizTalkSnapshot snapshot, string server)
{
var plan = CreateRestorePlan(snapshot, server);
plan.Mode = OperationMode.EmergencyRestore.ToString();
plan.Steps.Insert(0, Step(
"WindowsService",
string.Empty,
"ENTSSO",
server,
"Ensure Enterprise Single Sign-On service is running",
"Win32_Service",
"Name",
"ENTSSO",
"Start",
null,
(int)ServiceControllerStatus.Running));
return plan;
}
/// <summary>
/// Executes an operation plan or logs each step when dry-run mode is enabled.
/// </summary>
/// <param name="plan">The ordered plan to execute.</param>
/// <param name="options">The runtime options controlling server, dry-run and wait behavior.</param>
public void ExecutePlan(OperationPlan plan, OperationOptions options)
public OperationExecutionReport ExecutePlan(OperationPlan plan, OperationOptions options)
{
if (plan == null || options == null)
{
@@ -315,42 +354,37 @@ namespace BizTalkPlatformManagementTool.Services
{
throw new InvalidOperationException("The operation plan targets server '" + plan.Server + "' but execution was requested for '" + options.Server + "'.");
}
using (var client = CreateClient(options.Server))
var executor = new OperationPlanExecutor(_logger);
if (options.DryRun)
{
foreach (var step in plan.Steps)
// Dry-run benötigt absichtlich weder WMI-Verbindung noch Servicezugriff.
return executor.Execute(plan, options, null);
}
try
{
using (var runtime = new WmiOperationStepRuntime(options.Server, _logger))
{
if (!step.Execute)
{
_logger.Warning(step.Action + (string.IsNullOrEmpty(step.Warning) ? string.Empty : " " + step.Warning));
continue;
}
if (options.DryRun)
{
// Dry-run löst das Objekt absichtlich nicht erneut per WMI auf und führt keine Methode aus.
_logger.Info("DRY RUN: " + step.Action + " '" + step.Name + "'");
continue;
}
try
{
_logger.Info("Executing step: " + DescribeStep(step));
using (var instance = client.FindByProperty(step.WmiClass, step.KeyProperty, step.KeyValue))
{
if (instance == null)
{
throw new InvalidOperationException(step.Kind + " not found: " + step.Name);
}
ExecuteStep(client, instance, step, options);
}
}
catch (Exception ex)
{
throw new InvalidOperationException("Step failed: " + DescribeStep(step) + ". Error: " + ex.Message, ex);
}
return executor.Execute(plan, options, runtime);
}
}
catch (Exception ex)
{
var error = OperationPlanExecutor.FormatException(ex);
if (_logger != null)
{
_logger.Error("Plan runtime initialization failed. No executable step could be started. Error: " + error);
}
return new OperationExecutionReport
{
Mode = plan.Mode,
Server = plan.Server,
DryRun = false,
StartedAt = DateTimeOffset.Now.ToString("o"),
FinishedAt = DateTimeOffset.Now.ToString("o"),
InitializationError = error
};
}
}
/// <summary>
@@ -385,6 +419,25 @@ namespace BizTalkPlatformManagementTool.Services
return path;
}
/// <summary>
/// Saves a complete execution report, including every failure and continued step.
/// </summary>
/// <param name="outputDirectory">The directory where the report is written.</param>
/// <param name="fileName">The report file name.</param>
/// <param name="report">The report to persist.</param>
/// <returns>The saved report path.</returns>
public string SaveExecutionReport(string outputDirectory, string fileName, OperationExecutionReport report)
{
Directory.CreateDirectory(outputDirectory);
var path = Path.Combine(outputDirectory, fileName);
JsonFileStore.Save(path, report);
if (_logger != null)
{
_logger.Success("Saved execution report: " + path);
}
return path;
}
/// <summary>
/// Saves a diff and its report sidecars.
/// </summary>
@@ -402,79 +455,221 @@ namespace BizTalkPlatformManagementTool.Services
}
/// <summary>
/// Executes one concrete WMI operation step and waits for its target state.
/// Implements state-aware Windows-service and BizTalk-WMI execution for a plan step.
/// </summary>
/// <param name="client">The connected WMI client.</param>
/// <param name="instance">The resolved WMI object for the step.</param>
/// <param name="step">The operation step to execute.</param>
/// <param name="options">The runtime options controlling wait behavior.</param>
private void ExecuteStep(BizTalkWmiClient client, ManagementObject instance, OperationStep step, OperationOptions options)
private sealed class WmiOperationStepRuntime : IOperationStepRuntime, IDisposable
{
if (string.Equals(step.MethodName, "StopOrEnlist", StringComparison.OrdinalIgnoreCase))
/// <summary>Target server used for service and WMI operations.</summary>
private readonly string _server;
/// <summary>Operation logger.</summary>
private readonly OperationLogger _logger;
/// <summary>Lazily connected BizTalk WMI client.</summary>
private BizTalkWmiClient _client;
/// <summary>Initializes the production runtime adapter.</summary>
/// <param name="server">The target server.</param>
/// <param name="logger">The operation logger.</param>
public WmiOperationStepRuntime(string server, OperationLogger logger)
{
var status = BizTalkWmiClient.SafeGetInt32(instance, "Status", 0);
if (status == ArtifactStates.SendPortBound)
_server = string.IsNullOrWhiteSpace(server) ? Environment.MachineName : server.Trim();
_logger = logger;
}
/// <summary>Executes one Windows-service or BizTalk-WMI step state-aware.</summary>
/// <param name="step">The operation step.</param>
/// <param name="options">Timeout and polling options.</param>
/// <returns>The successful runtime outcome.</returns>
public RuntimeStepOutcome Execute(OperationStep step, OperationOptions options)
{
if (string.Equals(step.Kind, "WindowsService", StringComparison.OrdinalIgnoreCase))
{
client.InvokeMethod(instance, "Enlist");
return ExecuteWindowsService(step, options);
}
else if (status == ArtifactStates.SendPortStarted)
var client = EnsureClient();
using (var instance = client.FindByProperty(step.WmiClass, step.KeyProperty, step.KeyValue))
{
client.InvokeMethod(instance, "Stop");
if (instance == null)
{
throw new InvalidOperationException(step.Kind + " not found: " + step.Name);
}
if (IsTargetStateReached(instance, step))
{
return RuntimeStepOutcome.AlreadySatisfied;
}
ExecuteWmiStep(instance, step, options);
return RuntimeStepOutcome.Succeeded;
}
}
/// <summary>
/// Releases the lazily created WMI client.
/// </summary>
public void Dispose()
{
if (_client != null)
{
_client.Dispose();
_client = null;
}
}
/// <summary>
/// Connects to BizTalk WMI only when the first BizTalk step is reached,
/// allowing ENTSSO to start before any provider dependency is evaluated.
/// </summary>
/// <returns>The connected client.</returns>
private BizTalkWmiClient EnsureClient()
{
if (_client != null)
{
return _client;
}
var client = new BizTalkWmiClient(_server, _logger);
try
{
client.Connect();
_client = client;
return _client;
}
catch
{
client.Dispose();
throw;
}
}
/// <summary>Starts or resumes a Windows-service prerequisite and waits for Running.</summary>
private RuntimeStepOutcome ExecuteWindowsService(OperationStep step, OperationOptions options)
{
var targetServer = string.IsNullOrWhiteSpace(step.Server) ? _server : step.Server;
using (var service = new ServiceController(step.Name, targetServer))
{
service.Refresh();
if (service.Status == ServiceControllerStatus.Running)
{
return RuntimeStepOutcome.AlreadySatisfied;
}
if (service.Status == ServiceControllerStatus.StartPending
|| service.Status == ServiceControllerStatus.ContinuePending)
{
service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(Math.Max(1, options.WaitTimeoutSeconds)));
}
else if (service.Status == ServiceControllerStatus.PausePending)
{
service.WaitForStatus(ServiceControllerStatus.Paused, TimeSpan.FromSeconds(Math.Max(1, options.WaitTimeoutSeconds)));
service.Refresh();
_logger.Info("Continuing Windows service '" + step.Name + "' on " + targetServer + ".");
service.Continue();
service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(Math.Max(1, options.WaitTimeoutSeconds)));
}
else if (service.Status == ServiceControllerStatus.Paused)
{
_logger.Info("Continuing Windows service '" + step.Name + "' on " + targetServer + ".");
service.Continue();
service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(Math.Max(1, options.WaitTimeoutSeconds)));
}
else
{
if (service.Status == ServiceControllerStatus.StopPending)
{
service.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(Math.Max(1, options.WaitTimeoutSeconds)));
service.Refresh();
}
_logger.Info("Starting Windows service '" + step.Name + "' on " + targetServer + ".");
service.Start();
service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(Math.Max(1, options.WaitTimeoutSeconds)));
}
_logger.Success("Reached: Windows service '" + step.Name + "' is Running.");
return RuntimeStepOutcome.Succeeded;
}
}
/// <summary>Executes one BizTalk WMI method or pseudo-method and verifies the target state.</summary>
private void ExecuteWmiStep(ManagementObject instance, OperationStep step, OperationOptions options)
{
if (string.Equals(step.MethodName, "StopOrEnlist", StringComparison.OrdinalIgnoreCase))
{
var status = BizTalkWmiClient.SafeGetInt32(instance, "Status", 0);
if (status == ArtifactStates.SendPortBound)
{
_client.InvokeMethod(instance, "Enlist");
}
else if (status == ArtifactStates.SendPortStarted)
{
_client.InvokeMethod(instance, "Stop");
}
}
else if (string.Equals(step.MethodName, "StopIfStarted", StringComparison.OrdinalIgnoreCase))
{
if (BizTalkWmiClient.SafeGetInt32(instance, "OrchestrationStatus", 0) == ArtifactStates.OrchestrationStarted)
{
_client.InvokeMethod(instance, "Stop", ToObjects(step.Arguments));
}
}
else
{
_logger.Info("Already stopped: " + step.Name);
_client.InvokeMethod(instance, step.MethodName, ToObjects(step.Arguments));
}
}
else if (string.Equals(step.MethodName, "StopIfStarted", StringComparison.OrdinalIgnoreCase))
{
var status = BizTalkWmiClient.SafeGetInt32(instance, "OrchestrationStatus", 0);
if (status == ArtifactStates.OrchestrationStarted)
{
client.InvokeMethod(instance, "Stop", ToObjects(step.Arguments));
}
else
{
_logger.Info("No stop required: " + step.Name);
}
}
else
{
client.InvokeMethod(instance, step.MethodName, ToObjects(step.Arguments));
WaitForTarget(step, options);
}
WaitForTarget(client, step, options);
}
/// <summary>
/// Waits until a WMI object reaches the target state described by a plan step.
/// </summary>
/// <param name="client">The connected WMI client.</param>
/// <param name="step">The step whose target state should be verified.</param>
/// <param name="options">The runtime options controlling timeout and polling interval.</param>
private void WaitForTarget(BizTalkWmiClient client, OperationStep step, OperationOptions options)
{
if (!step.TargetState.HasValue)
/// <summary>Checks whether a live WMI object already matches the requested target state.</summary>
private static bool IsTargetStateReached(ManagementObject instance, OperationStep step)
{
if (step.Kind == "ReceiveLocation")
if (string.Equals(step.Kind, "ReceiveLocation", StringComparison.OrdinalIgnoreCase))
{
var shouldBeEnabled = string.Equals(step.MethodName, "Enable", StringComparison.OrdinalIgnoreCase);
client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => !BizTalkWmiClient.SafeGetBoolean(o, "IsDisabled", true) == shouldBeEnabled, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
var enabled = !BizTalkWmiClient.SafeGetBoolean(instance, "IsDisabled", true);
return enabled == string.Equals(step.MethodName, "Enable", StringComparison.OrdinalIgnoreCase);
}
return;
if (!step.TargetState.HasValue)
{
return false;
}
if (string.Equals(step.Kind, "SendPort", StringComparison.OrdinalIgnoreCase))
{
return BizTalkWmiClient.SafeGetInt32(instance, "Status", 0) == step.TargetState.Value;
}
if (string.Equals(step.Kind, "Orchestration", StringComparison.OrdinalIgnoreCase))
{
return BizTalkWmiClient.SafeGetInt32(instance, "OrchestrationStatus", 0) == step.TargetState.Value;
}
if (string.Equals(step.Kind, "HostInstance", StringComparison.OrdinalIgnoreCase))
{
return BizTalkWmiClient.SafeGetInt32(instance, "ServiceState", 0) == step.TargetState.Value;
}
return false;
}
if (step.Kind == "SendPort")
/// <summary>Waits for the WMI target state after a mutation.</summary>
private void WaitForTarget(OperationStep step, OperationOptions options)
{
client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "Status", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
}
else if (step.Kind == "Orchestration")
{
client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "OrchestrationStatus", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
}
else if (step.Kind == "HostInstance")
{
client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "ServiceState", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
if (!step.TargetState.HasValue)
{
if (step.Kind == "ReceiveLocation")
{
var shouldBeEnabled = string.Equals(step.MethodName, "Enable", StringComparison.OrdinalIgnoreCase);
_client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => !BizTalkWmiClient.SafeGetBoolean(o, "IsDisabled", true) == shouldBeEnabled, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
}
return;
}
if (step.Kind == "SendPort")
{
_client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "Status", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
}
else if (step.Kind == "Orchestration")
{
_client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "OrchestrationStatus", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
}
else if (step.Kind == "HostInstance")
{
_client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "ServiceState", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds);
}
}
}
@@ -0,0 +1,220 @@
using System;
using BizTalkPlatformManagementTool.Models;
namespace BizTalkPlatformManagementTool.Services
{
/// <summary>
/// Defines the successful outcomes returned by the runtime-specific execution layer.
/// </summary>
internal enum RuntimeStepOutcome
{
/// <summary>The requested method was executed and verified.</summary>
Succeeded,
/// <summary>The object was already in the requested target state.</summary>
AlreadySatisfied
}
/// <summary>
/// Abstracts one state-aware runtime step so best-effort orchestration can be
/// regression-tested without a BizTalk WMI provider.
/// </summary>
internal interface IOperationStepRuntime
{
/// <summary>
/// Executes one step or reports that its target state already exists.
/// </summary>
/// <param name="step">The operation-plan step.</param>
/// <param name="options">Timeout and polling options.</param>
/// <returns>The successful runtime outcome.</returns>
RuntimeStepOutcome Execute(OperationStep step, OperationOptions options);
}
/// <summary>
/// Runs every independent plan step, records durable outcomes and deliberately
/// continues after isolated failures.
/// </summary>
internal sealed class OperationPlanExecutor
{
/// <summary>Logger used for detailed progress and failure diagnostics.</summary>
private readonly OperationLogger _logger;
/// <summary>
/// Initializes a new best-effort plan executor.
/// </summary>
/// <param name="logger">The operation logger, or null for isolated tests.</param>
public OperationPlanExecutor(OperationLogger logger)
{
_logger = logger;
}
/// <summary>
/// Executes or simulates every plan step and returns a complete report.
/// </summary>
/// <param name="plan">The validated operation plan.</param>
/// <param name="options">The execution options.</param>
/// <param name="runtime">The runtime implementation; optional only during dry-run.</param>
/// <returns>A complete per-step execution report.</returns>
public OperationExecutionReport Execute(OperationPlan plan, OperationOptions options, IOperationStepRuntime runtime)
{
if (plan == null || options == null)
{
throw new ArgumentNullException(plan == null ? "plan" : "options");
}
if (!options.DryRun && runtime == null)
{
throw new ArgumentNullException("runtime");
}
var report = NewReport(plan, options);
for (var index = 0; index < plan.Steps.Count; index++)
{
var step = plan.Steps[index];
var stepResult = NewStepResult(index + 1, step);
try
{
if (!step.Execute)
{
stepResult.Outcome = OperationStepOutcomes.Skipped;
report.SkippedCount++;
Warning(step.Action + (string.IsNullOrWhiteSpace(step.Warning) ? string.Empty : " " + step.Warning));
}
else if (options.DryRun)
{
stepResult.Outcome = OperationStepOutcomes.DryRun;
report.DryRunCount++;
Info("DRY RUN: " + DescribeStep(step));
}
else
{
Info("Executing step " + (index + 1) + "/" + plan.Steps.Count + ": " + DescribeStep(step));
var outcome = runtime.Execute(step, options);
if (outcome == RuntimeStepOutcome.AlreadySatisfied)
{
stepResult.Outcome = OperationStepOutcomes.AlreadySatisfied;
report.AlreadySatisfiedCount++;
Info("Already in target state: " + DescribeStep(step));
}
else
{
stepResult.Outcome = OperationStepOutcomes.Succeeded;
report.SucceededCount++;
}
}
}
catch (Exception ex)
{
stepResult.Outcome = OperationStepOutcomes.Failed;
stepResult.Error = FormatException(ex);
report.FailedCount++;
Error("STEP FAILED; continuing with remaining independent steps: " + DescribeStep(step) + ". Error: " + stepResult.Error);
}
finally
{
stepResult.FinishedAt = DateTimeOffset.Now.ToString("o");
report.Steps.Add(stepResult);
}
}
report.FinishedAt = DateTimeOffset.Now.ToString("o");
var summary = "Plan execution summary: succeeded=" + report.SucceededCount
+ ", already_satisfied=" + report.AlreadySatisfiedCount
+ ", skipped=" + report.SkippedCount
+ ", dry_run=" + report.DryRunCount
+ ", failed=" + report.FailedCount + ".";
if (report.FailedCount == 0)
{
Success(summary);
}
else
{
Error(summary + " Operator review is required.");
}
return report;
}
/// <summary>Creates the common execution-report header.</summary>
/// <param name="plan">The source plan.</param>
/// <param name="options">The execution options.</param>
/// <returns>An initialized report.</returns>
private static OperationExecutionReport NewReport(OperationPlan plan, OperationOptions options)
{
return new OperationExecutionReport
{
Mode = plan.Mode,
Server = plan.Server,
DryRun = options.DryRun,
StartedAt = DateTimeOffset.Now.ToString("o")
};
}
/// <summary>Creates one report row from a plan step.</summary>
/// <param name="index">The one-based step index.</param>
/// <param name="step">The source step.</param>
/// <returns>An initialized step result.</returns>
private static OperationStepResult NewStepResult(int index, OperationStep step)
{
return new OperationStepResult
{
Index = index,
Kind = step == null ? string.Empty : step.Kind,
Application = step == null ? string.Empty : step.Application,
Name = step == null ? string.Empty : step.Name,
Action = step == null ? string.Empty : step.Action,
StartedAt = DateTimeOffset.Now.ToString("o")
};
}
/// <summary>Builds a stable operator-facing step description.</summary>
/// <param name="step">The plan step.</param>
/// <returns>The compact description.</returns>
private static string DescribeStep(OperationStep step)
{
if (step == null)
{
return "<unknown step>";
}
return step.Action + " '" + step.Name + "'"
+ " [kind=" + step.Kind
+ ", class=" + step.WmiClass
+ ", key=" + step.KeyProperty + "=" + (step.KeyValue ?? string.Empty)
+ ", method=" + step.MethodName
+ (string.IsNullOrWhiteSpace(step.Server) ? string.Empty : ", server=" + step.Server)
+ "]";
}
/// <summary>Formats the complete unique exception-message chain.</summary>
/// <param name="exception">The exception to format.</param>
/// <returns>A single diagnostic message.</returns>
internal static string FormatException(Exception exception)
{
if (exception == null)
{
return "Unknown operation error.";
}
var message = exception.Message;
var inner = exception.InnerException;
while (inner != null)
{
if (!string.IsNullOrWhiteSpace(inner.Message) && message.IndexOf(inner.Message, StringComparison.OrdinalIgnoreCase) < 0)
{
message += " Inner error: " + inner.Message;
}
inner = inner.InnerException;
}
return message;
}
/// <summary>Writes an informational message when a logger is available.</summary>
private void Info(string message) { if (_logger != null) _logger.Info(message); }
/// <summary>Writes a warning when a logger is available.</summary>
private void Warning(string message) { if (_logger != null) _logger.Warning(message); }
/// <summary>Writes an error when a logger is available.</summary>
private void Error(string message) { if (_logger != null) _logger.Error(message); }
/// <summary>Writes a success message when a logger is available.</summary>
private void Success(string message) { if (_logger != null) _logger.Success(message); }
}
}
+160 -17
View File
@@ -110,6 +110,11 @@ namespace BizTalkPlatformManagementTool.Ui
/// </summary>
private Button _restoreButton;
/// <summary>
/// Button that performs a state-aware best-effort recovery from an existing snapshot.
/// </summary>
private Button _emergencyRestoreButton;
/// <summary>
/// Button that clears visible grids and status.
/// </summary>
@@ -132,7 +137,7 @@ namespace BizTalkPlatformManagementTool.Ui
{
Text = "BizTalk Platform Management Tool";
Width = 1180;
Height = 760;
Height = 800;
MinimumSize = new Size(980, 640);
StartPosition = FormStartPosition.CenterScreen;
@@ -156,7 +161,7 @@ namespace BizTalkPlatformManagementTool.Ui
Padding = new Padding(10)
};
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 92));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 54));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 96));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 24));
Controls.Add(root);
@@ -248,13 +253,15 @@ namespace BizTalkPlatformManagementTool.Ui
/// <returns>The configured action panel.</returns>
private Control BuildActionPanel()
{
var panel = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.LeftToRight, Padding = new Padding(0, 8, 0, 0), WrapContents = false };
var panel = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.LeftToRight, Padding = new Padding(0, 8, 0, 0), WrapContents = true, AutoScroll = true };
_diagnoseButton = ActionButton("Diagnose", DiagnoseClick);
_beforeButton = ActionButton("Snapshot Before", BeforeClick);
_afterButton = ActionButton("Snapshot After", AfterClick);
_compareButton = ActionButton("Compare", CompareClick);
_shutdownButton = ActionButton("Shutdown", ShutdownClick);
_restoreButton = ActionButton("Restore", RestoreClick);
_emergencyRestoreButton = ActionButton("Emergency Restore", EmergencyRestoreClick);
_emergencyRestoreButton.Width = 142;
_clearButton = ActionButton("Clear", ClearClick);
_closeButton = ActionButton("Close", CloseClick);
@@ -264,6 +271,7 @@ namespace BizTalkPlatformManagementTool.Ui
panel.Controls.Add(_compareButton);
panel.Controls.Add(_shutdownButton);
panel.Controls.Add(_restoreButton);
panel.Controls.Add(_emergencyRestoreButton);
panel.Controls.Add(_clearButton);
panel.Controls.Add(_closeButton);
return panel;
@@ -381,13 +389,11 @@ namespace BizTalkPlatformManagementTool.Ui
_logger.Warning("Shutdown cancelled after plan review. No runtime state was changed.");
return;
}
_service.ExecutePlan(plan, options);
if (!options.DryRun)
{
var after = _service.CreateSnapshot(options.Server);
_service.SaveSnapshot(options.OutputDirectory, "shutdown-after.json", after);
ShowSnapshot(after);
}
var report = _service.ExecutePlan(plan, options);
CapturePostOperationSnapshot(options, report, "shutdown-after.json");
var reportPath = _service.SaveExecutionReport(options.OutputDirectory, "shutdown-result.json", report);
ShowExecutionReport(report);
ThrowWhenOperatorReviewIsRequired(report, reportPath);
});
}
@@ -411,16 +417,91 @@ namespace BizTalkPlatformManagementTool.Ui
_logger.Warning("Restore cancelled after plan review. No runtime state was changed.");
return;
}
_service.ExecutePlan(plan, options);
if (!options.DryRun)
{
var after = _service.CreateSnapshot(options.Server);
_service.SaveSnapshot(options.OutputDirectory, "restore-after.json", after);
ShowSnapshot(after);
}
var report = _service.ExecutePlan(plan, options);
CapturePostOperationSnapshot(options, report, "restore-after.json");
var reportPath = _service.SaveExecutionReport(options.OutputDirectory, "restore-result.json", report);
ShowExecutionReport(report);
ThrowWhenOperatorReviewIsRequired(report, reportPath);
});
}
/// <summary>
/// Handles emergency recovery from the preserved state file without creating
/// or overwriting <c>before.json</c>.
/// </summary>
/// <param name="sender">The control that raised the event.</param>
/// <param name="e">The event arguments.</param>
private void EmergencyRestoreClick(object sender, EventArgs e)
{
var options = GetOptions();
RunAsync("Preparing emergency restore from preserved state...", () =>
{
var sourcePath = ResolveStateFile(options);
var snapshot = JsonFileStore.Load<BizTalkSnapshot>(sourcePath);
SnapshotValidator.EnsureServerMatches(snapshot, options.Server);
var stamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");
var sourceCopyName = "emergency-source-before-" + stamp + ".json";
_service.SaveSnapshot(options.OutputDirectory, sourceCopyName, snapshot);
var plan = _service.CreateEmergencyRestorePlan(snapshot, options.Server);
var planName = "emergency-restore-plan-" + stamp + ".json";
var planPath = _service.SavePlan(options.OutputDirectory, planName, plan);
ShowPlan(plan);
if (!options.DryRun && !ConfirmEmergencyRestore(plan, options.Server, sourcePath, planPath))
{
_logger.Warning("Emergency restore cancelled after plan review. No runtime state was changed.");
return;
}
var report = _service.ExecutePlan(plan, options);
CapturePostOperationSnapshot(options, report, "emergency-restore-after-" + stamp + ".json");
var reportPath = _service.SaveExecutionReport(options.OutputDirectory, "emergency-restore-result-" + stamp + ".json", report);
ShowExecutionReport(report);
ThrowWhenOperatorReviewIsRequired(report, reportPath);
});
}
/// <summary>
/// Attempts the post-operation snapshot even after individual plan-step failures.
/// </summary>
/// <param name="options">The operation options.</param>
/// <param name="report">The report that receives a snapshot error.</param>
/// <param name="fileName">The snapshot file name.</param>
private void CapturePostOperationSnapshot(OperationOptions options, OperationExecutionReport report, string fileName)
{
if (options.DryRun)
{
return;
}
try
{
var after = _service.CreateSnapshot(options.Server);
_service.SaveSnapshot(options.OutputDirectory, fileName, after);
ShowSnapshot(after);
}
catch (Exception ex)
{
report.PostSnapshotError = FormatException(ex);
_logger.Error("Post-operation snapshot failed, but the execution report will still be saved. Error: " + report.PostSnapshotError);
}
}
/// <summary>
/// Converts a completed partial failure into the visible failed UI status only
/// after its complete execution report has been saved.
/// </summary>
/// <param name="report">The completed report.</param>
/// <param name="reportPath">The durable report path.</param>
private static void ThrowWhenOperatorReviewIsRequired(OperationExecutionReport report, string reportPath)
{
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);
}
}
/// <summary>
/// Handles the Clear button click by removing visible state without deleting files.
/// </summary>
@@ -536,6 +617,43 @@ namespace BizTalkPlatformManagementTool.Ui
return confirmed;
}
/// <summary>
/// Confirms the stronger emergency-recovery contract, including the preserved
/// source snapshot and automatic Enterprise SSO prerequisite.
/// </summary>
/// <param name="plan">The prepared emergency plan.</param>
/// <param name="server">The selected target server.</param>
/// <param name="sourcePath">The preserved source snapshot.</param>
/// <param name="planPath">The saved emergency plan.</param>
/// <returns>True when the operator explicitly approves the recovery.</returns>
private bool ConfirmEmergencyRestore(OperationPlan plan, string server, string sourcePath, string planPath)
{
var confirmed = false;
Action showConfirmation = () =>
{
var executableSteps = plan.Steps.Count(x => x.Execute);
var result = MessageBox.Show(
"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"
+ "Saved plan:\n" + planPath + "\n\nContinue now?",
"Confirm Emergency Restore",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
confirmed = result == DialogResult.Yes;
};
if (InvokeRequired)
{
Invoke(showConfirmation);
}
else
{
showConfirmation();
}
return confirmed;
}
/// <summary>
/// Displays a snapshot in the status grid and updates the environment indicator.
/// </summary>
@@ -604,6 +722,30 @@ namespace BizTalkPlatformManagementTool.Ui
});
}
/// <summary>
/// Displays the durable per-step outcomes after best-effort execution.
/// </summary>
/// <param name="report">The execution report to display.</param>
private void ShowExecutionReport(OperationExecutionReport report)
{
InvokeIfRequired(() =>
{
_statusGrid.Rows.Clear();
foreach (var item in report.Steps)
{
_statusGrid.Rows.Add(item.Kind, item.Application, item.Name, item.Outcome, item.Error ?? item.Action);
}
if (!string.IsNullOrWhiteSpace(report.InitializationError))
{
_statusGrid.Rows.Add("Runtime", string.Empty, report.Server, "Failed", report.InitializationError);
}
if (!string.IsNullOrWhiteSpace(report.PostSnapshotError))
{
_statusGrid.Rows.Add("Snapshot", string.Empty, report.Server, "Failed", report.PostSnapshotError);
}
});
}
/// <summary>
/// Appends one operation log entry to the log grid.
/// </summary>
@@ -646,6 +788,7 @@ namespace BizTalkPlatformManagementTool.Ui
_compareButton.Enabled = !busy;
_shutdownButton.Enabled = !busy;
_restoreButton.Enabled = !busy;
_emergencyRestoreButton.Enabled = !busy;
_clearButton.Enabled = !busy;
_closeButton.Enabled = !busy;
_statusLabel.Text = status;
@@ -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.1.3.0" name="BizTalkPlatformManagementTool" />
<assemblyIdentity version="2.2.0.0" name="BizTalkPlatformManagementTool" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>