d7c0135d0b
Review m-6. The XML doc said the token is cancelled on timeout only. ScriptRunGuard links it to the caller's token (CreateLinkedTokenSource(ct)), so it is also cancelled on shutdown or when the evaluating actor stops mid-run. The doc now says so. Refs #731 #734 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
142 lines
7.5 KiB
C#
142 lines
7.5 KiB
C#
using Serilog;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
|
|
|
/// <summary>
|
|
/// The API user scripts see as the global <c>ctx</c>. Abstract — concrete subclasses
|
|
/// (e.g. <c>VirtualTagScriptContext</c>, <c>AlarmScriptContext</c>) plug in the
|
|
/// actual tag-backend + logger + virtual-tag writer for each evaluation. By design
|
|
/// (see <c>docs/v2/plan.md</c>), scripts can read any tag, write only to virtual
|
|
/// tags, and have no other .NET reach — no HttpClient, no File, no Process, no
|
|
/// reflection.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Every member on this type MUST be serializable in the narrow sense that
|
|
/// <c>DependencyExtractor</c> can recognize tag-access call sites from the
|
|
/// script AST. Method names used from scripts are locked — renaming
|
|
/// <see cref="GetTag"/> or <see cref="SetVirtualTag"/> is a breaking change for every
|
|
/// authored script and the dependency extractor must update in lockstep.
|
|
/// </para>
|
|
/// <para>
|
|
/// New helpers (<see cref="Now"/>, <see cref="Deadband"/>) are additive: adding a
|
|
/// method doesn't invalidate existing scripts. Do not remove or rename without a
|
|
/// plan-level decision + migration for authored scripts.
|
|
/// </para>
|
|
/// </remarks>
|
|
public abstract class ScriptContext
|
|
{
|
|
/// <summary>
|
|
/// Read a tag's current value + quality + source timestamp. Path syntax is
|
|
/// <c>Enterprise/Site/Area/Line/Equipment/TagName</c> (forward-slash delimited,
|
|
/// matching the Equipment-namespace browse tree). Returns a
|
|
/// <see cref="DataValueSnapshot"/> so scripts branch on quality without a second
|
|
/// call.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <paramref name="path"/> MUST be a string literal in the script source — dynamic
|
|
/// paths (variables, concatenation, method-returned strings) are rejected at
|
|
/// publish by <c>DependencyExtractor</c>. This is intentional: the static
|
|
/// dependency set is required for the change-driven scheduler to subscribe to the
|
|
/// right upstream tags at load time.
|
|
/// <para>
|
|
/// The reserved <c>{{equip}}</c> token may appear in the path literal; it is
|
|
/// substituted with the owning equipment's tag-base prefix at deployment time.
|
|
/// See <c>docs/ScriptEditor.md</c> — Equipment-relative tag paths.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="path">The literal tag path to read.</param>
|
|
/// <returns>A snapshot of the tag's current value, quality, and source timestamp.</returns>
|
|
public abstract DataValueSnapshot GetTag(string path);
|
|
|
|
/// <summary>
|
|
/// Write a value to a virtual tag. Operator scripts cannot write to driver-sourced
|
|
/// tags — the OPC UA dispatch in <c>DriverNodeManager</c> rejects that separately
|
|
/// with <c>BadUserAccessDenied</c>. This method is the only write path
|
|
/// virtual tags have.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Path rules identical to <see cref="GetTag"/> — literal only, dependency
|
|
/// extractor tracks the write targets so the engine knows what downstream
|
|
/// subscribers to notify. The <c>{{equip}}</c> token is supported in the same way
|
|
/// as for <see cref="GetTag"/>.
|
|
/// </remarks>
|
|
/// <param name="path">The literal tag path to write to.</param>
|
|
/// <param name="value">The value to write to the virtual tag.</param>
|
|
public abstract void SetVirtualTag(string path, object? value);
|
|
|
|
/// <summary>
|
|
/// Current UTC timestamp. Prefer this over <see cref="DateTime.UtcNow"/> in
|
|
/// scripts so the harness can supply a deterministic clock for tests.
|
|
/// </summary>
|
|
public abstract DateTime Now { get; }
|
|
|
|
/// <summary>
|
|
/// Per-script Serilog logger. Output lands in the dedicated <c>scripts-*.log</c>
|
|
/// sink with structured property <c>ScriptName</c> = the script's configured name.
|
|
/// Use at error level to surface problems; main <c>opcua-*.log</c> receives a
|
|
/// companion WARN entry so operators see script errors in the primary log.
|
|
/// </summary>
|
|
public abstract ILogger Logger { get; }
|
|
|
|
/// <summary>
|
|
/// The current run's cancellation token. The run guard cancels it when the run exceeds its wall-clock
|
|
/// budget, and it is linked to the caller's token, so it is also cancelled when the caller cancels (host
|
|
/// shutdown, an actor stopping mid-run). A long-running script can poll <c>ctx.CancellationToken.IsCancellationRequested</c> (or call
|
|
/// <c>ThrowIfCancellationRequested()</c>) and end its own abandoned run instead of holding a pool thread
|
|
/// until it returns. Cooperative only: a script that never looks at it still runs to completion as an
|
|
/// orphan. <see cref="CancellationToken.None"/> outside a run.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Scoped per run through an <see cref="AsyncLocal{T}"/>, not stored on the context, because a context
|
|
/// object can be reused across runs (the scripted-alarm engine keeps one per alarm). An abandoned orphan
|
|
/// therefore keeps seeing its own cancelled token even after a newer run starts on the same context.
|
|
/// </remarks>
|
|
public CancellationToken CancellationToken => s_runToken.Value;
|
|
|
|
private static readonly AsyncLocal<CancellationToken> s_runToken = new();
|
|
|
|
/// <summary>
|
|
/// Makes <paramref name="token"/> the script-visible <see cref="CancellationToken"/> on the calling
|
|
/// execution context until the returned scope is disposed. Called by <c>ScriptEvaluator.RunAsync</c>
|
|
/// around the compiled delegate.
|
|
/// </summary>
|
|
/// <param name="token">The run token.</param>
|
|
/// <returns>A scope that restores the previous token.</returns>
|
|
internal static RunTokenScope EnterRun(CancellationToken token)
|
|
{
|
|
var previous = s_runToken.Value;
|
|
s_runToken.Value = token;
|
|
return new RunTokenScope(previous);
|
|
}
|
|
|
|
/// <summary>Restores the token that was current before <see cref="EnterRun"/>.</summary>
|
|
internal readonly struct RunTokenScope : IDisposable
|
|
{
|
|
private readonly CancellationToken _previous;
|
|
|
|
/// <summary>Captures the token to restore.</summary>
|
|
/// <param name="previous">The token current before the run.</param>
|
|
public RunTokenScope(CancellationToken previous) => _previous = previous;
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose() => s_runToken.Value = _previous;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deadband helper — returns <c>true</c> when <paramref name="current"/> differs
|
|
/// from <paramref name="previous"/> by more than <paramref name="tolerance"/>.
|
|
/// Useful for alarm predicates that shouldn't flicker on small noise. Pure
|
|
/// function; no side effects.
|
|
/// </summary>
|
|
/// <param name="current">The current value to check.</param>
|
|
/// <param name="previous">The previous value to compare against.</param>
|
|
/// <param name="tolerance">The minimum change magnitude to detect (must be >= 0).
|
|
/// Negative values cause the function to always return true; NaN always returns false.</param>
|
|
/// <returns><c>true</c> when <paramref name="current"/> differs from <paramref name="previous"/>
|
|
/// by more than <paramref name="tolerance"/>; otherwise <c>false</c>.</returns>
|
|
public static bool Deadband(double current, double previous, double tolerance)
|
|
=> Math.Abs(current - previous) > tolerance;
|
|
}
|