/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using Microsoft.Extensions.Logging; namespace Opc.Ua.Server { /// /// The interface that a server exposes to objects that it contains. /// public static partial class ServerUtils { private enum EventType { WriteValue, CreateItem, ModifyItem, QueueValue, FilterValue, DiscardValue, PublishValue } private class Event { public DateTime Timestamp; public EventType EventType; public NodeId NodeId; public uint ServerHandle; public DataValue Value; public MonitoringParameters? Parameters; public MonitoringMode MonitoringMode; } private static readonly Queue s_events = new(); private static bool s_eventsEnabled; /// /// Whether event queuing is enabled. /// public static bool EventsEnabled { get => s_eventsEnabled; set { if (s_eventsEnabled != value && !value) { lock (s_events) { s_events.Clear(); } } s_eventsEnabled = value; } } /// /// Reports a value written. /// public static void ReportWriteValue(NodeId nodeId, DataValue value, StatusCode error) { if (!s_eventsEnabled) { return; } lock (s_events) { var e = new Event { EventType = EventType.WriteValue, NodeId = nodeId, ServerHandle = 0, Timestamp = DateTime.UtcNow, Value = value, Parameters = null, MonitoringMode = MonitoringMode.Disabled }; if (StatusCode.IsBad(error)) { e.Value = new DataValue(value.WrappedValue, error); } s_events.Enqueue(e); } } /// /// Reports a value queued. /// public static void ReportQueuedValue(NodeId nodeId, uint serverHandle, DataValue value) { if (!s_eventsEnabled) { return; } lock (s_events) { var e = new Event { EventType = EventType.QueueValue, NodeId = nodeId, ServerHandle = serverHandle, Timestamp = DateTime.UtcNow, Value = value, Parameters = null, MonitoringMode = MonitoringMode.Disabled }; s_events.Enqueue(e); } } /// /// Reports a value excluded by the filter. /// public static void ReportFilteredValue(NodeId nodeId, uint serverHandle, DataValue value) { if (!s_eventsEnabled) { return; } lock (s_events) { var e = new Event { EventType = EventType.FilterValue, NodeId = nodeId, ServerHandle = serverHandle, Timestamp = DateTime.UtcNow, Value = value, Parameters = null, MonitoringMode = MonitoringMode.Disabled }; s_events.Enqueue(e); } } /// /// Reports a value discarded because of queue overflow. /// public static void ReportDiscardedValue(NodeId nodeId, uint serverHandle, DataValue value) { if (!s_eventsEnabled) { return; } lock (s_events) { var e = new Event { EventType = EventType.DiscardValue, NodeId = nodeId, ServerHandle = serverHandle, Timestamp = DateTime.UtcNow, Value = value, Parameters = null, MonitoringMode = MonitoringMode.Disabled }; s_events.Enqueue(e); } } /// /// Reports a value published. /// public static void ReportPublishValue(NodeId nodeId, uint serverHandle, DataValue value) { if (!s_eventsEnabled) { return; } lock (s_events) { var e = new Event { EventType = EventType.PublishValue, NodeId = nodeId, ServerHandle = serverHandle, Timestamp = DateTime.UtcNow, Value = value, Parameters = null, MonitoringMode = MonitoringMode.Disabled }; s_events.Enqueue(e); } } /// /// Reports a new monitored item. /// public static void ReportCreateMonitoredItem( NodeId nodeId, uint serverHandle, double samplingInterval, uint queueSize, bool discardOldest, MonitoringFilter filter, MonitoringMode monitoringMode) { if (!s_eventsEnabled) { return; } lock (s_events) { var e = new Event { EventType = EventType.CreateItem, NodeId = nodeId, ServerHandle = serverHandle, Timestamp = DateTime.UtcNow, Value = default, Parameters = new MonitoringParameters { SamplingInterval = samplingInterval, QueueSize = queueSize, DiscardOldest = discardOldest, Filter = new ExtensionObject(filter) }, MonitoringMode = monitoringMode }; s_events.Enqueue(e); } } /// /// Reports a modified monitored item. /// public static void ReportModifyMonitoredItem( NodeId nodeId, uint serverHandle, double samplingInterval, uint queueSize, bool discardOldest, MonitoringFilter filter, MonitoringMode monitoringMode) { if (!s_eventsEnabled) { return; } lock (s_events) { var e = new Event { EventType = EventType.ModifyItem, NodeId = nodeId, ServerHandle = serverHandle, Timestamp = DateTime.UtcNow, Value = default, Parameters = new MonitoringParameters { SamplingInterval = samplingInterval, QueueSize = queueSize, DiscardOldest = discardOldest, Filter = new ExtensionObject(filter) }, MonitoringMode = monitoringMode }; s_events.Enqueue(e); } } /// /// Fills in the diagnostic information after an error. /// public static uint CreateError( uint code, OperationContext context, List diagnosticInfos, int index, ILogger logger) { var error = new ServiceResult(code); if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos[index] = new DiagnosticInfo( error, context.DiagnosticsMask, false, context.StringTable, logger); } return error.Code; } /// /// Fills in the diagnostic information after an error. /// public static bool CreateError( uint code, List results, List diagnosticInfos, OperationContext context, ILogger logger) { var error = new ServiceResult(code); results.Add(error.Code); if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos.Add( new DiagnosticInfo(error, context.DiagnosticsMask, false, context.StringTable, logger)); return true; } return false; } /// /// Fills in the diagnostic information after an error. /// public static bool CreateError( uint code, List results, List diagnosticInfos, int index, OperationContext context, ILogger logger) { var error = new ServiceResult(code); results[index] = error.Code; if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos[index] = new DiagnosticInfo( error, context.DiagnosticsMask, false, context.StringTable, logger); return true; } return false; } /// /// Creates a place holder in the lists for the results. /// public static void CreateSuccess( List results, List diagnosticInfos, OperationContext context) { results.Add(StatusCodes.Good); if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos.Add(null!); } } /// /// Creates a collection of diagnostics from a set of errors. /// public static List? CreateDiagnosticInfoCollection( OperationContext context, IList errors, ILogger logger) { // all done if no diagnostics requested. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) == 0) { return null; } // create diagnostics. var results = new List(errors.Count); foreach (ServiceResult error in errors) { if (ServiceResult.IsBad(error)) { results.Add(new DiagnosticInfo( error, context.DiagnosticsMask, false, context.StringTable, logger)); } else { results.Add(null!); } } return results; } /// /// Creates a collection of status codes and diagnostics from a set of errors. /// public static List CreateStatusCodeCollection( OperationContext context, IList errors, out List diagnosticInfos, ILogger logger) { diagnosticInfos = null!; bool noErrors = true; var results = new List(errors.Count); foreach (ServiceResult error in errors) { if (ServiceResult.IsBad(error)) { results.Add(error.Code); noErrors = false; } else { results.Add(StatusCodes.Good); } } // only generate diagnostics if errors exist. if (noErrors) { diagnosticInfos = CreateDiagnosticInfoCollection(context, errors, logger)!; } return results; } /// /// Creates the diagnostic info and translates any strings. /// [Obsolete("Use CreateDiagnosticInfo with ILogger")] public static DiagnosticInfo CreateDiagnosticInfo( IServerInternal server, OperationContext context, ServiceResult error) { ILogger logger = AmbientMessageContext.Telemetry.CreateLogger("ServerUtils"); return CreateDiagnosticInfo(server, context, error, logger)!; } /// /// Creates the diagnostic info and translates any strings. /// /// The server. /// The context containing the string stable. /// The error to translate. /// A contextual logger to log to /// The diagnostics with references to the strings in the context string table. public static DiagnosticInfo? CreateDiagnosticInfo( IServerInternal server, OperationContext context, ServiceResult error, ILogger logger) { if (error == null) { return null; } ServiceResult? translatedError = error; // Only translate if ServiceLocalizedText is set, as DiagnosticInfo only uses // the translation for ServiceLocalizedText, not OperationLocalizedText if ((context.DiagnosticsMask & DiagnosticsMasks.ServiceLocalizedText) != 0) { translatedError = server.ResourceManager.Translate(context.PreferredLocales, error); } return new DiagnosticInfo( translatedError!, context.DiagnosticsMask, false, context.StringTable, logger); } } }