/* ======================================================================== * 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.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Opc.Ua.Server.Historian; using Opc.Ua.Server.NodeManager; namespace Opc.Ua.Server { /// /// A sample implementation of the IAsyncNodeManager interface. /// /// /// This node manager is a base class used in multiple samples. It implements the IAsyncNodeManager /// interface and allows sub-classes to override only the methods that they need. This example /// is not part of the SDK because most real implementations of a INodeManager will need to /// modify the behavior of the base class. /// public class AsyncCustomNodeManager : IAsyncNodeManager, INodeIdFactory, IDisposable { /// /// Initializes the node manager. /// protected AsyncCustomNodeManager( IServerInternal server, params string[] namespaceUris) : this(server, null!, false, namespaceUris) { } /// /// Initializes the node manager. /// protected AsyncCustomNodeManager( IServerInternal server, ILogger logger, params string[] namespaceUris) : this(server, null!, logger, namespaceUris) { } /// /// Initializes the node manager. /// protected AsyncCustomNodeManager( IServerInternal server, ApplicationConfiguration configuration, params string[] namespaceUris) : this(server, configuration, false, namespaceUris) { } /// /// Initializes the node manager. /// protected AsyncCustomNodeManager( IServerInternal server, ApplicationConfiguration configuration, ILogger logger, params string[] namespaceUris) : this(server, configuration, false, logger, namespaceUris) { } /// /// Initializes the node manager. /// protected AsyncCustomNodeManager( IServerInternal server, ApplicationConfiguration configuration, bool useSamplingGroups, params string[] namespaceUris) : this( server, configuration, useSamplingGroups, server.Telemetry.CreateLogger(), namespaceUris) { } /// /// Initializes the node manager. /// protected AsyncCustomNodeManager( IServerInternal server, ApplicationConfiguration configuration, bool useSamplingGroups, ILogger logger, params string[] namespaceUris) { // set defaults. MaxQueueSize = 1000; MaxDurableQueueSize = 200000; //default value in deprecated Conformance Unit Subscription Durable StorageLevel High if (configuration?.ServerConfiguration != null) { MaxQueueSize = (uint)configuration.ServerConfiguration.MaxNotificationQueueSize; MaxDurableQueueSize = (uint)configuration.ServerConfiguration .MaxDurableNotificationQueueSize; } // save a reference to the UA server instance that owns the node manager. Server = server; m_logger = logger; // all operations require information about the system SystemContext = Server.DefaultSystemContext.Copy(); // the node id factory assigns new node ids to new nodes. // the strategy used by a NodeManager depends on what kind of information it provides. SystemContext.NodeIdFactory = this; m_lastUsedNodeId = (uint)DateTime.UtcNow.Ticks & 0x7FFFFFFF; // add the uris to the server's namespace table and cache the indexes. ushort[] namespaceIndexes = []; if (namespaceUris != null) { namespaceIndexes = new ushort[namespaceUris.Length]; for (int ii = 0; ii < namespaceUris.Length; ii++) { namespaceIndexes[ii] = Server.NamespaceUris.GetIndexOrAppend(namespaceUris[ii]); } } // add the table of namespaces that are used by the NodeManager. m_namespaceUris = namespaceUris; m_namespaceIndexes = namespaceIndexes; m_syncNodeManager = (INodeManager3)this.ToSyncNodeManager(); // create a monitored item manager that owns sampling groups / monitoredNodes if (useSamplingGroups) { m_monitoredItemManager = new SamplingGroupMonitoredItemManager( this, server, configuration!); } else { m_monitoredItemManager = new MonitoredNodeMonitoredItemManager(this, server); } PredefinedNodes = []; RootNotifiers = []; } /// /// Frees any unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// An overrideable version of the Dispose. /// protected virtual void Dispose(bool disposing) { if (disposing) { m_writeSemaphore.Wait(500); try { PredefinedNodes.Clear(); } finally { m_writeSemaphore.Release(); } m_writeSemaphore.Dispose(); m_monitoredItemSemaphore.Wait(500); try { m_monitoredItemManager?.Dispose(); } finally { m_monitoredItemSemaphore.Release(); } m_monitoredItemSemaphore.Dispose(); m_componentCacheSemaphore.Dispose(); } } /// /// Creates the NodeId for the specified node. /// /// The context. /// The node. /// The new NodeId. public virtual NodeId New(ISystemContext context, NodeState node) { if (node.NodeId.IsNull) { uint id = Utils.IncrementIdentifier(ref m_lastUsedNodeId); return new NodeId(id, m_namespaceIndexes[0]); } return node.NodeId; } /// /// Gets the server that the node manager belongs to. /// public IServerInternal Server { get; } /// /// The default context to use. /// public ServerSystemContext SystemContext { get; } /// /// Gets the default index for the node manager's namespace. /// public ushort NamespaceIndex => m_namespaceIndexes[0]; /// /// Gets the namespace indexes owned by the node manager. /// /// The namespace indexes. public IReadOnlyList NamespaceIndexes => m_namespaceIndexes; /// /// Gets or sets the maximum size of a monitored item queue. /// /// The maximum size of a monitored item queue. public uint MaxQueueSize { get; set; } /// /// Gets or sets the maximum size of a durable monitored item queue. /// /// The maximum size of a durable monitored item queue. public uint MaxDurableQueueSize { get; set; } /// /// The root for the alias assigned to the node manager. /// public string AliasRoot { get; set; } = null!; /// /// The predefined nodes managed by the node manager. /// protected NodeIdDictionary PredefinedNodes { get; } /// /// The root notifiers for the node manager. /// protected NodeIdDictionary RootNotifiers { get; } /// /// Gets the table of nodes being monitored. /// protected NodeIdDictionary MonitoredNodes => m_monitoredItemManager.MonitoredNodes; /// /// Gets the table of monitored items managed by the node manager. /// protected ConcurrentDictionary MonitoredItems => m_monitoredItemManager.MonitoredItems; /// public INodeManager SyncNodeManager => m_syncNodeManager; /// /// Marks a node for dynamic scaling of event consumer tasks. When a /// monitored node is created for a node in this set, the /// will scale its consumer tasks with /// the number of event monitored items. The Server node /// () is always auto-opted-in. /// /// The of the node to opt in. /// /// true to opt in (default); false to opt out. /// protected void AllowMultipleEventConsumers(NodeId nodeId, bool enable = true) { if (nodeId.IsNull) { throw new ArgumentException("NodeId must not be null.", nameof(nodeId)); } if (enable) { MultiConsumerNodeIds[nodeId] = true; } else { MultiConsumerNodeIds.Remove(nodeId); } } /// public bool IsMultipleEventConsumerNode(NodeId nodeId) { return MultiConsumerNodeIds.ContainsKey(nodeId); } /// /// Sets the namespaces supported by the NodeManager. /// /// The namespace uris. protected void SetNamespaces(params string[] namespaceUris) { // add the uris to the server's namespace table and cache the indexes. ushort[] namespaceIndexes = new ushort[namespaceUris.Length]; for (int ii = 0; ii < namespaceUris.Length; ii++) { namespaceIndexes[ii] = Server.NamespaceUris.GetIndexOrAppend(namespaceUris[ii]); } // create the immutable table of namespaces that are used by the NodeManager. m_namespaceUris = namespaceUris; m_namespaceIndexes = namespaceIndexes; } /// /// Sets the namespace indexes supported by the NodeManager. /// protected void SetNamespaceIndexes(ushort[] namespaceIndexes) { string[] namespaceUris = new string[namespaceIndexes.Length]; for (int ii = 0; ii < namespaceIndexes.Length; ii++) { namespaceUris[ii] = Server.NamespaceUris.GetString(namespaceIndexes[ii])!; } // create the immutable table of namespaces that are used by the NodeManager. m_namespaceUris = namespaceUris; m_namespaceIndexes = namespaceIndexes; } /// /// Returns true if the namespace for the node id is one of the namespaces managed by the node manager. /// /// /// It is thread safe to call this method outside the node manager lock. /// /// The node id to check. /// True if the namespace is one of the nodes. protected virtual bool IsNodeIdInNamespace(NodeId nodeId) { // nulls are never a valid node. if (nodeId.IsNull) { return false; } // quickly exclude nodes that not in the namespace. return m_namespaceIndexes.Contains(nodeId.NamespaceIndex); } /// /// Returns the node if the handle refers to a node managed by this manager. /// /// /// It is thread safe to call this method outside the node manager lock. /// /// The handle to check. /// Non-null if the handle belongs to the node manager. protected virtual NodeHandle? IsHandleInNamespace(object managerHandle) { if (managerHandle is not NodeHandle source) { return null; } if (!IsNodeIdInNamespace(source.NodeId)) { return null; } return source; } /// /// Returns the state object for the specified node if it exists. /// public NodeState? Find(NodeId nodeId) { if (PredefinedNodes.TryGetValue(nodeId, out NodeState? node)) { return node; } return null; } /// /// Creates a new instance and assigns unique identifiers to all children. /// /// The operation context. /// An optional parent identifier. /// The reference type from the parent. /// The browse name. /// The instance to create. /// The cancellation token. /// The new node id. /// public async ValueTask CreateNodeAsync( ServerSystemContext context, NodeId parentId, NodeId referenceTypeId, QualifiedName browseName, BaseInstanceState instance, CancellationToken cancellationToken = default) { ServerSystemContext contextToUse = SystemContext.Copy(context); instance.ReferenceTypeId = referenceTypeId; if (!parentId.IsNull) { if (!PredefinedNodes.TryGetValue(parentId, out NodeState? parent)) { throw ServiceResultException.Create( StatusCodes.BadNodeIdUnknown, "Cannot find parent with id: {0}", parentId); } parent.AddChild(instance); } instance.Create(contextToUse, default, browseName, default, true); await AddPredefinedNodeAsync(contextToUse, instance, cancellationToken).ConfigureAwait(false); NodeId resultId = instance.NodeId; if (ModelChangeEmissionEnabled) { ModelChangeAggregator.RecordNodeAdded(resultId, instance.TypeDefinitionId); if (!parentId.IsNull) { ModelChangeAggregator.RecordReferenceAdded(parentId); } EmitModelChange(contextToUse); } return resultId; } /// /// Add a created instance and its children to the NodeManagers AddressSpace. Assigns NodeIds if needed and fixes ReferenceTargets after assigning NodeIds. /// /// The operation context. /// An optional parent identifier. /// The instance to create. /// The cancellation token. /// The node id of the Node that was added. /// public async ValueTask AddNodeAsync( ServerSystemContext context, NodeId parentId, BaseInstanceState instance, CancellationToken cancellationToken = default) { ServerSystemContext contextToUse = SystemContext.Copy(context); if (!parentId.IsNull) { if (!PredefinedNodes.TryGetValue(parentId, out NodeState? parent)) { throw ServiceResultException.Create( StatusCodes.BadNodeIdUnknown, "Cannot find parent with id: {0}", parentId); } parent.AddChild(instance); } var mappingTable = new Dictionary(); instance.AssignNodeIds(context, mappingTable); instance.UpdateReferenceTargets(context, mappingTable); await AddPredefinedNodeAsync(contextToUse, instance, cancellationToken).ConfigureAwait(false); NodeId resultId = instance.NodeId; if (ModelChangeEmissionEnabled) { ModelChangeAggregator.RecordNodeAdded(resultId, instance.TypeDefinitionId); if (!parentId.IsNull) { ModelChangeAggregator.RecordReferenceAdded(parentId); } EmitModelChange(contextToUse); } return resultId; } /// /// Adds an already-constructed node subtree (a "predefined" subtree) /// to this manager's address space while preserving the caller-supplied /// NodeIds. /// /// /// /// Use this when a node is materialized dynamically at runtime and the /// caller has already allocated NodeIds for the root and every child /// (e.g. via ). Unlike the /// /// overload, this method does not /// run AssignNodeIds โ€” the NodeIds set on the instance and its /// children are kept verbatim and used as PredefinedNodes keys. /// /// /// The supplied should already be attached to /// its parent (via parent.AddChild) when invoked, if a parent /// linkage is required for browse references. /// /// /// The pre-built node subtree to register. /// The cancellation token. /// /// is null. /// public async ValueTask AddPredefinedNodeAsync(NodeState node, CancellationToken cancellationToken = default) { if (node == null) { throw new ArgumentNullException(nameof(node)); } await AddPredefinedNodeAsync(SystemContext, node, cancellationToken).ConfigureAwait(false); } /// /// Replaces an existing PredefinedNodes entry in place. Used by /// runtime upgraders (e.g. ) that /// swap a passive child with a typed proxy after the original /// nodeset was loaded. /// /// /// The replacement only affects the manager's address-space index. /// Parent linkage and child traversal must already be wired up by /// the caller via parent.ReplaceChild. /// /// The NodeId whose entry should be replaced. /// The replacement node state. /// if an existing entry was replaced. internal bool ReplacePredefinedNode(NodeId nodeId, NodeState node) { if (nodeId.IsNull || node == null) { return false; } if (!PredefinedNodes.ContainsKey(nodeId)) { return false; } PredefinedNodes[nodeId] = node; return true; } /// /// Deletes a node and all of its children. /// public async ValueTask DeleteNodeAsync(ServerSystemContext context, NodeId nodeId, CancellationToken cancellationToken = default) { ServerSystemContext contextToUse = SystemContext.Copy(context); var referencesToRemove = new List(); if (!PredefinedNodes.TryGetValue(nodeId, out NodeState? node)) { return false; } NodeId? deletedTypeDefinition = (node as BaseInstanceState)?.TypeDefinitionId; await RemovePredefinedNodeAsync(contextToUse, node!, referencesToRemove, cancellationToken).ConfigureAwait(false); await RemoveRootNotifierAsync(node!, cancellationToken).ConfigureAwait(false); if (referencesToRemove.Count > 0) { await Server.NodeManager.RemoveReferencesAsync(referencesToRemove, cancellationToken).ConfigureAwait(false); } if (ModelChangeEmissionEnabled) { ModelChangeAggregator.RecordNodeDeleted(nodeId, deletedTypeDefinition); EmitModelChange(contextToUse); } return true; } /// /// /// Defaults to false so that existing node managers do not /// accept NodeManagement service requests. Sub-classes override this /// property to opt in. /// public virtual bool AllowNodeManagement => false; /// public virtual async ValueTask<(ServiceResult result, NodeId addedNodeId)> AddNodeAsync( OperationContext context, AddNodesItem item, CancellationToken cancellationToken = default) { if (item == null) { return (new ServiceResult(StatusCodes.BadNothingToDo), NodeId.Null); } if (item.BrowseName.IsNull) { return (new ServiceResult(StatusCodes.BadBrowseNameInvalid), NodeId.Null); } ServerSystemContext systemContext = SystemContext.Copy(context); var parentNodeId = ExpandedNodeId.ToNodeId(item.ParentNodeId, Server.NamespaceUris); if (parentNodeId.IsNull) { return (new ServiceResult(StatusCodes.BadParentNodeIdInvalid), NodeId.Null); } var typeDefinitionId = ExpandedNodeId.ToNodeId(item.TypeDefinition, Server.NamespaceUris); BaseInstanceState instance; try { instance = CreateInstanceForAddNodes(item, typeDefinitionId); } catch (ServiceResultException ex) { return (new ServiceResult(ex), NodeId.Null); } instance.ReferenceTypeId = item.ReferenceTypeId; instance.BrowseName = item.BrowseName; if (instance.DisplayName.IsNull) { instance.DisplayName = new LocalizedText(item.BrowseName.Name); } // Resolve target NodeId: honour caller-supplied NodeId when present and // valid, otherwise allocate a fresh one from this manager's namespace. // Some derived classes override `New(...)` to produce hierarchical // child identifiers from a parent; for service-set AddNodes the // instance has no parent yet, so we fall back to the base // identifier allocator to guarantee a non-null NodeId. NodeId newNodeId; if (item.RequestedNewNodeId.IsNull) { newNodeId = New(systemContext, instance); if (newNodeId.IsNull) { newNodeId = new NodeId( Utils.IncrementIdentifier(ref m_lastUsedNodeId), NamespaceIndex); } } else { newNodeId = ExpandedNodeId.ToNodeId(item.RequestedNewNodeId, Server.NamespaceUris); if (newNodeId.IsNull || !IsNodeIdInNamespace(newNodeId)) { return (new ServiceResult(StatusCodes.BadNodeIdRejected), NodeId.Null); } if (PredefinedNodes.ContainsKey(newNodeId)) { return (new ServiceResult(StatusCodes.BadNodeIdExists), NodeId.Null); } } instance.NodeId = newNodeId; // Detect duplicate browse name beneath the parent. For local // parents we use the in-memory child list. For cross-NodeManager // parents we scan THIS manager's PredefinedNodes for siblings // that hold an inverse reference to the same parent โ€” this // catches duplicates among nodes created via the SDK in this // NodeManager. if (PredefinedNodes.TryGetValue(parentNodeId, out NodeState? parentNode)) { var existingChildren = new List(); parentNode.GetChildren(systemContext, existingChildren); foreach (BaseInstanceState child in existingChildren) { if (child.BrowseName == item.BrowseName) { return (new ServiceResult(StatusCodes.BadBrowseNameDuplicated), NodeId.Null); } } parentNode.AddChild(instance); } else { foreach (NodeState existing in PredefinedNodes.Values) { if (existing is BaseInstanceState sibling && sibling.BrowseName == item.BrowseName && existing.ReferenceExists(item.ReferenceTypeId, true, parentNodeId)) { return (new ServiceResult(StatusCodes.BadBrowseNameDuplicated), NodeId.Null); } } // Cross-NodeManager parent: write the inverse edge so Browse from // the child still resolves the parent. The forward edge from // parent to child is written below via Server.NodeManager. instance.AddReference(item.ReferenceTypeId, true, parentNodeId); } try { await AddPredefinedNodeAsync(systemContext, instance, cancellationToken).ConfigureAwait(false); } catch (ServiceResultException ex) { return (new ServiceResult(ex), NodeId.Null); } // Always add the forward edge from parent โ†’ child via the master so // the parent's owning NodeManager records it (works whether parent // is local or remote). try { var forward = new List { new NodeStateReference(item.ReferenceTypeId, false, instance.NodeId) }; await Server.NodeManager.AddReferencesAsync( parentNodeId, forward, cancellationToken).ConfigureAwait(false); } catch (ServiceResultException ex) { return (new ServiceResult(ex), NodeId.Null); } if (ModelChangeEmissionEnabled) { ModelChangeAggregator.RecordNodeAdded(instance.NodeId, instance.TypeDefinitionId); ModelChangeAggregator.RecordReferenceAdded(parentNodeId); EmitModelChange(systemContext); } return (ServiceResult.Good, instance.NodeId); } /// public virtual async ValueTask DeleteNodeAsync( OperationContext context, DeleteNodesItem item, CancellationToken cancellationToken = default) { if (item == null) { return new ServiceResult(StatusCodes.BadNothingToDo); } if (item.NodeId.IsNull) { return new ServiceResult(StatusCodes.BadNodeIdInvalid); } ServerSystemContext systemContext = SystemContext.Copy(context); if (!PredefinedNodes.TryGetValue(item.NodeId, out NodeState? node)) { return new ServiceResult(StatusCodes.BadNodeIdUnknown); } NodeId? deletedTypeDefinition = (node as BaseInstanceState)?.TypeDefinitionId; var referencesToRemove = new List(); await RemovePredefinedNodeAsync(systemContext, node!, referencesToRemove, cancellationToken) .ConfigureAwait(false); await RemoveRootNotifierAsync(node!, cancellationToken).ConfigureAwait(false); if (item.DeleteTargetReferences && referencesToRemove.Count > 0) { await Server.NodeManager.RemoveReferencesAsync(referencesToRemove, cancellationToken) .ConfigureAwait(false); } if (ModelChangeEmissionEnabled) { ModelChangeAggregator.RecordNodeDeleted(item.NodeId, deletedTypeDefinition); EmitModelChange(systemContext); } return ServiceResult.Good; } /// public virtual ValueTask AddReferenceAsync( OperationContext context, AddReferencesItem item, CancellationToken cancellationToken = default) { if (item == null) { return new ValueTask(new ServiceResult(StatusCodes.BadNothingToDo)); } if (item.SourceNodeId.IsNull) { return new ValueTask(new ServiceResult(StatusCodes.BadSourceNodeIdInvalid)); } if (item.ReferenceTypeId.IsNull) { return new ValueTask(new ServiceResult(StatusCodes.BadReferenceTypeIdInvalid)); } if (item.TargetNodeId.IsNull) { return new ValueTask(new ServiceResult(StatusCodes.BadTargetNodeIdInvalid)); } if (!PredefinedNodes.TryGetValue(item.SourceNodeId, out NodeState? source)) { return new ValueTask(new ServiceResult(StatusCodes.BadSourceNodeIdInvalid)); } bool isInverse = !item.IsForward; if (source.ReferenceExists(item.ReferenceTypeId, isInverse, item.TargetNodeId)) { return new ValueTask(new ServiceResult(StatusCodes.BadDuplicateReferenceNotAllowed)); } source.AddReference(item.ReferenceTypeId, isInverse, item.TargetNodeId); return new ValueTask(ServiceResult.Good); } /// public virtual ValueTask DeleteReferenceAsync( OperationContext context, DeleteReferencesItem item, CancellationToken cancellationToken = default) { if (item == null) { return new ValueTask(new ServiceResult(StatusCodes.BadNothingToDo)); } if (item.SourceNodeId.IsNull) { return new ValueTask(new ServiceResult(StatusCodes.BadSourceNodeIdInvalid)); } if (item.ReferenceTypeId.IsNull) { return new ValueTask(new ServiceResult(StatusCodes.BadReferenceTypeIdInvalid)); } if (!PredefinedNodes.TryGetValue(item.SourceNodeId, out NodeState? source)) { return new ValueTask(new ServiceResult(StatusCodes.BadSourceNodeIdInvalid)); } bool isInverse = !item.IsForward; bool removed = source.RemoveReference(item.ReferenceTypeId, isInverse, item.TargetNodeId); if (!removed) { return new ValueTask(new ServiceResult(StatusCodes.BadNoMatch)); } return new ValueTask(ServiceResult.Good); } /// /// Creates a NodeState for an based on the /// requested node class and any node attributes the caller provided. /// /// /// Thrown when the supplied attributes are not valid for the node class /// or when the node class is not supported. /// private static BaseInstanceState CreateInstanceForAddNodes( AddNodesItem item, NodeId typeDefinitionId) { switch (item.NodeClass) { case NodeClass.Variable: { var variable = new BaseDataVariableState(null) { TypeDefinitionId = typeDefinitionId.IsNull ? VariableTypeIds.BaseDataVariableType : typeDefinitionId, AccessLevel = AccessLevels.CurrentReadOrWrite, UserAccessLevel = AccessLevels.CurrentReadOrWrite, DataType = DataTypeIds.BaseDataType, ValueRank = ValueRanks.Scalar }; if (!item.NodeAttributes.IsNull) { if (!item.NodeAttributes.TryGetValue(out VariableAttributes? attrs) || attrs == null) { throw new ServiceResultException(StatusCodes.BadNodeAttributesInvalid); } ApplyVariableAttributes(variable, attrs); } return variable; } case NodeClass.Object: { var instance = new BaseObjectState(null) { TypeDefinitionId = typeDefinitionId.IsNull ? ObjectTypeIds.BaseObjectType : typeDefinitionId }; if (!item.NodeAttributes.IsNull) { if (!item.NodeAttributes.TryGetValue(out ObjectAttributes? attrs) || attrs == null) { throw new ServiceResultException(StatusCodes.BadNodeAttributesInvalid); } ApplyObjectAttributes(instance, attrs); } return instance; } default: throw new ServiceResultException(StatusCodes.BadNodeClassInvalid); } } private static void ApplyVariableAttributes( BaseDataVariableState variable, VariableAttributes attributes) { uint mask = attributes.SpecifiedAttributes; if ((mask & (uint)NodeAttributesMask.DisplayName) != 0 && !attributes.DisplayName.IsNull) { variable.DisplayName = attributes.DisplayName; } if ((mask & (uint)NodeAttributesMask.Description) != 0 && !attributes.Description.IsNull) { variable.Description = attributes.Description; } if ((mask & (uint)NodeAttributesMask.DataType) != 0 && !attributes.DataType.IsNull) { variable.DataType = attributes.DataType; } if ((mask & (uint)NodeAttributesMask.ValueRank) != 0) { variable.ValueRank = attributes.ValueRank; } if ((mask & (uint)NodeAttributesMask.AccessLevel) != 0) { variable.AccessLevel = attributes.AccessLevel; } if ((mask & (uint)NodeAttributesMask.UserAccessLevel) != 0) { variable.UserAccessLevel = attributes.UserAccessLevel; } if ((mask & (uint)NodeAttributesMask.Historizing) != 0) { variable.Historizing = attributes.Historizing; } if ((mask & (uint)NodeAttributesMask.MinimumSamplingInterval) != 0) { variable.MinimumSamplingInterval = attributes.MinimumSamplingInterval; } if ((mask & (uint)NodeAttributesMask.Value) != 0) { variable.Value = attributes.Value; } } private static void ApplyObjectAttributes( BaseObjectState instance, ObjectAttributes attributes) { uint mask = attributes.SpecifiedAttributes; if ((mask & (uint)NodeAttributesMask.DisplayName) != 0 && !attributes.DisplayName.IsNull) { instance.DisplayName = attributes.DisplayName; } if ((mask & (uint)NodeAttributesMask.Description) != 0 && !attributes.Description.IsNull) { instance.Description = attributes.Description; } if ((mask & (uint)NodeAttributesMask.EventNotifier) != 0) { instance.EventNotifier = attributes.EventNotifier; } } /// /// Returns the namespaces used by the node manager. /// /// /// All NodeIds exposed by the node manager must be qualified by a namespace URI. This property /// returns the URIs used by the node manager. In this example all NodeIds use a single URI. /// /// public virtual IEnumerable NamespaceUris { get => m_namespaceUris!; protected set { if (value == null) { throw new ArgumentNullException(nameof(value)); } var namespaceUris = new List(value); SetNamespaces([.. namespaceUris]); } } /// /// Does any initialization required before the address space can be used. /// /// /// The externalReferences is an out parameter that allows the node manager to link to nodes /// in other node managers. For example, the 'Objects' node is managed by the CoreNodeManager and /// should have a reference to the root folder node(s) exposed by this node manager. /// public virtual ValueTask CreateAddressSpaceAsync( IDictionary> externalReferences, CancellationToken cancellationToken = default) { return LoadPredefinedNodesAsync(SystemContext, externalReferences, cancellationToken); } /// /// Loads a node set from a file or resource and adds them to the set of predefined nodes. /// public virtual async ValueTask LoadPredefinedNodesAsync( ISystemContext context, Assembly assembly, string resourcePath, IDictionary> externalReferences, CancellationToken cancellationToken = default) { // load the predefined nodes from an XML document. var predefinedNodes = new NodeStateCollection(); predefinedNodes.LoadFromResource(context, resourcePath, assembly, true); // add the predefined nodes to the node manager. for (int ii = 0; ii < predefinedNodes.Count; ii++) { await AddPredefinedNodeAsync(context, predefinedNodes[ii], cancellationToken).ConfigureAwait(false); } // ensure the reverse references exist. await AddReverseReferencesAsync(externalReferences, cancellationToken).ConfigureAwait(false); } /// /// Loads a node set from a file or resource and adds them to the set of predefined nodes. /// protected virtual ValueTask LoadPredefinedNodesAsync(ISystemContext context, CancellationToken cancellationToken = default) { return new ValueTask([]); } /// /// Loads a node set from a file or resource and adds them to the set of predefined nodes. /// protected virtual async ValueTask LoadPredefinedNodesAsync( ISystemContext context, IDictionary> externalReferences, CancellationToken cancellationToken = default) { // load the predefined nodes from an XML document. NodeStateCollection predefinedNodes = await LoadPredefinedNodesAsync(context, cancellationToken).ConfigureAwait(false); // add the predefined nodes to the node manager. for (int ii = 0; ii < predefinedNodes.Count; ii++) { await AddPredefinedNodeAsync(context, predefinedNodes[ii], cancellationToken).ConfigureAwait(false); } // ensure the reverse references exist. await AddReverseReferencesAsync(externalReferences, cancellationToken).ConfigureAwait(false); } /// /// Replaces the generic node with a node specific to the model. /// protected virtual ValueTask AddBehaviourToPredefinedNodeAsync( ISystemContext context, NodeState predefinedNode, CancellationToken cancellationToken = default) { return new ValueTask(predefinedNode); } /// /// Recursively indexes the node and its children. /// protected virtual async ValueTask AddPredefinedNodeAsync(ISystemContext context, NodeState node, CancellationToken cancellationToken = default) { // assign a default value to any variable in namespace 0 if (node is BaseVariableState nodeStateVar && nodeStateVar.NodeId.NamespaceIndex == 0 && nodeStateVar.Value.IsNull) { nodeStateVar.Value = TypeInfo.GetDefaultVariantValue( nodeStateVar.DataType, nodeStateVar.ValueRank, Server.TypeTree); } NodeState activeNode = await AddBehaviourToPredefinedNodeAsync(context, node, cancellationToken).ConfigureAwait(false); PredefinedNodes.AddOrUpdate(activeNode.NodeId, activeNode, (key, _) => activeNode); if (activeNode is BaseTypeState type) { AddTypesToTypeTree(type); } // update the root notifiers. if (RootNotifiers.TryGetValue(activeNode.NodeId, out NodeState? _)) { RootNotifiers[activeNode.NodeId] = activeNode; // need to prevent recursion with the server object. if (activeNode.NodeId != ObjectIds.Server) { lock (activeNode) { activeNode.OnReportEvent = OnReportEvent; if (!activeNode.ReferenceExists( ReferenceTypeIds.HasNotifier, true, ObjectIds.Server)) { activeNode.AddReference( ReferenceTypeIds.HasNotifier, true, ObjectIds.Server); } } } } var children = new List(); activeNode.GetChildren(context, children); for (int ii = 0; ii < children.Count; ii++) { // Propagate type hierarchy flag from parent to children if (activeNode.IsPartOfTypeHierarchy) { children[ii].IsPartOfTypeHierarchy = true; } await AddPredefinedNodeAsync(context, children[ii], cancellationToken).ConfigureAwait(false); } } /// /// Recursively indexes the node and its children. /// protected virtual async ValueTask RemovePredefinedNodeAsync( ISystemContext context, NodeState node, List referencesToRemove, CancellationToken cancellationToken = default) { if (!PredefinedNodes.TryRemove(node.NodeId, out _)) { return; } node.UpdateChangeMasks(NodeStateChangeMasks.Deleted); node.ClearChangeMasks(context, false); await OnNodeRemovedAsync(node, cancellationToken).ConfigureAwait(false); // remove from the parent. if (node is BaseInstanceState instance && instance.Parent != null) { instance.Parent.RemoveChild(instance); } // remove children. var children = new List(); node.GetChildren(context, children); for (int ii = 0; ii < children.Count; ii++) { node.RemoveChild(children[ii]); } for (int ii = 0; ii < children.Count; ii++) { await RemovePredefinedNodeAsync(context, children[ii], referencesToRemove, cancellationToken).ConfigureAwait(false); } // remove from type table. if (node is BaseTypeState type) { Server.TypeTree.Remove(type.NodeId); } // remove inverse references. var references = new List(); node.GetReferences(context, references); for (int ii = 0; ii < references.Count; ii++) { IReference reference = references[ii]; if (reference.TargetId.IsAbsolute) { continue; } var referenceToRemove = new LocalReference( (NodeId)reference.TargetId, reference.ReferenceTypeId, !reference.IsInverse, node.NodeId); referencesToRemove.Add(referenceToRemove); } } /// /// Called after a node has been deleted. /// protected virtual ValueTask OnNodeRemovedAsync(NodeState node, CancellationToken cancellationToken = default) { // overridden by the sub-class. return new ValueTask(); } /// /// Ensures that all reverse references exist. /// /// A list of references to add to external targets. /// The cancellation token protected virtual async ValueTask AddReverseReferencesAsync( IDictionary> externalReferences, CancellationToken cancellationToken = default) { foreach (KeyValuePair kvp in PredefinedNodes) { NodeState source = kvp.Value; var references = new List(); lock (source) { source.GetReferences(SystemContext, references); } for (int ii = 0; ii < references.Count; ii++) { IReference reference = references[ii]; // nothing to do with external nodes. if (reference.TargetId.IsNull || reference.TargetId.IsAbsolute) { continue; } // no need to add HasSubtype references since these are handled via the type table. if (reference.ReferenceTypeId == ReferenceTypeIds.HasSubtype) { continue; } var targetId = (NodeId)reference.TargetId; // check for data type encoding references. if (reference.IsInverse && reference.ReferenceTypeId == ReferenceTypeIds.HasEncoding) { Server.TypeTree.AddEncoding(targetId, source.NodeId); } // add inverse reference to internal targets. if (PredefinedNodes.TryGetValue(targetId, out NodeState? target)) { lock (target) { if (!target.ReferenceExists( reference.ReferenceTypeId, !reference.IsInverse, source.NodeId)) { target.AddReference( reference.ReferenceTypeId, !reference.IsInverse, source.NodeId); } } continue; } // check for inverse references to external notifiers. if (reference.IsInverse && reference.ReferenceTypeId == ReferenceTypeIds.HasNotifier) { await AddRootNotifierAsync(source, cancellationToken).ConfigureAwait(false); } // nothing more to do for references to nodes managed by this manager. if (IsNodeIdInNamespace(targetId)) { continue; } // add external reference. AddExternalReference( targetId, reference.ReferenceTypeId, !reference.IsInverse, source.NodeId, externalReferences); } } } /// /// Adds an external reference to the dictionary. /// protected void AddExternalReference( NodeId sourceId, NodeId referenceTypeId, bool isInverse, NodeId targetId, IDictionary> externalReferences) { // get list of references to external nodes. if (!externalReferences.TryGetValue(sourceId, out IList? referencesToAdd)) { externalReferences[sourceId] = referencesToAdd = []; } // add reserve reference from external node. var referenceToAdd = new ReferenceNode { ReferenceTypeId = referenceTypeId, IsInverse = isInverse, TargetId = targetId }; referencesToAdd.Add(referenceToAdd); } /// /// Recursively adds the types to the type tree. /// protected void AddTypesToTypeTree(BaseTypeState type) { if (!type.SuperTypeId.IsNull && !Server.TypeTree.IsKnown(type.SuperTypeId)) { AddTypesToTypeTree(type.SuperTypeId); } if (type.NodeClass != NodeClass.ReferenceType) { Server.TypeTree.AddSubtype(type.NodeId, type.SuperTypeId); } else { Server.TypeTree.AddReferenceSubtype(type.NodeId, type.SuperTypeId, type.BrowseName); } } /// /// Recursively adds the types to the type tree. /// protected void AddTypesToTypeTree(NodeId typeId) { if (!PredefinedNodes.TryGetValue(typeId, out NodeState? node)) { return; } if (node is not BaseTypeState type) { return; } AddTypesToTypeTree(type); } /// /// Finds the specified and checks if it is of the expected type. /// /// /// Returns null if not found or not of the correct type. public T FindPredefinedNode(NodeId nodeId) where T : NodeState { if (nodeId.IsNull) { return null!; } if (!PredefinedNodes.TryGetValue(nodeId, out NodeState? node)) { return null!; } if (typeof(T) != null && !typeof(T).IsInstanceOfType(node)) { return null!; } return node is T typedNode ? typedNode : null!; } /// /// Searches for a method in the ObjectType hierarchy of an instance node. /// Per OPC UA spec Part 4 section 5.12.2.2, the ObjectType of the Object or a super type /// of that ObjectType may be the source of a HasComponent reference to the method. /// /// The system context. /// The TypeDefinitionId of the object instance. /// The NodeId of the method to find. /// The found method state, or null if not found. private MethodState? FindMethodInTypeHierarchy(ISystemContext context, NodeId typeDefinitionId, NodeId methodId) { // A limit to prevent infinite loops in case of a circular type hierarchy in malformed address spaces. const int maxHierarchyDepth = 100; int depth = 0; NodeId typeId = typeDefinitionId; while (!typeId.IsNull && depth++ < maxHierarchyDepth) { NodeState typeNode = FindPredefinedNode(typeId); if (typeNode != null) { MethodState? method = typeNode.FindMethod(context, methodId); if (method != null) { return method; } // check for loose coupling via the type node. if (typeNode.ReferenceExists(ReferenceTypeIds.HasComponent, false, methodId)) { method = FindPredefinedNode(methodId); if (method != null) { return method; } } } typeId = Server.TypeTree.FindSuperType(typeId); } return null; } /// /// Frees any resources allocated for the address space. /// /// /// Iterates the predefined nodes and invokes on each /// root node so the and /// callbacks fire and /// is reported to subscribers. Nodes that /// are children of another predefined node are skipped because they are deleted /// recursively by their parent (avoiding duplicate callbacks). /// public virtual ValueTask DeleteAddressSpaceAsync(CancellationToken cancellationToken = default) { NodeState[] nodes = [.. PredefinedNodes.Values]; ISystemContext context = SystemContext; foreach (NodeState node in nodes) { if (node is BaseInstanceState instance && instance.Parent != null && !instance.Parent.NodeId.IsNull && PredefinedNodes.ContainsKey(instance.Parent.NodeId)) { continue; } node.Delete(context); } PredefinedNodes.Clear(); return default; } /// /// Returns a unique handle for the node. /// /// /// This must efficiently determine whether the node belongs to the node manager. If it does belong to /// NodeManager it should return a handle that does not require the NodeId to be validated again when /// the handle is passed into other methods such as 'Read' or 'Write'. /// public virtual async ValueTask GetManagerHandleAsync(NodeId nodeId, CancellationToken cancellationToken = default) { return await GetManagerHandleAsync(SystemContext, nodeId, null!, cancellationToken).ConfigureAwait(false); } /// /// Returns a unique handle for the node. /// /// /// It is thread safe to call this method outside the node manager lock. /// protected virtual ValueTask GetManagerHandleAsync( ServerSystemContext context, NodeId nodeId, IDictionary cache, CancellationToken cancellationToken = default) { if (!IsNodeIdInNamespace(nodeId)) { return new ValueTask(); } if (PredefinedNodes.TryGetValue(nodeId, out NodeState? node)) { return new ValueTask(new NodeHandle { NodeId = nodeId, Node = node, Validated = true }); } return new ValueTask(); } /// /// This method is used to add bi-directional references to nodes from other node managers. /// /// /// The additional references are optional, however, the NodeManager should support them. /// public virtual async ValueTask AddReferencesAsync(IDictionary> references, CancellationToken cancellationToken = default) { foreach (KeyValuePair> current in references) { // get the handle. NodeHandle source = await GetManagerHandleAsync(SystemContext, current.Key, null!, cancellationToken).ConfigureAwait(false); // only support external references to nodes that are stored in memory. if (source?.Node == null || !source.Validated) { continue; } // add reference to external target. foreach (IReference reference in current.Value) { lock (source.Node) { if (!source.Node.ReferenceExists( reference.ReferenceTypeId, reference.IsInverse, reference.TargetId)) { source.Node.AddReference( reference.ReferenceTypeId, reference.IsInverse, reference.TargetId); } } } } } /// /// This method is used to delete bi-directional references to nodes from other node managers. /// public virtual async ValueTask DeleteReferenceAsync( object sourceHandle, NodeId referenceTypeId, bool isInverse, ExpandedNodeId targetId, bool deleteBidirectional, CancellationToken cancellationToken = default) { // get the handle. NodeHandle? source = IsHandleInNamespace(sourceHandle); if (source == null) { return StatusCodes.BadNodeIdUnknown; } // only support external references to nodes that are stored in memory. if (!source.Validated || source.Node == null) { return StatusCodes.BadNotSupported; } // only support references to Source Areas. lock (source.Node) { source.Node.RemoveReference(referenceTypeId, isInverse, targetId); } if (deleteBidirectional) { // check if the target is also managed by this node manager. if (!targetId.IsAbsolute) { NodeHandle? target = await GetManagerHandleAsync(SystemContext, (NodeId)targetId, null!, cancellationToken).ConfigureAwait(false); if (target != null && target.Validated && target.Node != null) { lock (target.Node) { target.Node.RemoveReference(referenceTypeId, !isInverse, source.NodeId); } } } } return ServiceResult.Good; } private static readonly uint[] s_nodeMetaDataAttributes = [ Attributes.WriteMask, Attributes.UserWriteMask, Attributes.DataType, Attributes.ValueRank, Attributes.ArrayDimensions, Attributes.AccessLevel, Attributes.UserAccessLevel, Attributes.EventNotifier, Attributes.Executable, Attributes.UserExecutable, Attributes.AccessRestrictions, Attributes.RolePermissions, Attributes.UserRolePermissions ]; /// /// Returns the basic metadata for the node. Returns null if the node does not exist. /// /// /// This method validates any placeholder handle. /// public async ValueTask GetNodeMetadataAsync( OperationContext context, object targetHandle, BrowseResultMask resultMask, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); // check for valid handle. NodeHandle? handle = IsHandleInNamespace(targetHandle); if (handle == null) { return null!; } // validate node. NodeState? target = await ValidateNodeAsync(systemContext, handle, null!, cancellationToken).ConfigureAwait(false); if (target == null) { return null!; } var nodeMetadataValues = new Variant[s_nodeMetaDataAttributes.Length]; // read the attributes. lock (target) { target.ReadAttributes( systemContext, ref nodeMetadataValues, s_nodeMetaDataAttributes); } // construct the meta-data object. var metadata = new NodeMetadata(target, target.NodeId) { NodeClass = target.NodeClass, BrowseName = target.BrowseName, DisplayName = target.DisplayName, IsPartOfTypeHierarchy = target.IsPartOfTypeHierarchy }; if (nodeMetadataValues[0].TryGetValue(out uint writeMask) && nodeMetadataValues[1].TryGetValue(out uint userWriteMask)) { metadata.WriteMask = (AttributeWriteMask)(writeMask & userWriteMask); } metadata.DataType = nodeMetadataValues[2].GetNodeId(); if (nodeMetadataValues[3].TryGetValue(out int valueRank)) { metadata.ValueRank = valueRank; } if (nodeMetadataValues[4].TryGetValue(out ArrayOf arrayDimensions)) { metadata.ArrayDimensions = arrayDimensions; } if (nodeMetadataValues[5].TryGetValue(out byte accessLevel) && nodeMetadataValues[6].TryGetValue(out byte userAccessLevel)) { metadata.AccessLevel = (byte)(accessLevel & userAccessLevel); } if (nodeMetadataValues[7].TryGetValue(out byte eventNotifier)) { metadata.EventNotifier = eventNotifier; } if (nodeMetadataValues[8].TryGetValue(out bool executeAble) && nodeMetadataValues[9].TryGetValue(out bool userExecuteable)) { metadata.Executable = executeAble && userExecuteable; } if (nodeMetadataValues[10].TryGetValue(out ushort accessRestrictionType)) { metadata.AccessRestrictions = (AccessRestrictionType)accessRestrictionType; } if (nodeMetadataValues[11].TryGetStructure(out ArrayOf rolePermissions)) { metadata.RolePermissions = [.. rolePermissions]; } if (nodeMetadataValues[12].TryGetStructure(out ArrayOf userRolePermissions)) { metadata.UserRolePermissions = [.. userRolePermissions]; } await SetDefaultPermissionsAsync(systemContext, target, metadata, cancellationToken).ConfigureAwait(false); // get instance references. if (target is BaseInstanceState instance) { metadata.TypeDefinition = instance.TypeDefinitionId; metadata.ModellingRule = instance.ModellingRuleId; } // fill in the common attributes. return metadata; } /// /// Sets the AccessRestrictions, RolePermissions and UserRolePermissions values in the metadata /// /// private static void SetAccessAndRolePermissions(Variant[] values, NodeMetadata metadata) { if (values.Length != 3) { throw new ArgumentException("Values need to have a length of 3 to contain Access and Rolepermissions", nameof(values)); } if (values[0].TryGetValue(out ushort accessRestrictions)) { metadata.AccessRestrictions = (AccessRestrictionType)accessRestrictions; } if (values[1].TryGetStructure(out ArrayOf rolePermissions)) { metadata.RolePermissions = [.. rolePermissions]; } if (values[2].TryGetStructure(out ArrayOf userRolePermissions)) { metadata.UserRolePermissions = [.. userRolePermissions]; } } /// /// Reads and caches the Attributes used by the AccessRestrictions and RolePermission validation process /// /// The cache used to save the attributes /// The context /// The target for which the attributes are read and cached /// The key representing the NodeId for which the cache is kept /// The array to store the values of the attributes private static void ReadAndCacheValidationAttributes( Dictionary uniqueNodesServiceAttributes, ServerSystemContext systemContext, NodeState target, NodeId key, ref Variant[] values) { ReadValidationAttributes(systemContext, target, ref values); uniqueNodesServiceAttributes[key] = values; } /// /// Reads the Attributes used by the AccessRestrictions and RolePermission validation process /// /// The context /// The target for which the attributes are read and cached /// The array to store the values of the attributes private static void ReadValidationAttributes( ServerSystemContext systemContext, NodeState target, ref Variant[] values) { // This is the list of attributes to be populated by GetNodeMetadata from CustomNodeManagers. // The are originating from services in the context of AccessRestrictions and RolePermission validation. // For such calls the other attributes are ignored since reading them might trigger unnecessary callbacks lock (target) { target.ReadAttributes( systemContext, ref values, Attributes.AccessRestrictions, Attributes.RolePermissions, Attributes.UserRolePermissions); } } /// /// Browses the references from a node managed by the node manager. /// /// /// The continuation point is created for every browse operation and contains the browse parameters. /// The node manager can store its state information in the Data and Index properties. /// /// is null. /// public async ValueTask BrowseAsync( OperationContext context, ContinuationPoint continuationPoint, IList references, CancellationToken cancellationToken = default) { if (continuationPoint == null) { throw new ArgumentNullException(nameof(continuationPoint)); } ServerSystemContext systemContext = SystemContext.Copy(context); // check for valid view. ValidateViewDescription(systemContext, continuationPoint.View!); // check for valid handle. NodeHandle handle = IsHandleInNamespace(continuationPoint.NodeToBrowse) ?? throw new ServiceResultException(StatusCodes.BadNodeIdUnknown); // validate node. NodeState source = await ValidateNodeAsync(systemContext, handle, null!, cancellationToken).ConfigureAwait(false) ?? throw new ServiceResultException(StatusCodes.BadNodeIdUnknown); // check if node is in the view. if (!IsNodeInView(systemContext, continuationPoint, source)) { throw new ServiceResultException(StatusCodes.BadNodeNotInView); } // fetch list of references. if (continuationPoint.Data is not BrowserContext browserContext) { INodeBrowser browser; lock (source) { // create a new browser. browser = source.CreateBrowser( systemContext, continuationPoint.View, continuationPoint.ReferenceTypeId, continuationPoint.IncludeSubtypes, continuationPoint.BrowseDirection, default, null, false); } continuationPoint.Data = browserContext = new BrowserContext(browser); } // prevent multiple access the browser object. await browserContext.Semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { INodeBrowser browser = browserContext.Browser; // apply filters to references. var cache = new Dictionary(); for (IReference? reference = browser.Next(); reference != null; reference = browser.Next()) { // validate Browse permission ServiceResult serviceResult = await ValidateRolePermissionsAsync( context, ExpandedNodeId.ToNodeId(reference.TargetId, Server.NamespaceUris), PermissionType.Browse, cancellationToken).ConfigureAwait(false); if (ServiceResult.IsBad(serviceResult)) { // ignore reference continue; } // create the type definition reference. ReferenceDescription description = await GetReferenceDescriptionAsync( systemContext, cache, reference, continuationPoint, cancellationToken).ConfigureAwait(false); if (description == null) { continue; } // check if limit reached. if (continuationPoint.MaxResultsToReturn != 0 && references.Count >= continuationPoint.MaxResultsToReturn) { browser.Push(reference); return continuationPoint; } references.Add(description); } } finally { browserContext.Semaphore.Release(); } // release the continuation point if all done. continuationPoint.Dispose(); return null!; } /// /// Validates the view description passed to a browse request (throws on error). /// /// protected virtual void ValidateViewDescription( ServerSystemContext context, ViewDescription view) { if (ViewDescription.IsDefault(view)) { return; } _ = FindPredefinedNode(view.ViewId) ?? throw new ServiceResultException(StatusCodes.BadViewIdUnknown); if (view.Timestamp != DateTimeUtc.MinValue) { throw new ServiceResultException(StatusCodes.BadViewTimestampInvalid); } if (view.ViewVersion != 0) { throw new ServiceResultException(StatusCodes.BadViewVersionInvalid); } } /// /// Checks if the node is in the view. /// protected virtual bool IsNodeInView( ServerSystemContext context, ContinuationPoint continuationPoint, NodeState node) { if (continuationPoint == null || ViewDescription.IsDefault(continuationPoint.View)) { return true; } return IsNodeInView(context, continuationPoint!.View!.ViewId, node); } /// /// Checks if the node is in the view. /// protected virtual bool IsNodeInView( ServerSystemContext context, NodeId viewId, NodeState node) { ViewState view = FindPredefinedNode(viewId); return view != null; } /// /// Checks if the reference is in the view. /// protected virtual bool IsReferenceInView( ServerSystemContext context, ContinuationPoint continuationPoint, IReference reference) { return true; } /// /// Returns the references for the node that meets the criteria specified. /// protected async ValueTask GetReferenceDescriptionAsync( ServerSystemContext context, Dictionary cache, IReference reference, ContinuationPoint continuationPoint, CancellationToken cancellationToken = default) { _ = SystemContext.Copy(context); // create the type definition reference. var description = new ReferenceDescription { NodeId = reference.TargetId }; description.SetReferenceType( continuationPoint.ResultMask, reference.ReferenceTypeId, !reference.IsInverse); // check if reference is in the view. if (!IsReferenceInView(context, continuationPoint, reference)) { return null!; } // do not cache target parameters for remote nodes. if (reference.TargetId.IsAbsolute) { // only return remote references if no node class filter is specified. if (continuationPoint.NodeClassMask != 0) { return null!; } return description; } NodeState? target = null; // check for local reference. if (reference is NodeStateReference referenceInfo) { target = referenceInfo.Target; } // check for internal reference. if (target == null) { NodeHandle handle = await GetManagerHandleAsync(context, (NodeId)reference.TargetId, null!, cancellationToken).ConfigureAwait(false); if (handle != null) { target = await ValidateNodeAsync(context, handle, null!, cancellationToken).ConfigureAwait(false); } } // the target may be a reference to a node in another node manager. In these cases // the target attributes must be fetched by the caller. The Unfiltered flag tells the // caller to do that. if (target == null) { description.Unfiltered = true; return description; } // apply node class filter. if (continuationPoint.NodeClassMask != 0 && ((continuationPoint.NodeClassMask & (uint)target.NodeClass) == 0)) { return null!; } // check if target is in the view. if (!IsNodeInView(context, continuationPoint, target)) { return null!; } // look up the type definition. NodeId typeDefinition = default; if (target is BaseInstanceState instance) { typeDefinition = instance.TypeDefinitionId; } // set target attributes. description.SetTargetAttributes( continuationPoint.ResultMask, target.NodeClass, target.BrowseName, target.DisplayName, typeDefinition); return description; } /// /// Returns the target of the specified browse path fragment(s). /// /// /// If reference exists but the node manager does not know the browse name it must /// return the NodeId as an unresolvedTargetIds. The caller will try to check the /// browse name. /// public virtual async ValueTask TranslateBrowsePathAsync( OperationContext context, object sourceHandle, RelativePathElement relativePath, IList targetIds, IList unresolvedTargetIds, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); // check for valid handle. NodeHandle? handle = IsHandleInNamespace(sourceHandle); if (handle == null) { return; } // validate node. NodeState source = await ValidateNodeAsync(systemContext, handle, operationCache, cancellationToken).ConfigureAwait(false); if (source == null) { return; } INodeBrowser browser; // get list of references that relative path. lock (source) { browser = source.CreateBrowser( systemContext, null, relativePath.ReferenceTypeId, relativePath.IncludeSubtypes, relativePath.IsInverse ? BrowseDirection.Inverse : BrowseDirection.Forward, relativePath.TargetName, null, false); } // check the browse names. try { for (IReference? reference = browser.Next(); reference != null; reference = browser.Next()) { // ignore unknown external references. if (reference.TargetId.IsAbsolute) { continue; } NodeState? target = null; // check for local reference. if (reference is NodeStateReference referenceInfo) { target = referenceInfo.Target; } if (target == null) { var targetId = (NodeId)reference.TargetId; // the target may be a reference to a node in another node manager. if (!IsNodeIdInNamespace(targetId)) { unresolvedTargetIds.Add((NodeId)reference.TargetId); continue; } // look up the target manually. NodeHandle targetHandle = await GetManagerHandleAsync( systemContext, targetId, operationCache, cancellationToken).ConfigureAwait(false); if (targetHandle == null) { continue; } // validate target. target = await ValidateNodeAsync(systemContext, targetHandle, operationCache, cancellationToken).ConfigureAwait(false); if (target == null) { continue; } } // check browse name. if (target.BrowseName == relativePath.TargetName && !targetIds.Contains(reference.TargetId)) { targetIds.Add(reference.TargetId); } } } finally { browser.Dispose(); } } /// /// Reads the value for the specified attribute. /// public virtual async ValueTask ReadAsync( OperationContext context, double maxAge, ArrayOf nodesToRead, IList values, IList errors, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToValidate = new List(); for (int ii = 0; ii < nodesToRead.Count; ii++) { ReadValueId nodeToRead = nodesToRead[ii]; // skip items that have already been processed. if (nodeToRead.Processed) { continue; } // check for valid handle. NodeHandle handle = await GetManagerHandleAsync( systemContext, nodeToRead.NodeId, operationCache, cancellationToken).ConfigureAwait(false); if (handle == null) { continue; } // owned by this node manager. nodeToRead.Processed = true; // create an initial value (default ctor already sets // WrappedValue = Variant.Null, ServerTimestamp / // SourceTimestamp = DateTimeUtc.MinValue, StatusCode = Good). DataValue value = values[ii] = new DataValue(); // check if the node is a area in memory. if (handle.Node == null) { errors[ii] = StatusCodes.BadNodeIdUnknown; // must validate node in a separate operation handle.Index = ii; nodesToValidate.Add(handle); continue; } // read the attribute value. (errors[ii], value) = await handle.Node.ReadAttributeAsync( systemContext, nodeToRead.AttributeId, nodeToRead.ParsedIndexRange, nodeToRead.DataEncoding, value, cancellationToken).ConfigureAwait(false); // Set timestamps after ReadAttribute to ensure consistency // For Value attributes, match ServerTimestamp to SourceTimestamp // For other attributes, just ensure ServerTimestamp is set if (nodeToRead.AttributeId == Attributes.Value) { if (value.SourceTimestamp == DateTimeUtc.MinValue) { value = value.WithSourceTimestamp(DateTimeUtc.Now); } value = value.WithServerTimestamp(value.SourceTimestamp); } else { // For non-value attributes, only ServerTimestamp is relevant if (value.ServerTimestamp == DateTimeUtc.MinValue) { value = value.WithServerTimestamp(DateTimeUtc.Now); } } // Commit any With-chain rebindings back into the results array. values[ii] = value; #if DEBUG if (nodeToRead.AttributeId == Attributes.Value) { m_logger.LogTrace( Utils.TraceMasks.ServiceDetail, "READ: NodeId={NodeId} Value={Value} Range={Range}", nodeToRead.NodeId, value.WrappedValue, nodeToRead.IndexRange); } #endif } // check for nothing to do. if (nodesToValidate.Count == 0) { return; } // validates the nodes (reads values from the underlying data source if required). await ReadAsync(systemContext, nodesToRead, values, errors, nodesToValidate, operationCache, cancellationToken).ConfigureAwait(false); } /// /// Finds a node in the dynamic cache. /// /// The current context. /// The node handle. /// The cache to search. /// The cancellation token. /// The node if found. Null otherwise. protected virtual ValueTask FindNodeInCacheAsync( ServerSystemContext context, NodeHandle handle, IDictionary cache, CancellationToken cancellationToken = default) { // not valid if no root. if (handle == null) { return default; } // check if previously validated. if (handle.Validated) { return new ValueTask(handle.Node); } // construct id for root node. NodeId rootId = handle.RootId; if (cache != null) { // lookup component in local cache for request. if (cache.TryGetValue(handle.NodeId, out NodeState? target)) { return new ValueTask(target); } // lookup root in local cache for request. if (!string.IsNullOrEmpty(handle.ComponentPath) && cache.TryGetValue(rootId, out target)) { NodeState? child; lock (target) { child = target.FindChildBySymbolicName(context, handle.ComponentPath); } // component exists. if (child != null) { return new ValueTask(child); } } } // lookup component in shared cache. return new ValueTask(LookupNodeInComponentCache(context, handle)!); } /// /// Marks the handle as validated and saves the node in the dynamic cache. /// protected virtual NodeState ValidationComplete( ServerSystemContext context, NodeHandle handle, NodeState node, IDictionary cache) { handle.Node = node; handle.Validated = true; if (cache != null && handle != null) { cache[handle.NodeId] = node; } return node; } /// /// Verifies that the specified node exists. /// protected virtual async ValueTask ValidateNodeAsync( ServerSystemContext context, NodeHandle handle, IDictionary cache, CancellationToken cancellationToken = default) { // lookup in cache. NodeState target = await FindNodeInCacheAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (target != null) { handle.Node = target; handle.Validated = true; return handle.Node; } // return default. return handle.Node; } /// /// Validates the nodes and reads the values from the underlying source. /// /// The context. /// The nodes to read. /// The values. /// The errors. /// The nodes to validate. /// The cache. /// The cancellation token. protected virtual async ValueTask ReadAsync( ServerSystemContext context, ArrayOf nodesToRead, IList values, IList errors, List nodesToValidate, IDictionary cache, CancellationToken cancellationToken = default) { for (int ii = 0; ii < nodesToValidate.Count; ii++) { NodeHandle handle = nodesToValidate[ii]; // validate node. NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } ReadValueId nodeToRead = nodesToRead[handle.Index]; DataValue value = values[handle.Index]; // update the attribute value. // Async path โ€” the default NodeState.ReadAttributeAsync re-takes // `lock(this)` around the synchronous fallback when no async // hook is set, so behaviour is preserved bit-for-bit. When a // BaseVariableState async hook is set the lock is dropped on // the await, allowing true async I/O. (errors[handle.Index], value) = await source.ReadAttributeAsync( context, nodeToRead.AttributeId, nodeToRead.ParsedIndexRange, nodeToRead.DataEncoding, value, cancellationToken).ConfigureAwait(false); values[handle.Index] = value; } } /// /// Writes the value for the specified attributes. /// public virtual async ValueTask WriteAsync( OperationContext context, ArrayOf nodesToWrite, IList errors, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToValidate = new List(); await m_writeSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { for (int ii = 0; ii < nodesToWrite.Count; ii++) { WriteValue nodeToWrite = nodesToWrite[ii]; // skip items that have already been processed. if (nodeToWrite.Processed) { continue; } // check for valid handle. NodeHandle handle = await GetManagerHandleAsync( systemContext, nodeToWrite.NodeId, operationCache, cancellationToken).ConfigureAwait(false); if (handle == null) { continue; } // owned by this node manager. nodeToWrite.Processed = true; // index range is not supported. if (nodeToWrite.AttributeId != Attributes.Value && !string.IsNullOrEmpty(nodeToWrite.IndexRange)) { errors[ii] = StatusCodes.BadWriteNotSupported; continue; } // check if the node is a area in memory. if (handle.Node == null) { errors[ii] = StatusCodes.BadNodeIdUnknown; // must validate node in a separate operation. handle.Index = ii; nodesToValidate.Add(handle); continue; } // check if the node is AnalogItem and the values are outside the InstrumentRange. if (handle.Node is AnalogItemState analogItemState && analogItemState.InstrumentRange != null) { try { Variant doubleVariant = nodeToWrite.Value.WrappedValue .ConvertTo(BuiltInType.Double); if (doubleVariant.TypeInfo.IsArray) { bool isOutOfRange = false; foreach (double newValue in doubleVariant.GetDoubleArray()) { if (newValue > analogItemState.InstrumentRange.Value.High || newValue < analogItemState.InstrumentRange.Value.Low) { isOutOfRange = true; break; } } if (isOutOfRange) { errors[ii] = StatusCodes.BadOutOfRange; continue; } } else { double newValue = doubleVariant.GetDouble(); if (newValue > analogItemState.InstrumentRange.Value.High || newValue < analogItemState.InstrumentRange.Value.Low) { errors[ii] = StatusCodes.BadOutOfRange; continue; } } } catch { //skip the InstrumentRange check if the transformation isn't possible. } } #if DEBUG m_logger.LogTrace( Utils.TraceMasks.ServiceDetail, "WRITE: NodeId={NodeId} Value={Value} Range={Range}", nodeToWrite.NodeId, nodeToWrite.Value.WrappedValue, nodeToWrite.IndexRange); #endif var propertyState = handle.Node as PropertyState; Variant previousPropertyValue = propertyState?.Value ?? default; Variant oldValue = default; if (Server?.Auditing == true) { //current server supports auditing // read the old value for the purpose of auditing var oldDataValue = new DataValue(); (_, oldDataValue) = await handle.Node.ReadAttributeAsync( systemContext, nodeToWrite.AttributeId, nodeToWrite.ParsedIndexRange, QualifiedName.Null, oldDataValue, cancellationToken).ConfigureAwait(false); oldValue = oldDataValue.WrappedValue; } // write the attribute value. errors[ii] = await handle.Node.WriteAttributeAsync( systemContext, nodeToWrite.AttributeId, nodeToWrite.ParsedIndexRange, nodeToWrite.Value, cancellationToken).ConfigureAwait(false); // report the write value audit event Server.ReportAuditWriteUpdateEvent( systemContext, nodeToWrite, oldValue, errors[ii]?.StatusCode ?? StatusCodes.Good, m_logger); if (!ServiceResult.IsGood(errors[ii])) { continue; } if (propertyState != null) { CheckIfSemanticsHaveChanged( systemContext, propertyState, nodeToWrite.Value, previousPropertyValue); } //not needed for sampling groups if (m_monitoredItemManager is MonitoredNodeMonitoredItemManager) { // updates to source finished - report changes to monitored items. handle.Node.ClearChangeMasks(systemContext, true); } } // check for nothing to do. if (nodesToValidate.Count == 0) { return; } } finally { m_writeSemaphore.Release(); } // validates the nodes and writes the value to the underlying system. await WriteAsync( systemContext, nodesToWrite, errors, nodesToValidate, operationCache, cancellationToken).ConfigureAwait(false); } private void CheckIfSemanticsHaveChanged( ServerSystemContext systemContext, PropertyState property, Variant newPropertyValue, Variant previousPropertyValue) { // check if the changed property is one that can trigger semantic changes string? propertyName = property.BrowseName.Name; if (propertyName is not BrowseNames.EURange and not BrowseNames.InstrumentRange and not BrowseNames.EngineeringUnits and not BrowseNames.Title and not BrowseNames.AxisDefinition and not BrowseNames.FalseState and not BrowseNames.TrueState and not BrowseNames.EnumStrings and not BrowseNames.XAxisDefinition and not BrowseNames.YAxisDefinition and not BrowseNames.ZAxisDefinition) { return; } // ceck if property value changed if (Utils.IsEqual(newPropertyValue, previousPropertyValue)) { return; } foreach (KeyValuePair kvp in MonitoredItems) { if (kvp.Value is not ISampledDataChangeMonitoredItem monitoredItem || monitoredItem.AttributeId != Attributes.Value) { continue; } // Try to get the node from the handle if (monitoredItem.ManagerHandle is not NodeHandle handle || handle.Node == null) { continue; } NodeState node = handle.Node; BaseInstanceState? propertyState = node.FindChild( systemContext, property!.BrowseName); if (propertyState != null && property != null && propertyState.NodeId == property.NodeId) { if (( node is AnalogItemState && (propertyName == BrowseNames.EURange || propertyName == BrowseNames.EngineeringUnits) ) || ( node is TwoStateDiscreteState && (propertyName == BrowseNames.FalseState || propertyName == BrowseNames.TrueState) ) || (node is MultiStateDiscreteState && (propertyName == BrowseNames.EnumStrings)) || ( node is ArrayItemState && ( propertyName == BrowseNames.InstrumentRange || propertyName == BrowseNames.EURange || propertyName == BrowseNames.EngineeringUnits || propertyName == BrowseNames.Title) ) || ( (node is YArrayItemState || node is XYArrayItemState) && ( propertyName == BrowseNames.InstrumentRange || propertyName == BrowseNames.EURange || propertyName == BrowseNames.EngineeringUnits || propertyName == BrowseNames.Title || propertyName == BrowseNames.XAxisDefinition) ) || ( node is ImageItemState && ( propertyName == BrowseNames.InstrumentRange || propertyName == BrowseNames.EURange || propertyName == BrowseNames.EngineeringUnits || propertyName == BrowseNames.Title || propertyName == BrowseNames.XAxisDefinition || propertyName == BrowseNames.YAxisDefinition) ) || ( node is CubeItemState && ( propertyName == BrowseNames.InstrumentRange || propertyName == BrowseNames.EURange || propertyName == BrowseNames.EngineeringUnits || propertyName == BrowseNames.Title || propertyName == BrowseNames.XAxisDefinition || propertyName == BrowseNames.YAxisDefinition || propertyName == BrowseNames.ZAxisDefinition) ) || ( node is NDimensionArrayItemState && ( propertyName == BrowseNames.InstrumentRange || propertyName == BrowseNames.EURange || propertyName == BrowseNames.EngineeringUnits || propertyName == BrowseNames.Title || propertyName == BrowseNames.AxisDefinition))) { monitoredItem.SetSemanticsChanged(); var value = new DataValue(Variant.Null, StatusCodes.Good, DateTimeUtc.MinValue, DateTime.UtcNow); lock (node) { node.ReadAttribute( systemContext, Attributes.Value, monitoredItem.IndexRange, default, ref value); } monitoredItem.QueueValue(value, ServiceResult.Good, true); RaiseSemanticChangeEvent(systemContext, node, property); } } } } /// /// Raises a semantic change event to notify clients that the structure or meaning of a node has changed. /// protected void RaiseSemanticChangeEvent(ISystemContext systemContext, NodeState node, PropertyState property) { var e = new SemanticChangeEventState(null); var message = new TranslationInfo( "SemanticChangeEvent", "en-US", "SemanticChangeEvent."); e.Initialize(systemContext, null, EventSeverity.Min, new LocalizedText(message)); e.SetChildValue( systemContext, BrowseNames.SourceNode, ObjectIds.Server, false); e.SetChildValue(systemContext, BrowseNames.SourceName, "Server", false); e.CreateOrReplaceChanges(systemContext, null!); e!.Changes!.Value = new[] { new SemanticChangeStructureDataType { Affected = node.NodeId, AffectedType = property.TypeDefinitionId } }.ToArrayOf(); Server.ReportEvent(e); } /// /// Raises a GeneralModelChangeEvent to notify clients that nodes or references /// have been added or removed in the address space. /// /// /// Per Part 5 ยง9.32.2 only nodes that carry a NodeVersion /// property may trigger a ModelChangeEvent. When /// is true /// (the default), entries whose Affected node is owned by this /// node manager and lacks a NodeVersion are dropped. The /// affected nodes that survive the filter have their NodeVersion /// bumped. Set to /// false to opt back into the legacy unconditional /// behaviour. /// protected void RaiseGeneralModelChangeEvent( ISystemContext systemContext, ArrayOf changes) { if (changes.Count == 0) { return; } if (RequireNodeVersionForModelChange) { changes = FilterChangesByNodeVersion(systemContext, changes); if (changes.Count == 0) { return; } } var e = new GeneralModelChangeEventState(null); var message = new TranslationInfo( "GeneralModelChangeEvent", "en-US", "Address space model changed."); e.Initialize(systemContext, null, EventSeverity.Low, new LocalizedText(message)); e.SetChildValue( systemContext, BrowseNames.SourceNode, ObjectIds.Server, false); e.SetChildValue(systemContext, BrowseNames.SourceName, "Server", false); e.CreateOrReplaceChanges(systemContext, null!); e!.Changes!.Value = changes; Server.ReportEvent(e); } /// /// Raises a BaseModelChangeEvent for the supplied source /// node โ€” used when something about the address space changed /// but the framework cannot describe the change as a list of /// ModelChangeStructureDataType entries (Part 5 ยง9.32.5). /// /// /// The default source is per /// Part 5 ยง9.32.3, which models the whole address space as the /// default View. /// protected void RaiseBaseModelChangeEvent( ISystemContext systemContext, NodeId sourceNode = default) { NodeId effectiveSource = sourceNode.IsNull ? ObjectIds.Server : sourceNode; var e = new BaseModelChangeEventState(null); var message = new TranslationInfo( "BaseModelChangeEvent", "en-US", "Address space model changed."); e.Initialize(systemContext, null, EventSeverity.Low, new LocalizedText(message)); e.SetChildValue(systemContext, BrowseNames.SourceNode, effectiveSource, false); e.SetChildValue(systemContext, BrowseNames.SourceName, "Server", false); Server.ReportEvent(e); } /// /// Returns a new containing only those /// entries whose /// Affected node carries a NodeVersion property โ€” when /// the node is owned by this node manager. Affected nodes that /// belong to a different node manager (or that cannot be /// resolved) pass through unchanged. Bumps the NodeVersion of /// every entry that is kept. /// private ArrayOf FilterChangesByNodeVersion( ISystemContext systemContext, ArrayOf changes) { var kept = new List(changes.Count); for (int ii = 0; ii < changes.Count; ii++) { ModelChangeStructureDataType entry = changes[ii]; NodeId affected = entry.Affected; if (affected.IsNull) { continue; } if (!PredefinedNodes.TryGetValue(affected, out NodeState? affectedNode)) { // Not owned by this manager โ€” pass through unchanged. kept.Add(entry); continue; } if (!affectedNode.HasNodeVersion()) { continue; } affectedNode.BumpNodeVersion(systemContext); kept.Add(entry); } return new ArrayOf(kept.ToArray()); } /// /// The model change aggregator used by / /// /// to batch address-space changes per publish cycle. /// protected ModelChangeAggregator ModelChangeAggregator { get; } = new ModelChangeAggregator(); /// /// Gets or sets whether the framework automatically emits /// GeneralModelChangeEventType from /// / . /// Default: true. /// public bool ModelChangeEmissionEnabled { get; set; } = true; /// /// Gets or sets whether /// and the auto-emit paths require the affected node to carry a /// NodeVersion property (per Part 5 ยง9.32.2). When /// true (the default) nodes without a NodeVersion neither /// appear in the emitted Changes[] array nor cause a /// GeneralModelChangeEvent to be raised on their own. /// Set to false to opt back into the legacy /// unconditional behaviour. /// public bool RequireNodeVersionForModelChange { get; set; } = true; /// /// Marks as eligible to trigger /// ModelChangeEvents by attaching a NodeVersion /// property in this manager's primary namespace if none is /// present, and wiring an OnWriteValue handler that /// posts a BaseModelChangeEvent whenever the property is /// written by an external client (Part 5 ยง9.32.2). Idempotent. /// /// The node to enable tracking on. /// /// Optional namespace index for the new NodeVersion property. /// Defaults to this manager's . /// /// The existing or newly attached NodeVersion property. public PropertyState EnableModelChangeTrackingFor(NodeState node, ushort? namespaceIndex = null) { return node.EnableModelChangeTracking( namespaceIndex ?? NamespaceIndex, (ctx, owner) => RaiseBaseModelChangeEvent(ctx, owner.NodeId)); } /// /// Drains the and emits a /// single GeneralModelChangeEvent. No-op when no changes /// are pending. /// protected void EmitModelChange(ISystemContext systemContext) { if (!ModelChangeAggregator.HasPending) { return; } ArrayOf changes = ModelChangeAggregator.Drain(); RaiseGeneralModelChangeEvent(systemContext, changes); } /// /// Validates the nodes and writes the value to the underlying system. /// /// The context. /// The nodes to write. /// The errors. /// The nodes to validate. /// The cache. /// The cancellation token. protected virtual async ValueTask WriteAsync( ServerSystemContext context, ArrayOf nodesToWrite, IList errors, List nodesToValidate, IDictionary cache, CancellationToken cancellationToken = default) { // validates the nodes (reads values from the underlying data source if required). for (int ii = 0; ii < nodesToValidate.Count; ii++) { NodeHandle handle = nodesToValidate[ii]; await m_writeSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { // validate node. NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } WriteValue nodeToWrite = nodesToWrite[handle.Index]; // Async path โ€” see ReadAsync for the locking rationale. errors[handle.Index] = await source.WriteAttributeAsync( context, nodeToWrite.AttributeId, nodeToWrite.ParsedIndexRange, nodeToWrite.Value, cancellationToken).ConfigureAwait(false); // updates to source finished - report changes to monitored items. source.ClearChangeMasks(context, false); } finally { m_writeSemaphore.Release(); } } } /// /// Reads the history for the specified nodes. /// public virtual async ValueTask HistoryReadAsync( OperationContext context, HistoryReadDetails details, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, ArrayOf nodesToRead, IList results, IList errors, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToProcess = new List(); for (int ii = 0; ii < nodesToRead.Count; ii++) { HistoryReadValueId nodeToRead = nodesToRead[ii]; // skip items that have already been processed. if (nodeToRead.Processed) { continue; } // check for valid handle. NodeHandle handle = await GetManagerHandleAsync( systemContext, nodeToRead.NodeId, operationCache, cancellationToken).ConfigureAwait(false); if (handle == null) { continue; } // owned by this node manager. nodeToRead.Processed = true; // create an initial result. HistoryReadResult result = results[ii] = new HistoryReadResult(); result.HistoryData = default; result.ContinuationPoint = default; result.StatusCode = StatusCodes.Good; // check if the node is a area in memory. if (handle.Node == null) { errors[ii] = StatusCodes.BadNodeIdUnknown; // must validate node in a separate operation handle.Index = ii; nodesToProcess.Add(handle); continue; } errors[ii] = StatusCodes.BadHistoryOperationUnsupported; // check for data history variable. if (handle.Node is BaseVariableState variable && (variable.AccessLevel & AccessLevels.HistoryRead) != 0) { handle.Index = ii; nodesToProcess.Add(handle); continue; } // check for event history object. if (handle.Node is BaseObjectState notifier && (notifier.EventNotifier & EventNotifiers.HistoryRead) != 0) { handle.Index = ii; nodesToProcess.Add(handle); } } // check for nothing to do. if (nodesToProcess.Count == 0) { return; } // validates the nodes (reads values from the underlying data source if required). await HistoryReadAsync( systemContext, details, timestampsToReturn, releaseContinuationPoints, nodesToRead, results, errors, nodesToProcess, operationCache, cancellationToken).ConfigureAwait(false); } /// /// Returns the historian provider for a node. The default /// implementation returns null, which causes the dispatcher /// to fall back to the server-wide /// . /// /// /// /// Subclasses can override this to bind a specific /// to nodes owned by this node /// manager โ€” for example a database-backed historian for variables /// in this manager's namespace, while the server-wide registry /// covers everything else. /// /// protected virtual IHistorianProvider? GetHistorianProvider(NodeState node) { return null; } private IHistorianProvider? ResolveHistorianProvider(NodeState node) { return HistorianDispatcher.ResolveProvider(Server, node, GetHistorianProvider(node)); } /// /// Releases the continuation points. /// protected virtual async ValueTask HistoryReleaseContinuationPointsAsync( ServerSystemContext context, ArrayOf nodesToRead, IList errors, List nodesToProcess, IDictionary cache, CancellationToken cancellationToken = default) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; // validate node. NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } errors[handle.Index] = HistorianDispatcher.ReleaseContinuationPoint( context, nodesToRead[handle.Index]); } } /// /// Reads raw history data. /// protected virtual async ValueTask HistoryReadRawModifiedAsync( ServerSystemContext context, ReadRawModifiedDetails details, TimestampsToReturn timestampsToReturn, ArrayOf nodesToRead, IList results, IList errors, List nodesToProcess, IDictionary cache, CancellationToken cancellationToken = default) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } // Route HistoryRead on an Annotations property to the // parent variable's annotation provider (Part 11 ยง5.2.7). if (HistorianDispatcher.IsAnnotationsProperty(source)) { BaseVariableState? parent = HistorianDispatcher.GetAnnotationsParent(source); if (parent == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } IHistorianProvider? annotationProvider = ResolveHistorianProvider(parent); if (annotationProvider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } errors[handle.Index] = await HistorianDispatcher.DispatchAnnotationReadAsync( context, annotationProvider, parent, nodesToRead[handle.Index], details, timestampsToReturn, results[handle.Index], cancellationToken).ConfigureAwait(false); continue; } if (source is not BaseVariableState) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } IHistorianProvider? provider = ResolveHistorianProvider(source); if (provider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } errors[handle.Index] = await HistorianDispatcher.DispatchRawReadAsync( context, provider, source, nodesToRead[handle.Index], details, timestampsToReturn, results[handle.Index], cancellationToken).ConfigureAwait(false); } } /// /// Reads processed history data. /// protected virtual async ValueTask HistoryReadProcessedAsync( ServerSystemContext context, ReadProcessedDetails details, TimestampsToReturn timestampsToReturn, ArrayOf nodesToRead, IList results, IList errors, List nodesToProcess, IDictionary cache, CancellationToken cancellationToken = default) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } if (source is not BaseVariableState) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } IHistorianProvider? provider = ResolveHistorianProvider(source); if (provider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } NodeId aggregateId = !details.AggregateType.IsNull && details.AggregateType.Count > 0 ? (handle.Index < details.AggregateType.Count ? details.AggregateType[handle.Index] : details.AggregateType[0]) : NodeId.Null; errors[handle.Index] = await HistorianDispatcher.DispatchProcessedReadAsync( context, provider, source, nodesToRead[handle.Index], details, aggregateId, timestampsToReturn, results[handle.Index], cancellationToken).ConfigureAwait(false); } } /// /// Reads history data at specified times. /// protected virtual async ValueTask HistoryReadAtTimeAsync( ServerSystemContext context, ReadAtTimeDetails details, TimestampsToReturn timestampsToReturn, ArrayOf nodesToRead, IList results, IList errors, List nodesToProcess, IDictionary cache, CancellationToken cancellationToken = default) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } if (source is not BaseVariableState) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } IHistorianProvider? provider = ResolveHistorianProvider(source); if (provider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } errors[handle.Index] = await HistorianDispatcher.DispatchAtTimeReadAsync( context, provider, source, nodesToRead[handle.Index], details, timestampsToReturn, results[handle.Index], cancellationToken).ConfigureAwait(false); } } /// /// Reads history events. /// protected virtual async ValueTask HistoryReadEventsAsync( ServerSystemContext context, ReadEventDetails details, TimestampsToReturn timestampsToReturn, ArrayOf nodesToRead, IList results, IList errors, List nodesToProcess, IDictionary cache, CancellationToken cancellationToken = default) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } IHistorianProvider? provider = ResolveHistorianProvider(source); if (provider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } errors[handle.Index] = await HistorianDispatcher.DispatchEventReadAsync( context, provider, source, nodesToRead[handle.Index], details, timestampsToReturn, results[handle.Index], cancellationToken).ConfigureAwait(false); } } /// /// Validates the nodes and reads the values from the underlying source. /// /// protected virtual async ValueTask HistoryReadAsync( ServerSystemContext context, HistoryReadDetails details, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, ArrayOf nodesToRead, IList results, IList errors, List nodesToProcess, IDictionary cache, CancellationToken cancellationToken = default) { // check if continuation points are being released. if (releaseContinuationPoints) { await HistoryReleaseContinuationPointsAsync( context, nodesToRead, errors, nodesToProcess, cache, cancellationToken).ConfigureAwait(false); return; } // check timestamps to return. if (timestampsToReturn is < TimestampsToReturn.Source or > TimestampsToReturn.Neither) { throw new ServiceResultException(StatusCodes.BadTimestampsToReturnInvalid); } // If any node-to-read carries a continuation point the dispatcher // will restore the saved request (including its original // timestamps and NumValuesPerNode). Continuation-point reads // therefore commonly arrive with MinValue/MinValue timestamps // and NumValuesPerNode = 0 โ€” skip the timestamp validation in // that case so subsequent pages aren't rejected. bool hasContinuationPoint = false; for (int idx = 0; idx < nodesToRead.Count; idx++) { if (!nodesToRead[idx].ContinuationPoint.IsEmpty) { hasContinuationPoint = true; break; } } // handle raw data request. if (details is ReadRawModifiedDetails readRawModifiedDetails) { if (!hasContinuationPoint) { // at least one must be provided. if (readRawModifiedDetails.StartTime == DateTimeUtc.MinValue && readRawModifiedDetails.EndTime == DateTimeUtc.MinValue) { throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument); } // if one is null the num values must be provided. if (readRawModifiedDetails.StartTime == DateTimeUtc.MinValue || readRawModifiedDetails.EndTime == DateTimeUtc.MinValue) { if (readRawModifiedDetails.NumValuesPerNode == 0) { throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument); } } } await HistoryReadRawModifiedAsync( context, readRawModifiedDetails, timestampsToReturn, nodesToRead, results, errors, nodesToProcess, cache, cancellationToken).ConfigureAwait(false); return; } // handle processed data request. if (details is ReadProcessedDetails readProcessedDetails) { // check the list of aggregates. if (readProcessedDetails.AggregateType.Count != nodesToRead.Count) { throw new ServiceResultException(StatusCodes.BadAggregateListMismatch); } if (!hasContinuationPoint) { // check start/end time. if (readProcessedDetails.StartTime == DateTimeUtc.MinValue || readProcessedDetails.EndTime == DateTimeUtc.MinValue) { throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument); } } await HistoryReadProcessedAsync( context, readProcessedDetails, timestampsToReturn, nodesToRead, results, errors, nodesToProcess, cache, cancellationToken).ConfigureAwait(false); return; } // handle raw data at time request. if (details is ReadAtTimeDetails readAtTimeDetails) { await HistoryReadAtTimeAsync( context, readAtTimeDetails, timestampsToReturn, nodesToRead, results, errors, nodesToProcess, cache, cancellationToken).ConfigureAwait(false); return; } // handle read events request. if (details is ReadEventDetails readEventDetails) { if (!hasContinuationPoint) { // check start/end time and max values. if (readEventDetails.NumValuesPerNode == 0) { if (readEventDetails.StartTime == DateTimeUtc.MinValue || readEventDetails.EndTime == DateTimeUtc.MinValue) { throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument); } } else if (readEventDetails.StartTime == DateTimeUtc.MinValue && readEventDetails.EndTime == DateTimeUtc.MinValue) { throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument); } // validate the event filter. EventFilter.Result result = readEventDetails.Filter.Validate( new FilterContext(Server.NamespaceUris, Server.TypeTree, context, Server.Telemetry)); if (ServiceResult.IsBad(result.Status)) { throw new ServiceResultException(result.Status); } } // read the event history. await HistoryReadEventsAsync( context, readEventDetails, timestampsToReturn, nodesToRead, results, errors, nodesToProcess, cache, cancellationToken).ConfigureAwait(false); } } /// /// Updates the history for the specified nodes. /// public virtual async ValueTask HistoryUpdateAsync( OperationContext context, Type detailsType, ArrayOf nodesToUpdate, IList results, IList errors, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToProcess = new List(); await m_writeSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { for (int ii = 0; ii < nodesToUpdate.Count; ii++) { HistoryUpdateDetails nodeToUpdate = nodesToUpdate[ii]; // skip items that have already been processed. if (nodeToUpdate.Processed) { continue; } // check for valid handle. NodeHandle handle = await GetManagerHandleAsync( systemContext, nodeToUpdate.NodeId, operationCache, cancellationToken).ConfigureAwait(false); if (handle == null) { continue; } // owned by this node manager. nodeToUpdate.Processed = true; // create an initial result. HistoryUpdateResult result = results[ii] = new HistoryUpdateResult(); result.StatusCode = StatusCodes.Good; // check if the node is a area in memory. if (handle.Node == null) { errors[ii] = StatusCodes.BadNodeIdUnknown; // must validate node in a separate operation handle.Index = ii; nodesToProcess.Add(handle); continue; } errors[ii] = StatusCodes.BadHistoryOperationUnsupported; // check for data history variable. if (handle.Node is BaseVariableState variable && (variable.AccessLevel & AccessLevels.HistoryWrite) != 0) { handle.Index = ii; nodesToProcess.Add(handle); continue; } // check for event history object. if (handle.Node is BaseObjectState notifier && (notifier.EventNotifier & EventNotifiers.HistoryWrite) != 0) { handle.Index = ii; nodesToProcess.Add(handle); } } // check for nothing to do. if (nodesToProcess.Count == 0) { return; } } finally { m_writeSemaphore.Release(); } // validates the nodes and updates. await HistoryUpdateAsync( systemContext, detailsType, nodesToUpdate, results, errors, nodesToProcess, operationCache, cancellationToken).ConfigureAwait(false); } /// /// Validates the nodes and updates the history. /// protected virtual async ValueTask HistoryUpdateAsync( ServerSystemContext context, Type detailsType, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache, CancellationToken cancellationToken) { // handle update data request. if (detailsType == typeof(UpdateDataDetails)) { var details = new UpdateDataDetails[nodesToUpdate.Count]; for (int ii = 0; ii < details.Length; ii++) { details[ii] = (UpdateDataDetails)nodesToUpdate[ii]; } await HistoryUpdateDataAsync(context, details, results, errors, nodesToProcess, cache, cancellationToken).ConfigureAwait(false); return; } // handle update structure data request. if (detailsType == typeof(UpdateStructureDataDetails)) { var details = new UpdateStructureDataDetails[nodesToUpdate.Count]; for (int ii = 0; ii < details.Length; ii++) { details[ii] = (UpdateStructureDataDetails)nodesToUpdate[ii]; } await HistoryUpdateStructureDataAsync( context, details, results, errors, nodesToProcess, cache, cancellationToken).ConfigureAwait(false); return; } // handle update events request. if (detailsType == typeof(UpdateEventDetails)) { var details = new UpdateEventDetails[nodesToUpdate.Count]; for (int ii = 0; ii < details.Length; ii++) { details[ii] = (UpdateEventDetails)nodesToUpdate[ii]; } await HistoryUpdateEventsAsync(context, details, results, errors, nodesToProcess, cache, cancellationToken).ConfigureAwait(false); return; } // handle delete raw data request. if (detailsType == typeof(DeleteRawModifiedDetails)) { var details = new DeleteRawModifiedDetails[nodesToUpdate.Count]; for (int ii = 0; ii < details.Length; ii++) { details[ii] = (DeleteRawModifiedDetails)nodesToUpdate[ii]; } await HistoryDeleteRawModifiedAsync(context, details, results, errors, nodesToProcess, cache, cancellationToken).ConfigureAwait(false); return; } // handle delete at time request. if (detailsType == typeof(DeleteAtTimeDetails)) { var details = new DeleteAtTimeDetails[nodesToUpdate.Count]; for (int ii = 0; ii < details.Length; ii++) { details[ii] = (DeleteAtTimeDetails)nodesToUpdate[ii]; } await HistoryDeleteAtTimeAsync(context, details, results, errors, nodesToProcess, cache, cancellationToken).ConfigureAwait(false); return; } // handle delete at time request. if (detailsType == typeof(DeleteEventDetails)) { var details = new DeleteEventDetails[nodesToUpdate.Count]; for (int ii = 0; ii < details.Length; ii++) { details[ii] = (DeleteEventDetails)nodesToUpdate[ii]; } await HistoryDeleteEventsAsync(context, details, results, errors, nodesToProcess, cache, cancellationToken).ConfigureAwait(false); } } /// /// Updates the data history for one or more nodes. /// protected virtual async ValueTask HistoryUpdateDataAsync( ServerSystemContext context, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache, CancellationToken cancellationToken) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } if (source is not BaseVariableState) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } IHistorianProvider? provider = ResolveHistorianProvider(source); if (provider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } errors[handle.Index] = await HistorianDispatcher.DispatchUpdateDataAsync( context, provider, source, nodesToUpdate[handle.Index], results[handle.Index], cancellationToken).ConfigureAwait(false); } } /// /// Updates the structured data history (Part 11 ยง5.2.7 /// Annotations) for one or more nodes. /// protected virtual async ValueTask HistoryUpdateStructureDataAsync( ServerSystemContext context, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache, CancellationToken cancellationToken) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } if (!HistorianDispatcher.IsAnnotationsProperty(source)) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } BaseVariableState? parent = HistorianDispatcher.GetAnnotationsParent(source); if (parent == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } IHistorianProvider? provider = ResolveHistorianProvider(parent); if (provider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } errors[handle.Index] = await HistorianDispatcher.DispatchAnnotationUpdateAsync( context, provider, parent, nodesToUpdate[handle.Index], results[handle.Index], cancellationToken).ConfigureAwait(false); } } /// /// Updates the event history for one or more nodes. /// protected virtual async ValueTask HistoryUpdateEventsAsync( ServerSystemContext context, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache, CancellationToken cancellationToken = default) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } IHistorianProvider? provider = ResolveHistorianProvider(source); if (provider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } errors[handle.Index] = await HistorianDispatcher.DispatchUpdateEventAsync( context, provider, source, nodesToUpdate[handle.Index], results[handle.Index], cancellationToken).ConfigureAwait(false); } } /// /// Deletes the data history for one or more nodes. /// protected virtual async ValueTask HistoryDeleteRawModifiedAsync( ServerSystemContext context, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache, CancellationToken cancellationToken = default) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } if (source is not BaseVariableState) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } IHistorianProvider? provider = ResolveHistorianProvider(source); if (provider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } errors[handle.Index] = await HistorianDispatcher.DispatchDeleteRawAsync( context, provider, source, nodesToUpdate[handle.Index], results[handle.Index], cancellationToken).ConfigureAwait(false); } } /// /// Deletes the data history at specified timestamps for one or more nodes. /// protected virtual async ValueTask HistoryDeleteAtTimeAsync( ServerSystemContext context, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache, CancellationToken cancellationToken = default) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } if (source is not BaseVariableState) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } IHistorianProvider? provider = ResolveHistorianProvider(source); if (provider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } errors[handle.Index] = await HistorianDispatcher.DispatchDeleteAtTimeAsync( context, provider, source, nodesToUpdate[handle.Index], results[handle.Index], cancellationToken).ConfigureAwait(false); } } /// /// Deletes the event history for one or more nodes. /// protected virtual async ValueTask HistoryDeleteEventsAsync( ServerSystemContext context, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache, CancellationToken cancellationToken = default) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } IHistorianProvider? provider = ResolveHistorianProvider(source); if (provider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } errors[handle.Index] = await HistorianDispatcher.DispatchDeleteEventsAsync( context, provider, source, nodesToUpdate[handle.Index], results[handle.Index], cancellationToken).ConfigureAwait(false); } } /// /// Resolves the effective method for a call request. /// public virtual async ValueTask FindMethodStateAsync( OperationContext context, CallMethodRequest methodToCall, CancellationToken cancellationToken = default) { if (methodToCall == null || methodToCall.ObjectId.IsNull || methodToCall.MethodId.IsNull) { return null!; } ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); NodeHandle? handle = await GetManagerHandleAsync( systemContext, methodToCall.ObjectId, operationCache, cancellationToken).ConfigureAwait(false); if (handle == null) { return null!; } NodeState? source = await ValidateNodeAsync(systemContext, handle, operationCache, cancellationToken).ConfigureAwait(false); if (source == null) { return null!; } MethodState? method; lock (source) { method = source.FindMethod(systemContext, methodToCall.MethodId); } if (method != null) { return method; } bool referenceExists; lock (source) { referenceExists = source.ReferenceExists( ReferenceTypeIds.HasComponent, false, methodToCall.MethodId); } if (referenceExists) { method = FindPredefinedNode(methodToCall.MethodId); } if (method == null && source is BaseInstanceState instanceState) { method = FindMethodInTypeHierarchy(systemContext, instanceState.TypeDefinitionId, methodToCall.MethodId); } return method!; } /// /// Asynchronously calls a method defined on an object. /// public virtual async ValueTask CallAsync( OperationContext context, ArrayOf methodsToCall, IList results, IList errors, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); for (int ii = 0; ii < methodsToCall.Count; ii++) { CallMethodRequest methodToCall = methodsToCall[ii]; // skip items that have already been processed. if (methodToCall.Processed) { continue; } // check for valid handle. NodeHandle handle = await GetManagerHandleAsync( systemContext, methodToCall.ObjectId, operationCache, cancellationToken).ConfigureAwait(false); if (handle == null) { continue; } // owned by this node manager. methodToCall.Processed = true; // validate the source node. if (await ValidateNodeAsync(systemContext, handle, operationCache, cancellationToken).ConfigureAwait(false) == null) { errors[ii] = StatusCodes.BadNodeIdUnknown; continue; } MethodState? method = await FindMethodStateAsync( context, methodToCall, cancellationToken).ConfigureAwait(false); if (method == null) { errors[ii] = StatusCodes.BadMethodInvalid; continue; } // validate the role permissions for method to be executed, // it may be a different MethodState that does not have the MethodId specified in the method call errors[ii] = await ValidateRolePermissionsAsync( context, method.NodeId, PermissionType.Call, cancellationToken).ConfigureAwait(false); if (ServiceResult.IsBad(errors[ii])) { continue; } // call the method. CallMethodResult result = results[ii] = new CallMethodResult(); errors[ii] = await CallAsync( systemContext, methodToCall, method, result, cancellationToken).ConfigureAwait(false); } } /// /// Asynchronously calls a method on an object. /// protected virtual async ValueTask CallAsync( ISystemContext context, CallMethodRequest methodToCall, MethodState method, CallMethodResult result, CancellationToken cancellationToken = default) { var systemContext = context as ServerSystemContext; var argumentErrors = new List(); var outputArguments = new List(); ServiceResult callResult = await method.CallAsync( context, methodToCall.ObjectId, methodToCall.InputArguments, argumentErrors, outputArguments, cancellationToken ).ConfigureAwait(false); if (ServiceResult.IsBad(callResult)) { return callResult; } // check for argument errors. bool argumentsValid = true; var inputArgumentResults = new List(); var inputArgumentDiagnosticInfos = new List(); for (int jj = 0; jj < argumentErrors.Count; jj++) { ServiceResult argumentError = argumentErrors[jj]; if (argumentError != null) { inputArgumentResults.Add(argumentError.StatusCode); if (ServiceResult.IsBad(argumentError)) { argumentsValid = false; } // only fill in diagnostic info if it is requested. if (systemContext!.OperationContext != null && (systemContext.OperationContext.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { if (ServiceResult.IsBad(argumentError)) { inputArgumentDiagnosticInfos.Add( new DiagnosticInfo( argumentError, systemContext.OperationContext.DiagnosticsMask, false, systemContext.OperationContext.StringTable, m_logger)); } else { inputArgumentDiagnosticInfos.Add(null!); } } } } // check for validation errors. if (!argumentsValid) { // Per OPC UA Part 4, Section 5.12: InputArgumentResults must be empty // when StatusCode is Good. Therefore set here to the argument results // and return a Bad status code. result.InputArgumentResults = inputArgumentResults; result.InputArgumentDiagnosticInfos = inputArgumentDiagnosticInfos; result.StatusCode = StatusCodes.BadInvalidArgument; return result.StatusCode; } // return output arguments. result.OutputArguments = outputArguments; // return the actual result of the original call return callResult; } /// /// Subscribes or unsubscribes to events produced by the specified source. /// /// /// This method is called when a event subscription is created or deletes. The node manager /// must start/stop reporting events for the specified object and all objects below it in /// the notifier hierarchy. /// public virtual async ValueTask SubscribeToEventsAsync( OperationContext context, object sourceId, uint subscriptionId, IEventMonitoredItem monitoredItem, bool unsubscribe, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); // check for valid handle. NodeHandle? handle = IsHandleInNamespace(sourceId); if (handle == null) { return StatusCodes.BadNodeIdInvalid; } // check for valid node. NodeState source = await ValidateNodeAsync(systemContext, handle, null!, cancellationToken).ConfigureAwait(false); if (source == null) { return StatusCodes.BadNodeIdUnknown; } // subscribe to events. return await SubscribeToEventsAsync(systemContext, source, monitoredItem, unsubscribe, cancellationToken).ConfigureAwait(false); } /// /// Subscribes or unsubscribes to events produced by all event sources. /// /// /// This method is called when a event subscription is created or deleted. The node /// manager must start/stop reporting events for all objects that it manages. /// public virtual async ValueTask SubscribeToAllEventsAsync( OperationContext context, uint subscriptionId, IEventMonitoredItem monitoredItem, bool unsubscribe, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); // A client has subscribed to the Server object which means all events produced // by this manager must be reported. This is done by incrementing the monitoring // reference count for all root notifiers. foreach (KeyValuePair kvp in RootNotifiers) { await SubscribeToEventsAsync( systemContext, kvp.Value, monitoredItem, unsubscribe, cancellationToken).ConfigureAwait(false); } return ServiceResult.Good; } /// /// Adds a root notifier. /// /// The notifier. /// The cancellation token. /// /// A root notifier is a notifier owned by the NodeManager that is not the target of a /// HasNotifier reference. These nodes need to be linked directly to the Server object. /// protected virtual async ValueTask AddRootNotifierAsync(NodeState notifier, CancellationToken cancellationToken = default) { RootNotifiers.AddOrUpdate(notifier.NodeId, notifier, (key, _) => notifier); // need to prevent recursion with the server object. if (notifier.NodeId != ObjectIds.Server) { lock (notifier) { notifier.OnReportEvent = OnReportEvent; if (!notifier.ReferenceExists( ReferenceTypeIds.HasNotifier, true, ObjectIds.Server)) { notifier.AddReference(ReferenceTypeIds.HasNotifier, true, ObjectIds.Server); } } } // subscribe to existing events. if (Server.EventManager != null) { IList monitoredItems = Server.EventManager .GetMonitoredItems(); for (int ii = 0; ii < monitoredItems.Count; ii++) { if (monitoredItems[ii].MonitoringAllEvents) { await SubscribeToEventsAsync(SystemContext, notifier, monitoredItems[ii], true, cancellationToken).ConfigureAwait(false); } } } } /// /// Removes a root notifier previously added with AddRootNotifier. /// /// The notifier. /// The cancellation token. protected virtual ValueTask RemoveRootNotifierAsync( NodeState notifier, CancellationToken cancellationToken = default) { if (RootNotifiers.TryRemove(notifier.NodeId, out notifier!)) { lock (notifier) { notifier.OnReportEvent = null; if (notifier.ReferenceExists( ReferenceTypeIds.HasNotifier, true, ObjectIds.Server)) { notifier.RemoveReference( ReferenceTypeIds.HasNotifier, true, ObjectIds.Server); } } } return default; } /// /// Reports an event for a root notifier. /// protected virtual void OnReportEvent( ISystemContext context, NodeState node, IFilterTarget e) { Server.ReportEvent(context, e); } /// /// Subscribes to events. /// /// The context. /// The source. /// The monitored item. /// if set to true [unsubscribe]. /// The cancellation token. /// Any error code. protected virtual async ValueTask SubscribeToEventsAsync( ServerSystemContext context, NodeState source, IEventMonitoredItem monitoredItem, bool unsubscribe, CancellationToken cancellationToken = default) { await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { (MonitoredNode2? monitoredNode, ServiceResult serviceResult) = m_monitoredItemManager! .SubscribeToEvents( context, source, monitoredItem, unsubscribe); // This call recursively updates a reference count all nodes in the notifier // hierarchy below the area. Sources with a reference count of 0 do not have // any active subscriptions so they do not need to report events. lock (source) { source.SetAreEventsMonitored(context, !unsubscribe, true); } // signal update. await OnSubscribeToEventsAsync(context, monitoredNode!, unsubscribe, cancellationToken).ConfigureAwait(false); // all done. return serviceResult; } finally { m_monitoredItemSemaphore.Release(); } } /// /// Called after subscribing/unsubscribing to events. /// /// The context. /// The monitored node. /// if set to true unsubscribing. /// The cancellation token. protected virtual ValueTask OnSubscribeToEventsAsync( ServerSystemContext context, MonitoredNode2 monitoredNode, bool unsubscribe, CancellationToken cancellationToken = default) { // defined by the sub-class return new ValueTask(); } /// /// Tells the node manager to refresh any conditions associated with the specified monitored items. /// /// /// This method is called when the condition refresh method is called for a subscription. /// The node manager must create a refresh event for each condition monitored by the subscription. /// public virtual async ValueTask ConditionRefreshAsync( OperationContext context, IList monitoredItems, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); for (int ii = 0; ii < monitoredItems.Count; ii++) { // the IEventMonitoredItem should always be MonitoredItems since they are created by the MasterNodeManager. IEventMonitoredItem monitoredItem = monitoredItems[ii]; if (monitoredItem == null) { continue; } var events = new List(); var nodesToRefresh = new List(); await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { // check for server subscription. if (monitoredItem.NodeId == ObjectIds.Server) { if (!RootNotifiers.IsEmpty) { nodesToRefresh.AddRange(RootNotifiers.Values); } } else { // check if monitored Item is managed by this node manager if (!MonitoredItems.ContainsKey(monitoredItem.Id)) { continue; } // get the refresh events. nodesToRefresh.Add(((NodeHandle)monitoredItem.ManagerHandle).Node); } } finally { m_monitoredItemSemaphore.Release(); } // block and wait for the refresh. for (int jj = 0; jj < nodesToRefresh.Count; jj++) { nodesToRefresh[jj].ConditionRefresh(systemContext, events, true); } // queue the events. for (int jj = 0; jj < events.Count; jj++) { // verify if the event can be received by the current monitored item ServiceResult result = await ValidateEventRolePermissionsAsync(monitoredItem, events[jj], cancellationToken).ConfigureAwait(false); if (ServiceResult.IsBad(result)) { continue; } monitoredItem.QueueEvent(events[jj]); } } // all done. return ServiceResult.Good; } /// /// Restore a set of monitored items after a restart. /// /// is null. /// public virtual async ValueTask RestoreMonitoredItemsAsync( IList itemsToRestore, IList monitoredItems, IUserIdentity savedOwnerIdentity, CancellationToken cancellationToken = default) { if (itemsToRestore == null) { throw new ArgumentNullException(nameof(itemsToRestore)); } if (monitoredItems == null) { throw new ArgumentNullException(nameof(monitoredItems)); } if (Server.IsRunning) { throw new InvalidOperationException( "Subscription restore can only occur on startup"); } ServerSystemContext systemContext = SystemContext.Copy(); IDictionary operationCache = new NodeIdDictionary(); var nodesToValidate = new List(); for (int ii = 0; ii < itemsToRestore.Count; ii++) { IStoredMonitoredItem itemToCreate = itemsToRestore[ii]; // skip items that have already been processed. if (itemToCreate.IsRestored) { continue; } // check for valid handle. NodeHandle handle = await GetManagerHandleAsync( systemContext, itemToCreate.NodeId, operationCache, cancellationToken).ConfigureAwait(false); if (handle == null) { continue; } // owned by this node manager. itemToCreate.IsRestored = true; handle.Index = ii; nodesToValidate.Add(handle); } // check for nothing to do. if (nodesToValidate.Count == 0) { return; } await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { // validates the nodes (reads values from the underlying data source if required). for (int ii = 0; ii < nodesToValidate.Count; ii++) { NodeHandle handle = nodesToValidate[ii]; bool success; IMonitoredItem? monitoredItem = null; try { // validate node. NodeState source = await ValidateNodeAsync(systemContext, handle, operationCache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } IStoredMonitoredItem itemToCreate = itemsToRestore[handle.Index]; // create monitored item. success = RestoreMonitoredItem( systemContext, handle, itemToCreate, savedOwnerIdentity, out monitoredItem); if (!success) { continue; } // save the monitored item. monitoredItems[handle.Index] = monitoredItem!; monitoredItem = null; // ownership transferred } finally { (monitoredItem as IDisposable)?.Dispose(); } } m_monitoredItemManager.ApplyChanges(); } finally { m_monitoredItemSemaphore.Release(); } // do any post processing. OnCreateMonitoredItemsComplete(systemContext, monitoredItems); } /// /// Restore a single monitored Item after a restart /// /// true if sucesfully restored protected virtual bool RestoreMonitoredItem( ServerSystemContext context, NodeHandle handle, IStoredMonitoredItem storedMonitoredItem, IUserIdentity savedOwnerIdentity, out IMonitoredItem? monitoredItem) { monitoredItem = null; // validate attribute. if (!Attributes.IsValid(handle.Node.NodeClass, storedMonitoredItem.AttributeId)) { return false; } // put an upper limit on queue size. storedMonitoredItem.QueueSize = SubscriptionManager.CalculateRevisedQueueSize( storedMonitoredItem.IsDurable, storedMonitoredItem.QueueSize, MaxQueueSize, MaxDurableQueueSize); bool success = m_monitoredItemManager.RestoreMonitoredItem( Server, m_syncNodeManager.ToAsyncNodeManager(), context, handle, storedMonitoredItem, savedOwnerIdentity, AddNodeToComponentCache, out ISampledDataChangeMonitoredItem restoredItem); monitoredItem = restoredItem; // report change. OnMonitoredItemCreated(context, handle, restoredItem); return true; } /// /// Creates a new set of monitored items for a set of variables. /// /// /// This method only handles data change subscriptions. Event subscriptions are created by the SDK. /// public virtual async ValueTask CreateMonitoredItemsAsync( OperationContext context, uint subscriptionId, double publishingInterval, TimestampsToReturn timestampsToReturn, ArrayOf itemsToCreate, IList errors, IList filterErrors, IList monitoredItems, bool createDurable, MonitoredItemIdFactory monitoredItemIdFactory, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToValidate = new List(); var createdItems = new List(); for (int ii = 0; ii < itemsToCreate.Count; ii++) { MonitoredItemCreateRequest itemToCreate = itemsToCreate[ii]; // skip items that have already been processed. if (itemToCreate.Processed) { continue; } ReadValueId itemToMonitor = itemToCreate.ItemToMonitor; // check for valid handle. NodeHandle handle = await GetManagerHandleAsync( systemContext, itemToMonitor.NodeId, operationCache, cancellationToken).ConfigureAwait(false); if (handle == null) { continue; } // owned by this node manager. itemToCreate.Processed = true; // must validate node in a separate operation. errors[ii] = StatusCodes.BadNodeIdUnknown; handle.Index = ii; nodesToValidate.Add(handle); } // check for nothing to do. if (nodesToValidate.Count == 0) { return; } await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { // validates the nodes (reads values from the underlying data source if required). for (int ii = 0; ii < nodesToValidate.Count; ii++) { NodeHandle handle = nodesToValidate[ii]; MonitoringFilterResult? filterResult; IMonitoredItem? monitoredItem; // validate node. NodeState source = await ValidateNodeAsync(systemContext, handle, operationCache, cancellationToken).ConfigureAwait(false); if (source == null) { continue; } MonitoredItemCreateRequest itemToCreate = itemsToCreate[handle.Index]; // create monitored item. (errors[handle.Index], filterResult, monitoredItem) = await CreateMonitoredItemAsync( systemContext, handle, subscriptionId, publishingInterval, context.DiagnosticsMask, timestampsToReturn, itemToCreate, createDurable, monitoredItemIdFactory, cancellationToken).ConfigureAwait(false); // save any filter error details. filterErrors[handle.Index] = filterResult!; if (ServiceResult.IsBad(errors[handle.Index])) { continue; } // save the monitored item. monitoredItems[handle.Index] = monitoredItem!; createdItems.Add(monitoredItem!); } m_monitoredItemManager.ApplyChanges(); } finally { m_monitoredItemSemaphore.Release(); } // do any post processing. OnCreateMonitoredItemsComplete(systemContext, createdItems); } /// /// Called when a batch of monitored items has been created. /// protected virtual void OnCreateMonitoredItemsComplete( ServerSystemContext context, IList monitoredItems) { // defined by the sub-class } /// /// Creates a new set of monitored items for a set of variables. /// /// /// This method only handles data change subscriptions. Event subscriptions are created by the SDK. /// protected virtual async ValueTask<(ServiceResult error, MonitoringFilterResult? filterResult, IMonitoredItem? monitoredItem)> CreateMonitoredItemAsync( ServerSystemContext context, NodeHandle handle, uint subscriptionId, double publishingInterval, DiagnosticsMasks diagnosticsMasks, TimestampsToReturn timestampsToReturn, MonitoredItemCreateRequest itemToCreate, bool createDurable, MonitoredItemIdFactory monitoredItemId, CancellationToken cancellationToken = default) { MonitoringFilterResult? filterResult = null; IMonitoredItem? monitoredItem = null; // validate parameters. MonitoringParameters parameters = itemToCreate.RequestedParameters; // validate attribute. if (!Attributes.IsValid(handle.Node.NodeClass, itemToCreate.ItemToMonitor.AttributeId)) { return (StatusCodes.BadAttributeIdInvalid, filterResult, monitoredItem); } // determine the sampling interval. double samplingInterval = itemToCreate.RequestedParameters.SamplingInterval; if (samplingInterval < 0) { samplingInterval = publishingInterval; } // ensure minimum sampling interval is not exceeded. if (itemToCreate.ItemToMonitor.AttributeId == Attributes.Value && handle.Node is BaseVariableState variable && samplingInterval < variable.MinimumSamplingInterval) { samplingInterval = variable.MinimumSamplingInterval; } // put a large upper limit on sampling. if (samplingInterval == double.MaxValue) { samplingInterval = 365 * 24 * 3600 * 1000.0; } // put an upper limit on queue size. uint revisedQueueSize = SubscriptionManager.CalculateRevisedQueueSize( createDurable, itemToCreate.RequestedParameters.QueueSize, MaxQueueSize, MaxDurableQueueSize); // validate the monitoring filter. ValidateMonitoringFilterResult validateMonitoringFilterResult = await ValidateMonitoringFilterAsync( context, handle, itemToCreate.ItemToMonitor.AttributeId, samplingInterval, revisedQueueSize, parameters.Filter, cancellationToken).ConfigureAwait(false); if (ServiceResult.IsBad(validateMonitoringFilterResult.StatusCode)) { return (validateMonitoringFilterResult.StatusCode, filterResult, monitoredItem); } ISampledDataChangeMonitoredItem dataChangeMonitoredItem = m_monitoredItemManager.CreateMonitoredItem( Server, m_syncNodeManager.ToAsyncNodeManager(), context, handle, subscriptionId, publishingInterval, diagnosticsMasks, timestampsToReturn, itemToCreate, validateMonitoringFilterResult.Range, validateMonitoringFilterResult.FilterToUse, samplingInterval, revisedQueueSize, createDurable, monitoredItemId, AddNodeToComponentCache); monitoredItem = dataChangeMonitoredItem; // report the initial value. ServiceResult error = ReadInitialValue(context, handle, dataChangeMonitoredItem); if (ServiceResult.IsBad(error)) { if (error.StatusCode == StatusCodes.BadAttributeIdInvalid || error.StatusCode == StatusCodes.BadDataEncodingInvalid || error.StatusCode == StatusCodes.BadDataEncodingUnsupported) { return (error, filterResult, monitoredItem); } error = StatusCodes.Good; } // report change. OnMonitoredItemCreated(context, handle, dataChangeMonitoredItem); return (error, filterResult, monitoredItem); } /// /// Reads the initial value for a monitored item. /// /// The context. /// The item handle. /// The monitored item. protected virtual ServiceResult ReadInitialValue( ISystemContext context, NodeHandle handle, IDataChangeMonitoredItem2 monitoredItem) { var initialValue = new DataValue( Variant.Null, StatusCodes.BadWaitingForInitialData, DateTimeUtc.MinValue, DateTime.UtcNow); ServiceResult error = handle.Node.ReadAttribute( context, monitoredItem.AttributeId, monitoredItem.IndexRange, monitoredItem.DataEncoding, ref initialValue); monitoredItem.QueueValue(initialValue, error, true); return error; } /// /// Called after creating a MonitoredItem. /// /// The context. /// The handle for the node. /// The monitored item. protected virtual void OnMonitoredItemCreated( ServerSystemContext context, NodeHandle handle, ISampledDataChangeMonitoredItem monitoredItem) { // overridden by the sub-class. } /// /// Validates Role permissions for the specified NodeId /// public virtual async ValueTask ValidateRolePermissionsAsync( OperationContext operationContext, NodeId nodeId, PermissionType requestedPermission, CancellationToken cancellationToken = default) { if (requestedPermission == PermissionType.None) { // no permission is required hence the validation passes. return StatusCodes.Good; } (object? nodeHandle, IAsyncNodeManager? nodeManager) = await Server.NodeManager .GetManagerHandleAsync(nodeId, cancellationToken).ConfigureAwait(false); if (nodeHandle == null || nodeManager == null) { // ignore unknown nodes. return StatusCodes.Good; } NodeMetadata nodeMetadata = await nodeManager.GetNodeMetadataAsync( operationContext, nodeHandle, BrowseResultMask.All, cancellationToken).ConfigureAwait(false); return MasterNodeManager.ValidateRolePermissions( operationContext, nodeMetadata, requestedPermission); } /// /// Validates if the specified event monitored item has enough permissions to receive the specified event /// public ValueTask ValidateEventRolePermissionsAsync( IEventMonitoredItem monitoredItem, IFilterTarget filterTarget, CancellationToken cancellationToken = default) { NodeId eventTypeId = default; NodeId sourceNodeId = default; var baseEventState = filterTarget as BaseEventState; if (baseEventState == null && filterTarget is InstanceStateSnapshot snapshot) { // try to get the event instance from snapshot object baseEventState = snapshot.Handle as BaseEventState; } if (baseEventState != null) { eventTypeId = baseEventState.EventType?.Value ?? default; sourceNodeId = baseEventState.SourceNode?.Value ?? default; } var operationContext = new OperationContext(monitoredItem); return ValidateEventReceivePermissionsAsync( operationContext, eventTypeId, sourceNodeId, cancellationToken); } /// /// Validates on both the /// event type node and the source node of an event. Spec reference /// Part 3 ยง8.55 (PermissionType bit 11): "A Client only receives an /// Event if this bit is set on the Node identified by the /// EventTypeId field and on the Node identified by the SourceNode /// field." Exposed so callers that have already extracted the two /// NodeIds (for example a per-monitored-item cache) can avoid the /// extra event-state inspection that /// performs. /// public async ValueTask ValidateEventReceivePermissionsAsync( OperationContext operationContext, NodeId eventTypeId, NodeId sourceNodeId, CancellationToken cancellationToken = default) { // validate the event type id permissions as specified ServiceResult result = await ValidateRolePermissionsAsync( operationContext, eventTypeId, PermissionType.ReceiveEvents, cancellationToken).ConfigureAwait(false); if (ServiceResult.IsBad(result)) { return result; } // validate the source node id permissions as specified return await ValidateRolePermissionsAsync( operationContext, sourceNodeId, PermissionType.ReceiveEvents, cancellationToken).ConfigureAwait(false); } /// /// Represents the result of validating a monitoring filter, including the filter to use, the applicable range, /// and the result of the filter evaluation. /// /// This class encapsulates the necessary information to determine which monitoring /// filter is applicable and the outcome of its evaluation within a specified range. public class ValidateMonitoringFilterResult { /// /// The Status of the Validation. /// public StatusCode StatusCode { get; set; } /// /// The filter to use /// public MonitoringFilter FilterToUse { get; set; } = null!; /// /// The range to use /// public Range Range { get; set; } = null!; /// /// The filter result /// public MonitoringFilterResult FilterResult { get; set; } = null!; } /// /// Validates the monitoring filter specified by the client. /// protected virtual async ValueTask ValidateMonitoringFilterAsync( ServerSystemContext context, NodeHandle handle, uint attributeId, double samplingInterval, uint queueSize, ExtensionObject filter, CancellationToken cancellationToken = default) { var result = new ValidateMonitoringFilterResult(); // nothing to do if the filter is not specified. if (filter.IsNull) { result.StatusCode = StatusCodes.Good; return result; } // extension objects wrap any data structure. must check that the client provided the correct structure. if (ExtensionObject.ToEncodeable(filter) is not DataChangeFilter deadbandFilter) { if (ExtensionObject.ToEncodeable(filter) is not AggregateFilter aggregateFilter || attributeId != Attributes.Value) { result.StatusCode = StatusCodes.BadFilterNotAllowed; return result; } if (!Server.AggregateManager.IsSupported(aggregateFilter.AggregateType)) { result.StatusCode = StatusCodes.BadAggregateNotSupported; return result; } var revisedFilter = new ServerAggregateFilter { AggregateType = aggregateFilter.AggregateType, StartTime = aggregateFilter.StartTime, ProcessingInterval = aggregateFilter.ProcessingInterval, AggregateConfiguration = aggregateFilter.AggregateConfiguration, Stepped = false }; StatusCode error = await ReviseAggregateFilterAsync( context, handle, samplingInterval, queueSize, revisedFilter, cancellationToken).ConfigureAwait(false); if (StatusCode.IsBad(error)) { result.StatusCode = error; return result; } var aggregateFilterResult = new AggregateFilterResult { RevisedProcessingInterval = aggregateFilter.ProcessingInterval, RevisedStartTime = aggregateFilter.StartTime, RevisedAggregateConfiguration = aggregateFilter.AggregateConfiguration }; result.FilterToUse = revisedFilter; result.FilterResult = aggregateFilterResult; result.StatusCode = StatusCodes.Good; return result; } // deadband filters only allowed for variable values. if (attributeId != Attributes.Value) { result.StatusCode = StatusCodes.BadFilterNotAllowed; return result; } if (handle.Node is not BaseVariableState variable) { result.StatusCode = StatusCodes.BadFilterNotAllowed; return result; } // check for status filter. if (deadbandFilter.DeadbandType == (uint)DeadbandType.None) { result.FilterToUse = deadbandFilter; result.StatusCode = StatusCodes.Good; return result; } // deadband filters can only be used for numeric values. if (!Server.TypeTree.IsTypeOf(variable.DataType, DataTypeIds.Number)) { result.StatusCode = StatusCodes.BadFilterNotAllowed; return result; } // nothing more to do for absolute filters. if (deadbandFilter.DeadbandType == (uint)DeadbandType.Absolute) { result.FilterToUse = deadbandFilter; result.StatusCode = StatusCodes.Good; return result; } // need to look up the EU range if a percent filter is requested. if (deadbandFilter.DeadbandType == (uint)DeadbandType.Percent) { lock (handle.Node) { if (handle.Node.FindChild( context, QualifiedName.From(BrowseNames.EURange)) is not PropertyState property) { result.StatusCode = StatusCodes.BadMonitoredItemFilterUnsupported; return result; } Range tmpRange; bool gotRange = property.Value.TryGetStructure(out tmpRange!); if (!gotRange) { result.StatusCode = StatusCodes.BadMonitoredItemFilterUnsupported; return result; } Range range = tmpRange; result.FilterToUse = deadbandFilter; result.Range = range!; result.StatusCode = StatusCodes.Good; return result; } } // no other type of filter supported. result.StatusCode = StatusCodes.BadFilterNotAllowed; return result; } /// /// Revises an aggregate filter (may require knowledge of the variable being used). /// /// The context. /// The handle. /// The sampling interval for the monitored item. /// The queue size for the monitored item. /// The filter to revise. /// The cancellation token. /// Good if the protected virtual ValueTask ReviseAggregateFilterAsync( ServerSystemContext context, NodeHandle handle, double samplingInterval, uint queueSize, ServerAggregateFilter filterToUse, CancellationToken cancellationToken = default) { if (filterToUse.ProcessingInterval < samplingInterval) { filterToUse.ProcessingInterval = samplingInterval; } if (filterToUse.ProcessingInterval < Server.AggregateManager.MinimumProcessingInterval) { filterToUse.ProcessingInterval = Server.AggregateManager.MinimumProcessingInterval; } DateTime earliestStartTime = DateTime.UtcNow.AddMilliseconds( -(queueSize - 1) * filterToUse.ProcessingInterval); if (earliestStartTime > filterToUse.StartTime) { filterToUse.StartTime = earliestStartTime; } if (filterToUse.AggregateConfiguration.UseServerCapabilitiesDefaults) { filterToUse.AggregateConfiguration = Server.AggregateManager .GetDefaultConfiguration(default); } return new ValueTask(StatusCodes.Good); } /// /// Modifies the parameters for a set of monitored items. /// public virtual async ValueTask ModifyMonitoredItemsAsync( OperationContext context, TimestampsToReturn timestampsToReturn, IList monitoredItems, ArrayOf itemsToModify, IList errors, IList filterErrors, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); var nodesInNamespace = new List<(int, NodeHandle)>(monitoredItems.Count); for (int ii = 0; ii < monitoredItems.Count; ii++) { MonitoredItemModifyRequest itemToModify = itemsToModify[ii]; // skip items that have already been processed. if (itemToModify.Processed || monitoredItems[ii] == null) { continue; } // check handle. NodeHandle? handle = IsHandleInNamespace(monitoredItems[ii].ManagerHandle); if (handle == null) { continue; } nodesInNamespace.Add((ii, handle)); } if (nodesInNamespace.Count == 0) { return; } var modifiedItems = new List(); await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { foreach ((int, NodeHandle) nodeInNamespace in nodesInNamespace) { int ii = nodeInNamespace.Item1; NodeHandle handle = nodeInNamespace.Item2; MonitoredItemModifyRequest itemToModify = itemsToModify[ii]; // owned by this node manager. itemToModify.Processed = true; // modify the monitored item. (ServiceResult error, MonitoringFilterResult? filterResult) = await ModifyMonitoredItemAsync( systemContext, context.DiagnosticsMask, timestampsToReturn, monitoredItems[ii], itemToModify, handle, cancellationToken).ConfigureAwait(false); errors[ii] = error; filterErrors[ii] = filterResult!; // save the modified item. if (ServiceResult.IsGood(errors[ii])) { modifiedItems.Add(monitoredItems[ii]); } } m_monitoredItemManager.ApplyChanges(); } finally { m_monitoredItemSemaphore.Release(); } // do any post processing. await OnModifyMonitoredItemsCompleteAsync(systemContext, modifiedItems, cancellationToken).ConfigureAwait(false); } /// /// Called when a batch of monitored items has been modified. /// protected virtual ValueTask OnModifyMonitoredItemsCompleteAsync( ServerSystemContext context, IList monitoredItems, CancellationToken cancellationToken = default) { // defined by the sub-class return new ValueTask(); } /// /// Modifies the parameters for a monitored item. /// protected virtual async ValueTask<(ServiceResult error, MonitoringFilterResult? filterResult)> ModifyMonitoredItemAsync( ServerSystemContext context, DiagnosticsMasks diagnosticsMasks, TimestampsToReturn timestampsToReturn, IMonitoredItem monitoredItem, MonitoredItemModifyRequest itemToModify, NodeHandle handle, CancellationToken cancellationToken = default) { // check for valid monitored item. var datachangeItem = monitoredItem as ISampledDataChangeMonitoredItem; // validate parameters. MonitoringParameters parameters = itemToModify.RequestedParameters; double previousSamplingInterval = datachangeItem!.SamplingInterval; // check if the variable needs to be sampled. double samplingInterval = itemToModify.RequestedParameters.SamplingInterval; if (samplingInterval < 0) { samplingInterval = previousSamplingInterval; } // ensure minimum sampling interval is not exceeded. if (datachangeItem.AttributeId == Attributes.Value && handle.Node is BaseVariableState variable && samplingInterval < variable.MinimumSamplingInterval) { samplingInterval = variable.MinimumSamplingInterval; } // put a large upper limit on sampling. if (samplingInterval == double.MaxValue) { samplingInterval = 365 * 24 * 3600 * 1000.0; } // put an upper limit on queue size. uint revisedQueueSize = SubscriptionManager.CalculateRevisedQueueSize( monitoredItem.IsDurable, itemToModify.RequestedParameters.QueueSize, MaxQueueSize, MaxDurableQueueSize); // validate the monitoring filter. ValidateMonitoringFilterResult validateMonitoringFilterResult = await ValidateMonitoringFilterAsync( context, handle, datachangeItem.AttributeId, samplingInterval, revisedQueueSize, parameters.Filter, cancellationToken).ConfigureAwait(false); if (ServiceResult.IsBad(validateMonitoringFilterResult.StatusCode)) { return (validateMonitoringFilterResult.StatusCode, validateMonitoringFilterResult.FilterResult); } // modify the monitored item parameters. ServiceResult? error = m_monitoredItemManager!.ModifyMonitoredItem( context, diagnosticsMasks, timestampsToReturn, validateMonitoringFilterResult.FilterToUse, validateMonitoringFilterResult.Range, samplingInterval, revisedQueueSize, datachangeItem, itemToModify); // report change. if (ServiceResult.IsGood(error)) { await OnMonitoredItemModifiedAsync(context, handle, datachangeItem, cancellationToken).ConfigureAwait(false); } return (error!, validateMonitoringFilterResult.FilterResult); } /// /// Called after modifying a MonitoredItem. /// /// The context. /// The handle for the node. /// The monitored item. /// The cancellation token. protected virtual ValueTask OnMonitoredItemModifiedAsync( ServerSystemContext context, NodeHandle handle, ISampledDataChangeMonitoredItem monitoredItem, CancellationToken cancellationToken = default) { // overridden by the sub-class. return new ValueTask(); } /// /// Deletes a set of monitored items. /// public virtual async ValueTask DeleteMonitoredItemsAsync( OperationContext context, IList monitoredItems, IList processedItems, IList errors, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); var nodesInNamespace = new List<(int, NodeHandle)>(monitoredItems.Count); for (int ii = 0; ii < monitoredItems.Count; ii++) { // skip items that have already been processed. if (processedItems[ii] || monitoredItems[ii] == null) { continue; } // check handle. NodeHandle? handle = IsHandleInNamespace(monitoredItems[ii].ManagerHandle); if (handle == null) { continue; } nodesInNamespace.Add((ii, handle)); } if (nodesInNamespace.Count == 0) { return; } var deletedItems = new List(); await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { foreach ((int, NodeHandle) nodeInNamespace in nodesInNamespace) { int ii = nodeInNamespace.Item1; NodeHandle handle = nodeInNamespace.Item2; // owned by this node manager. processedItems[ii] = true; errors[ii] = await DeleteMonitoredItemAsync(systemContext, monitoredItems[ii], handle, cancellationToken).ConfigureAwait(false); // save the modified item. if (ServiceResult.IsGood(errors[ii])) { deletedItems.Add(monitoredItems[ii]); RemoveNodeFromComponentCache(systemContext, handle); } } m_monitoredItemManager.ApplyChanges(); } finally { m_monitoredItemSemaphore.Release(); } // do any post processing. await OnDeleteMonitoredItemsCompleteAsync(systemContext, deletedItems, cancellationToken).ConfigureAwait(false); } /// /// Called when a batch of monitored items has been modified. /// protected virtual ValueTask OnDeleteMonitoredItemsCompleteAsync( ServerSystemContext context, IList monitoredItems, CancellationToken cancellationToken = default) { // defined by the sub-class return new ValueTask(); } /// /// Deletes a monitored item. /// protected virtual async ValueTask DeleteMonitoredItemAsync( ServerSystemContext context, IMonitoredItem monitoredItem, NodeHandle handle, CancellationToken cancellationToken = default) { var sampledDataChangeMonitoredItem = monitoredItem as ISampledDataChangeMonitoredItem; StatusCode statusCode = m_monitoredItemManager.DeleteMonitoredItem( context, sampledDataChangeMonitoredItem!, handle); // report change. await OnMonitoredItemDeletedAsync(context, handle, sampledDataChangeMonitoredItem!, cancellationToken).ConfigureAwait(false); return statusCode; } /// /// Called after deleting a MonitoredItem. /// /// The context. /// The handle for the node. /// The monitored item. /// The cancellation token. protected virtual ValueTask OnMonitoredItemDeletedAsync( ServerSystemContext context, NodeHandle handle, ISampledDataChangeMonitoredItem monitoredItem, CancellationToken cancellationToken = default) { // overridden by the sub-class. return new ValueTask(); } /// /// Transfers a set of monitored items. /// /// The context. /// Whether the subscription should send initial values after transfer. /// The set of monitoring items to update. /// The list of bool with items that were already processed. /// Any errors. /// The cancellation token. public virtual async ValueTask TransferMonitoredItemsAsync( OperationContext context, bool sendInitialValues, IList monitoredItems, IList processedItems, IList errors, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); var transferredItems = new List(); await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { for (int ii = 0; ii < monitoredItems.Count; ii++) { // skip items that have already been processed. if (processedItems[ii] || monitoredItems[ii] == null) { continue; } // check handle. NodeHandle? handle = IsHandleInNamespace(monitoredItems[ii].ManagerHandle); if (handle == null) { continue; } // owned by this node manager. processedItems[ii] = true; transferredItems.Add(monitoredItems[ii]); if (sendInitialValues) { monitoredItems[ii].SetupResendDataTrigger(); } errors[ii] = StatusCodes.Good; } } finally { m_monitoredItemSemaphore.Release(); } // do any post processing. await OnMonitoredItemsTransferredAsync(systemContext, transferredItems, cancellationToken).ConfigureAwait(false); } /// /// Called after transfer of MonitoredItems. /// /// The context. /// The transferred monitored items. /// The cancellation token. protected virtual ValueTask OnMonitoredItemsTransferredAsync( ServerSystemContext context, IList monitoredItems, CancellationToken cancellationToken = default) { // defined by the sub-class return new ValueTask(); } /// /// Changes the monitoring mode for a set of monitored items. /// /// The context. /// The monitoring mode. /// The set of monitoring items to update. /// Flags indicating which items have been processed. /// Any errors. /// The cancellation token. public virtual async ValueTask SetMonitoringModeAsync( OperationContext context, MonitoringMode monitoringMode, IList monitoredItems, IList processedItems, IList errors, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); var nodesInNamespace = new List<(int, NodeHandle)>(monitoredItems.Count); for (int ii = 0; ii < monitoredItems.Count; ii++) { // skip items that have already been processed. if (processedItems[ii] || monitoredItems[ii] == null) { continue; } // check handle. NodeHandle? handle = IsHandleInNamespace(monitoredItems[ii].ManagerHandle); if (handle == null) { continue; } nodesInNamespace.Add((ii, handle)); } if (nodesInNamespace.Count == 0) { return; } var changedItems = new List(); await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { foreach ((int, NodeHandle) nodeInNamespace in nodesInNamespace) { int ii = nodeInNamespace.Item1; NodeHandle handle = nodeInNamespace.Item2; // indicate whether it was processed or not. processedItems[ii] = true; // update monitoring mode. errors[ii] = await SetMonitoringModeAsync( systemContext, monitoredItems[ii], monitoringMode, handle, cancellationToken).ConfigureAwait(false); // save the modified item. if (ServiceResult.IsGood(errors[ii])) { changedItems.Add(monitoredItems[ii]); } } m_monitoredItemManager.ApplyChanges(); } finally { m_monitoredItemSemaphore.Release(); } // do any post processing. await OnSetMonitoringModeCompleteAsync(systemContext, changedItems, cancellationToken).ConfigureAwait(false); } /// /// Called when a batch of monitored items has their monitoring mode changed. /// protected virtual ValueTask OnSetMonitoringModeCompleteAsync( ServerSystemContext context, IList monitoredItems, CancellationToken cancellationToken = default) { // defined by the sub-class return new ValueTask(); } /// /// Changes the monitoring mode for an item. /// protected virtual async ValueTask SetMonitoringModeAsync( ServerSystemContext context, IMonitoredItem monitoredItem, MonitoringMode monitoringMode, NodeHandle handle, CancellationToken cancellationToken = default) { var sampledDataChangeMonitoredItem = monitoredItem as ISampledDataChangeMonitoredItem; (ServiceResult result, MonitoringMode? previousMode) = await m_monitoredItemManager .SetMonitoringModeAsync( context, sampledDataChangeMonitoredItem!, monitoringMode, handle, cancellationToken).ConfigureAwait(false); // report change. if (ServiceResult.IsGood(result) && previousMode != monitoringMode) { await OnMonitoringModeChangedAsync( context, handle, sampledDataChangeMonitoredItem!, (MonitoringMode)previousMode!, monitoringMode, cancellationToken).ConfigureAwait(false); } return result; } /// /// Called after changing the MonitoringMode for a MonitoredItem. /// /// The context. /// The handle for the node. /// The monitored item. /// The previous monitoring mode. /// The current monitoring mode. /// The cancellation token. protected virtual ValueTask OnMonitoringModeChangedAsync( ServerSystemContext context, NodeHandle handle, ISampledDataChangeMonitoredItem monitoredItem, MonitoringMode previousMode, MonitoringMode monitoringMode, CancellationToken cancellationToken = default) { // overridden by the sub-class. return new ValueTask(); } /// /// Called when a session is closed. /// public virtual ValueTask SessionClosingAsync( OperationContext context, NodeId sessionId, bool deleteSubscriptions, CancellationToken cancellationToken = default) { // overridden by the sub-class. return new ValueTask(); } /// /// Called when a session is activated and the user identity has changed. /// Invalidates cached role permissions for all monitored items belonging to the session /// so that permissions are re-evaluated on the next data change notification. /// public virtual ValueTask SessionActivatedAsync( OperationContext context, NodeId sessionId, CancellationToken cancellationToken = default) { foreach (MonitoredNode2 monitoredNode in MonitoredNodes.Values) { monitoredNode.InvalidatePermissionCacheForSession(sessionId); } return default; } /// /// Returns true if a node is in a view. /// public virtual async ValueTask IsNodeInViewAsync( OperationContext context, NodeId viewId, object nodeHandle, CancellationToken cancellationToken = default) { if (nodeHandle is not NodeHandle handle) { return false; } if (handle.Node != null) { return await IsNodeInViewAsync(context, viewId, handle.Node, cancellationToken).ConfigureAwait(false); } return false; } /// /// Returns the metadata containing the AccessRestrictions, RolePermissions and UserRolePermissions for the node. /// Returns null if the node does not exist. /// /// /// This method validates any placeholder handle. /// public async ValueTask GetPermissionMetadataAsync( OperationContext context, object targetHandle, BrowseResultMask resultMask, Dictionary uniqueNodesServiceAttributesCache, bool permissionsOnly, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = SystemContext.Copy(context); // check for valid handle. NodeHandle? handle = IsHandleInNamespace(targetHandle); if (handle == null) { return null!; } // validate node. NodeState? target = await ValidateNodeAsync(systemContext, handle, null!, cancellationToken).ConfigureAwait(false); if (target == null) { return null!; } var values = new Variant[3]; // construct the meta-data object. var metadata = new NodeMetadata(target, target.NodeId) { IsPartOfTypeHierarchy = target.IsPartOfTypeHierarchy }; // Treat the case of calls originating from the optimized services that use the cache (Read, Browse and Call services) if (uniqueNodesServiceAttributesCache != null) { NodeId key = handle.NodeId; if (uniqueNodesServiceAttributesCache.ContainsKey(key)) { if (uniqueNodesServiceAttributesCache[key].Length != 3) { ReadAndCacheValidationAttributes( uniqueNodesServiceAttributesCache, systemContext, target, key, ref values); } else { // Retrieve value from cache values = uniqueNodesServiceAttributesCache[key]; } } else { ReadAndCacheValidationAttributes( uniqueNodesServiceAttributesCache, systemContext, target, key, ref values); } SetAccessAndRolePermissions(values, metadata); } // All other calls that do not use the cache else if (permissionsOnly) { ReadValidationAttributes(systemContext, target, ref values); SetAccessAndRolePermissions(values, metadata); } await SetDefaultPermissionsAsync(systemContext, target, metadata, cancellationToken).ConfigureAwait(false); return metadata; } /// /// Set the metadata default permission values for DefaultAccessRestrictions, DefaultRolePermissions and DefaultUserRolePermissions /// private async ValueTask SetDefaultPermissionsAsync( ServerSystemContext systemContext, NodeState target, NodeMetadata metadata, CancellationToken cancellationToken = default) { // check if NamespaceMetadata is defined for NamespaceIndex of the node. NamespaceMetadataState? namespaceMetadataState = await Server.NodeManager.ConfigurationNodeManager!.GetNamespaceMetadataStateAsync(target.NodeId.NamespaceIndex, cancellationToken) .ConfigureAwait(false); if (namespaceMetadataState != null) { Variant value = default; DateTimeUtc sourceTimestamp = DateTimeUtc.MinValue; if (namespaceMetadataState.DefaultAccessRestrictions != null) { // get DefaultAccessRestrictions for Namespace namespaceMetadataState.DefaultAccessRestrictions .ReadAttribute( systemContext, Attributes.Value, ref value, ref sourceTimestamp); if (!value.IsNull) { metadata.DefaultAccessRestrictions = value.GetEnumeration(); } } if (namespaceMetadataState.DefaultRolePermissions != null) { // get DefaultRolePermissions for Namespace namespaceMetadataState.DefaultRolePermissions .ReadAttribute( systemContext, Attributes.Value, ref value, ref sourceTimestamp); if (!value.IsNull && value.TryGetStructure(out ArrayOf rolePermissions)) { metadata.DefaultRolePermissions = rolePermissions; } } if (namespaceMetadataState.DefaultUserRolePermissions != null) { // get DefaultUserRolePermissions for Namespace namespaceMetadataState.DefaultUserRolePermissions .ReadAttribute( systemContext, Attributes.Value, ref value, ref sourceTimestamp); if (!value.IsNull && value.TryGetStructure(out ArrayOf userRolePermissions)) { metadata.DefaultUserRolePermissions = userRolePermissions; } } } } /// /// Stores a reference count for entries in the component cache. /// private class CacheEntry { public int RefCount; public NodeState? Entry; } /// /// Looks up a component in cache. /// protected NodeState? LookupNodeInComponentCache(ISystemContext context, NodeHandle handle) { if (m_componentCache == null) { return null; } m_componentCacheSemaphore.Wait(); try { CacheEntry? entry = null; if (!string.IsNullOrEmpty(handle.ComponentPath)) { if (m_componentCache.TryGetValue(handle.RootId, out entry)) { return entry.Entry!.FindChildBySymbolicName(context, handle.ComponentPath); } } else if (m_componentCache.TryGetValue(handle.NodeId, out entry)) { return entry.Entry; } return null; } finally { m_componentCacheSemaphore.Release(); } } /// /// Removes a reference to a component in thecache. /// protected void RemoveNodeFromComponentCache(ISystemContext context, NodeHandle handle) { if (handle == null) { return; } m_componentCacheSemaphore.Wait(); try { if (m_componentCache != null) { NodeId nodeId = handle.NodeId; if (!string.IsNullOrEmpty(handle.ComponentPath)) { nodeId = handle.RootId; } if (m_componentCache.TryGetValue(nodeId, out CacheEntry? entry)) { entry.RefCount--; if (entry.RefCount == 0) { m_componentCache.Remove(nodeId); } } } } finally { m_componentCacheSemaphore.Release(); } } /// /// Adds a node to the component cache. /// protected NodeState AddNodeToComponentCache( ISystemContext context, NodeHandle handle, NodeState node) { if (handle == null) { return node; } m_componentCacheSemaphore.Wait(); try { m_componentCache ??= []; // check if a component is actually specified. if (!string.IsNullOrEmpty(handle.ComponentPath)) { if (m_componentCache.TryGetValue(handle.RootId, out CacheEntry? entry)) { entry.RefCount++; if (!string.IsNullOrEmpty(handle.ComponentPath)) { return entry.Entry! .FindChildBySymbolicName(context, handle.ComponentPath)!; } return entry.Entry!; } NodeState root = node.GetHierarchyRoot(); if (root != null) { entry = new CacheEntry { RefCount = 1, Entry = root }; m_componentCache.Add(handle.RootId, entry); } } // simply add the node to the cache. else { if (m_componentCache.TryGetValue(handle.NodeId, out CacheEntry? entry)) { entry.RefCount++; return entry.Entry!; } entry = new CacheEntry { RefCount = 1, Entry = node }; m_componentCache.Add(handle.NodeId, entry); } return node; } finally { m_componentCacheSemaphore.Release(); } } private IReadOnlyList? m_namespaceUris; private ushort[] m_namespaceIndexes; private NodeIdDictionary? m_componentCache; private readonly SemaphoreSlim m_componentCacheSemaphore = new(1, 1); /// /// A logger to use /// #pragma warning disable IDE1006 // Naming Styles protected ILogger m_logger { get; } #pragma warning restore IDE1006 // Naming Styles /// /// A wrapper for the browser that provides a lock. /// private class BrowserContext : IDisposable { public INodeBrowser Browser { get; } public SemaphoreSlim Semaphore { get; } = new(1, 1); public BrowserContext(INodeBrowser browser) { Browser = browser; } public void Dispose() { Browser.Dispose(); Semaphore.Dispose(); } } /// /// the monitored item manager of the NodeManager /// protected IMonitoredItemManager m_monitoredItemManager; /// /// the sync NodeManager adapter /// protected INodeManager3 m_syncNodeManager; /// /// The synchronaization primitive used to synchronize write operations; /// protected SemaphoreSlim m_writeSemaphore = new(1, 1); /// /// The synchronaization primitive used to protect access to operations affecting the MonitoredItems owned by the NodeManager. /// protected SemaphoreSlim m_monitoredItemSemaphore = new(1, 1); /// /// Set of s that opt into multiple event consumer /// task handling. Nodes in this set will use dynamic scaling of /// consumer tasks based on the number of event monitored items. /// internal NodeIdDictionary MultiConsumerNodeIds { get; } = []; /// /// Counter for the NodeIdFactory.New Method /// private uint m_lastUsedNodeId; } }