0ffd4bdbea
Comment/XML-doc changes only, no code logic: - strip project-tracking references (WP*, P*-*, row-NN, #NN, Task N) from comments, keeping the explanatory rationale - add missing summaries, <param>, <returns>, <typeparam> and <inheritdoc/> - replace misused <inheritdoc/> with real docs; where <inheritdoc/> was combined with useful remarks, move the rationale into // comments - drop stale replicationActor <param> on DeploymentManagerActor ctor Remaining 5 are false positives (CWE-1236 x2, failures-1, year-0001, M365). Build 0 errors / 0 warnings.
232 lines
13 KiB
C#
232 lines
13 KiB
C#
using System.CommandLine;
|
|
using System.CommandLine.Parsing;
|
|
using System.Text.Json;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
|
|
|
/// <summary>
|
|
/// The <c>flow-debug</c> command group (Flow Debugger design 2026-09-05 §7.1; script entry
|
|
/// point 2026-09-08 §6): a Live run of an inbound API method, a site script, or a
|
|
/// value-change trigger with every routed site run traced, and a Replay of a past execution
|
|
/// chain from the central audit store. The three run commands are Deployer-only commands
|
|
/// answered by the node the CLI is pointed at; a standby central refuses them with
|
|
/// <c>STANDBY_NODE</c>. Replay is served from any node and accepts a wider role set.
|
|
/// </summary>
|
|
public static class FlowDebugCommands
|
|
{
|
|
/// <summary>
|
|
/// Slack the CLI's HTTP wait adds over the server's own bound (arch-review F4): the CLI
|
|
/// process is one more hop out from the management Ask than the Central UI's in-process
|
|
/// call, so it carries a larger fixed margin than <c>FlowDebugService.ClientSlack</c> (15s).
|
|
/// </summary>
|
|
private const int ClientSlackSeconds = 25;
|
|
|
|
/// <summary>Builds the <c>flow-debug</c> command with its subcommands using the given shared CLI options.</summary>
|
|
/// <param name="urlOption">Shared management URL option.</param>
|
|
/// <param name="formatOption">Shared output format option.</param>
|
|
/// <param name="usernameOption">Shared username option for authentication.</param>
|
|
/// <param name="passwordOption">Shared password option for authentication.</param>
|
|
/// <returns>The configured <c>flow-debug</c> command.</returns>
|
|
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var command = new Command("flow-debug") { Description = "Flow Debugger: trace an inbound API method, a site script, or a value-change flow end to end, or replay a past one" };
|
|
|
|
command.Add(BuildRunMethod(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildRunScript(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildRunValueChange(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildReplay(urlOption, formatOption, usernameOption, passwordOption));
|
|
|
|
return command;
|
|
}
|
|
|
|
private static Command BuildRunMethod(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var methodIdOption = new Option<int>("--method-id") { Description = "Inbound API method ID", Required = true };
|
|
var paramsOption = new Option<string?>("--params") { Description = "Flat JSON object of method parameters (validated against the method's schema)" };
|
|
var timeoutOption = new Option<int>("--timeout-seconds")
|
|
{
|
|
Description = "Client-side wait for the run; set to the method's TimeoutSeconds + 25 for long methods. "
|
|
+ "The server caps long-running commands at its own ManagementService:LongRunningCommandTimeout "
|
|
+ "(default 5 min), so a larger value still 504s at that ceiling",
|
|
DefaultValueFactory = _ => 120,
|
|
};
|
|
var showLogsOption = new Option<bool>("--show-logs") { Description = "Print each step's ScriptLog lines and exception detail as indented lines under its row (table output only)" };
|
|
var cmd = new Command("run-method") { Description = "Run an inbound API method on the active central node with full tracing and print the flow's steps" };
|
|
cmd.Add(methodIdOption);
|
|
cmd.Add(paramsOption);
|
|
cmd.Add(timeoutOption);
|
|
cmd.Add(showLogsOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
var timeoutSeconds = result.GetValue(timeoutOption);
|
|
var showLogs = result.GetValue(showLogsOption);
|
|
if (timeoutSeconds <= 0)
|
|
{
|
|
OutputFormatter.WriteError("--timeout-seconds must be greater than zero.", "INVALID_ARGUMENT");
|
|
return 1;
|
|
}
|
|
|
|
JsonElement? parameters;
|
|
try
|
|
{
|
|
parameters = ParseParameters(result.GetValue(paramsOption));
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
OutputFormatter.WriteError($"--params is not valid JSON: {ex.Message}", "INVALID_PARAMS");
|
|
return 1;
|
|
}
|
|
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
new FlowDebugRunMethodCommand(result.GetValue(methodIdOption), parameters),
|
|
timeout: TimeSpan.FromSeconds(timeoutSeconds),
|
|
tableProjector: json => FlowDebugTableProjection.ProjectEvents(json, showLogs));
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
private static Command BuildRunScript(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var instanceIdOption = new Option<int>("--instance-id") { Description = "Instance ID", Required = true };
|
|
var scriptOption = new Option<string>("--script")
|
|
{
|
|
Description = "Canonical script name, as shown by the Flow Debugger picker / the flattened configuration (e.g. Manual or Module.Script)",
|
|
Required = true,
|
|
};
|
|
var paramsOption = new Option<string?>("--params") { Description = "Flat JSON object of parameters for a Call/Interval/Expression/no-trigger script (validated against the script's ParameterDefinitions)" };
|
|
var valueOption = new Option<string?>("--value") { Description = "Value to write to the trigger attribute of a ValueChange/Conditional script (canonical string form)" };
|
|
var waitOption = new Option<int>("--wait-seconds") { Description = "Write kinds: how long to wait for the triggered run before NO_TRIGGER_WITHIN_WINDOW", DefaultValueFactory = _ => 45 };
|
|
var timeoutOption = new Option<int>("--timeout-seconds")
|
|
{
|
|
Description = "Client-side wait for the run; raise above the script's ExecutionTimeoutSeconds + 25 for a long script. "
|
|
+ "The server caps long-running commands at its own ManagementService:LongRunningCommandTimeout "
|
|
+ "(default 5 min), so a larger value still 504s at that ceiling",
|
|
DefaultValueFactory = _ => 120,
|
|
};
|
|
var showLogsOption = new Option<bool>("--show-logs") { Description = "Print each step's ScriptLog lines and exception detail as indented lines under its row (table output only)" };
|
|
var cmd = new Command("run-script") { Description = "Run one site script with tracing — a ValueChange/Conditional script is fired by writing its trigger attribute (--value); any other script is called directly (--params)" };
|
|
cmd.Add(instanceIdOption);
|
|
cmd.Add(scriptOption);
|
|
cmd.Add(paramsOption);
|
|
cmd.Add(valueOption);
|
|
cmd.Add(waitOption);
|
|
cmd.Add(timeoutOption);
|
|
cmd.Add(showLogsOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
var waitSeconds = result.GetValue(waitOption);
|
|
var timeoutSeconds = result.GetValue(timeoutOption);
|
|
var showLogs = result.GetValue(showLogsOption);
|
|
if (waitSeconds <= 0)
|
|
{
|
|
OutputFormatter.WriteError("--wait-seconds must be greater than zero.", "INVALID_ARGUMENT");
|
|
return 1;
|
|
}
|
|
|
|
if (timeoutSeconds <= 0)
|
|
{
|
|
OutputFormatter.WriteError("--timeout-seconds must be greater than zero.", "INVALID_ARGUMENT");
|
|
return 1;
|
|
}
|
|
|
|
JsonElement? parameters;
|
|
try
|
|
{
|
|
parameters = ParseParameters(result.GetValue(paramsOption));
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
OutputFormatter.WriteError($"--params is not valid JSON: {ex.Message}", "INVALID_PARAMS");
|
|
return 1;
|
|
}
|
|
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
new FlowDebugRunScriptCommand(
|
|
result.GetValue(instanceIdOption),
|
|
result.GetValue(scriptOption)!,
|
|
parameters,
|
|
result.GetValue(valueOption),
|
|
waitSeconds),
|
|
timeout: TimeSpan.FromSeconds(Math.Max(timeoutSeconds, waitSeconds + ClientSlackSeconds)),
|
|
tableProjector: json => FlowDebugTableProjection.ProjectEvents(json, showLogs));
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
private static Command BuildRunValueChange(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var instanceIdOption = new Option<int>("--instance-id") { Description = "Instance ID", Required = true };
|
|
var attributeOption = new Option<string>("--attribute") { Description = "Canonical attribute name to write", Required = true };
|
|
var valueOption = new Option<string>("--value") { Description = "Value to write (canonical string form)", Required = true };
|
|
var waitOption = new Option<int>("--wait-seconds") { Description = "How long to wait for a triggered script run before reporting NO_TRIGGER_WITHIN_WINDOW", DefaultValueFactory = _ => 45 };
|
|
var showLogsOption = new Option<bool>("--show-logs") { Description = "Print each step's ScriptLog lines and exception detail as indented lines under its row (table output only)" };
|
|
var cmd = new Command("run-value-change") { Description = "Write an attribute through the traced path and capture the script run(s) the change triggers — the attribute-level path (also fires Expression triggers); prefer run-script to run a named script" };
|
|
cmd.Add(instanceIdOption);
|
|
cmd.Add(attributeOption);
|
|
cmd.Add(valueOption);
|
|
cmd.Add(waitOption);
|
|
cmd.Add(showLogsOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
var waitSeconds = result.GetValue(waitOption);
|
|
var showLogs = result.GetValue(showLogsOption);
|
|
if (waitSeconds <= 0)
|
|
{
|
|
OutputFormatter.WriteError("--wait-seconds must be greater than zero.", "INVALID_ARGUMENT");
|
|
return 1;
|
|
}
|
|
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
new FlowDebugRunValueChangeCommand(
|
|
result.GetValue(instanceIdOption),
|
|
result.GetValue(attributeOption)!,
|
|
result.GetValue(valueOption)!,
|
|
waitSeconds),
|
|
timeout: TimeSpan.FromSeconds(waitSeconds + ClientSlackSeconds),
|
|
tableProjector: json => FlowDebugTableProjection.ProjectEvents(json, showLogs));
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
private static Command BuildReplay(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var executionIdOption = new Option<Guid>("--execution-id") { Description = "Any execution ID in the chain (from an audit row)", Required = true };
|
|
var showLogsOption = new Option<bool>("--show-logs") { Description = "Print each step's ScriptLog lines and exception detail as indented lines under its row (table output only)" };
|
|
var cmd = new Command("replay") { Description = "Replay a past flow from the central audit store" };
|
|
cmd.Add(executionIdOption);
|
|
cmd.Add(showLogsOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
var showLogs = result.GetValue(showLogsOption);
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
new FlowDebugReplayCommand(result.GetValue(executionIdOption)),
|
|
tableProjector: json => FlowDebugTableProjection.ProjectEvents(json, showLogs));
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the <c>--params</c> value into a detached <see cref="JsonElement"/>; null/blank
|
|
/// means "no parameters". Throws <see cref="JsonException"/> on malformed input or a
|
|
/// non-object root (the inbound API body is always a flat JSON object).
|
|
/// </summary>
|
|
/// <param name="json">The raw <c>--params</c> JSON text, or null/blank for none.</param>
|
|
/// <returns>The parsed root object, or null when no parameters were supplied.</returns>
|
|
internal static JsonElement? ParseParameters(string? json)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
return null;
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
if (doc.RootElement.ValueKind != JsonValueKind.Object)
|
|
throw new JsonException("expected a JSON object at the root");
|
|
|
|
return doc.RootElement.Clone();
|
|
}
|
|
}
|