using System.Collections.Concurrent; using Serilog; using Serilog.Core; using ZB.MOM.WW.OtOpcUa.Commons.Engines; using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms; using ZB.MOM.WW.OtOpcUa.Core.VirtualTags; namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// /// over today's in-process machinery: a /// per context kind and one (lmxopcua#780, design §11). It produces the outcomes and /// reason strings the seams' own in-process paths produce, which makes it the parity baseline the out-of-process /// runner is tested against, and the in-process half of the future shadow mode (P2). /// /// /// The seams do not route their path through this class: they keep their /// existing guard code, so the default mode is unchanged byte for byte. Unlike those paths, this runner remembers /// compile failures by source for every kind (Calculation already did, R07:P4). /// public sealed class InProcessScriptRunner : IScriptRunner, IDisposable { private readonly CompiledScriptCache _valueCache = new(); private readonly CompiledScriptCache _predicateCache = new(); private readonly ConcurrentDictionary _compileFailures = new(StringComparer.Ordinal); private readonly ScriptRunGuard _guard; /// Initializes the runner. /// Run-guard knobs; null ⇒ defaults. /// Where quarantine lines go; null ⇒ Serilog's global logger. public InProcessScriptRunner(ScriptingOptions? scripting = null, ILogger? logger = null) { _guard = new ScriptRunGuard(scripting, logger); } /// The run guard (diagnostics / tests). public ScriptRunGuard RunGuard => _guard; /// public async Task RunAsync(ScriptInvocation invocation, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(invocation); var key = ScopedKey(invocation.Kind, invocation.Key); var failureKey = ScopedKey(invocation.Kind, invocation.Source); if (_compileFailures.TryGetValue(failureKey, out var known)) return ScriptRunOutcome.Failed(known); var logger = invocation.ScriptLogger ?? Logger.None; try { switch (invocation.Kind) { case ScriptContextKind.VirtualTag: case ScriptContextKind.Calculation: { var evaluator = await Task.Run(() => _valueCache.GetOrCompile(invocation.Source), ct).ConfigureAwait(false); var context = new VirtualTagContext(invocation.Inputs, static (_, _) => { }, logger); var value = await _guard.RunAsync(key, invocation.Source, evaluator, context, invocation.Timeout, ct) .ConfigureAwait(false); return ScriptRunOutcome.Ok(value); } case ScriptContextKind.AlarmPredicate: { var evaluator = await Task.Run(() => _predicateCache.GetOrCompile(invocation.Source), ct).ConfigureAwait(false); var context = new AlarmPredicateContext(invocation.Inputs, logger); var value = await _guard.RunAsync(key, invocation.Source, evaluator, context, invocation.Timeout, ct) .ConfigureAwait(false); return ScriptRunOutcome.Ok(value); } default: return ScriptRunOutcome.Failed($"unsupported script kind {invocation.Kind}"); } } catch (CompilationErrorException ex) { return RememberCompileFailure(failureKey, $"compile error: {ex.Message}"); } catch (ScriptSandboxViolationException ex) { return RememberCompileFailure(failureKey, $"sandbox violation: {ex.Message}"); } catch (ScriptTimeoutException) { return ScriptRunOutcome.Failed($"{ScriptEvalFailureReasons.TimedOutPrefix}{invocation.Timeout.TotalSeconds:F1}s"); } catch (ScriptRunRefusedException ex) { return ScriptRunOutcome.Failed(ex.Message); } catch (ScriptQuarantinedException ex) { return ScriptRunOutcome.Failed(ex.Message); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { return ScriptRunOutcome.Failed("evaluation cancelled"); } catch (Exception ex) { return ScriptRunOutcome.Failed($"script threw: {ex.Message}"); } } /// public void Reset(ScriptContextKind kind) { var prefix = ScopedKey(kind, string.Empty); foreach (var k in _compileFailures.Keys) if (k.StartsWith(prefix, StringComparison.Ordinal)) _compileFailures.TryRemove(k, out _); foreach (var k in _guard.QuarantinedKeys) if (k.StartsWith(prefix, StringComparison.Ordinal)) _guard.Reset(k); } /// public IReadOnlyCollection QuarantinedKeys(ScriptContextKind kind) { var prefix = ScopedKey(kind, string.Empty); return _guard.QuarantinedKeys .Where(k => k.StartsWith(prefix, StringComparison.Ordinal)) .Select(k => k[prefix.Length..]) .ToArray(); } /// public void Dispose() { _valueCache.Dispose(); _predicateCache.Dispose(); } /// The runner-internal key: {kind}:{key}. /// The context kind. /// The seam's key. /// The namespaced key. public static string ScopedKey(ScriptContextKind kind, string key) => $"{(int)kind}:{key}"; private ScriptRunOutcome RememberCompileFailure(string failureKey, string reason) { _compileFailures.TryAdd(failureKey, reason); return ScriptRunOutcome.Failed(reason); } }