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);
}
}