feat: wire ExternalSystem, Database, and Notify APIs into script runtime
IServiceProvider now flows through the actor chain (DeploymentManagerActor → InstanceActor → ScriptActor → ScriptExecutionActor) so scripts can resolve IExternalSystemClient, IDatabaseGateway, and INotificationDeliveryService from DI. ScriptGlobals exposes ExternalSystem, Database, Notify, and Scripts as top-level properties so scripts can use them without the Instance. prefix.
This commit is contained in:
@@ -241,7 +241,8 @@ akka {{
|
||||
siteRuntimeOptionsValue,
|
||||
dmLogger,
|
||||
dclManager,
|
||||
siteHealthCollector)),
|
||||
siteHealthCollector,
|
||||
_serviceProvider)),
|
||||
terminationMessage: PoisonPill.Instance,
|
||||
settings: ClusterSingletonManagerSettings.Create(_actorSystem!)
|
||||
.WithRole(siteRole)
|
||||
|
||||
@@ -4,7 +4,6 @@ using ScadaLink.CentralUI;
|
||||
using ScadaLink.ClusterInfrastructure;
|
||||
using ScadaLink.Communication;
|
||||
using ScadaLink.ConfigurationDatabase;
|
||||
using ScadaLink.DataConnectionLayer;
|
||||
using ScadaLink.DeploymentManager;
|
||||
using ScadaLink.ExternalSystemGateway;
|
||||
using ScadaLink.HealthMonitoring;
|
||||
@@ -15,9 +14,6 @@ using ScadaLink.InboundAPI;
|
||||
using ScadaLink.ManagementService;
|
||||
using ScadaLink.NotificationService;
|
||||
using ScadaLink.Security;
|
||||
using ScadaLink.SiteEventLogging;
|
||||
using ScadaLink.SiteRuntime;
|
||||
using ScadaLink.StoreAndForward;
|
||||
using ScadaLink.TemplateEngine;
|
||||
using Serilog;
|
||||
|
||||
@@ -98,7 +94,7 @@ try
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<AkkaHostedService>());
|
||||
|
||||
// Options binding
|
||||
BindSharedOptions(builder.Services, builder.Configuration);
|
||||
SiteServiceRegistration.BindSharedOptions(builder.Services, builder.Configuration);
|
||||
builder.Services.Configure<SecurityOptions>(builder.Configuration.GetSection("ScadaLink:Security"));
|
||||
builder.Services.Configure<InboundApiOptions>(builder.Configuration.GetSection("ScadaLink:InboundApi"));
|
||||
|
||||
@@ -148,35 +144,7 @@ try
|
||||
|
||||
builder.ConfigureServices((context, services) =>
|
||||
{
|
||||
// Shared components
|
||||
services.AddClusterInfrastructure();
|
||||
services.AddCommunication();
|
||||
services.AddSiteHealthMonitoring();
|
||||
services.AddExternalSystemGateway();
|
||||
services.AddNotificationService();
|
||||
|
||||
// Health report transport: sends SiteHealthReport to SiteCommunicationActor via Akka
|
||||
services.AddSingleton<ISiteIdentityProvider, SiteIdentityProvider>();
|
||||
services.AddSingleton<IHealthReportTransport, AkkaHealthReportTransport>();
|
||||
|
||||
// Site-only components — AddSiteRuntime registers SiteStorageService with SQLite path
|
||||
// and site-local repository implementations (IExternalSystemRepository, INotificationRepository)
|
||||
var siteDbPath = context.Configuration["ScadaLink:Database:SiteDbPath"] ?? "site.db";
|
||||
services.AddSiteRuntime($"Data Source={siteDbPath}");
|
||||
services.AddDataConnectionLayer();
|
||||
services.AddStoreAndForward();
|
||||
services.AddSiteEventLogging();
|
||||
|
||||
// WP-13: Akka.NET bootstrap via hosted service
|
||||
services.AddSingleton<AkkaHostedService>();
|
||||
services.AddHostedService(sp => sp.GetRequiredService<AkkaHostedService>());
|
||||
|
||||
// Options binding
|
||||
BindSharedOptions(services, context.Configuration);
|
||||
services.Configure<SiteRuntimeOptions>(context.Configuration.GetSection("ScadaLink:SiteRuntime"));
|
||||
services.Configure<DataConnectionOptions>(context.Configuration.GetSection("ScadaLink:DataConnection"));
|
||||
services.Configure<StoreAndForwardOptions>(context.Configuration.GetSection("ScadaLink:StoreAndForward"));
|
||||
services.Configure<SiteEventLogOptions>(context.Configuration.GetSection("ScadaLink:SiteEventLog"));
|
||||
SiteServiceRegistration.Configure(services, context.Configuration);
|
||||
});
|
||||
|
||||
var host = builder.Build();
|
||||
@@ -197,17 +165,6 @@ finally
|
||||
await Log.CloseAndFlushAsync();
|
||||
}
|
||||
|
||||
static void BindSharedOptions(IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.Configure<NodeOptions>(config.GetSection("ScadaLink:Node"));
|
||||
services.Configure<ClusterOptions>(config.GetSection("ScadaLink:Cluster"));
|
||||
services.Configure<DatabaseOptions>(config.GetSection("ScadaLink:Database"));
|
||||
services.Configure<CommunicationOptions>(config.GetSection("ScadaLink:Communication"));
|
||||
services.Configure<HealthMonitoringOptions>(config.GetSection("ScadaLink:HealthMonitoring"));
|
||||
services.Configure<NotificationOptions>(config.GetSection("ScadaLink:Notification"));
|
||||
services.Configure<LoggingOptions>(config.GetSection("ScadaLink:Logging"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exposes the auto-generated Program class for test infrastructure (e.g. WebApplicationFactory).
|
||||
/// </summary>
|
||||
|
||||
63
src/ScadaLink.Host/SiteServiceRegistration.cs
Normal file
63
src/ScadaLink.Host/SiteServiceRegistration.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using ScadaLink.ClusterInfrastructure;
|
||||
using ScadaLink.Communication;
|
||||
using ScadaLink.DataConnectionLayer;
|
||||
using ScadaLink.ExternalSystemGateway;
|
||||
using ScadaLink.HealthMonitoring;
|
||||
using ScadaLink.Host.Actors;
|
||||
using ScadaLink.NotificationService;
|
||||
using ScadaLink.SiteEventLogging;
|
||||
using ScadaLink.SiteRuntime;
|
||||
using ScadaLink.StoreAndForward;
|
||||
|
||||
namespace ScadaLink.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Extracted site-role DI registrations so both Program.cs and tests
|
||||
/// use the same composition root.
|
||||
/// </summary>
|
||||
public static class SiteServiceRegistration
|
||||
{
|
||||
public static void Configure(IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
// Shared components
|
||||
services.AddClusterInfrastructure();
|
||||
services.AddCommunication();
|
||||
services.AddSiteHealthMonitoring();
|
||||
services.AddExternalSystemGateway();
|
||||
services.AddNotificationService();
|
||||
|
||||
// Health report transport: sends SiteHealthReport to SiteCommunicationActor via Akka
|
||||
services.AddSingleton<ISiteIdentityProvider, SiteIdentityProvider>();
|
||||
services.AddSingleton<IHealthReportTransport, AkkaHealthReportTransport>();
|
||||
|
||||
// Site-only components — AddSiteRuntime registers SiteStorageService with SQLite path
|
||||
// and site-local repository implementations (IExternalSystemRepository, INotificationRepository)
|
||||
var siteDbPath = config["ScadaLink:Database:SiteDbPath"] ?? "site.db";
|
||||
services.AddSiteRuntime($"Data Source={siteDbPath}");
|
||||
services.AddDataConnectionLayer();
|
||||
services.AddStoreAndForward();
|
||||
services.AddSiteEventLogging();
|
||||
|
||||
// WP-13: Akka.NET bootstrap via hosted service
|
||||
services.AddSingleton<AkkaHostedService>();
|
||||
services.AddHostedService(sp => sp.GetRequiredService<AkkaHostedService>());
|
||||
|
||||
// Options binding
|
||||
BindSharedOptions(services, config);
|
||||
services.Configure<SiteRuntimeOptions>(config.GetSection("ScadaLink:SiteRuntime"));
|
||||
services.Configure<DataConnectionOptions>(config.GetSection("ScadaLink:DataConnection"));
|
||||
services.Configure<StoreAndForwardOptions>(config.GetSection("ScadaLink:StoreAndForward"));
|
||||
services.Configure<SiteEventLogOptions>(config.GetSection("ScadaLink:SiteEventLog"));
|
||||
}
|
||||
|
||||
public static void BindSharedOptions(IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.Configure<NodeOptions>(config.GetSection("ScadaLink:Node"));
|
||||
services.Configure<ClusterOptions>(config.GetSection("ScadaLink:Cluster"));
|
||||
services.Configure<DatabaseOptions>(config.GetSection("ScadaLink:Database"));
|
||||
services.Configure<CommunicationOptions>(config.GetSection("ScadaLink:Communication"));
|
||||
services.Configure<HealthMonitoringOptions>(config.GetSection("ScadaLink:HealthMonitoring"));
|
||||
services.Configure<NotificationOptions>(config.GetSection("ScadaLink:Notification"));
|
||||
services.Configure<LoggingOptions>(config.GetSection("ScadaLink:Logging"));
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
private readonly ILogger<DeploymentManagerActor> _logger;
|
||||
private readonly IActorRef? _dclManager;
|
||||
private readonly ISiteHealthCollector? _healthCollector;
|
||||
private readonly IServiceProvider? _serviceProvider;
|
||||
private readonly Dictionary<string, IActorRef> _instanceActors = new();
|
||||
private int _totalDeployedCount;
|
||||
|
||||
@@ -45,7 +46,8 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
SiteRuntimeOptions options,
|
||||
ILogger<DeploymentManagerActor> logger,
|
||||
IActorRef? dclManager = null,
|
||||
ISiteHealthCollector? healthCollector = null)
|
||||
ISiteHealthCollector? healthCollector = null,
|
||||
IServiceProvider? serviceProvider = null)
|
||||
{
|
||||
_storage = storage;
|
||||
_compilationService = compilationService;
|
||||
@@ -54,6 +56,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
_options = options;
|
||||
_dclManager = dclManager;
|
||||
_healthCollector = healthCollector;
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
// Lifecycle commands
|
||||
@@ -561,7 +564,8 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
_options,
|
||||
loggerFactory.CreateLogger<InstanceActor>(),
|
||||
_dclManager,
|
||||
_healthCollector));
|
||||
_healthCollector,
|
||||
_serviceProvider));
|
||||
|
||||
var actorRef = Context.ActorOf(props, instanceName);
|
||||
_instanceActors[instanceName] = actorRef;
|
||||
|
||||
@@ -39,6 +39,7 @@ public class InstanceActor : ReceiveActor
|
||||
private readonly SiteRuntimeOptions _options;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ISiteHealthCollector? _healthCollector;
|
||||
private readonly IServiceProvider? _serviceProvider;
|
||||
private readonly Dictionary<string, object?> _attributes = new();
|
||||
private readonly Dictionary<string, string> _attributeQualities = new();
|
||||
private readonly Dictionary<string, AlarmState> _alarmStates = new();
|
||||
@@ -64,7 +65,8 @@ public class InstanceActor : ReceiveActor
|
||||
SiteRuntimeOptions options,
|
||||
ILogger logger,
|
||||
IActorRef? dclManager = null,
|
||||
ISiteHealthCollector? healthCollector = null)
|
||||
ISiteHealthCollector? healthCollector = null,
|
||||
IServiceProvider? serviceProvider = null)
|
||||
{
|
||||
_instanceUniqueName = instanceUniqueName;
|
||||
_storage = storage;
|
||||
@@ -75,6 +77,7 @@ public class InstanceActor : ReceiveActor
|
||||
_logger = logger;
|
||||
_dclManager = dclManager;
|
||||
_healthCollector = healthCollector;
|
||||
_serviceProvider = serviceProvider;
|
||||
|
||||
// Deserialize the flattened configuration
|
||||
_configuration = JsonSerializer.Deserialize<FlattenedConfiguration>(configJson);
|
||||
@@ -479,7 +482,8 @@ public class InstanceActor : ReceiveActor
|
||||
_sharedScriptLibrary,
|
||||
_options,
|
||||
_logger,
|
||||
_healthCollector));
|
||||
_healthCollector,
|
||||
_serviceProvider));
|
||||
|
||||
var actorRef = Context.ActorOf(props, $"script-{script.CanonicalName}");
|
||||
_scriptActors[script.CanonicalName] = actorRef;
|
||||
|
||||
@@ -31,6 +31,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
private readonly SiteRuntimeOptions _options;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ISiteHealthCollector? _healthCollector;
|
||||
private readonly IServiceProvider? _serviceProvider;
|
||||
|
||||
private Script<object?>? _compiledScript;
|
||||
private ScriptTriggerConfig? _triggerConfig;
|
||||
@@ -49,7 +50,8 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
SharedScriptLibrary sharedScriptLibrary,
|
||||
SiteRuntimeOptions options,
|
||||
ILogger logger,
|
||||
ISiteHealthCollector? healthCollector = null)
|
||||
ISiteHealthCollector? healthCollector = null,
|
||||
IServiceProvider? serviceProvider = null)
|
||||
{
|
||||
_scriptName = scriptName;
|
||||
_instanceName = instanceName;
|
||||
@@ -59,6 +61,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
_healthCollector = healthCollector;
|
||||
_serviceProvider = serviceProvider;
|
||||
_minTimeBetweenRuns = scriptConfig.MinTimeBetweenRuns;
|
||||
|
||||
// Parse trigger configuration
|
||||
@@ -212,7 +215,8 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
replyTo,
|
||||
correlationId,
|
||||
_logger,
|
||||
_healthCollector));
|
||||
_healthCollector,
|
||||
_serviceProvider));
|
||||
|
||||
Context.ActorOf(props, executionId);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Akka.Actor;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ScadaLink.Commons.Interfaces.Services;
|
||||
using ScadaLink.Commons.Messages.ScriptExecution;
|
||||
using ScadaLink.HealthMonitoring;
|
||||
using ScadaLink.SiteRuntime.Scripts;
|
||||
@@ -30,7 +32,8 @@ public class ScriptExecutionActor : ReceiveActor
|
||||
IActorRef replyTo,
|
||||
string correlationId,
|
||||
ILogger logger,
|
||||
ISiteHealthCollector? healthCollector = null)
|
||||
ISiteHealthCollector? healthCollector = null,
|
||||
IServiceProvider? serviceProvider = null)
|
||||
{
|
||||
// Immediately begin execution
|
||||
var self = Self;
|
||||
@@ -39,7 +42,7 @@ public class ScriptExecutionActor : ReceiveActor
|
||||
ExecuteScript(
|
||||
scriptName, instanceName, compiledScript, parameters, callDepth,
|
||||
instanceActor, sharedScriptLibrary, options, replyTo, correlationId,
|
||||
self, parent, logger, healthCollector);
|
||||
self, parent, logger, healthCollector, serviceProvider);
|
||||
}
|
||||
|
||||
private static void ExecuteScript(
|
||||
@@ -56,16 +59,31 @@ public class ScriptExecutionActor : ReceiveActor
|
||||
IActorRef self,
|
||||
IActorRef parent,
|
||||
ILogger logger,
|
||||
ISiteHealthCollector? healthCollector)
|
||||
ISiteHealthCollector? healthCollector,
|
||||
IServiceProvider? serviceProvider)
|
||||
{
|
||||
var timeout = TimeSpan.FromSeconds(options.ScriptExecutionTimeoutSeconds);
|
||||
|
||||
// CTS must be created inside the async lambda so it outlives this method
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
IServiceScope? serviceScope = null;
|
||||
using var cts = new CancellationTokenSource(timeout);
|
||||
try
|
||||
{
|
||||
// Resolve integration services from DI (scoped lifetime)
|
||||
IExternalSystemClient? externalSystemClient = null;
|
||||
IDatabaseGateway? databaseGateway = null;
|
||||
INotificationDeliveryService? notificationService = null;
|
||||
|
||||
if (serviceProvider != null)
|
||||
{
|
||||
serviceScope = serviceProvider.CreateScope();
|
||||
externalSystemClient = serviceScope.ServiceProvider.GetService<IExternalSystemClient>();
|
||||
databaseGateway = serviceScope.ServiceProvider.GetService<IDatabaseGateway>();
|
||||
notificationService = serviceScope.ServiceProvider.GetService<INotificationDeliveryService>();
|
||||
}
|
||||
|
||||
var context = new ScriptRuntimeContext(
|
||||
instanceActor,
|
||||
self,
|
||||
@@ -74,7 +92,10 @@ public class ScriptExecutionActor : ReceiveActor
|
||||
options.MaxScriptCallDepth,
|
||||
timeout,
|
||||
instanceName,
|
||||
logger);
|
||||
logger,
|
||||
externalSystemClient,
|
||||
databaseGateway,
|
||||
notificationService);
|
||||
|
||||
var globals = new ScriptGlobals
|
||||
{
|
||||
@@ -123,6 +144,8 @@ public class ScriptExecutionActor : ReceiveActor
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Dispose the DI scope (and scoped services) after script execution completes
|
||||
serviceScope?.Dispose();
|
||||
// Stop self after execution completes
|
||||
self.Tell(PoisonPill.Instance);
|
||||
}
|
||||
|
||||
@@ -178,4 +178,28 @@ public class ScriptGlobals
|
||||
public IReadOnlyDictionary<string, object?> Parameters { get; set; } =
|
||||
new Dictionary<string, object?>();
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Top-level ExternalSystem access for scripts (delegates to Instance.ExternalSystem).
|
||||
/// Usage: ExternalSystem.Call("systemName", "methodName", params)
|
||||
/// </summary>
|
||||
public ScriptRuntimeContext.ExternalSystemHelper ExternalSystem => Instance.ExternalSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Top-level Database access for scripts (delegates to Instance.Database).
|
||||
/// Usage: Database.Connection("name") or Database.CachedWrite("name", "sql", params)
|
||||
/// </summary>
|
||||
public ScriptRuntimeContext.DatabaseHelper Database => Instance.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Top-level Notify access for scripts (delegates to Instance.Notify).
|
||||
/// Usage: Notify.To("listName").Send("subject", "message")
|
||||
/// </summary>
|
||||
public ScriptRuntimeContext.NotifyHelper Notify => Instance.Notify;
|
||||
|
||||
/// <summary>
|
||||
/// Top-level Scripts access for shared script calls (delegates to Instance.Scripts).
|
||||
/// Usage: Scripts.CallShared("scriptName", params)
|
||||
/// </summary>
|
||||
public ScriptRuntimeContext.ScriptCallHelper Scripts => Instance.Scripts;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user