using ZB.MOM.WW.ScadaBridge.Commons.Types; using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts; namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis; /// /// Runtime globals for the Test Run sandbox. Mirrors the real site-runtime /// ScriptGlobals surface (ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts) member-for-member /// so the same user code that runs at a site also compiles and runs here. /// /// Instance-context members — Instance.GetAttribute/SetAttribute/CallScript, /// Attributes, Children, Parent — need a live deployed /// instance. With no instance bound they throw ; /// with one bound (see ) they route to it. /// /// ExternalSystem, Database, and Scripts.CallShared run /// against central's real services and fire for real; Notify is a /// signature-faithful no-op fake. None of them depend on a bound instance. /// public class SandboxScriptHost { /// /// Script parameters passed to the sandbox. /// public ScriptParameters Parameters { get; init; } = new(); /// /// Cancellation token for the sandbox execution. /// public CancellationToken CancellationToken { get; init; } /// /// Alarm context for the sandbox. /// public AlarmContext? Alarm { get; init; } /// /// Script scope defining the execution context. /// public ScriptScope Scope { get; init; } = ScriptScope.Root; /// /// Instance context providing access to deployed instance data. /// public SandboxInstanceContext Instance { get; init; } = new(); /// /// Helper for external system calls. /// public SandboxExternalHelper ExternalSystem => Instance.ExternalSystem; /// /// Helper for database operations. /// public SandboxDatabaseHelper Database => Instance.Database; /// /// Helper for sending notifications. /// public SandboxNotifyHelper Notify => Instance.Notify; /// /// Helper for calling scripts. /// public SandboxScriptCallHelper Scripts => Instance.Scripts; /// /// Accessor for attributes scoped to the current instance. /// public SandboxAttributeAccessor Attributes => new(Instance, Scope.SelfPath); /// /// Accessor for child compositions. /// public SandboxChildrenAccessor Children => new(Instance, Scope.SelfPath); /// /// Accessor for the parent composition, or null if at root. /// public SandboxCompositionAccessor? Parent => Scope.ParentPath == null ? null : new SandboxCompositionAccessor(Instance, Scope.ParentPath); } /// /// Backs the sandbox Instance when a Test Run is bound to a real /// deployed instance. Null when unbound. The implementation routes to the /// instance cross-site over the cluster transport. /// public interface ISandboxInstanceGateway { /// /// Gets the value of an attribute with the specified canonical name. /// /// The canonical name of the attribute. /// Cancellation token. /// The attribute value, or null if not found. Task GetAttributeAsync(string canonicalName, CancellationToken ct); /// /// Sets the value of an attribute with the specified canonical name. /// /// The canonical name of the attribute. /// The value to set. /// Cancellation token. /// A task that represents the asynchronous operation. Task SetAttributeAsync(string canonicalName, string value, CancellationToken ct); /// /// Calls a script with the specified canonical name. /// /// The canonical name of the script. /// Script parameters, or null if none. /// Cancellation token. /// The script result, or null if none. Task CallScriptAsync( string canonicalScriptName, IReadOnlyDictionary? parameters, CancellationToken ct); } /// /// Sandbox mirror of ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ScriptRuntimeContext — /// the Instance global. Attribute and sibling-script access needs a real /// deployed instance: with no gateway wired it throws; with one (a bound /// instance) it routes cross-site. ExternalSystem/Database/ /// Scripts run against central's real services regardless of binding; /// Notify is a signature-faithful no-op fake. /// public class SandboxInstanceContext { private readonly ISandboxInstanceGateway? _gateway; /// /// Helper for external system calls. /// public SandboxExternalHelper ExternalSystem { get; } /// /// Helper for database operations. /// public SandboxDatabaseHelper Database { get; } /// /// Helper for sending notifications. /// public SandboxNotifyHelper Notify { get; } /// /// Helper for calling scripts. /// public SandboxScriptCallHelper Scripts { get; } /// /// Initializes a new instance of the SandboxInstanceContext. /// /// Gateway for accessing deployed instance data, or null if unbound. /// External system helper, or null to create a default. /// Database helper, or null to create a default. /// Notification helper, or null to create a default. /// Script call helper, or null to create a default. public SandboxInstanceContext( ISandboxInstanceGateway? gateway = null, SandboxExternalHelper? external = null, SandboxDatabaseHelper? database = null, SandboxNotifyHelper? notify = null, SandboxScriptCallHelper? scripts = null) { _gateway = gateway; ExternalSystem = external ?? new SandboxExternalHelper(null, ""); Database = database ?? new SandboxDatabaseHelper(null, ""); Notify = notify ?? new SandboxNotifyHelper(); Scripts = scripts ?? new SandboxScriptCallHelper(null); } /// /// Gets the value of an attribute. /// /// The name of the attribute. /// The attribute value, or null if not found. public Task GetAttribute(string attributeName) { if (_gateway == null) throw new ScriptSandboxException( $"GetAttribute(\"{attributeName}\") needs a deployed instance — " + "bind one in Test Run to read live attribute values."); return _gateway.GetAttributeAsync(attributeName, CancellationToken.None); } /// /// Sets the value of an attribute. /// /// The name of the attribute. /// The value to set. public void SetAttribute(string attributeName, string value) { if (_gateway == null) throw new ScriptSandboxException( $"SetAttribute(\"{attributeName}\") needs a deployed instance — " + "bind one in Test Run to write attribute values."); _gateway.SetAttributeAsync(attributeName, value, CancellationToken.None).GetAwaiter().GetResult(); } /// /// Calls a sibling script. /// /// The name of the script. /// Script parameters, or null if none. /// The script result, or null if none. public Task CallScript(string scriptName, object? parameters = null) { if (_gateway == null) throw new ScriptSandboxException( $"CallScript(\"{scriptName}\") needs a deployed instance — " + "bind one in Test Run to call sibling scripts."); return _gateway.CallScriptAsync(scriptName, ScriptArgs.Normalize(parameters), CancellationToken.None); } } /// /// Sandbox mirror of ScriptRuntimeContext.ScriptCallHelper — /// Scripts.CallShared(...). Compiles and runs the named shared script in /// the same sandbox via the wired delegate. /// public class SandboxScriptCallHelper { private readonly Func?, CancellationToken, Task>? _callShared; /// /// Initializes a new instance of the SandboxScriptCallHelper. /// /// Delegate for calling shared scripts, or null if not available. public SandboxScriptCallHelper( Func?, CancellationToken, Task>? callShared) { _callShared = callShared; } /// /// Calls a shared script. /// /// The name of the shared script. /// Script parameters, or null if none. /// Cancellation token. /// The script result, or null if none. public Task CallShared( string scriptName, object? parameters = null, CancellationToken cancellationToken = default) { if (_callShared == null) throw new ScriptSandboxException( $"Scripts.CallShared(\"{scriptName}\") — shared-script catalog not configured for Test Run."); return _callShared(scriptName, ScriptArgs.Normalize(parameters), cancellationToken); } } /// /// Sandbox mirror of ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.AttributeAccessor — /// scope-aware Attributes["X"] access anchored at a canonical-name prefix. /// public class SandboxAttributeAccessor { private readonly SandboxInstanceContext _ctx; /// /// The scope prefix for attribute resolution. /// public string ScopePrefix { get; } /// /// Initializes a new instance of the SandboxAttributeAccessor. /// /// The sandbox instance context. /// The scope prefix for attribute names. public SandboxAttributeAccessor(SandboxInstanceContext ctx, string prefix) { _ctx = ctx; ScopePrefix = prefix; } /// /// Resolves a key to its fully qualified name within the current scope. /// /// The attribute key. /// The fully qualified attribute name. public string Resolve(string key) => ScopePrefix.Length == 0 ? key : ScopePrefix + "." + key; /// /// Gets or sets an attribute value by key. /// /// The attribute key. /// The attribute value, or null if not found. public object? this[string key] { get => _ctx.GetAttribute(Resolve(key)).GetAwaiter().GetResult(); set => _ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty); } /// /// Gets an attribute value asynchronously. /// /// The attribute key. /// The attribute value, or null if not found. public Task GetAsync(string key) => _ctx.GetAttribute(Resolve(key)); /// /// Sets an attribute value asynchronously. /// /// The attribute key. /// The value to set, or null. /// A task representing the operation. public Task SetAsync(string key, object? value) { _ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty); return Task.CompletedTask; } } /// /// Sandbox mirror of ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.CompositionAccessor — /// a view of one composition: its attributes plus an invokable CallScript. /// public class SandboxCompositionAccessor { private readonly SandboxInstanceContext _ctx; /// /// The path to the composition within the instance hierarchy. /// public string Path { get; } /// /// Accessor for attributes within the composition. /// public SandboxAttributeAccessor Attributes { get; } /// /// Initializes a new instance of the SandboxCompositionAccessor. /// /// The sandbox instance context. /// The path to the composition within the instance hierarchy. public SandboxCompositionAccessor(SandboxInstanceContext ctx, string path) { _ctx = ctx; Path = path; Attributes = new SandboxAttributeAccessor(ctx, path); } /// /// Resolves a script name to its fully qualified name within the composition. /// /// The script name. /// The fully qualified script name. public string ResolveScript(string scriptName) => Path.Length == 0 ? scriptName : Path + "." + scriptName; /// /// Calls a script within the composition. /// /// The name of the script. /// Script parameters, or null if none. /// The script result, or null if none. public Task CallScript(string scriptName, object? parameters = null) => _ctx.CallScript(ResolveScript(scriptName), parameters); } /// /// Sandbox mirror of ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ChildrenAccessor — /// dictionary-style access to child compositions. /// public class SandboxChildrenAccessor { private readonly SandboxInstanceContext _ctx; private readonly string _selfPath; /// /// Initializes a new instance of the SandboxChildrenAccessor. /// /// The sandbox instance context. /// The path to the parent composition. public SandboxChildrenAccessor(SandboxInstanceContext ctx, string selfPath) { _ctx = ctx; _selfPath = selfPath; } /// /// Gets a child composition by name. /// /// The name of the child composition. /// An accessor for the child composition. public SandboxCompositionAccessor this[string compositionName] { get { var path = _selfPath.Length == 0 ? compositionName : _selfPath + "." + compositionName; return new SandboxCompositionAccessor(_ctx, path); } } } /// /// Distinct exception so the Test Run pipeline can label sandbox-only /// limitations differently from genuine runtime errors in user code. /// public class ScriptSandboxException : Exception { /// /// Initializes a new instance of the ScriptSandboxException. /// /// The exception message. public ScriptSandboxException(string message) : base(message) { } }