using System.Collections.Concurrent; using Microsoft.Extensions.Logging; using ZB.MOM.WW.OtOpcUa.Commons.Engines; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms; using ZB.MOM.WW.OtOpcUa.Core.Scripting; using SerilogLogger = Serilog.ILogger; namespace ZB.MOM.WW.OtOpcUa.Host.Engines; /// /// F9b — production binding. Compiles each unique /// predicate once via against /// and caches the resulting evaluator. Predicates are /// pure functions returning bool: /// throws so a misbehaving script can't smuggle a side effect into alarm evaluation. /// /// Failure modes (compile error, sandbox violation, runtime exception, timeout) all surface /// as ; /// preserves the prior state on failure (does not flip Active/Inactive). /// public sealed class RoslynScriptedAlarmEvaluator : IScriptedAlarmEvaluator, IDisposable { private readonly ConcurrentDictionary> _cache = new(StringComparer.Ordinal); private readonly ILogger _logger; private readonly SerilogLogger _scriptRoot; private readonly TimeSpan _runTimeout; private bool _disposed; /// Initializes a new instance of the Roslyn scripted alarm evaluator. /// Logger for diagnostic messages (host diagnostics). /// Root script logger; user ctx.Logger.* output flows through this to the Script-log page. /// Optional timeout for script evaluation; defaults to 2 seconds. public RoslynScriptedAlarmEvaluator( ILogger logger, ScriptRootLogger scriptRoot, TimeSpan? runTimeout = null) { _logger = logger; _scriptRoot = (scriptRoot ?? throw new ArgumentNullException(nameof(scriptRoot))).Logger; _runTimeout = runTimeout ?? TimeSpan.FromSeconds(2); } /// Evaluates a scripted alarm predicate against provided dependencies. /// The alarm identifier for logging purposes. /// The predicate expression to evaluate. /// Variables available to the predicate expression. /// Evaluation result with success flag and active state or failure reason. public ScriptedAlarmEvalResult Evaluate(string alarmId, string predicate, IReadOnlyDictionary dependencies) { if (_disposed) return ScriptedAlarmEvalResult.Failure("evaluator disposed"); if (string.IsNullOrWhiteSpace(predicate)) return ScriptedAlarmEvalResult.Failure("empty predicate"); ScriptEvaluator evaluator; try { evaluator = _cache.GetOrAdd(predicate, ScriptEvaluator.Compile); } catch (CompilationErrorException ex) { _logger.LogWarning(ex, "Alarm {Id}: predicate compile failed", alarmId); return ScriptedAlarmEvalResult.Failure($"compile error: {ex.Message}"); } catch (ScriptSandboxViolationException ex) { _logger.LogWarning(ex, "Alarm {Id}: predicate sandbox violation", alarmId); return ScriptedAlarmEvalResult.Failure($"sandbox violation: {ex.Message}"); } catch (Exception ex) { _logger.LogWarning(ex, "Alarm {Id}: predicate compile threw", alarmId); return ScriptedAlarmEvalResult.Failure($"compile failure: {ex.Message}"); } var readCache = BuildReadCache(dependencies); // Per-evaluation script logger: bind both ScriptId and AlarmId from the alarm id so the // Script-log page can attribute each line to the owning scripted alarm. var scriptLog = _scriptRoot .ForContext(ScriptLoggerFactory.ScriptIdProperty, alarmId) .ForContext(ScriptLoggerFactory.AlarmIdProperty, alarmId); var context = new AlarmPredicateContext(readCache, scriptLog); try { using var cts = new CancellationTokenSource(_runTimeout); var active = evaluator.RunAsync(context, cts.Token).GetAwaiter().GetResult(); return ScriptedAlarmEvalResult.Ok(active); } catch (OperationCanceledException) { return ScriptedAlarmEvalResult.Failure($"predicate timed out after {_runTimeout.TotalSeconds:F1}s"); } catch (Exception ex) { _logger.LogWarning(ex, "Alarm {Id}: predicate execution threw", alarmId); return ScriptedAlarmEvalResult.Failure($"predicate threw: {ex.Message}"); } } private static IReadOnlyDictionary BuildReadCache( IReadOnlyDictionary deps) { var nowUtc = DateTime.UtcNow; var cache = new Dictionary(StringComparer.Ordinal); foreach (var kv in deps) { cache[kv.Key] = new DataValueSnapshot(kv.Value, StatusCode: 0u, SourceTimestampUtc: nowUtc, ServerTimestampUtc: nowUtc); } return cache; } /// Disposes the evaluator and all cached script evaluators. public void Dispose() { if (_disposed) return; _disposed = true; foreach (var ev in _cache.Values) { try { ev.Dispose(); } catch { /* best-effort */ } } _cache.Clear(); } }