/* ======================================================================== * 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 INodeManager interface. /// /// /// This node manager is a base class used in multiple samples. It implements the INodeManager /// 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 partial class CustomNodeManager2 : INodeManager3, INodeIdFactory, IDisposable { /// /// Initializes the node manager. /// protected CustomNodeManager2( IServerInternal server, params string[] namespaceUris) : this(server, null!, false, namespaceUris) { } /// /// Initializes the node manager. /// protected CustomNodeManager2( IServerInternal server, ILogger logger, params string[] namespaceUris) : this(server, null!, logger, namespaceUris) { } /// /// Initializes the node manager. /// protected CustomNodeManager2( IServerInternal server, ApplicationConfiguration configuration, params string[] namespaceUris) : this(server, configuration, false, namespaceUris) { } /// /// Initializes the node manager. /// protected CustomNodeManager2( IServerInternal server, ApplicationConfiguration configuration, ILogger logger, params string[] namespaceUris) : this(server, configuration, false, logger, namespaceUris) { } /// /// Initializes the node manager. /// protected CustomNodeManager2( IServerInternal server, ApplicationConfiguration configuration, bool useSamplingGroups, params string[] namespaceUris) : this( server, configuration, useSamplingGroups, server.Telemetry.CreateLogger(), namespaceUris) { } /// /// Initializes the node manager. /// protected CustomNodeManager2( 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; // 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; // create a monitored item manager that owns sampling groups / monitoredNodes if (useSamplingGroups) { m_monitoredItemManager = new SamplingGroupMonitoredItemManager( this.ToAsyncNodeManager(), server, configuration!); } else { m_monitoredItemManager = new MonitoredNodeMonitoredItemManager(this.ToAsyncNodeManager(), server); } PredefinedNodes = []; } /// /// 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) { lock (Lock) { m_monitoredItemManager?.Dispose(); PredefinedNodes.Clear(); } } } /// /// Creates the NodeId for the specified node. /// /// The context. /// The node. /// The new NodeId. public virtual NodeId New(ISystemContext context, NodeState node) { return node.NodeId; } /// /// Acquires the lock on the node manager. /// public object Lock { get; } = new object(); /// /// 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 List RootNotifiers { get; private set; } = null!; /// /// 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; /// /// 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 new node id. /// public NodeId CreateNode( ServerSystemContext context, NodeId parentId, NodeId referenceTypeId, QualifiedName browseName, BaseInstanceState instance) { ServerSystemContext contextToUse = SystemContext.Copy(context); NodeId resultId; lock (Lock) { instance.ReferenceTypeId = referenceTypeId; NodeState? parent = null; if (!parentId.IsNull) { if (!PredefinedNodes.TryGetValue(parentId, out parent)) { throw ServiceResultException.Create( StatusCodes.BadNodeIdUnknown, "Cannot find parent with id: {0}", parentId); } parent.AddChild(instance); } instance.Create(contextToUse, default, browseName, default, true); AddPredefinedNode(contextToUse, instance); resultId = instance.NodeId; } // Auto-emit GeneralModelChangeEvent for dynamic CreateNode usage. // This is best-effort: callers can opt out by overriding // EmitModelChange or setting ModelChangeEmissionEnabled to false. if (ModelChangeEmissionEnabled) { ModelChangeAggregator.RecordNodeAdded(resultId, instance.TypeDefinitionId); if (!parentId.IsNull) { ModelChangeAggregator.RecordReferenceAdded(parentId); } EmitModelChange(contextToUse); } return resultId; } /// /// Deletes a node and all of its children. /// public bool DeleteNode(ServerSystemContext context, NodeId nodeId) { ServerSystemContext contextToUse = SystemContext.Copy(context); var referencesToRemove = new List(); if (!PredefinedNodes.TryGetValue(nodeId, out NodeState? node)) { return false; } NodeId? deletedTypeDefinition = (node as BaseInstanceState)?.TypeDefinitionId; RemovePredefinedNode(contextToUse, node!, referencesToRemove); RemoveRootNotifier(node!); if (referencesToRemove.Count > 0) { #pragma warning disable CS0618 // RemoveReferences is obsolete; DeleteNode is itself sync legacy API tracked for removal. Server.NodeManager.RemoveReferences(referencesToRemove); #pragma warning restore CS0618 } if (ModelChangeEmissionEnabled) { ModelChangeAggregator.RecordNodeDeleted(nodeId, deletedTypeDefinition); EmitModelChange(contextToUse); } return true; } /// /// Searches the node id in all node managers /// [Obsolete("Use IServerInteral.IMasterNodeManager.FindNodeInAddressSpaceAsync instead.")] public NodeState? FindNodeInAddressSpace(NodeId nodeId) { return Server.NodeManager.FindNodeInAddressSpaceAsync(nodeId).AsTask().GetAwaiter().GetResult(); } /// /// 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 void CreateAddressSpace( IDictionary> externalReferences) { LoadPredefinedNodes(SystemContext, externalReferences); } /// /// Loads a node set from a file or resource and adds them to the set of predefined nodes. /// public virtual void LoadPredefinedNodes( ISystemContext context, Assembly assembly, string resourcePath, IDictionary> externalReferences) { // 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++) { AddPredefinedNode(context, predefinedNodes[ii]); } // ensure the reverse references exist. AddReverseReferences(externalReferences); } /// /// Loads a node set from a file or resource and adds them to the set of predefined nodes. /// protected virtual NodeStateCollection? LoadPredefinedNodes(ISystemContext context) { return []; } /// /// Loads a node set from a file or resource and adds them to the set of predefined nodes. /// protected virtual void LoadPredefinedNodes( ISystemContext context, IDictionary> externalReferences) { // load the predefined nodes from an XML document. NodeStateCollection? predefinedNodes = LoadPredefinedNodes(context); // add the predefined nodes to the node manager. for (int ii = 0; ii < predefinedNodes!.Count; ii++) { AddPredefinedNode(context, predefinedNodes[ii]); } // ensure the reverse references exist. AddReverseReferences(externalReferences); } /// /// Replaces the generic node with a node specific to the model. /// protected virtual NodeState AddBehaviourToPredefinedNode( ISystemContext context, NodeState predefinedNode) { return predefinedNode; } /// /// Recursively indexes the node and its children. /// protected virtual void AddPredefinedNode(ISystemContext context, NodeState node) { // 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 = AddBehaviourToPredefinedNode(context, node); PredefinedNodes.AddOrUpdate(activeNode.NodeId, activeNode, (key, _) => activeNode); if (activeNode is BaseTypeState type) { AddTypesToTypeTree(type); } lock (Lock) { // update the root notifiers. if (RootNotifiers != null) { for (int ii = 0; ii < RootNotifiers.Count; ii++) { if (RootNotifiers[ii].NodeId == activeNode.NodeId) { RootNotifiers[ii] = activeNode; // need to prevent recursion with the server object. if (activeNode.NodeId != ObjectIds.Server) { activeNode.OnReportEvent = OnReportEvent; if (!activeNode.ReferenceExists( ReferenceTypeIds.HasNotifier, true, ObjectIds.Server)) { activeNode.AddReference( ReferenceTypeIds.HasNotifier, true, ObjectIds.Server); } } break; } } } } 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; } AddPredefinedNode(context, children[ii]); } } /// /// Recursively indexes the node and its children. /// protected virtual void RemovePredefinedNode( ISystemContext context, NodeState node, List referencesToRemove) { if (!PredefinedNodes.TryRemove(node.NodeId, out _)) { return; } node.UpdateChangeMasks(NodeStateChangeMasks.Deleted); node.ClearChangeMasks(context, false); OnNodeRemoved(node); // 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++) { RemovePredefinedNode(context, children[ii], referencesToRemove); } // 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 void OnNodeRemoved(NodeState node) { // overridden by the sub-class. } /// /// Ensures that all reverse references exist. /// /// A list of references to add to external targets. protected virtual void AddReverseReferences( IDictionary> externalReferences) { foreach (NodeState source in PredefinedNodes.Values) { var references = new List(); 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)) { 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) { AddRootNotifier(source); } // 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. [Obsolete("Use FindPredefinedNode instead.")] public NodeState? FindPredefinedNode(NodeId nodeId, Type expectedType) { if (nodeId.IsNull) { return null; } if (!PredefinedNodes.TryGetValue(nodeId, out NodeState? node)) { return null; } if (expectedType != null && !expectedType.IsInstanceOfType(node)) { return null; } return node; } /// /// 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 void DeleteAddressSpace() { 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(); } /// /// 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 object? GetManagerHandle(NodeId nodeId) { return GetManagerHandle(SystemContext, nodeId, null!); } /// /// Returns a unique handle for the node. /// /// /// It is thread safe to call this method outside the node manager lock. /// protected virtual NodeHandle? GetManagerHandle( ServerSystemContext context, NodeId nodeId, IDictionary cache) { if (!IsNodeIdInNamespace(nodeId)) { return null; } if (PredefinedNodes.TryGetValue(nodeId, out NodeState? node)) { return new NodeHandle { NodeId = nodeId, Node = node, Validated = true }; } return null; } /// /// 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 void AddReferences(IDictionary> references) { lock (Lock) { foreach (KeyValuePair> current in references) { // get the handle. NodeHandle? source = GetManagerHandle(SystemContext, current.Key, null!); // 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) { 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 ServiceResult DeleteReference( object sourceHandle, NodeId referenceTypeId, bool isInverse, ExpandedNodeId targetId, bool deleteBidirectional) { // 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; } lock (Lock) { // only support references to Source Areas. source.Node.RemoveReference(referenceTypeId, isInverse, targetId); if (deleteBidirectional) { // check if the target is also managed by this node manager. if (!targetId.IsAbsolute) { NodeHandle? target = GetManagerHandle(SystemContext, (NodeId)targetId, null!); if (target != null && target.Validated && target.Node != null) { target.Node.RemoveReference(referenceTypeId, !isInverse, source.NodeId); } } } return ServiceResult.Good; } } /// /// Returns the basic metadata for the node. Returns null if the node does not exist. /// /// /// This method validates any placeholder handle. /// public virtual NodeMetadata? GetNodeMetadata( OperationContext context, object targetHandle, BrowseResultMask resultMask) { ServerSystemContext systemContext = SystemContext.Copy(context); // check for valid handle. NodeHandle? handle = IsHandleInNamespace(targetHandle); if (handle == null) { return null; } lock (Lock) { // validate node. NodeState? target = ValidateNode(systemContext, handle, null!); if (target == null) { return null; } // read the attributes. ArrayOf values = target.ReadAttributes( systemContext, 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); // construct the meta-data object. var metadata = new NodeMetadata(target, target.NodeId) { NodeClass = target.NodeClass, BrowseName = target.BrowseName, DisplayName = target.DisplayName }; if (values[0].TryGetValue(out uint writeMask) && values[1].TryGetValue(out uint userWriteMask)) { metadata.WriteMask = (AttributeWriteMask)(writeMask & userWriteMask); } if (values[2].TryGetValue(out NodeId dataType)) { metadata.DataType = dataType; } if (values[3].TryGetValue(out int valueRank)) { metadata.ValueRank = valueRank; } if (values[4].TryGetValue(out ArrayOf arrayDimensions)) { metadata.ArrayDimensions = arrayDimensions; } if (values[5].TryGetValue(out byte accessLevel) && values[6].TryGetValue(out byte userAccessLevel)) { metadata.AccessLevel = (byte)(accessLevel & userAccessLevel); } if (values[7].TryGetValue(out byte eventNotifier)) { metadata.EventNotifier = eventNotifier; } if (values[8].TryGetValue(out bool executable) && values[9].TryGetValue(out bool userExecutable)) { metadata.Executable = executable && userExecutable; } if (values[10].TryGetValue(out ushort accessRestriction)) { metadata.AccessRestrictions = (AccessRestrictionType)accessRestriction; } if (values[11].TryGetStructure( out ArrayOf rolePermissions)) { metadata.RolePermissions = rolePermissions; } if (values[12].TryGetStructure( out ArrayOf userRolePermissions)) { metadata.UserRolePermissions = userRolePermissions; } SetDefaultPermissions(systemContext, target, metadata); // 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(ArrayOf values, NodeMetadata metadata) { 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 values of the attributes private static ArrayOf ReadAndCacheValidationAttributes( Dictionary uniqueNodesServiceAttributes, ServerSystemContext systemContext, NodeState target, NodeId key) { ArrayOf values = ReadValidationAttributes(systemContext, target); uniqueNodesServiceAttributes[key] = values.ToArray()!; return 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 values of the attributes private static ArrayOf ReadValidationAttributes( ServerSystemContext systemContext, NodeState target) { // 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 return target.ReadAttributes( systemContext, 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 virtual void Browse( OperationContext context, ref ContinuationPoint continuationPoint, IList references) { if (continuationPoint == null) { throw new ArgumentNullException(nameof(continuationPoint)); } if (references == null) { throw new ArgumentNullException(nameof(references)); } ServerSystemContext systemContext = SystemContext.Copy(context); // check for valid view. ValidateViewDescription(systemContext, continuationPoint.View!); INodeBrowser? browser = null; // check for valid handle. NodeHandle handle = IsHandleInNamespace(continuationPoint.NodeToBrowse) ?? throw new ServiceResultException(StatusCodes.BadNodeIdUnknown); lock (Lock) { // validate node. NodeState source = ValidateNode(systemContext, handle, null!) ?? throw new ServiceResultException(StatusCodes.BadNodeIdUnknown); // check if node is in the view. if (!IsNodeInView(systemContext, continuationPoint, source)) { throw new ServiceResultException(StatusCodes.BadNodeNotInView); } // check for previous continuation point. browser = continuationPoint.Data as INodeBrowser; // fetch list of references. if (browser == null) { // create a new browser. continuationPoint.Data = browser = source.CreateBrowser( systemContext, continuationPoint.View, continuationPoint.ReferenceTypeId, continuationPoint.IncludeSubtypes, continuationPoint.BrowseDirection, default, null, false); } } // prevent multiple access the browser object. lock (browser) { // apply filters to references. var cache = new Dictionary(); for (IReference? reference = browser.Next(); reference != null; reference = browser.Next()) { // validate Browse permission ServiceResult serviceResult = ValidateRolePermissions( context, ExpandedNodeId.ToNodeId(reference.TargetId, Server.NamespaceUris), PermissionType.Browse); if (ServiceResult.IsBad(serviceResult)) { // ignore reference continue; } // create the type definition reference. ReferenceDescription? description = GetReferenceDescription( systemContext, cache, reference, continuationPoint); if (description == null) { continue; } // check if limit reached. if (continuationPoint.MaxResultsToReturn != 0 && references.Count >= continuationPoint.MaxResultsToReturn) { browser.Push(reference); return; } references.Add(description); } // release the continuation point if all done. continuationPoint.Dispose(); continuationPoint = 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 != DateTime.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 virtual ReferenceDescription? GetReferenceDescription( ServerSystemContext context, Dictionary cache, IReference reference, ContinuationPoint continuationPoint) { _ = 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 = GetManagerHandle(context, (NodeId)reference.TargetId, null!); if (handle != null) { target = ValidateNode(context, handle, null!); } } // 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 void TranslateBrowsePath( OperationContext context, object sourceHandle, RelativePathElement relativePath, IList targetIds, IList unresolvedTargetIds) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); // check for valid handle. NodeHandle? handle = IsHandleInNamespace(sourceHandle); if (handle == null) { return; } lock (Lock) { // validate node. NodeState? source = ValidateNode(systemContext, handle, operationCache); if (source == null) { return; } // get list of references that relative path. INodeBrowser 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 = GetManagerHandle( systemContext, targetId, operationCache); if (targetHandle == null) { continue; } // validate target. target = ValidateNode(systemContext, targetHandle, operationCache); 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 void Read( OperationContext context, double maxAge, ArrayOf nodesToRead, IList values, IList errors) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToValidate = new List(); lock (Lock) { 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 = GetManagerHandle( systemContext, nodeToRead.NodeId, operationCache); 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] = handle.Node.ReadAttribute( systemContext, nodeToRead.AttributeId, nodeToRead.ParsedIndexRange, nodeToRead.DataEncoding, ref value); // 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). Read(systemContext, nodesToRead, values, errors, nodesToValidate, operationCache); } /// /// Finds a node in the dynamic cache. /// /// The current context. /// The node handle. /// The cache to search. /// The node if found. Null otherwise. protected virtual NodeState? FindNodeInCache( ServerSystemContext context, NodeHandle handle, IDictionary cache) { // not valid if no root. if (handle == null) { return null; } // check if previously validated. if (handle.Validated) { return 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 target; } // lookup root in local cache for request. if (!string.IsNullOrEmpty(handle.ComponentPath) && cache.TryGetValue(rootId, out target)) { target = target.FindChildBySymbolicName(context, handle.ComponentPath); // component exists. if (target != null) { return target; } } } // lookup component in shared cache. return 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 NodeState? ValidateNode( ServerSystemContext context, NodeHandle handle, IDictionary cache) { // lookup in cache. NodeState? target = FindNodeInCache(context, handle, cache); 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. protected virtual void Read( ServerSystemContext context, ArrayOf nodesToRead, IList values, IList errors, List nodesToValidate, IDictionary cache) { for (int ii = 0; ii < nodesToValidate.Count; ii++) { NodeHandle handle = nodesToValidate[ii]; lock (Lock) { // validate node. NodeState? source = ValidateNode(context, handle, cache); if (source == null) { continue; } ReadValueId nodeToRead = nodesToRead[handle.Index]; DataValue value = values[handle.Index]; // update the attribute value. errors[handle.Index] = source.ReadAttribute( context, nodeToRead.AttributeId, nodeToRead.ParsedIndexRange, nodeToRead.DataEncoding, ref value); values[handle.Index] = value; } } } /// /// Writes the value for the specified attributes. /// public virtual void Write( OperationContext context, ArrayOf nodesToWrite, IList errors) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToValidate = new List(); lock (Lock) { 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 = GetManagerHandle( systemContext, nodeToWrite.NodeId, operationCache); 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; DataValue oldValue = default; if (Server?.Auditing == true) { // read the old value for the purpose of auditing handle.Node.ReadAttribute( systemContext, nodeToWrite.AttributeId, nodeToWrite.ParsedIndexRange, default, ref oldValue); } // write the attribute value. errors[ii] = handle.Node.WriteAttribute( systemContext, nodeToWrite.AttributeId, nodeToWrite.ParsedIndexRange, nodeToWrite.Value); // report the write value audit event Server.ReportAuditWriteUpdateEvent( systemContext, nodeToWrite, oldValue.WrappedValue, 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; } } // validates the nodes and writes the value to the underlying system. Write(systemContext, nodesToWrite, errors, nodesToValidate, operationCache); } 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); 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 ยง6.4.32, /// changes should be aggregated and reported in batches. /// /// /// 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. /// /// The system context. /// The aggregated set of model changes. 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)) { 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. Custom node managers can also call /// ModelChangeAggregator.Record* directly when mutating /// the address space outside the standard hooks, then call /// to publish. /// protected ModelChangeAggregator ModelChangeAggregator { get; } = new ModelChangeAggregator(); /// /// Gets or sets whether the framework automatically emits /// GeneralModelChangeEventType from /// / . Default: true. Set to /// false if the derived node manager prefers manual control /// over event emission (e.g. to batch many operations and emit /// once at the end). /// 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 with the accumulated /// changes. 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. protected virtual void Write( ServerSystemContext context, ArrayOf nodesToWrite, IList errors, List nodesToValidate, IDictionary cache) { // validates the nodes (reads values from the underlying data source if required). for (int ii = 0; ii < nodesToValidate.Count; ii++) { NodeHandle handle = nodesToValidate[ii]; lock (Lock) { // validate node. NodeState? source = ValidateNode(context, handle, cache); if (source == null) { continue; } WriteValue nodeToWrite = nodesToWrite[handle.Index]; // write the attribute value. errors[handle.Index] = source.WriteAttribute( context, nodeToWrite.AttributeId, nodeToWrite.ParsedIndexRange, nodeToWrite.Value); // updates to source finished - report changes to monitored items. source.ClearChangeMasks(context, false); } } } /// /// Reads the history for the specified nodes. /// public virtual void HistoryRead( OperationContext context, HistoryReadDetails details, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, ArrayOf nodesToRead, IList results, IList errors) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToProcess = new List(); lock (Lock) { 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 = GetManagerHandle( systemContext, nodeToRead.NodeId, operationCache); 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). HistoryRead( systemContext, details, timestampsToReturn, releaseContinuationPoints, nodesToRead, results, errors, nodesToProcess, operationCache); } /// /// Returns the historian provider for a node. Default returns /// null which causes the dispatcher to fall back to the /// server-wide . /// 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 void HistoryReleaseContinuationPoints( ServerSystemContext context, ArrayOf nodesToRead, IList errors, List nodesToProcess, IDictionary cache) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState? source = ValidateNode(context, handle, cache); if (source == null) { continue; } errors[handle.Index] = HistorianDispatcher.ReleaseContinuationPoint( context, nodesToRead[handle.Index]); } } /// /// Reads raw history data. /// protected virtual void HistoryReadRawModified( ServerSystemContext context, ReadRawModifiedDetails details, TimestampsToReturn timestampsToReturn, ArrayOf nodesToRead, IList results, IList errors, List nodesToProcess, IDictionary cache) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState? source = ValidateNode(context, handle, cache); if (source == null) { continue; } 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] = HistorianDispatcher.DispatchAnnotationReadAsync( context, annotationProvider, parent, nodesToRead[handle.Index], details, timestampsToReturn, results[handle.Index], CancellationToken.None) .AsTask().GetAwaiter().GetResult(); 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] = HistorianDispatcher.DispatchRawReadAsync( context, provider, source, nodesToRead[handle.Index], details, timestampsToReturn, results[handle.Index], CancellationToken.None) .AsTask().GetAwaiter().GetResult(); } } /// /// Reads processed history data. /// protected virtual void HistoryReadProcessed( ServerSystemContext context, ReadProcessedDetails details, TimestampsToReturn timestampsToReturn, ArrayOf nodesToRead, IList results, IList errors, List nodesToProcess, IDictionary cache) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState? source = ValidateNode(context, handle, cache); 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] = HistorianDispatcher.DispatchProcessedReadAsync( context, provider, source, nodesToRead[handle.Index], details, aggregateId, timestampsToReturn, results[handle.Index], CancellationToken.None).AsTask().GetAwaiter().GetResult(); } } /// /// Reads history data at specified times. /// protected virtual void HistoryReadAtTime( ServerSystemContext context, ReadAtTimeDetails details, TimestampsToReturn timestampsToReturn, ArrayOf nodesToRead, IList results, IList errors, List nodesToProcess, IDictionary cache) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState? source = ValidateNode(context, handle, cache); 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] = HistorianDispatcher.DispatchAtTimeReadAsync( context, provider, source, nodesToRead[handle.Index], details, timestampsToReturn, results[handle.Index], CancellationToken.None) .AsTask().GetAwaiter().GetResult(); } } /// /// Reads history events. /// protected virtual void HistoryReadEvents( ServerSystemContext context, ReadEventDetails details, TimestampsToReturn timestampsToReturn, ArrayOf nodesToRead, IList results, IList errors, List nodesToProcess, IDictionary cache) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState? source = ValidateNode(context, handle, cache); if (source == null) { continue; } IHistorianProvider? provider = ResolveHistorianProvider(source); if (provider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } errors[handle.Index] = HistorianDispatcher.DispatchEventReadAsync( context, provider, source, nodesToRead[handle.Index], details, timestampsToReturn, results[handle.Index], CancellationToken.None) .AsTask().GetAwaiter().GetResult(); } } /// /// Validates the nodes and reads the values from the underlying source. /// /// protected virtual void HistoryRead( ServerSystemContext context, HistoryReadDetails details, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, ArrayOf nodesToRead, IList results, IList errors, List nodesToProcess, IDictionary cache) { // check if continuation points are being released. if (releaseContinuationPoints) { HistoryReleaseContinuationPoints( context, nodesToRead, errors, nodesToProcess, cache); return; } // check timestamps to return. if (timestampsToReturn is < TimestampsToReturn.Source or > TimestampsToReturn.Neither) { throw new ServiceResultException(StatusCodes.BadTimestampsToReturnInvalid); } // handle raw data request. if (details is ReadRawModifiedDetails readRawModifiedDetails) { // at least one must be provided. if (readRawModifiedDetails.StartTime == DateTime.MinValue && readRawModifiedDetails.EndTime == DateTime.MinValue) { throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument); } // if one is null the num values must be provided. if (readRawModifiedDetails.StartTime == DateTime.MinValue || readRawModifiedDetails.EndTime == DateTime.MinValue) { if (readRawModifiedDetails.NumValuesPerNode == 0) { throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument); } } HistoryReadRawModified( context, readRawModifiedDetails, timestampsToReturn, nodesToRead, results, errors, nodesToProcess, cache); 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); } // check start/end time. if (readProcessedDetails.StartTime == DateTime.MinValue || readProcessedDetails.EndTime == DateTime.MinValue) { throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument); } HistoryReadProcessed( context, readProcessedDetails, timestampsToReturn, nodesToRead, results, errors, nodesToProcess, cache); return; } // handle raw data at time request. if (details is ReadAtTimeDetails readAtTimeDetails) { HistoryReadAtTime( context, readAtTimeDetails, timestampsToReturn, nodesToRead, results, errors, nodesToProcess, cache); return; } // handle read events request. if (details is ReadEventDetails readEventDetails) { // check start/end time and max values. if (readEventDetails.NumValuesPerNode == 0) { if (readEventDetails.StartTime == DateTime.MinValue || readEventDetails.EndTime == DateTime.MinValue) { throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument); } } else if (readEventDetails.StartTime == DateTime.MinValue && readEventDetails.EndTime == DateTime.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. HistoryReadEvents( context, readEventDetails, timestampsToReturn, nodesToRead, results, errors, nodesToProcess, cache); } } /// /// Updates the history for the specified nodes. /// public virtual void HistoryUpdate( OperationContext context, Type detailsType, ArrayOf nodesToUpdate, IList results, IList errors) { ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); var nodesToProcess = new List(); lock (Lock) { 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 = GetManagerHandle( systemContext, nodeToUpdate.NodeId, operationCache); 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; } } // validates the nodes and updates. HistoryUpdate( systemContext, detailsType, nodesToUpdate, results, errors, nodesToProcess, operationCache); } /// /// Validates the nodes and updates the history. /// protected virtual void HistoryUpdate( ServerSystemContext context, Type detailsType, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache) { // 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]; } HistoryUpdateData(context, details, results, errors, nodesToProcess, cache); 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]; } HistoryUpdateStructureData( context, details, results, errors, nodesToProcess, cache); 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]; } HistoryUpdateEvents(context, details, results, errors, nodesToProcess, cache); 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]; } HistoryDeleteRawModified(context, details, results, errors, nodesToProcess, cache); 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]; } HistoryDeleteAtTime(context, details, results, errors, nodesToProcess, cache); 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]; } HistoryDeleteEvents(context, details, results, errors, nodesToProcess, cache); } } /// /// Updates the data history for one or more nodes. /// protected virtual void HistoryUpdateData( ServerSystemContext context, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState? source = ValidateNode(context, handle, cache); 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] = HistorianDispatcher.DispatchUpdateDataAsync( context, provider, source, nodesToUpdate[handle.Index], results[handle.Index], CancellationToken.None) .AsTask().GetAwaiter().GetResult(); } } /// /// Updates the structured data history (Part 11 ยง5.2.7 Annotations). /// protected virtual void HistoryUpdateStructureData( ServerSystemContext context, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState? source = ValidateNode(context, handle, cache); 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] = HistorianDispatcher.DispatchAnnotationUpdateAsync( context, provider, parent, nodesToUpdate[handle.Index], results[handle.Index], CancellationToken.None) .AsTask().GetAwaiter().GetResult(); } } /// /// Updates the event history for one or more nodes. /// protected virtual void HistoryUpdateEvents( ServerSystemContext context, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState? source = ValidateNode(context, handle, cache); if (source == null) { continue; } IHistorianProvider? provider = ResolveHistorianProvider(source); if (provider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } errors[handle.Index] = HistorianDispatcher.DispatchUpdateEventAsync( context, provider, source, nodesToUpdate[handle.Index], results[handle.Index], CancellationToken.None) .AsTask().GetAwaiter().GetResult(); } } /// /// Deletes the data history for one or more nodes. /// protected virtual void HistoryDeleteRawModified( ServerSystemContext context, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState? source = ValidateNode(context, handle, cache); 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] = HistorianDispatcher.DispatchDeleteRawAsync( context, provider, source, nodesToUpdate[handle.Index], results[handle.Index], CancellationToken.None) .AsTask().GetAwaiter().GetResult(); } } /// /// Deletes the data history at specified timestamps for one or more nodes. /// protected virtual void HistoryDeleteAtTime( ServerSystemContext context, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState? source = ValidateNode(context, handle, cache); 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] = HistorianDispatcher.DispatchDeleteAtTimeAsync( context, provider, source, nodesToUpdate[handle.Index], results[handle.Index], CancellationToken.None) .AsTask().GetAwaiter().GetResult(); } } /// /// Deletes the event history for one or more nodes. /// protected virtual void HistoryDeleteEvents( ServerSystemContext context, ArrayOf nodesToUpdate, IList results, IList errors, List nodesToProcess, IDictionary cache) { for (int ii = 0; ii < nodesToProcess.Count; ii++) { NodeHandle handle = nodesToProcess[ii]; NodeState? source = ValidateNode(context, handle, cache); if (source == null) { continue; } IHistorianProvider? provider = ResolveHistorianProvider(source); if (provider == null) { errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; continue; } errors[handle.Index] = HistorianDispatcher.DispatchDeleteEventsAsync( context, provider, source, nodesToUpdate[handle.Index], results[handle.Index], CancellationToken.None) .AsTask().GetAwaiter().GetResult(); } } /// /// Calls a method on the specified nodes. /// public virtual void Call( OperationContext context, ArrayOf methodsToCall, IList results, IList errors) { #pragma warning disable CA2012 // Use ValueTasks correctly _ = CallInternalAsync( context, methodsToCall, results, errors, sync: true); #pragma warning restore CA2012 // Use ValueTasks correctly } /// /// Resolves the effective method for a call request. /// public virtual MethodState FindMethodState( OperationContext context, CallMethodRequest methodToCall) { if (methodToCall == null || methodToCall.ObjectId.IsNull || methodToCall.MethodId.IsNull) { return null!; } ServerSystemContext systemContext = SystemContext.Copy(context); IDictionary operationCache = new NodeIdDictionary(); NodeHandle? handle = GetManagerHandle( systemContext, methodToCall.ObjectId, operationCache); if (handle == null) { return null!; } lock (Lock) { NodeState? source = ValidateNode(systemContext, handle, operationCache); if (source == null) { return null!; } MethodState? method = source.FindMethod(systemContext, methodToCall.MethodId); if (method != null) { return method; } if (source.ReferenceExists( ReferenceTypeIds.HasComponent, false, methodToCall.MethodId)) { method = FindPredefinedNode(methodToCall.MethodId); } if (method == null && source is BaseInstanceState instanceState) { method = FindMethodInTypeHierarchy(systemContext, instanceState.TypeDefinitionId, methodToCall.MethodId); } return method!; } } /// /// Calls a method on the specified nodes. /// protected virtual async ValueTask CallInternalAsync( OperationContext context, ArrayOf methodsToCall, IList results, IList errors, bool sync, 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; } MethodState? method = null; // check for valid handle. NodeHandle? handle = GetManagerHandle( systemContext, methodToCall.ObjectId, operationCache); if (handle == null) { continue; } lock (Lock) { // owned by this node manager. methodToCall.Processed = true; // validate the source node. NodeState? source = ValidateNode(systemContext, handle, operationCache); if (source == null) { errors[ii] = StatusCodes.BadNodeIdUnknown; continue; } method = FindMethodState(context, methodToCall); 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] = ValidateRolePermissions( context, method.NodeId, PermissionType.Call); if (ServiceResult.IsBad(errors[ii])) { continue; } } // call the method. CallMethodResult result = results[ii] = new CallMethodResult(); if (sync) { #pragma warning disable CA1849 // Call async methods when in an async method errors[ii] = Call(systemContext, methodToCall, method, result); #pragma warning restore CA1849 // Call async methods when in an async method } else { errors[ii] = await CallAsync( systemContext, methodToCall, method, result, cancellationToken).ConfigureAwait(false); } } } /// /// Calls a method on an object. /// protected virtual async ValueTask CallInternalAsync( ISystemContext context, CallMethodRequest methodToCall, MethodState method, CallMethodResult result, bool sync, CancellationToken cancellationToken = default) { var systemContext = context as ServerSystemContext; var argumentErrors = new List(); var outputArguments = new List(); ServiceResult callResult; if (sync) { callResult = method.Call( context, methodToCall.ObjectId, methodToCall.InputArguments, argumentErrors, outputArguments); } else { 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 only set here. 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; } /// /// Asynchronously calls a method on an object. /// protected virtual ValueTask CallAsync( ISystemContext context, CallMethodRequest methodToCall, MethodState method, CallMethodResult result, CancellationToken cancellationToken = default) { return CallInternalAsync( context, methodToCall, method, result, sync: false, cancellationToken); } /// /// Calls a method on an object. /// protected virtual ServiceResult Call( ISystemContext context, CallMethodRequest methodToCall, MethodState method, CallMethodResult result) { #pragma warning disable CA2012 // Use ValueTasks correctly ValueTask syncResult = CallInternalAsync( context, methodToCall, method, result, sync: true); #pragma warning restore CA2012 // Use ValueTasks correctly return syncResult.Result; } /// /// 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 ServiceResult SubscribeToEvents( OperationContext context, object sourceId, uint subscriptionId, IEventMonitoredItem monitoredItem, bool unsubscribe) { ServerSystemContext systemContext = SystemContext.Copy(context); // check for valid handle. NodeHandle? handle = IsHandleInNamespace(sourceId); if (handle == null) { return StatusCodes.BadNodeIdInvalid; } lock (Lock) { // check for valid node. NodeState? source = ValidateNode(systemContext, handle, null!); if (source == null) { return StatusCodes.BadNodeIdUnknown; } // subscribe to events. return SubscribeToEvents(systemContext, source, monitoredItem, unsubscribe); } } /// /// 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 ServiceResult SubscribeToAllEvents( OperationContext context, uint subscriptionId, IEventMonitoredItem monitoredItem, bool unsubscribe) { ServerSystemContext systemContext = SystemContext.Copy(context); lock (Lock) { // 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. if (RootNotifiers != null) { for (int ii = 0; ii < RootNotifiers.Count; ii++) { SubscribeToEvents( systemContext, RootNotifiers[ii], monitoredItem, unsubscribe); } } return ServiceResult.Good; } } /// /// Adds a root notifier. /// /// The notifier. /// /// 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 void AddRootNotifier(NodeState notifier) { lock (Lock) { RootNotifiers ??= []; bool mustAdd = true; for (int ii = 0; ii < RootNotifiers.Count; ii++) { if (ReferenceEquals(notifier, RootNotifiers[ii])) { return; } if (RootNotifiers[ii].NodeId == notifier.NodeId) { RootNotifiers[ii] = notifier; mustAdd = false; break; } } if (mustAdd) { RootNotifiers.Add(notifier); } // need to prevent recursion with the server object. if (notifier.NodeId != ObjectIds.Server) { 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) { SubscribeToEvents(SystemContext, notifier, monitoredItems[ii], true); } } } } } /// /// Removes a root notifier previously added with AddRootNotifier. /// /// The notifier. protected virtual void RemoveRootNotifier(NodeState notifier) { lock (Lock) { if (RootNotifiers != null) { for (int ii = 0; ii < RootNotifiers.Count; ii++) { if (ReferenceEquals(notifier, RootNotifiers[ii])) { notifier.OnReportEvent = null; notifier.RemoveReference( ReferenceTypeIds.HasNotifier, true, ObjectIds.Server); RootNotifiers.RemoveAt(ii); break; } } } } } /// /// 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]. /// Any error code. protected virtual ServiceResult SubscribeToEvents( ServerSystemContext context, NodeState source, IEventMonitoredItem monitoredItem, bool unsubscribe) { (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. source.SetAreEventsMonitored(context, !unsubscribe, true); // signal update. OnSubscribeToEvents(context, monitoredNode!, unsubscribe); // all done. return serviceResult; } /// /// Called after subscribing/unsubscribing to events. /// /// The context. /// The monitored node. /// if set to true unsubscribing. protected virtual void OnSubscribeToEvents( ServerSystemContext context, MonitoredNode2 monitoredNode, bool unsubscribe) { // defined by the sub-class } /// /// 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 ServiceResult ConditionRefresh( OperationContext context, IList monitoredItems) { 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(); lock (Lock) { // check for server subscription. if (monitoredItem.NodeId == ObjectIds.Server) { if (RootNotifiers != null) { nodesToRefresh.AddRange(RootNotifiers); } } 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); } } // 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 = ValidateEventRolePermissions(monitoredItem, events[jj]); 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 void RestoreMonitoredItems( IList itemsToRestore, IList monitoredItems, IUserIdentity savedOwnerIdentity) { 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 = GetManagerHandle( systemContext, itemToCreate.NodeId, operationCache); 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; } // 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 = false; IMonitoredItem? monitoredItem = null; try { lock (Lock) { // validate node. NodeState? source = ValidateNode(systemContext, handle, operationCache); 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(); } } lock (Lock) { m_monitoredItemManager.ApplyChanges(); } // 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, this.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 void CreateMonitoredItems( OperationContext context, uint subscriptionId, double publishingInterval, TimestampsToReturn timestampsToReturn, ArrayOf itemsToCreate, IList errors, IList filterErrors, IList monitoredItems, bool createDurable, MonitoredItemIdFactory monitoredItemIdFactory) { 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 = GetManagerHandle( systemContext, itemToMonitor.NodeId, operationCache); 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; } // 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 = null; IMonitoredItem? monitoredItem = null; try { lock (Lock) { // validate node. NodeState? source = ValidateNode(systemContext, handle, operationCache); if (source == null) { continue; } MonitoredItemCreateRequest itemToCreate = itemsToCreate[handle.Index]; // create monitored item. errors[handle.Index] = CreateMonitoredItem( systemContext, handle, subscriptionId, publishingInterval, context.DiagnosticsMask, timestampsToReturn, itemToCreate, createDurable, monitoredItemIdFactory, out filterResult, out monitoredItem); } // 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!); monitoredItem = null; // ownership transferred } finally { (monitoredItem as IDisposable)?.Dispose(); } } lock (Lock) { m_monitoredItemManager.ApplyChanges(); } // 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 ServiceResult CreateMonitoredItem( ServerSystemContext context, NodeHandle handle, uint subscriptionId, double publishingInterval, DiagnosticsMasks diagnosticsMasks, TimestampsToReturn timestampsToReturn, MonitoredItemCreateRequest itemToCreate, bool createDurable, MonitoredItemIdFactory monitoredItemId, out MonitoringFilterResult filterResult, out IMonitoredItem? monitoredItem) { filterResult = null!; monitoredItem = null; // validate parameters. MonitoringParameters parameters = itemToCreate.RequestedParameters; // validate attribute. if (!Attributes.IsValid(handle.Node.NodeClass, itemToCreate.ItemToMonitor.AttributeId)) { return StatusCodes.BadAttributeIdInvalid; } // 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. ServiceResult error = ValidateMonitoringFilter( context, handle, itemToCreate.ItemToMonitor.AttributeId, samplingInterval, revisedQueueSize, parameters.Filter, out MonitoringFilter filterToUse, out Range euRange, out filterResult); if (ServiceResult.IsBad(error)) { return error; } ISampledDataChangeMonitoredItem dataChangeMonitoredItem = m_monitoredItemManager.CreateMonitoredItem( Server, this.ToAsyncNodeManager(), context, handle, subscriptionId, publishingInterval, diagnosticsMasks, timestampsToReturn, itemToCreate, euRange, filterToUse, samplingInterval, revisedQueueSize, createDurable, monitoredItemId, AddNodeToComponentCache); monitoredItem = dataChangeMonitoredItem; // report the initial value. error = ReadInitialValue(context, handle, dataChangeMonitoredItem); if (ServiceResult.IsBad(error)) { if (error.StatusCode == StatusCodes.BadAttributeIdInvalid || error.StatusCode == StatusCodes.BadDataEncodingInvalid || error.StatusCode == StatusCodes.BadDataEncodingUnsupported) { return error; } error = StatusCodes.Good; } // report change. OnMonitoredItemCreated(context, handle, dataChangeMonitoredItem); return error; } /// /// 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, DateTime.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 ServiceResult ValidateRolePermissions( OperationContext operationContext, NodeId nodeId, PermissionType requestedPermission) { if (requestedPermission == PermissionType.None) { // no permission is required hence the validation passes. return StatusCodes.Good; } object? nodeHandle = Server.NodeManager .GetManagerHandle(nodeId, out INodeManager? nodeManager); if (nodeHandle == null || nodeManager == null) { // ignore unknown nodes. return StatusCodes.Good; } NodeMetadata? nodeMetadata = nodeManager.GetNodeMetadata( operationContext, nodeHandle, BrowseResultMask.All); return MasterNodeManager.ValidateRolePermissions( operationContext, nodeMetadata!, requestedPermission); } /// /// Validates if the specified event monitored item has enough permissions to receive the specified event /// public ServiceResult ValidateEventRolePermissions( IEventMonitoredItem monitoredItem, IFilterTarget filterTarget) { 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 ValidateEventReceivePermissions(operationContext, eventTypeId, sourceNodeId); } /// /// Validates on both the /// event type node and the source node of an event. Spec reference /// Part 3 ยง8.55 (PermissionType bit 11). 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 ServiceResult ValidateEventReceivePermissions( OperationContext operationContext, NodeId eventTypeId, NodeId sourceNodeId) { // validate the event type id permissions as specified ServiceResult result = ValidateRolePermissions( operationContext, eventTypeId, PermissionType.ReceiveEvents); if (ServiceResult.IsBad(result)) { return result; } // validate the source node id permissions as specified return ValidateRolePermissions( operationContext, sourceNodeId, PermissionType.ReceiveEvents); } /// /// Validates the monitoring filter specified by the client. /// protected virtual StatusCode ValidateMonitoringFilter( ServerSystemContext context, NodeHandle handle, uint attributeId, double samplingInterval, uint queueSize, ExtensionObject filter, out MonitoringFilter filterToUse, out Range range, out MonitoringFilterResult result) { range = null!; filterToUse = null!; result = null!; // nothing to do if the filter is not specified. if (filter.IsNull) { return StatusCodes.Good; } // 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) { return StatusCodes.BadFilterNotAllowed; } if (!Server.AggregateManager.IsSupported(aggregateFilter.AggregateType)) { return StatusCodes.BadAggregateNotSupported; } var revisedFilter = new ServerAggregateFilter { AggregateType = aggregateFilter.AggregateType, StartTime = aggregateFilter.StartTime, ProcessingInterval = aggregateFilter.ProcessingInterval, AggregateConfiguration = aggregateFilter.AggregateConfiguration, Stepped = false }; StatusCode error = ReviseAggregateFilter( context, handle, samplingInterval, queueSize, revisedFilter); if (StatusCode.IsBad(error)) { return error; } var aggregateFilterResult = new AggregateFilterResult { RevisedProcessingInterval = aggregateFilter.ProcessingInterval, RevisedStartTime = aggregateFilter.StartTime, RevisedAggregateConfiguration = aggregateFilter.AggregateConfiguration }; filterToUse = revisedFilter; result = aggregateFilterResult; return StatusCodes.Good; } // deadband filters only allowed for variable values. if (attributeId != Attributes.Value) { return StatusCodes.BadFilterNotAllowed; } if (handle.Node is not BaseVariableState variable) { return StatusCodes.BadFilterNotAllowed; } // check for status filter. if (deadbandFilter.DeadbandType == (uint)DeadbandType.None) { filterToUse = deadbandFilter; return StatusCodes.Good; } // deadband filters can only be used for numeric values. if (!Server.TypeTree.IsTypeOf(variable.DataType, DataTypeIds.Number)) { return StatusCodes.BadFilterNotAllowed; } // nothing more to do for absolute filters. if (deadbandFilter.DeadbandType == (uint)DeadbandType.Absolute) { filterToUse = deadbandFilter; return StatusCodes.Good; } // need to look up the EU range if a percent filter is requested. if (deadbandFilter.DeadbandType == (uint)DeadbandType.Percent) { if (handle.Node.FindChild( context, QualifiedName.From(BrowseNames.EURange)) is not PropertyState property) { return StatusCodes.BadMonitoredItemFilterUnsupported; } if (!property.Value.TryGetStructure(out range!)) { return StatusCodes.BadMonitoredItemFilterUnsupported; } filterToUse = deadbandFilter; return StatusCodes.Good; } // no other type of filter supported. return StatusCodes.BadFilterNotAllowed; } /// /// 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. /// Good if the protected virtual StatusCode ReviseAggregateFilter( ServerSystemContext context, NodeHandle handle, double samplingInterval, uint queueSize, ServerAggregateFilter filterToUse) { 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 StatusCodes.Good; } /// /// Modifies the parameters for a set of monitored items. /// public virtual void ModifyMonitoredItems( OperationContext context, TimestampsToReturn timestampsToReturn, IList monitoredItems, ArrayOf itemsToModify, IList errors, IList filterErrors) { 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(); lock (Lock) { 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. errors[ii] = ModifyMonitoredItem( systemContext, context.DiagnosticsMask, timestampsToReturn, monitoredItems[ii], itemToModify, handle, out MonitoringFilterResult? filterResult)!; // save any filter error details. filterErrors[ii] = filterResult!; // save the modified item. if (ServiceResult.IsGood(errors[ii])) { modifiedItems.Add(monitoredItems[ii]); } } } lock (Lock) { m_monitoredItemManager.ApplyChanges(); } // do any post processing. OnModifyMonitoredItemsComplete(systemContext, modifiedItems); } /// /// Called when a batch of monitored items has been modified. /// protected virtual void OnModifyMonitoredItemsComplete( ServerSystemContext context, IList monitoredItems) { // defined by the sub-class } /// /// Modifies the parameters for a monitored item. /// protected virtual ServiceResult? ModifyMonitoredItem( ServerSystemContext context, DiagnosticsMasks diagnosticsMasks, TimestampsToReturn timestampsToReturn, IMonitoredItem monitoredItem, MonitoredItemModifyRequest itemToModify, NodeHandle handle, out MonitoringFilterResult? filterResult) { // 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. ServiceResult? error = ValidateMonitoringFilter( context, handle, datachangeItem.AttributeId, samplingInterval, revisedQueueSize, parameters.Filter, out MonitoringFilter filterToUse, out Range euRange, out filterResult); if (ServiceResult.IsBad(error)) { return error; } // modify the monitored item parameters. error = m_monitoredItemManager!.ModifyMonitoredItem( context, diagnosticsMasks, timestampsToReturn, filterToUse, euRange, samplingInterval, revisedQueueSize, datachangeItem, itemToModify); // report change. if (ServiceResult.IsGood(error)) { OnMonitoredItemModified(context, handle, datachangeItem); } return error; } /// /// Called after modifying a MonitoredItem. /// /// The context. /// The handle for the node. /// The monitored item. protected virtual void OnMonitoredItemModified( ServerSystemContext context, NodeHandle handle, ISampledDataChangeMonitoredItem monitoredItem) { // overridden by the sub-class. } /// /// Deletes a set of monitored items. /// public virtual void DeleteMonitoredItems( OperationContext context, IList monitoredItems, IList processedItems, IList errors) { 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(); lock (Lock) { foreach ((int, NodeHandle) nodeInNamespace in nodesInNamespace) { int ii = nodeInNamespace.Item1; NodeHandle handle = nodeInNamespace.Item2; // owned by this node manager. processedItems[ii] = true; errors[ii] = DeleteMonitoredItem(systemContext, monitoredItems[ii], handle); // save the modified item. if (ServiceResult.IsGood(errors[ii])) { deletedItems.Add(monitoredItems[ii]); RemoveNodeFromComponentCache(systemContext, handle); } } } lock (Lock) { m_monitoredItemManager.ApplyChanges(); } // do any post processing. OnDeleteMonitoredItemsComplete(systemContext, deletedItems); } /// /// Called when a batch of monitored items has been modified. /// protected virtual void OnDeleteMonitoredItemsComplete( ServerSystemContext context, IList monitoredItems) { // defined by the sub-class } /// /// Deletes a monitored item. /// protected virtual ServiceResult DeleteMonitoredItem( ServerSystemContext context, IMonitoredItem monitoredItem, NodeHandle handle) { var sampledDataChangeMonitoredItem = monitoredItem as ISampledDataChangeMonitoredItem; StatusCode statusCode = m_monitoredItemManager.DeleteMonitoredItem( context, sampledDataChangeMonitoredItem!, handle); // report change. OnMonitoredItemDeleted(context, handle, sampledDataChangeMonitoredItem!); return statusCode; } /// /// Called after deleting a MonitoredItem. /// /// The context. /// The handle for the node. /// The monitored item. protected virtual void OnMonitoredItemDeleted( ServerSystemContext context, NodeHandle handle, ISampledDataChangeMonitoredItem monitoredItem) { // overridden by the sub-class. } /// /// 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. public virtual void TransferMonitoredItems( OperationContext context, bool sendInitialValues, IList monitoredItems, IList processedItems, IList errors) { ServerSystemContext systemContext = SystemContext.Copy(context); var transferredItems = new List(); lock (Lock) { 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; } } // do any post processing. OnMonitoredItemsTransferred(systemContext, transferredItems); } /// /// Called after transfer of MonitoredItems. /// /// The context. /// The transferred monitored items. protected virtual void OnMonitoredItemsTransferred( ServerSystemContext context, IList monitoredItems) { // defined by the sub-class } /// /// 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. public virtual void SetMonitoringMode( OperationContext context, MonitoringMode monitoringMode, IList monitoredItems, IList processedItems, IList errors) { 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(); lock (Lock) { 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] = SetMonitoringMode( systemContext, monitoredItems[ii], monitoringMode, handle); // save the modified item. if (ServiceResult.IsGood(errors[ii])) { changedItems.Add(monitoredItems[ii]); } } } lock (Lock) { m_monitoredItemManager.ApplyChanges(); } // do any post processing. OnSetMonitoringModeComplete(systemContext, changedItems); } /// /// Called when a batch of monitored items has their monitoring mode changed. /// protected virtual void OnSetMonitoringModeComplete( ServerSystemContext context, IList monitoredItems) { // defined by the sub-class } /// /// Changes the monitoring mode for an item. /// protected virtual ServiceResult SetMonitoringMode( ServerSystemContext context, IMonitoredItem monitoredItem, MonitoringMode monitoringMode, NodeHandle handle) { var sampledDataChangeMonitoredItem = monitoredItem as ISampledDataChangeMonitoredItem; (ServiceResult result, MonitoringMode? previousMode) = m_monitoredItemManager .SetMonitoringModeAsync( context, sampledDataChangeMonitoredItem!, monitoringMode, handle).AsTask().GetAwaiter().GetResult(); // report change. if (ServiceResult.IsGood(result) && previousMode != monitoringMode) { OnMonitoringModeChanged( context, handle, sampledDataChangeMonitoredItem!, (MonitoringMode)previousMode!, monitoringMode); } 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. protected virtual void OnMonitoringModeChanged( ServerSystemContext context, NodeHandle handle, ISampledDataChangeMonitoredItem monitoredItem, MonitoringMode previousMode, MonitoringMode monitoringMode) { // overridden by the sub-class. } /// /// Called when a session is closed. /// public virtual void SessionClosing( OperationContext context, NodeId sessionId, bool deleteSubscriptions) { } /// /// 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 void SessionActivated(OperationContext context, NodeId sessionId) { foreach (MonitoredNode2 monitoredNode in m_monitoredItemManager.MonitoredNodes.Values) { monitoredNode.InvalidatePermissionCacheForSession(sessionId); } } /// /// Returns true if a node is in a view. /// public virtual bool IsNodeInView(OperationContext context, NodeId viewId, object nodeHandle) { if (nodeHandle is not NodeHandle handle) { return false; } if (handle.Node != null) { return IsNodeInView(context, viewId, handle.Node); } 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 virtual NodeMetadata? GetPermissionMetadata( OperationContext context, object targetHandle, BrowseResultMask resultMask, Dictionary uniqueNodesServiceAttributesCache, bool permissionsOnly) { ServerSystemContext systemContext = SystemContext.Copy(context); // check for valid handle. NodeHandle? handle = IsHandleInNamespace(targetHandle); if (handle == null) { return null; } lock (Lock) { // validate node. NodeState? target = ValidateNode(systemContext, handle, null!); if (target == null) { return null; } ArrayOf values = default; // 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 == 0) { values = ReadAndCacheValidationAttributes( uniqueNodesServiceAttributesCache, systemContext, target, key); } else { // Retrieve value from cache values = uniqueNodesServiceAttributesCache[key]; } } else { values = ReadAndCacheValidationAttributes( uniqueNodesServiceAttributesCache, systemContext, target, key); } SetAccessAndRolePermissions(values, metadata); } // All other calls that do not use the cache else if (permissionsOnly) { values = ReadValidationAttributes(systemContext, target); SetAccessAndRolePermissions(values, metadata); } SetDefaultPermissions(systemContext, target, metadata); return metadata; } } /// /// Set the metadata default permission values for DefaultAccessRestrictions, DefaultRolePermissions and DefaultUserRolePermissions /// private void SetDefaultPermissions( ServerSystemContext systemContext, NodeState target, NodeMetadata metadata) { // check if NamespaceMetadata is defined for NamespaceUri string? namespaceUri = Server.NamespaceUris.GetString(target.NodeId.NamespaceIndex); NamespaceMetadataState namespaceMetadataState = Server.NodeManager.ConfigurationNodeManager!.GetNamespaceMetadataStateAsync(namespaceUri!).AsTask().GetAwaiter().GetResult()!; if (namespaceMetadataState != null) { ArrayOf namespaceMetadataValues; if (namespaceMetadataState.DefaultAccessRestrictions != null) { // get DefaultAccessRestrictions for Namespace namespaceMetadataValues = namespaceMetadataState.DefaultAccessRestrictions .ReadAttributes(systemContext, Attributes.Value); if (namespaceMetadataValues[0].TryGetValue(out AccessRestrictionType accessRestrictions)) { metadata.DefaultAccessRestrictions = accessRestrictions; } } if (namespaceMetadataState.DefaultRolePermissions != null) { // get DefaultRolePermissions for Namespace namespaceMetadataValues = namespaceMetadataState.DefaultRolePermissions .ReadAttributes(systemContext, Attributes.Value); if (namespaceMetadataValues[0].TryGetStructure( out ArrayOf rolePermissions)) { metadata.DefaultRolePermissions = rolePermissions; } } if (namespaceMetadataState.DefaultUserRolePermissions != null) { // get DefaultUserRolePermissions for Namespace namespaceMetadataValues = namespaceMetadataState.DefaultUserRolePermissions .ReadAttributes(systemContext, Attributes.Value); if (namespaceMetadataValues[0].TryGetStructure( out ArrayOf rolePermissions)) { metadata.DefaultUserRolePermissions = rolePermissions; } } } } /// /// 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) { lock (Lock) { if (m_componentCache == null) { return null; } 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; } } /// /// Removes a reference to a component in thecache. /// protected void RemoveNodeFromComponentCache(ISystemContext context, NodeHandle handle) { lock (Lock) { if (handle == null) { return; } 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); } } } } } /// /// Adds a node to the component cache. /// protected NodeState AddNodeToComponentCache( ISystemContext context, NodeHandle handle, NodeState node) { lock (Lock) { if (handle == null) { return node; } 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; } } private IReadOnlyList? m_namespaceUris; private ushort[] m_namespaceIndexes; private NodeIdDictionary? m_componentCache; /// /// A logger to use /// #pragma warning disable IDE1006 // Naming Styles protected ILogger m_logger { get; } #pragma warning restore IDE1006 // Naming Styles /// /// the monitored item manager of the NodeManager /// protected IMonitoredItemManager m_monitoredItemManager; } }