feat: add CLI debug snapshot command for one-shot instance state inspection
Adds `debug snapshot --id <int>` to query a running instance's current attribute values and alarm states without the subscribe/stream overhead of the debug view. Routes through ManagementActor → CommunicationService → site DeploymentManager → InstanceActor using the existing remote query pattern.
This commit is contained in:
31
src/ScadaLink.CLI/Commands/DebugCommands.cs
Normal file
31
src/ScadaLink.CLI/Commands/DebugCommands.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Parsing;
|
||||
using ScadaLink.Commons.Messages.Management;
|
||||
|
||||
namespace ScadaLink.CLI.Commands;
|
||||
|
||||
public static class DebugCommands
|
||||
{
|
||||
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption)
|
||||
{
|
||||
var command = new Command("debug") { Description = "Runtime debugging" };
|
||||
|
||||
command.Add(BuildSnapshot(contactPointsOption, formatOption));
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command BuildSnapshot(Option<string> contactPointsOption, Option<string> formatOption)
|
||||
{
|
||||
var idOption = new Option<int>("--id") { Description = "Instance ID", Required = true };
|
||||
var cmd = new Command("snapshot") { Description = "Get a point-in-time snapshot of instance attribute values and alarm states" };
|
||||
cmd.Add(idOption);
|
||||
cmd.SetAction(async (ParseResult result) =>
|
||||
{
|
||||
return await CommandHelpers.ExecuteCommandAsync(
|
||||
result, contactPointsOption, formatOption,
|
||||
new DebugSnapshotCommand(result.GetValue(idOption)));
|
||||
});
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ rootCommand.Add(NotificationCommands.Build(contactPointsOption, formatOption));
|
||||
rootCommand.Add(SecurityCommands.Build(contactPointsOption, formatOption));
|
||||
rootCommand.Add(AuditLogCommands.Build(contactPointsOption, formatOption));
|
||||
rootCommand.Add(HealthCommands.Build(contactPointsOption, formatOption));
|
||||
rootCommand.Add(DebugCommands.Build(contactPointsOption, formatOption));
|
||||
rootCommand.Add(SharedScriptCommands.Build(contactPointsOption, formatOption));
|
||||
rootCommand.Add(DbConnectionCommands.Build(contactPointsOption, formatOption));
|
||||
rootCommand.Add(ApiMethodCommands.Build(contactPointsOption, formatOption));
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace ScadaLink.Commons.Messages.DebugView;
|
||||
|
||||
public record DebugSnapshotRequest(
|
||||
string InstanceUniqueName,
|
||||
string CorrelationId);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ScadaLink.Commons.Messages.Management;
|
||||
|
||||
public record DebugSnapshotCommand(int InstanceId);
|
||||
@@ -104,6 +104,9 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
|
||||
Receive<SubscribeDebugViewRequest>(msg => _deploymentManagerProxy.Forward(msg));
|
||||
Receive<UnsubscribeDebugViewRequest>(msg => _deploymentManagerProxy.Forward(msg));
|
||||
|
||||
// Pattern 6a: Debug Snapshot (one-shot) — forward to Deployment Manager
|
||||
Receive<DebugSnapshotRequest>(msg => _deploymentManagerProxy.Forward(msg));
|
||||
|
||||
// Pattern 7: Remote Queries
|
||||
Receive<EventLogQueryRequest>(msg =>
|
||||
{
|
||||
|
||||
@@ -130,7 +130,17 @@ public class CommunicationService
|
||||
GetActor().Tell(new SiteEnvelope(siteId, request));
|
||||
}
|
||||
|
||||
// ── Pattern 6: Health Reporting (site→central, Tell) ──
|
||||
// ── Pattern 6a: Debug Snapshot (one-shot, request/response) ──
|
||||
|
||||
public async Task<DebugViewSnapshot> RequestDebugSnapshotAsync(
|
||||
string siteId, DebugSnapshotRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var envelope = new SiteEnvelope(siteId, request);
|
||||
return await GetActor().Ask<DebugViewSnapshot>(
|
||||
envelope, _options.QueryTimeout, cancellationToken);
|
||||
}
|
||||
|
||||
// ── Pattern 6b: Health Reporting (site→central, Tell) ──
|
||||
// Health reports are received by central, not sent. No method needed here.
|
||||
|
||||
// ── Pattern 7: Remote Queries ──
|
||||
|
||||
@@ -12,6 +12,7 @@ using ScadaLink.Commons.Entities.Notifications;
|
||||
using ScadaLink.Commons.Entities.Security;
|
||||
using ScadaLink.Commons.Entities.Sites;
|
||||
using ScadaLink.Commons.Interfaces.Repositories;
|
||||
using ScadaLink.Commons.Messages.DebugView;
|
||||
using ScadaLink.Commons.Messages.Management;
|
||||
using ScadaLink.Commons.Messages.RemoteQuery;
|
||||
using ScadaLink.DeploymentManager;
|
||||
@@ -109,7 +110,8 @@ public class ManagementActor : ReceiveActor
|
||||
CreateInstanceCommand or MgmtDeployInstanceCommand or MgmtEnableInstanceCommand
|
||||
or MgmtDisableInstanceCommand or MgmtDeleteInstanceCommand
|
||||
or SetConnectionBindingsCommand
|
||||
or MgmtDeployArtifactsCommand => "Deployment",
|
||||
or MgmtDeployArtifactsCommand
|
||||
or DebugSnapshotCommand => "Deployment",
|
||||
|
||||
// Read-only queries -- any authenticated user
|
||||
_ => null
|
||||
@@ -234,6 +236,7 @@ public class ManagementActor : ReceiveActor
|
||||
// Remote Queries
|
||||
QueryEventLogsCommand cmd => await HandleQueryEventLogs(sp, cmd),
|
||||
QueryParkedMessagesCommand cmd => await HandleQueryParkedMessages(sp, cmd),
|
||||
DebugSnapshotCommand cmd => await HandleDebugSnapshot(sp, cmd),
|
||||
|
||||
_ => throw new NotSupportedException($"Unknown command type: {command.GetType().Name}")
|
||||
};
|
||||
@@ -1105,4 +1108,19 @@ public class ManagementActor : ReceiveActor
|
||||
DateTimeOffset.UtcNow);
|
||||
return await commService.QueryParkedMessagesAsync(cmd.SiteIdentifier, request);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDebugSnapshot(IServiceProvider sp, DebugSnapshotCommand cmd)
|
||||
{
|
||||
var instanceRepo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
var instance = await instanceRepo.GetInstanceByIdAsync(cmd.InstanceId)
|
||||
?? throw new InvalidOperationException($"Instance {cmd.InstanceId} not found.");
|
||||
|
||||
var siteRepo = sp.GetRequiredService<ISiteRepository>();
|
||||
var site = await siteRepo.GetSiteByIdAsync(instance.SiteId)
|
||||
?? throw new InvalidOperationException($"Site {instance.SiteId} not found.");
|
||||
|
||||
var commService = sp.GetRequiredService<CommunicationService>();
|
||||
var request = new DebugSnapshotRequest(instance.UniqueName, Guid.NewGuid().ToString("N"));
|
||||
return await commService.RequestDebugSnapshotAsync(site.SiteIdentifier, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// Debug View — route to Instance Actors
|
||||
Receive<SubscribeDebugViewRequest>(RouteDebugViewSubscribe);
|
||||
Receive<UnsubscribeDebugViewRequest>(RouteDebugViewUnsubscribe);
|
||||
Receive<DebugSnapshotRequest>(RouteDebugSnapshot);
|
||||
|
||||
// Internal startup messages
|
||||
Receive<StartupConfigsLoaded>(HandleStartupConfigsLoaded);
|
||||
@@ -453,6 +454,22 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
}
|
||||
|
||||
private void RouteDebugSnapshot(DebugSnapshotRequest request)
|
||||
{
|
||||
if (_instanceActors.TryGetValue(request.InstanceUniqueName, out var instanceActor))
|
||||
{
|
||||
instanceActor.Forward(request);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Debug snapshot for unknown instance {Instance}", request.InstanceUniqueName);
|
||||
Sender.Tell(new DebugViewSnapshot(
|
||||
request.InstanceUniqueName, Array.Empty<Commons.Messages.Streaming.AttributeValueChanged>(),
|
||||
Array.Empty<Commons.Messages.Streaming.AlarmStateChanged>(), DateTimeOffset.UtcNow));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-33: Handles system-wide artifact deployment (shared scripts, external systems, etc.).
|
||||
/// Persists artifacts to SiteStorageService and recompiles shared scripts.
|
||||
|
||||
@@ -133,6 +133,9 @@ public class InstanceActor : ReceiveActor
|
||||
Receive<SubscribeDebugViewRequest>(HandleSubscribeDebugView);
|
||||
Receive<UnsubscribeDebugViewRequest>(HandleUnsubscribeDebugView);
|
||||
|
||||
// Debug snapshot (one-shot, no subscription)
|
||||
Receive<DebugSnapshotRequest>(HandleDebugSnapshot);
|
||||
|
||||
// Handle internal messages
|
||||
Receive<LoadOverridesResult>(HandleOverridesLoaded);
|
||||
}
|
||||
@@ -399,6 +402,35 @@ public class InstanceActor : ReceiveActor
|
||||
_instanceUniqueName, request.CorrelationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One-shot debug snapshot — returns current state without registering a subscriber.
|
||||
/// </summary>
|
||||
private void HandleDebugSnapshot(DebugSnapshotRequest request)
|
||||
{
|
||||
var attributeValues = _attributes.Select(kvp => new AttributeValueChanged(
|
||||
_instanceUniqueName,
|
||||
kvp.Key,
|
||||
kvp.Key,
|
||||
kvp.Value,
|
||||
_attributeQualities.GetValueOrDefault(kvp.Key, "Good"),
|
||||
DateTimeOffset.UtcNow)).ToList();
|
||||
|
||||
var alarmStates = _alarmStates.Select(kvp => new AlarmStateChanged(
|
||||
_instanceUniqueName,
|
||||
kvp.Key,
|
||||
kvp.Value,
|
||||
0,
|
||||
DateTimeOffset.UtcNow)).ToList();
|
||||
|
||||
var snapshot = new DebugViewSnapshot(
|
||||
_instanceUniqueName,
|
||||
attributeValues,
|
||||
alarmStates,
|
||||
DateTimeOffset.UtcNow);
|
||||
|
||||
Sender.Tell(snapshot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes attribute change to stream and notifies child Script/Alarm actors.
|
||||
/// WP-22: Tell for attribute notifications (fire-and-forget, never blocks).
|
||||
|
||||
Reference in New Issue
Block a user