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