17d9a4cd2c
Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB
666 lines
32 KiB
C#
666 lines
32 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
|
|
|
/// <summary>
|
|
/// Runtime globals for the Test Run sandbox. Mirrors the real site-runtime
|
|
/// <c>ScriptGlobals</c> 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 — <c>Instance.GetAttribute/SetAttribute/CallScript</c>,
|
|
/// <c>Attributes</c>, <c>Children</c>, <c>Parent</c> — need a live deployed
|
|
/// instance. With no instance bound they throw <see cref="ScriptSandboxException"/>;
|
|
/// with one bound (see <see cref="SandboxInstanceContext"/>) they route to it.
|
|
///
|
|
/// <c>ExternalSystem</c>, <c>Database</c>, and <c>Scripts.CallShared</c> run
|
|
/// against central's real services and fire for real; <c>Notify</c> is a
|
|
/// signature-faithful no-op fake. None of them depend on a bound instance.
|
|
/// </summary>
|
|
public class SandboxScriptHost
|
|
{
|
|
/// <summary>
|
|
/// Script parameters passed to the sandbox.
|
|
/// </summary>
|
|
public ScriptParameters Parameters { get; init; } = new();
|
|
|
|
/// <summary>
|
|
/// Cancellation token for the sandbox execution.
|
|
/// </summary>
|
|
public CancellationToken CancellationToken { get; init; }
|
|
|
|
/// <summary>
|
|
/// Alarm context for the sandbox.
|
|
/// </summary>
|
|
public AlarmContext? Alarm { get; init; }
|
|
|
|
/// <summary>
|
|
/// Script scope defining the execution context.
|
|
/// </summary>
|
|
public ScriptScope Scope { get; init; } = ScriptScope.Root;
|
|
|
|
/// <summary>
|
|
/// Instance context providing access to deployed instance data.
|
|
/// </summary>
|
|
public SandboxInstanceContext Instance { get; init; } = new();
|
|
|
|
/// <summary>
|
|
/// Helper for external system calls.
|
|
/// </summary>
|
|
public SandboxExternalHelper ExternalSystem => Instance.ExternalSystem;
|
|
|
|
/// <summary>
|
|
/// Helper for database operations.
|
|
/// </summary>
|
|
public SandboxDatabaseHelper Database => Instance.Database;
|
|
|
|
/// <summary>
|
|
/// Helper for sending notifications.
|
|
/// </summary>
|
|
public SandboxNotifyHelper Notify => Instance.Notify;
|
|
|
|
/// <summary>
|
|
/// Helper for calling scripts.
|
|
/// </summary>
|
|
public SandboxScriptCallHelper Scripts => Instance.Scripts;
|
|
|
|
/// <summary>
|
|
/// Accessor for attributes scoped to the current instance.
|
|
/// </summary>
|
|
public SandboxAttributeAccessor Attributes => new(Instance, Scope.SelfPath);
|
|
|
|
/// <summary>
|
|
/// Accessor for child compositions.
|
|
/// </summary>
|
|
public SandboxChildrenAccessor Children => new(Instance, Scope.SelfPath);
|
|
|
|
/// <summary>
|
|
/// Accessor for the parent composition, or null if at root.
|
|
/// </summary>
|
|
public SandboxCompositionAccessor? Parent =>
|
|
Scope.ParentPath == null ? null : new SandboxCompositionAccessor(Instance, Scope.ParentPath);
|
|
|
|
/// <summary>
|
|
/// Read-only accessor for the instance's current alarm conditions, mirroring the site
|
|
/// runtime's <c>Alarms</c> global (MES alarm-status API §5.2). Present so a script using
|
|
/// <c>Alarms.CurrentAsync()</c> BINDS in the Test Run editor — without it the editor
|
|
/// would show a spurious CS1061 on a script that deploys and runs perfectly.
|
|
/// </summary>
|
|
public SandboxAlarmsAccessor Alarms { get; } = new();
|
|
|
|
/// <summary>
|
|
/// Sandbox mirror of the site runtime's <c>Log</c> global (step logs design 2026-09-09 D4).
|
|
/// Present so a script that logs BINDS in the Test Run editor; lines go to the Test Run
|
|
/// console pane and are never audited. Delegates to <see cref="SandboxInstanceContext.Log"/>
|
|
/// exactly as the runtime's <c>ScriptGlobals.Log => Instance.Log</c> does, so
|
|
/// <c>Log</c> and <c>Instance.Log</c> are the same object here too.
|
|
/// </summary>
|
|
public SandboxLog Log => Instance.Log;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sandbox mirror of <c>ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ScriptLogger</c> (step logs
|
|
/// design 2026-09-09 D4). Signature-faithful so the same script body compiles here and at a site,
|
|
/// but a Test Run has no execution to trace and no audit trail: every line is written to
|
|
/// <see cref="Console.Out"/>, which the sandbox captures into
|
|
/// <c>SandboxRunResult.ConsoleOutput</c> (see <see cref="SandboxConsoleCapture"/>) and shows in
|
|
/// the editor's console pane. Nothing here reaches the node log or the audit log.
|
|
/// </summary>
|
|
public class SandboxLog
|
|
{
|
|
/// <summary>Writes a debug-level line to the Test Run console pane.</summary>
|
|
/// <param name="message">The message text.</param>
|
|
/// <param name="exception">An optional exception, rendered on a following line.</param>
|
|
public void Debug(string message, Exception? exception = null) => Write("debug", message, exception);
|
|
|
|
/// <summary>Writes an information-level line to the Test Run console pane.</summary>
|
|
/// <param name="message">The message text.</param>
|
|
/// <param name="exception">An optional exception, rendered on a following line.</param>
|
|
public void Info(string message, Exception? exception = null) => Write("info", message, exception);
|
|
|
|
/// <summary>Writes a warning-level line to the Test Run console pane.</summary>
|
|
/// <param name="message">The message text.</param>
|
|
/// <param name="exception">An optional exception, rendered on a following line.</param>
|
|
public void Warn(string message, Exception? exception = null) => Write("warn", message, exception);
|
|
|
|
/// <summary>
|
|
/// Writes an error-level line to the Test Run console pane. Like the runtime logger, this
|
|
/// does NOT fail the run — it is a message, not a throw.
|
|
/// </summary>
|
|
/// <param name="message">The message text.</param>
|
|
/// <param name="exception">An optional exception, rendered on a following line.</param>
|
|
public void Error(string message, Exception? exception = null) => Write("error", message, exception);
|
|
|
|
private static void Write(string level, string message, Exception? exception)
|
|
{
|
|
Console.Out.WriteLine($"[{level}] {message}");
|
|
if (exception != null)
|
|
{
|
|
Console.Out.WriteLine(exception.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sandbox mirror of <c>ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.AlarmsAccessor</c>.
|
|
/// 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 <see cref="ScriptSandboxException"/> rather than quietly returning an
|
|
/// empty list that would read as "nothing is in alarm".
|
|
/// </summary>
|
|
public class SandboxAlarmsAccessor
|
|
{
|
|
/// <summary>
|
|
/// Mirrors <c>AlarmsAccessor.CurrentAsync</c> for compile parity; unsupported at run
|
|
/// time in the central Test Run sandbox.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Token used to cancel the read (unused in the sandbox).</param>
|
|
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/> in the sandbox.</returns>
|
|
public Task<IReadOnlyList<ScriptAlarm>> 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.");
|
|
|
|
/// <summary>
|
|
/// Mirrors <c>AlarmsAccessor.MesReportableAsync</c> for compile parity; unsupported at
|
|
/// run time in the central Test Run sandbox.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Token used to cancel the read (unused in the sandbox).</param>
|
|
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/> in the sandbox.</returns>
|
|
public Task<IReadOnlyList<ScriptAlarm>> MesReportableAsync(CancellationToken cancellationToken = default)
|
|
=> throw new ScriptSandboxException(
|
|
"Alarms.MesReportableAsync() 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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Backs the sandbox <c>Instance</c> 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.
|
|
/// </summary>
|
|
public interface ISandboxInstanceGateway
|
|
{
|
|
/// <summary>
|
|
/// Gets the value of an attribute with the specified canonical name.
|
|
/// </summary>
|
|
/// <param name="canonicalName">The canonical name of the attribute.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
/// <returns>The attribute value, or null if not found.</returns>
|
|
Task<object?> GetAttributeAsync(string canonicalName, CancellationToken ct);
|
|
|
|
/// <summary>
|
|
/// Sets the value of an attribute with the specified canonical name.
|
|
/// </summary>
|
|
/// <param name="canonicalName">The canonical name of the attribute.</param>
|
|
/// <param name="value">The value to set.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
Task SetAttributeAsync(string canonicalName, string value, CancellationToken ct);
|
|
|
|
/// <summary>
|
|
/// Calls a script with the specified canonical name.
|
|
/// </summary>
|
|
/// <param name="canonicalScriptName">The canonical name of the script.</param>
|
|
/// <param name="parameters">Script parameters, or null if none.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
/// <returns>The script result, or null if none.</returns>
|
|
Task<object?> CallScriptAsync(
|
|
string canonicalScriptName, IReadOnlyDictionary<string, object?>? parameters, CancellationToken ct);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="canonicalName">The (already scope-resolved) canonical attribute name.</param>
|
|
/// <param name="targetValueEncoded">The <c>AttributeValueCodec</c>-encoded target value, or null for "any change".</param>
|
|
/// <param name="timeout">Maximum time to wait.</param>
|
|
/// <param name="requireGoodQuality">When true, only a Good-quality value satisfies the wait.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
/// <returns>The wait outcome (matched flag, matched value, quality, timed-out flag).</returns>
|
|
Task<WaitResult> WaitForAttributeAsync(
|
|
string canonicalName, string? targetValueEncoded, TimeSpan timeout, bool requireGoodQuality, CancellationToken ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sandbox mirror of <c>ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ScriptRuntimeContext</c> —
|
|
/// the <c>Instance</c> 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. <c>ExternalSystem</c>/<c>Database</c>/
|
|
/// <c>Scripts</c> run against central's real services regardless of binding;
|
|
/// <c>Notify</c> is a signature-faithful no-op fake.
|
|
/// </summary>
|
|
public class SandboxInstanceContext
|
|
{
|
|
private readonly ISandboxInstanceGateway? _gateway;
|
|
|
|
/// <summary>
|
|
/// Helper for external system calls.
|
|
/// </summary>
|
|
public SandboxExternalHelper ExternalSystem { get; }
|
|
|
|
/// <summary>
|
|
/// Helper for database operations.
|
|
/// </summary>
|
|
public SandboxDatabaseHelper Database { get; }
|
|
|
|
/// <summary>
|
|
/// Helper for sending notifications.
|
|
/// </summary>
|
|
public SandboxNotifyHelper Notify { get; }
|
|
|
|
/// <summary>
|
|
/// Helper for calling scripts.
|
|
/// </summary>
|
|
public SandboxScriptCallHelper Scripts { get; }
|
|
|
|
/// <summary>
|
|
/// Sandbox mirror of <c>ScriptRuntimeContext.Log</c> (step logs design 2026-09-09 D4), so
|
|
/// <c>Instance.Log.Info("…")</c> binds and prints in the Test Run console pane.
|
|
/// </summary>
|
|
public SandboxLog Log { get; } = new();
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the SandboxInstanceContext.
|
|
/// </summary>
|
|
/// <param name="gateway">Gateway for accessing deployed instance data, or null if unbound.</param>
|
|
/// <param name="external">External system helper, or null to create a default.</param>
|
|
/// <param name="database">Database helper, or null to create a default.</param>
|
|
/// <param name="notify">Notification helper, or null to create a default.</param>
|
|
/// <param name="scripts">Script call helper, or null to create a default.</param>
|
|
public SandboxInstanceContext(
|
|
ISandboxInstanceGateway? gateway = null,
|
|
SandboxExternalHelper? external = null,
|
|
SandboxDatabaseHelper? database = null,
|
|
SandboxNotifyHelper? notify = null,
|
|
SandboxScriptCallHelper? scripts = null)
|
|
{
|
|
_gateway = gateway;
|
|
ExternalSystem = external ?? new SandboxExternalHelper(null, "<sandbox>");
|
|
Database = database ?? new SandboxDatabaseHelper(null, "<sandbox>");
|
|
Notify = notify ?? new SandboxNotifyHelper();
|
|
Scripts = scripts ?? new SandboxScriptCallHelper(null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of an attribute.
|
|
/// </summary>
|
|
/// <param name="attributeName">The name of the attribute.</param>
|
|
/// <returns>The attribute value, or null if not found.</returns>
|
|
public Task<object?> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the value of an attribute.
|
|
/// </summary>
|
|
/// <param name="attributeName">The name of the attribute.</param>
|
|
/// <param name="value">The value to set.</param>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calls a sibling script.
|
|
/// </summary>
|
|
/// <param name="scriptName">The name of the script.</param>
|
|
/// <param name="parameters">Script parameters, or null if none.</param>
|
|
/// <returns>The script result, or null if none.</returns>
|
|
public Task<object?> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="canonicalName">The (already scope-resolved) canonical attribute name.</param>
|
|
/// <param name="targetValueEncoded">The codec-encoded target value, or null for "any change".</param>
|
|
/// <param name="timeout">Maximum time to wait.</param>
|
|
/// <param name="requireGoodQuality">When true, only a Good-quality value satisfies the wait.</param>
|
|
/// <returns>The wait outcome.</returns>
|
|
public Task<WaitResult> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sandbox mirror of <c>ScriptRuntimeContext.ScriptCallHelper</c> —
|
|
/// <c>Scripts.CallShared(...)</c>. Compiles and runs the named shared script in
|
|
/// the same sandbox via the wired delegate.
|
|
/// </summary>
|
|
public class SandboxScriptCallHelper
|
|
{
|
|
private readonly Func<string, IReadOnlyDictionary<string, object?>?, CancellationToken, Task<object?>>? _callShared;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the SandboxScriptCallHelper.
|
|
/// </summary>
|
|
/// <param name="callShared">Delegate for calling shared scripts, or null if not available.</param>
|
|
public SandboxScriptCallHelper(
|
|
Func<string, IReadOnlyDictionary<string, object?>?, CancellationToken, Task<object?>>? callShared)
|
|
{
|
|
_callShared = callShared;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calls a shared script.
|
|
/// </summary>
|
|
/// <param name="scriptName">The name of the shared script.</param>
|
|
/// <param name="parameters">Script parameters, or null if none.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The script result, or null if none.</returns>
|
|
public Task<object?> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sandbox mirror of <c>ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.AttributeAccessor</c> —
|
|
/// scope-aware <c>Attributes["X"]</c> access anchored at a canonical-name prefix.
|
|
/// </summary>
|
|
public class SandboxAttributeAccessor
|
|
{
|
|
private readonly SandboxInstanceContext _ctx;
|
|
|
|
/// <summary>
|
|
/// The scope prefix for attribute resolution.
|
|
/// </summary>
|
|
public string ScopePrefix { get; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the SandboxAttributeAccessor.
|
|
/// </summary>
|
|
/// <param name="ctx">The sandbox instance context.</param>
|
|
/// <param name="prefix">The scope prefix for attribute names.</param>
|
|
public SandboxAttributeAccessor(SandboxInstanceContext ctx, string prefix)
|
|
{
|
|
_ctx = ctx;
|
|
ScopePrefix = prefix;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a key to its fully qualified name within the current scope.
|
|
/// </summary>
|
|
/// <param name="key">The attribute key.</param>
|
|
/// <returns>The fully qualified attribute name.</returns>
|
|
public string Resolve(string key) =>
|
|
ScopePrefix.Length == 0 ? key : ScopePrefix + "." + key;
|
|
|
|
/// <summary>
|
|
/// Gets or sets an attribute value by key.
|
|
/// </summary>
|
|
/// <param name="key">The attribute key.</param>
|
|
/// <returns>The attribute value, or null if not found.</returns>
|
|
public object? this[string key]
|
|
{
|
|
get => _ctx.GetAttribute(Resolve(key)).GetAwaiter().GetResult();
|
|
set => _ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an attribute value asynchronously.
|
|
/// </summary>
|
|
/// <param name="key">The attribute key.</param>
|
|
/// <returns>The attribute value, or null if not found.</returns>
|
|
public Task<object?> GetAsync(string key) => _ctx.GetAttribute(Resolve(key));
|
|
|
|
/// <summary>
|
|
/// Sets an attribute value asynchronously.
|
|
/// </summary>
|
|
/// <param name="key">The attribute key.</param>
|
|
/// <param name="value">The value to set, or null.</param>
|
|
/// <returns>A task representing the operation.</returns>
|
|
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.
|
|
|
|
/// <summary>
|
|
/// Sandbox stand-in for <c>AttributeAccessor.WriteBatchAndWaitAsync</c>: present
|
|
/// for editor/compile parity, throws <see cref="ScriptSandboxException"/> when run
|
|
/// in Test Run (no device batch-write transport here).
|
|
/// </summary>
|
|
/// <param name="values">The attribute values to write as a batch.</param>
|
|
/// <param name="flagKey">The flag attribute key to set to signal completion.</param>
|
|
/// <param name="flagValue">The value to set on the flag attribute.</param>
|
|
/// <param name="responseKey">The attribute key to observe for the device's response.</param>
|
|
/// <param name="responseValue">The response value to wait for.</param>
|
|
/// <param name="timeout">The maximum time to wait for the response.</param>
|
|
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/> in the sandbox.</returns>
|
|
public Task<bool> WriteBatchAndWaitAsync(
|
|
IReadOnlyDictionary<string, object?> values, string flagKey, object? flagValue,
|
|
string responseKey, object? responseValue, TimeSpan timeout)
|
|
=> throw NotInSandbox(nameof(WriteBatchAndWaitAsync));
|
|
|
|
/// <summary>
|
|
/// Sandbox stand-in for the reset-aware <c>AttributeAccessor.WriteBatchAndWaitAsync</c>
|
|
/// overload (Gitea #36): present for editor/compile parity, throws
|
|
/// <see cref="ScriptSandboxException"/> when run in Test Run (no device batch-write transport).
|
|
/// </summary>
|
|
/// <param name="values">The attribute values to write as a batch.</param>
|
|
/// <param name="flagKey">The flag attribute key to set to signal completion.</param>
|
|
/// <param name="flagValue">The value to set on the flag attribute.</param>
|
|
/// <param name="responseKey">The attribute key to observe for the device's response.</param>
|
|
/// <param name="responseValue">The response value to wait for.</param>
|
|
/// <param name="timeout">The maximum time to wait for the response.</param>
|
|
/// <param name="resetKey">The attribute written first and waited on before the batch.</param>
|
|
/// <param name="resetValue">
|
|
/// The reset value the device must echo before the batch is written. MUST NOT be null — the
|
|
/// real accessor throws <see cref="System.ArgumentException"/>, because a null would write an
|
|
/// empty string and then wait on "any change", reinstating the race the overload closes.
|
|
/// </param>
|
|
/// <param name="resetTimeout">The maximum time to wait for the reset echo; <c>null</c> means <paramref name="timeout"/>.</param>
|
|
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/> in the sandbox.</returns>
|
|
public Task<HandshakeResult> WriteBatchAndWaitAsync(
|
|
IReadOnlyDictionary<string, object?> values, string flagKey, object? flagValue,
|
|
string responseKey, object? responseValue, TimeSpan timeout,
|
|
string resetKey, object? resetValue, TimeSpan? resetTimeout = null)
|
|
=> throw NotInSandbox(nameof(WriteBatchAndWaitAsync));
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// <see cref="ScriptSandboxException"/> when the Test Run has no instance bound.
|
|
/// </summary>
|
|
/// <param name="key">The attribute key to observe.</param>
|
|
/// <param name="targetValue">The value to wait for (codec-normalized), or null for "any change".</param>
|
|
/// <param name="timeout">The maximum time to wait.</param>
|
|
/// <param name="requireGoodQuality">Whether the attribute must also have good quality to satisfy the wait.</param>
|
|
/// <returns>True when the attribute reached the target within the timeout; false on timeout.</returns>
|
|
public async Task<bool> WaitAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false)
|
|
=> (await _ctx.WaitForAttribute(
|
|
Resolve(key), AttributeValueCodec.Encode(targetValue), timeout, requireGoodQuality)).Matched;
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="ScriptSandboxException"/>.
|
|
/// </summary>
|
|
/// <param name="key">The attribute key to observe.</param>
|
|
/// <param name="predicate">The predicate the attribute value must satisfy.</param>
|
|
/// <param name="timeout">The maximum time to wait.</param>
|
|
/// <param name="requireGoodQuality">Whether the attribute must also have good quality to satisfy the wait.</param>
|
|
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/> in the sandbox.</returns>
|
|
public Task<bool> WaitAsync(string key, Func<object?, bool> predicate, TimeSpan timeout, bool requireGoodQuality = false)
|
|
=> throw PredicateWaitNotRoutable(nameof(WaitAsync));
|
|
|
|
/// <summary>
|
|
/// Value-equality attribute wait returning the full <see cref="WaitResult"/> — routed
|
|
/// to the bound deployed instance cross-site. Needs a bound instance; throws
|
|
/// <see cref="ScriptSandboxException"/> when the Test Run has no instance bound.
|
|
/// </summary>
|
|
/// <param name="key">The attribute key to observe.</param>
|
|
/// <param name="targetValue">The value to wait for (codec-normalized), or null for "any change".</param>
|
|
/// <param name="timeout">The maximum time to wait.</param>
|
|
/// <param name="requireGoodQuality">Whether the attribute must also have good quality to satisfy the wait.</param>
|
|
/// <returns>The wait outcome (matched flag, matched value, quality, timed-out flag).</returns>
|
|
public Task<WaitResult> WaitForAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false)
|
|
=> _ctx.WaitForAttribute(Resolve(key), AttributeValueCodec.Encode(targetValue), timeout, requireGoodQuality);
|
|
|
|
/// <summary>
|
|
/// Predicate-form attribute wait returning the full <see cref="WaitResult"/>.
|
|
/// Unsupported in Test Run for the same reason as the predicate <see cref="WaitAsync(string, Func{object?, bool}, TimeSpan, bool)"/>
|
|
/// overload — an in-process predicate cannot be routed to the remote site.
|
|
/// </summary>
|
|
/// <param name="key">The attribute key to observe.</param>
|
|
/// <param name="predicate">The predicate the attribute value must satisfy.</param>
|
|
/// <param name="timeout">The maximum time to wait.</param>
|
|
/// <param name="requireGoodQuality">Whether the attribute must also have good quality to satisfy the wait.</param>
|
|
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/> in the sandbox.</returns>
|
|
public Task<WaitResult> WaitForAsync(string key, Func<object?, bool> 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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sandbox mirror of <c>ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.CompositionAccessor</c> —
|
|
/// a view of one composition: its attributes plus an invokable <c>CallScript</c>.
|
|
/// </summary>
|
|
public class SandboxCompositionAccessor
|
|
{
|
|
private readonly SandboxInstanceContext _ctx;
|
|
|
|
/// <summary>
|
|
/// The path to the composition within the instance hierarchy.
|
|
/// </summary>
|
|
public string Path { get; }
|
|
|
|
/// <summary>
|
|
/// Accessor for attributes within the composition.
|
|
/// </summary>
|
|
public SandboxAttributeAccessor Attributes { get; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the SandboxCompositionAccessor.
|
|
/// </summary>
|
|
/// <param name="ctx">The sandbox instance context.</param>
|
|
/// <param name="path">The path to the composition within the instance hierarchy.</param>
|
|
public SandboxCompositionAccessor(SandboxInstanceContext ctx, string path)
|
|
{
|
|
_ctx = ctx;
|
|
Path = path;
|
|
Attributes = new SandboxAttributeAccessor(ctx, path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a script name to its fully qualified name within the composition.
|
|
/// </summary>
|
|
/// <param name="scriptName">The script name.</param>
|
|
/// <returns>The fully qualified script name.</returns>
|
|
public string ResolveScript(string scriptName) =>
|
|
Path.Length == 0 ? scriptName : Path + "." + scriptName;
|
|
|
|
/// <summary>
|
|
/// Calls a script within the composition.
|
|
/// </summary>
|
|
/// <param name="scriptName">The name of the script.</param>
|
|
/// <param name="parameters">Script parameters, or null if none.</param>
|
|
/// <returns>The script result, or null if none.</returns>
|
|
public Task<object?> CallScript(string scriptName, object? parameters = null)
|
|
=> _ctx.CallScript(ResolveScript(scriptName), parameters);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sandbox mirror of <c>ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ChildrenAccessor</c> —
|
|
/// dictionary-style access to child compositions.
|
|
/// </summary>
|
|
public class SandboxChildrenAccessor
|
|
{
|
|
private readonly SandboxInstanceContext _ctx;
|
|
private readonly string _selfPath;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the SandboxChildrenAccessor.
|
|
/// </summary>
|
|
/// <param name="ctx">The sandbox instance context.</param>
|
|
/// <param name="selfPath">The path to the parent composition.</param>
|
|
public SandboxChildrenAccessor(SandboxInstanceContext ctx, string selfPath)
|
|
{
|
|
_ctx = ctx;
|
|
_selfPath = selfPath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a child composition by name.
|
|
/// </summary>
|
|
/// <param name="compositionName">The name of the child composition.</param>
|
|
/// <returns>An accessor for the child composition.</returns>
|
|
public SandboxCompositionAccessor this[string compositionName]
|
|
{
|
|
get
|
|
{
|
|
var path = _selfPath.Length == 0
|
|
? compositionName
|
|
: _selfPath + "." + compositionName;
|
|
return new SandboxCompositionAccessor(_ctx, path);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Distinct exception so the Test Run pipeline can label sandbox-only
|
|
/// limitations differently from genuine runtime errors in user code.
|
|
/// </summary>
|
|
public class ScriptSandboxException : Exception
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the ScriptSandboxException.
|
|
/// </summary>
|
|
/// <param name="message">The exception message.</param>
|
|
public ScriptSandboxException(string message) : base(message) { }
|
|
}
|