diff --git a/src/ScadaLink.Communication/Actors/SiteCommunicationActor.cs b/src/ScadaLink.Communication/Actors/SiteCommunicationActor.cs index 2040268..1bcfd2d 100644 --- a/src/ScadaLink.Communication/Actors/SiteCommunicationActor.cs +++ b/src/ScadaLink.Communication/Actors/SiteCommunicationActor.cs @@ -5,6 +5,7 @@ using ScadaLink.Commons.Messages.Artifacts; using ScadaLink.Commons.Messages.DebugView; using ScadaLink.Commons.Messages.Deployment; using ScadaLink.Commons.Messages.Health; +using ScadaLink.Commons.Messages.InboundApi; using ScadaLink.Commons.Messages.Integration; using ScadaLink.Commons.Messages.Lifecycle; using ScadaLink.Commons.Messages.RemoteQuery; @@ -107,6 +108,9 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers // Pattern 6a: Debug Snapshot (one-shot) — forward to Deployment Manager Receive(msg => _deploymentManagerProxy.Forward(msg)); + // Inbound API Route.To().Call() — forward to Deployment Manager for instance routing + Receive(msg => _deploymentManagerProxy.Forward(msg)); + // Pattern 7: Remote Queries Receive(msg => { diff --git a/src/ScadaLink.Host/Program.cs b/src/ScadaLink.Host/Program.cs index 98ca6ec..cdedc71 100644 --- a/src/ScadaLink.Host/Program.cs +++ b/src/ScadaLink.Host/Program.cs @@ -129,6 +129,19 @@ try app.MapStaticAssets(); app.MapCentralUI(); app.MapInboundAPI(); + + // Compile and register all Inbound API method scripts at startup + using (var scope = app.Services.CreateScope()) + { + var apiRepo = scope.ServiceProvider.GetRequiredService(); + var executor = app.Services.GetRequiredService(); + var methods = await apiRepo.GetAllApiMethodsAsync(); + foreach (var method in methods) + { + executor.CompileAndRegister(method); + } + } + await app.RunAsync(); } else if (nodeRole.Equals("Site", StringComparison.OrdinalIgnoreCase)) diff --git a/src/ScadaLink.InboundAPI/InboundScriptExecutor.cs b/src/ScadaLink.InboundAPI/InboundScriptExecutor.cs index 881ebc5..fd0cdc8 100644 --- a/src/ScadaLink.InboundAPI/InboundScriptExecutor.cs +++ b/src/ScadaLink.InboundAPI/InboundScriptExecutor.cs @@ -1,4 +1,6 @@ using System.Text.Json; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; using Microsoft.Extensions.Logging; using ScadaLink.Commons.Entities.InboundApi; @@ -6,11 +8,7 @@ namespace ScadaLink.InboundAPI; /// /// WP-3: Executes the C# script associated with an inbound API method. -/// The script receives input parameters and a route helper, and returns a result -/// that is serialized as the JSON response. -/// -/// In a full implementation this would use Roslyn scripting. For now, scripts -/// are a simple dispatch table so the rest of the pipeline can be tested end-to-end. +/// Compiles method scripts via Roslyn and caches compiled delegates. /// public class InboundScriptExecutor { @@ -24,13 +22,73 @@ public class InboundScriptExecutor /// /// Registers a compiled script handler for a method name. - /// In production, this would be called after Roslyn compilation of the method's Script property. /// public void RegisterHandler(string methodName, Func> handler) { _scriptHandlers[methodName] = handler; } + /// + /// Compiles and registers a single API method script. + /// + public bool CompileAndRegister(ApiMethod method) + { + if (string.IsNullOrWhiteSpace(method.Script)) + { + _logger.LogWarning("API method {Method} has no script code", method.Name); + return false; + } + + try + { + var scriptOptions = ScriptOptions.Default + .WithReferences( + typeof(object).Assembly, + typeof(Enumerable).Assembly, + typeof(Dictionary<,>).Assembly, + typeof(RouteHelper).Assembly, + typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly) + .WithImports( + "System", + "System.Collections.Generic", + "System.Linq", + "System.Threading.Tasks"); + + var compiled = CSharpScript.Create( + method.Script, + scriptOptions, + globalsType: typeof(InboundScriptContext)); + + var diagnostics = compiled.Compile(); + var errors = diagnostics + .Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error) + .Select(d => d.GetMessage()) + .ToList(); + + if (errors.Count > 0) + { + _logger.LogWarning( + "API method {Method} script compilation failed: {Errors}", + method.Name, string.Join("; ", errors)); + return false; + } + + _scriptHandlers[method.Name] = async ctx => + { + var state = await compiled.RunAsync(ctx, ctx.CancellationToken); + return state.ReturnValue; + }; + + _logger.LogInformation("API method {Method} script compiled and registered", method.Name); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to compile API method {Method} script", method.Name); + return false; + } + } + /// /// Executes the script for the given method with the provided context. /// diff --git a/src/ScadaLink.InboundAPI/ScadaLink.InboundAPI.csproj b/src/ScadaLink.InboundAPI/ScadaLink.InboundAPI.csproj index 59f3185..c75e249 100644 --- a/src/ScadaLink.InboundAPI/ScadaLink.InboundAPI.csproj +++ b/src/ScadaLink.InboundAPI/ScadaLink.InboundAPI.csproj @@ -20,4 +20,8 @@ + + + + diff --git a/src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs b/src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs index 52f81e4..9de0b17 100644 --- a/src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs +++ b/src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs @@ -3,7 +3,9 @@ using Microsoft.Extensions.Logging; using ScadaLink.Commons.Messages.Artifacts; using ScadaLink.Commons.Messages.DebugView; using ScadaLink.Commons.Messages.Deployment; +using ScadaLink.Commons.Messages.InboundApi; using ScadaLink.Commons.Messages.Lifecycle; +using ScadaLink.Commons.Messages.ScriptExecution; using ScadaLink.Commons.Types.Enums; using ScadaLink.HealthMonitoring; using ScadaLink.SiteRuntime.Messages; @@ -77,6 +79,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers Receive(RouteDebugViewUnsubscribe); Receive(RouteDebugSnapshot); + // Inbound API Route.To().Call() — route to Instance Actors + Receive(RouteInboundApiCall); + // Internal startup messages Receive(HandleStartupConfigsLoaded); Receive(HandleStartNextBatch); @@ -486,6 +491,41 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers } } + // ── Inbound API routing ── + + private void RouteInboundApiCall(RouteToCallRequest request) + { + if (_instanceActors.TryGetValue(request.InstanceUniqueName, out var instanceActor)) + { + // Convert to ScriptCallRequest and Ask the Instance Actor + var scriptCall = new ScriptCallRequest( + request.ScriptName, request.Parameters, 0, request.CorrelationId); + var sender = Sender; + instanceActor.Ask(scriptCall, TimeSpan.FromSeconds(30)) + .ContinueWith(t => + { + if (t.IsCompletedSuccessfully) + { + var result = t.Result; + return new RouteToCallResponse( + request.CorrelationId, result.Success, result.ReturnValue, + result.ErrorMessage, DateTimeOffset.UtcNow); + } + return new RouteToCallResponse( + request.CorrelationId, false, null, + t.Exception?.GetBaseException().Message ?? "Script call timed out", + DateTimeOffset.UtcNow); + }).PipeTo(sender); + } + else + { + Sender.Tell(new RouteToCallResponse( + request.CorrelationId, false, null, + $"Instance '{request.InstanceUniqueName}' not found on this site.", + DateTimeOffset.UtcNow)); + } + } + /// /// WP-33: Handles system-wide artifact deployment (shared scripts, external systems, etc.). /// Persists artifacts to SiteStorageService and recompiles shared scripts.