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); /// /// Read-only accessor for the instance's current alarm conditions, mirroring the site /// runtime's Alarms global (MES alarm-status API §5.2). Present so a script using /// Alarms.CurrentAsync() BINDS in the Test Run editor — without it the editor /// would show a spurious CS1061 on a script that deploys and runs perfectly. /// public SandboxAlarmsAccessor Alarms { get; } = new(); } /// /// Sandbox mirror of ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.AlarmsAccessor. /// Exists for editor/compile parity only: alarm state is held per-instance by the site's /// Instance Actor and there is no central Test Run route to it, so an actual call throws a /// clearly-labelled rather than quietly returning an /// empty list that would read as "nothing is in alarm". /// public class SandboxAlarmsAccessor { /// /// Mirrors AlarmsAccessor.CurrentAsync for compile parity; unsupported at run /// time in the central Test Run sandbox. /// /// Token used to cancel the read (unused in the sandbox). /// Never returns; always throws in the sandbox. public Task> CurrentAsync(CancellationToken cancellationToken = default) => throw new ScriptSandboxException( "Alarms.CurrentAsync() reads the site Instance Actor's live alarm conditions, " + "which aren't available in the central Test Run sandbox — deploy to a site to " + "exercise alarm-reading scripts."); } /// /// 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); /// /// Blocks until the named attribute reaches a target value (value-equality, /// codec-encoded), optionally quality-gated, or the timeout elapses — routed to the /// bound deployed instance cross-site. Value-equality only: a script-side predicate /// cannot be routed, so the sandbox predicate overloads stay unsupported. /// /// The (already scope-resolved) canonical attribute name. /// The AttributeValueCodec-encoded target value, or null for "any change". /// Maximum time to wait. /// When true, only a Good-quality value satisfies the wait. /// Cancellation token. /// The wait outcome (matched flag, matched value, quality, timed-out flag). Task WaitForAttributeAsync( string canonicalName, string? targetValueEncoded, TimeSpan timeout, bool requireGoodQuality, 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); } /// /// Routes a value-equality attribute wait to the bound deployed instance cross-site. /// Needs a bound instance (the site owns the event-driven waiter); throws when unbound. /// /// The (already scope-resolved) canonical attribute name. /// The codec-encoded target value, or null for "any change". /// Maximum time to wait. /// When true, only a Good-quality value satisfies the wait. /// The wait outcome. public Task WaitForAttribute( string canonicalName, string? targetValueEncoded, TimeSpan timeout, bool requireGoodQuality) { if (_gateway == null) throw new ScriptSandboxException( $"WaitForAttribute(\"{canonicalName}\") needs a deployed instance — " + "bind one in Test Run to exercise the event-driven attribute waiter."); return _gateway.WaitForAttributeAsync( canonicalName, targetValueEncoded, timeout, requireGoodQuality, 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; } // Batch-write/wait helpers. These mirror the runtime AttributeAccessor // (SiteRuntime/Scripts/ScopeAccessors.cs) and the deploy-gate // ScriptCompileSurface member-for-member so instance scripts using them COMPILE // in the editor and pass Test Run analysis — previously the // sandbox omitted them and the editor false-flagged valid scripts with CS1061. // Execution needs the site's DCL batch path + event-driven attribute waiter, // for which the central Test Run sandbox has no transport, so each throws a // clearly-labelled ScriptSandboxException; the same code validates/deploys/runs // unchanged at a site. /// /// Sandbox stand-in for AttributeAccessor.WriteBatchAndWaitAsync: present /// for editor/compile parity, throws when run /// in Test Run (no device batch-write transport here). /// /// The attribute values to write as a batch. /// The flag attribute key to set to signal completion. /// The value to set on the flag attribute. /// The attribute key to observe for the device's response. /// The response value to wait for. /// The maximum time to wait for the response. /// Never returns; always throws in the sandbox. public Task WriteBatchAndWaitAsync( IReadOnlyDictionary values, string flagKey, object? flagValue, string responseKey, object? responseValue, TimeSpan timeout) => throw NotInSandbox(nameof(WriteBatchAndWaitAsync)); /// /// Value-equality attribute wait — routed to the bound deployed instance cross-site /// (the same real event-driven waiter a site uses). Needs a bound instance; throws /// when the Test Run has no instance bound. /// /// The attribute key to observe. /// The value to wait for (codec-normalized), or null for "any change". /// The maximum time to wait. /// Whether the attribute must also have good quality to satisfy the wait. /// True when the attribute reached the target within the timeout; false on timeout. public async Task WaitAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false) => (await _ctx.WaitForAttribute( Resolve(key), AttributeValueCodec.Encode(targetValue), timeout, requireGoodQuality)).Matched; /// /// Predicate-form attribute wait. Unsupported in Test Run because a script-side /// predicate (an in-process lambda) cannot be evaluated on the remote site — only the /// value-equality overloads route. Throws a clearly-labelled . /// /// The attribute key to observe. /// The predicate the attribute value must satisfy. /// The maximum time to wait. /// Whether the attribute must also have good quality to satisfy the wait. /// Never returns; always throws in the sandbox. public Task WaitAsync(string key, Func predicate, TimeSpan timeout, bool requireGoodQuality = false) => throw PredicateWaitNotRoutable(nameof(WaitAsync)); /// /// Value-equality attribute wait returning the full — routed /// to the bound deployed instance cross-site. Needs a bound instance; throws /// when the Test Run has no instance bound. /// /// The attribute key to observe. /// The value to wait for (codec-normalized), or null for "any change". /// The maximum time to wait. /// Whether the attribute must also have good quality to satisfy the wait. /// The wait outcome (matched flag, matched value, quality, timed-out flag). public Task WaitForAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false) => _ctx.WaitForAttribute(Resolve(key), AttributeValueCodec.Encode(targetValue), timeout, requireGoodQuality); /// /// Predicate-form attribute wait returning the full . /// Unsupported in Test Run for the same reason as the predicate /// overload — an in-process predicate cannot be routed to the remote site. /// /// The attribute key to observe. /// The predicate the attribute value must satisfy. /// The maximum time to wait. /// Whether the attribute must also have good quality to satisfy the wait. /// Never returns; always throws in the sandbox. public Task WaitForAsync(string key, Func predicate, TimeSpan timeout, bool requireGoodQuality = false) => throw PredicateWaitNotRoutable(nameof(WaitForAsync)); private static ScriptSandboxException NotInSandbox(string member) => new($"{member}(...) drives live device tags and the site's event-driven " + "attribute waiter, which aren't available in the central Test Run sandbox — " + "deploy to a site to exercise batch-write/wait handshakes."); private static ScriptSandboxException PredicateWaitNotRoutable(string member) => new($"{member}(...) with a predicate can't run in Test Run — a script-side " + "predicate is an in-process lambda that cannot be evaluated on the remote " + "site. Use the value-equality overload (which routes to the bound instance), " + "or deploy to a site to exercise predicate waits."); } /// /// 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) { } }