From 23a026e3de0cbe867b878ee2e0254eadbf809ff7 Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Sun, 15 Feb 2026 11:55:02 +0100 Subject: [PATCH 1/6] Use Interfaces Introduced with 1.5.378 Refactor to use interfaces for node managers and server Replaced concrete types with interfaces (e.g., IMasterNodeManager, ICoreNodeManager, IDiagnosticsNodeManager, IConfigurationNodeManager, IServerBase) across method signatures, properties, and internal structures to improve abstraction and flexibility. --- .../ReferenceServer/ReferenceServer.cs | 2 +- .../ApplicationConfigurationBuilder.cs | 2 +- .../Opc.Ua.Configuration/ApplicationInstance.cs | 4 ++-- .../Opc.Ua.Configuration/IApplicationInstance.cs | 4 ++-- .../GlobalDiscoverySampleServer.cs | 2 +- .../Diagnostics/DiagnosticsNodeManager.cs | 14 +++++++------- .../NodeManager/IMasterNodeManager.cs | 6 +++--- .../Opc.Ua.Server/NodeManager/MasterNodeManager.cs | 11 +++++------ Libraries/Opc.Ua.Server/Server/IServerInternal.cs | 10 +++++----- .../Opc.Ua.Server/Server/ServerInternalData.cs | 10 +++++----- Libraries/Opc.Ua.Server/Server/StandardServer.cs | 4 ++-- .../ReferenceServerWithLimits.cs | 2 +- 12 files changed, 35 insertions(+), 36 deletions(-) diff --git a/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs b/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs index 538760be6..b9f016177 100644 --- a/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs +++ b/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs @@ -85,7 +85,7 @@ public ReferenceServer(ITelemetryContext telemetry) /// always creates a CoreNodeManager which handles the built-in nodes defined by the specification. /// Any additional NodeManagers are expected to handle application specific nodes. /// - protected override MasterNodeManager CreateMasterNodeManager( + protected override IMasterNodeManager CreateMasterNodeManager( IServerInternal server, ApplicationConfiguration configuration) { diff --git a/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs b/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs index 3a73ba222..2f5601964 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs @@ -52,7 +52,7 @@ public ApplicationConfigurationBuilder(ApplicationInstance applicationInstance) /// /// The application instance used to build the configuration. /// - public ApplicationInstance ApplicationInstance { get; } + public IApplicationInstance ApplicationInstance { get; } /// /// The application configuration. diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs index 8a33d2fdb..35e0afea3 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs @@ -97,7 +97,7 @@ public ApplicationInstance( public Type ConfigurationType { get; set; } /// - public ServerBase Server { get; private set; } + public IServerBase Server { get; private set; } /// public ApplicationConfiguration ApplicationConfiguration { get; set; } @@ -114,7 +114,7 @@ public ApplicationInstance( public bool DisableCertificateAutoCreation { get; set; } /// - public async Task StartAsync(ServerBase server) + public async Task StartAsync(IServerBase server) { Server = server; diff --git a/Libraries/Opc.Ua.Configuration/IApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/IApplicationInstance.cs index fa79a7f67..9ab50aad3 100644 --- a/Libraries/Opc.Ua.Configuration/IApplicationInstance.cs +++ b/Libraries/Opc.Ua.Configuration/IApplicationInstance.cs @@ -93,7 +93,7 @@ public interface IApplicationInstance /// Gets the server. /// /// The server. - ServerBase Server { get; } + IServerBase Server { get; } /// /// Adds a Certificate to the Trusted Store of the Application, needed e.g. for the GDS to trust it´s own CA @@ -143,7 +143,7 @@ public interface IApplicationInstance /// Starts the UA server. /// /// The server. - Task StartAsync(ServerBase server); + Task StartAsync(IServerBase server); /// /// Stops the UA server. diff --git a/Libraries/Opc.Ua.Gds.Server.Common/GlobalDiscoverySampleServer.cs b/Libraries/Opc.Ua.Gds.Server.Common/GlobalDiscoverySampleServer.cs index 5f6b86f1e..e03a41b3e 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/GlobalDiscoverySampleServer.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/GlobalDiscoverySampleServer.cs @@ -94,7 +94,7 @@ protected override void OnServerStarted(IServerInternal server) /// by the specification. /// Any additional NodeManagers are expected to handle application specific nodes. /// - protected override MasterNodeManager CreateMasterNodeManager( + protected override IMasterNodeManager CreateMasterNodeManager( IServerInternal server, ApplicationConfiguration configuration) { diff --git a/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs b/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs index c916ea283..96964374f 100644 --- a/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs @@ -219,7 +219,7 @@ SetSubscriptionDurableMethodState setSubscriptionDurable /// /// Called when a client sets a subscription as durable. /// - public ServiceResult OnSetSubscriptionDurable( + protected ServiceResult OnSetSubscriptionDurable( ISystemContext context, MethodState method, NodeId objectId, @@ -237,7 +237,7 @@ public ServiceResult OnSetSubscriptionDurable( /// /// Called when a client gets the monitored items of a subscription. /// - public ServiceResult OnGetMonitoredItems( + protected ServiceResult OnGetMonitoredItems( ISystemContext context, MethodState method, IList inputArguments, @@ -283,7 +283,7 @@ public ServiceResult OnGetMonitoredItems( /// /// Called when a client initiates resending of all data monitored items in a Subscription. /// - public ServiceResult OnResendData( + protected ServiceResult OnResendData( ISystemContext context, MethodState method, IList inputArguments, @@ -345,7 +345,7 @@ public ServiceResult OnLockServer( /// /// Called when a client locks the server. /// - public ServiceResult OnUnlockServer( + protected ServiceResult OnUnlockServer( ISystemContext context, MethodState method, IList inputArguments, @@ -1433,7 +1433,7 @@ private static void FilterOutUnAuthorized( /// /// Set custom role permissions for desired node /// - private ServiceResult OnReadUserRolePermissions( + protected ServiceResult OnReadUserRolePermissions( ISystemContext context, NodeState node, ref RolePermissionTypeCollection value) @@ -1485,7 +1485,7 @@ from roleId in s_kWellKnownRoles /// /// Does a scan before the diagnostics are read. /// - private void OnBeforeReadDiagnostics( + protected void OnBeforeReadDiagnostics( ISystemContext context, BaseVariableValue variable, NodeState component) @@ -1509,7 +1509,7 @@ private void OnBeforeReadDiagnostics( /// /// Does a scan before the diagnostics are read. /// - private ServiceResult OnReadDiagnosticsArray( + protected ServiceResult OnReadDiagnosticsArray( ISystemContext context, NodeState node, ref object value) diff --git a/Libraries/Opc.Ua.Server/NodeManager/IMasterNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/IMasterNodeManager.cs index 18882d062..76a364663 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/IMasterNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/IMasterNodeManager.cs @@ -47,17 +47,17 @@ public interface IMasterNodeManager /// /// Returns the configuration node manager. /// - ConfigurationNodeManager ConfigurationNodeManager { get; } + IConfigurationNodeManager ConfigurationNodeManager { get; } /// /// Returns the core node manager. /// - CoreNodeManager CoreNodeManager { get; } + ICoreNodeManager CoreNodeManager { get; } /// /// Returns the diagnostics node manager. /// - DiagnosticsNodeManager DiagnosticsNodeManager { get; } + IDiagnosticsNodeManager DiagnosticsNodeManager { get; } /// /// The node managers being managed. diff --git a/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs index 39105fc9b..cd01a0eec 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs @@ -303,15 +303,14 @@ protected static PermissionType GetHistoryPermissionType(PerformUpdateType updat } /// - public CoreNodeManager CoreNodeManager => m_nodeManagers[1].SyncNodeManager as CoreNodeManager; + public ICoreNodeManager CoreNodeManager => m_nodeManagers[1].SyncNodeManager as ICoreNodeManager; /// - public DiagnosticsNodeManager DiagnosticsNodeManager - => m_nodeManagers[0].SyncNodeManager as DiagnosticsNodeManager; - + public IDiagnosticsNodeManager DiagnosticsNodeManager + => m_nodeManagers[0].SyncNodeManager as IDiagnosticsNodeManager; /// - public ConfigurationNodeManager ConfigurationNodeManager - => m_nodeManagers[0].SyncNodeManager as ConfigurationNodeManager; + public IConfigurationNodeManager ConfigurationNodeManager + => m_nodeManagers[0].SyncNodeManager as IConfigurationNodeManager; /// public virtual async ValueTask StartupAsync(CancellationToken cancellationToken = default) diff --git a/Libraries/Opc.Ua.Server/Server/IServerInternal.cs b/Libraries/Opc.Ua.Server/Server/IServerInternal.cs index bbc42ec23..33f49e96e 100644 --- a/Libraries/Opc.Ua.Server/Server/IServerInternal.cs +++ b/Libraries/Opc.Ua.Server/Server/IServerInternal.cs @@ -96,25 +96,25 @@ public interface IServerInternal : IAuditEventServer, IDisposable /// The master node manager for the server. /// /// The node manager. - MasterNodeManager NodeManager { get; } + IMasterNodeManager NodeManager { get; } /// /// The internal node manager for the servers. /// /// The core node manager. - CoreNodeManager CoreNodeManager { get; } + ICoreNodeManager CoreNodeManager { get; } /// /// Returns the node manager that managers the server diagnostics. /// /// The diagnostics node manager. - DiagnosticsNodeManager DiagnosticsNodeManager { get; } + IDiagnosticsNodeManager DiagnosticsNodeManager { get; } /// /// Returns the node manager that managers the server configuration. /// /// The configuration node manager. - ConfigurationNodeManager ConfigurationNodeManager { get; } + IConfigurationNodeManager ConfigurationNodeManager { get; } /// /// The manager for events that all components use to queue events that occur. @@ -298,7 +298,7 @@ void CreateServerObject( /// Stores the MasterNodeManager and the CoreNodeManager /// /// The node manager. - void SetNodeManager(MasterNodeManager nodeManager); + void SetNodeManager(IMasterNodeManager nodeManager); /// /// Stores the MainNodeManagerFactory diff --git a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs index 1f7ed1199..8c272fd59 100644 --- a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs +++ b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs @@ -148,7 +148,7 @@ protected virtual void Dispose(bool disposing) /// Stores the MasterNodeManager, the DiagnosticsNodeManager and the CoreNodeManager /// /// The node manager. - public void SetNodeManager(MasterNodeManager nodeManager) + public void SetNodeManager(IMasterNodeManager nodeManager) { NodeManager = nodeManager; DiagnosticsNodeManager = nodeManager.DiagnosticsNodeManager; @@ -285,7 +285,7 @@ public void SetModellingRulesManager(ModellingRulesManager modellingRulesManager /// The master node manager for the server. /// /// The node manager. - public MasterNodeManager NodeManager { get; private set; } + public IMasterNodeManager NodeManager { get; private set; } /// public IMainNodeManagerFactory MainNodeManagerFactory { get; private set; } @@ -294,16 +294,16 @@ public void SetModellingRulesManager(ModellingRulesManager modellingRulesManager /// The internal node manager for the servers. /// /// The core node manager. - public CoreNodeManager CoreNodeManager { get; private set; } + public ICoreNodeManager CoreNodeManager { get; private set; } /// /// Returns the node manager that managers the server diagnostics. /// /// The diagnostics node manager. - public DiagnosticsNodeManager DiagnosticsNodeManager { get; private set; } + public IDiagnosticsNodeManager DiagnosticsNodeManager { get; private set; } /// - public ConfigurationNodeManager ConfigurationNodeManager { get; private set; } + public IConfigurationNodeManager ConfigurationNodeManager { get; private set; } /// /// The manager for events that all components use to queue events that occur. diff --git a/Libraries/Opc.Ua.Server/Server/StandardServer.cs b/Libraries/Opc.Ua.Server/Server/StandardServer.cs index 2491a65a0..ab121649a 100644 --- a/Libraries/Opc.Ua.Server/Server/StandardServer.cs +++ b/Libraries/Opc.Ua.Server/Server/StandardServer.cs @@ -2804,7 +2804,7 @@ await base.StartApplicationAsync(configuration, cancellationToken) // create the master node manager. m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - CreateMasterNodeManager."); - MasterNodeManager masterNodeManager = CreateMasterNodeManager( + IMasterNodeManager masterNodeManager = CreateMasterNodeManager( m_serverInternal, configuration); @@ -3384,7 +3384,7 @@ protected virtual ResourceManager CreateResourceManager( /// The server. /// The configuration. /// Returns the master node manager for the server, the return type is . - protected virtual MasterNodeManager CreateMasterNodeManager( + protected virtual IMasterNodeManager CreateMasterNodeManager( IServerInternal server, ApplicationConfiguration configuration) { diff --git a/Tests/Opc.Ua.Client.Tests/ReferenceServerWithLimits.cs b/Tests/Opc.Ua.Client.Tests/ReferenceServerWithLimits.cs index 7524098e8..9f8b5bb57 100644 --- a/Tests/Opc.Ua.Client.Tests/ReferenceServerWithLimits.cs +++ b/Tests/Opc.Ua.Client.Tests/ReferenceServerWithLimits.cs @@ -102,7 +102,7 @@ public void SetMaxNumberOfContinuationPoints(uint maxNumberOfContinuationPoints) } } - protected override MasterNodeManager CreateMasterNodeManager( + protected override IMasterNodeManager CreateMasterNodeManager( IServerInternal server, ApplicationConfiguration configuration) { From 75473b3c8d2772d55341daa298267cd5e571b503 Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Sun, 15 Feb 2026 13:19:29 +0100 Subject: [PATCH 2/6] Refactor CoreNodeManager to be Based on CustomNodeManager2 --- Docs/CoreNodeManagerVsCustomNodeManager2.md | 70 + .../NodeManager/CoreNodeManager.Obsolete.cs | 3682 +++++++++++++++++ .../NodeManager/CoreNodeManager.cs | 3639 +--------------- .../NodeManager/MainNodeManagerFactory.cs | 2 +- .../Server/ServerInternalData.cs | 4 + 5 files changed, 3809 insertions(+), 3588 deletions(-) create mode 100644 Docs/CoreNodeManagerVsCustomNodeManager2.md create mode 100644 Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.Obsolete.cs diff --git a/Docs/CoreNodeManagerVsCustomNodeManager2.md b/Docs/CoreNodeManagerVsCustomNodeManager2.md new file mode 100644 index 000000000..b59965654 --- /dev/null +++ b/Docs/CoreNodeManagerVsCustomNodeManager2.md @@ -0,0 +1,70 @@ +# CoreNodeManager vs CustomNodeManager2 + +This document outlines the key differences in behavior and implementation between `CoreNodeManager` and `CustomNodeManager2` within the OPC UA .NET Standard Stack. + +`CoreNodeManager` is typically used for managing the internal nodes of the Server (Namespace 0) or simple static node sets. `CustomNodeManager2` is designed as a base class for developers implementing custom node managers with specific business logic, dynamic behavior, or backing stores. + +## 1. Storage & Data Structures + +| Feature | CoreNodeManager | CustomNodeManager2 | +| :--- | :--- | :--- | +| **Node Storage** | Uses a `NodeTable` (`m_nodes`) internally. | Uses a `NodeIdDictionary` (`PredefinedNodes`). | +| **Node Type** | Manages `ILocalNode` interface objects. | Manages `NodeState` objects (and subclasses). | +| **Handle Type** | `GetManagerHandle` returns the `ILocalNode` instance directly. | `GetManagerHandle` returns a `NodeHandle` wrapper containing the `NodeState` and validation status. | +| **Locking** | Uses `DataLock` (object). | Uses `Lock` (object). | +| **Namespace** | Typically manages dynamic nodes in specific indexes or internal server nodes. | Designed to manage specific namespaces passed in the constructor. Uses `IsNodeIdInNamespace` checks. | + +## 2. Extensibility + +| Feature | CoreNodeManager | CustomNodeManager2 | +| :--- | :--- | :--- | +| **Design Intent** | Sealed-like behavior. Not primarily designed for inheritance or overriding behavior. | Highly extensible. Most methods (`Read`, `Write`, `Browse`, `Call`) are `virtual` to allow custom overrides. | +| **Node Factory** | Does not implement `INodeIdFactory`. | Implements `INodeIdFactory` to generate new NodeIds for the system context. | +| **Address Space** | `CreateAddressSpace` is often empty (`ImportNodes` is used instead). | `CreateAddressSpace` invokes `LoadPredefinedNodes` to load nodes from resources/assemblies. | + +## 3. Operational Behavior + +### Reading & Writing +* **CoreNodeManager**: + * **Read**: Directly invokes `ILocalNode.Read`. + * **Write**: Performs basic type checking (expected data type/value rank) and invokes `ILocalNode.Write`. +* **CustomNodeManager2**: + * **Read**: Validates the node handle, supports operation caching, and invokes `NodeState.ReadAttribute`. Handles timestamp synchronization (e.g., matching ServerTimestamp to SourceTimestamp for Value attributes). + * **Write**: + * Performs **Range Checks** for `AnalogItemState` (InstrumentRange). + * Generates **Audit Events** (`Server.ReportAuditWriteUpdateEvent`). + * Detects **Semantic Changes** (e.g., changes to `EURange`, `EnumStrings`) and updates monitored items accordingly. + +### Method Calls +* **CoreNodeManager**: + * **Browse**: Iterates over references stored in `ILocalNode`. Basic masking and filtering. + * **Translate**: Basic search through internal references. +* **CustomNodeManager2**: + * **Browse**: Uses `NodeState.CreateBrowser`. Explicitly validates `PermissionType.Browse`. Supports Views (`IsNodeInView`). + * **Translate**: Uses `CreateBrowser` to navigate path. Supports resolving targets in other node managers via `unresolvedTargetIds`. + +## 4. Monitoring & Subscriptions + +| Feature | CoreNodeManager | CustomNodeManager2 | +| :--- | :--- | :--- | +| **Manager** | Uses `SamplingGroupManager` directly. | Uses `IMonitoredItemManager` abstraction (defaults to `SamplingGroupMonitoredItemManager` or `MonitoredNodeMonitoredItemManager`). | +| **Filter Validation** | Validates `DataChangeFilter` specifically (deadband, EU Range). | Delegates validation to `ValidateMonitoringFilter`, supports `AggregateFilter` (if supported by server) and `DataChangeFilter`. | +| **Events** | Basic event subscription support (`SubscribeToEvents` checks `EventNotifier` bit). | **Full Event Support**:
- Manages `RootNotifiers`.
- Propagates events via `SubscribeToAllEvents`.
- Implements `ConditionRefresh`.
- Validates `PermissionType.ReceiveEvents`. | + +## 5. History + +* **CoreNodeManager**: + * `HistoryRead` / `HistoryUpdate`: Iterates nodes and returns `BadNotReadable` / `BadNotWritable` (or `BadHistoryOperationUnsupported` implicit). No infrastructure for history. +* **CustomNodeManager2**: + * Provides scaffold methods (`HistoryReadRawModified`, `HistoryReadProcessed`, `HistoryUpdateData`, etc.). + * Checks `AccessLevels.HistoryRead/Write` and `EventNotifier.HistoryRead/Write`. + * Default implementation returns `BadHistoryOperationUnsupported`, but is structured for easy overriding in derived classes. + +## 6. Security + +* **CoreNodeManager**: + * Checks `AccessLevel`, `UserAccessLevel`, `WriteMask` in `Write`. + * Loads Role Permissions into metadata. +* **CustomNodeManager2**: + * Explicitly calls `MasterNodeManager.ValidateRolePermissions` during `Browse`, `Call`, and Event processing. + * Reads and caches validation attributes (`AccessRestrictions`, `RolePermissions`) for optimized access. diff --git a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.Obsolete.cs b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.Obsolete.cs new file mode 100644 index 000000000..cc529ff13 --- /dev/null +++ b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.Obsolete.cs @@ -0,0 +1,3682 @@ +/* ======================================================================== + * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Opc.Ua.Server +{ + /// + [Obsolete("CoreNodeManager is deprecated. Please use CoreNodeManager2 instead.")] + public class CoreNodeManager : INodeManager, IDisposable, ICoreNodeManager + { + /// + /// Initializes the object with default values. + /// + public CoreNodeManager( + IServerInternal server, + ApplicationConfiguration configuration, + ushort dynamicNamespaceIndex) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Server = server ?? throw new ArgumentNullException(nameof(server)); + m_logger = server.Telemetry.CreateLogger(); + + m_nodes = new NodeTable(server.NamespaceUris, server.ServerUris, server.TypeTree); + m_monitoredItems = []; + m_defaultMinimumSamplingInterval = 1000; + m_namespaceUris = []; + m_dynamicNamespaceIndex = dynamicNamespaceIndex; + + // use namespace 1 if out of range. + if (m_dynamicNamespaceIndex == 0 || + m_dynamicNamespaceIndex >= server.NamespaceUris.Count) + { + m_dynamicNamespaceIndex = 1; + } + + m_samplingGroupManager = new SamplingGroupManager( + server, + this, + (uint)configuration.ServerConfiguration.MaxNotificationQueueSize, + (uint)configuration.ServerConfiguration.MaxDurableNotificationQueueSize, + configuration.ServerConfiguration.AvailableSamplingRates); + } + + /// + /// 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) + { + List nodes = null; + + lock (DataLock) + { + nodes = [.. m_nodes]; + m_nodes.Clear(); + + m_monitoredItems.Clear(); + } + + foreach (INode node in nodes) + { + Utils.SilentDispose(node); + } + + Utils.SilentDispose(m_samplingGroupManager); + } + } + + /// + /// Acquires the lock on the node manager. + /// + public object DataLock { get; } = new object(); + + /// + public void ImportNodes(ISystemContext context, IEnumerable predefinedNodes) + { + ImportNodes(context, predefinedNodes, false); + } + + /// + public void ImportNodes( + ISystemContext context, + IEnumerable predefinedNodes, + bool isInternal) + { + var nodesToExport = new NodeTable( + Server.NamespaceUris, + Server.ServerUris, + Server.TypeTree); + + foreach (NodeState node in predefinedNodes) + { + node.Export(context, nodesToExport); + } + + lock (DataLock) + { + foreach (ILocalNode nodeToExport in nodesToExport.OfType()) + { + AttachNode(nodeToExport, isInternal); + } + } + } + + /// + public IEnumerable NamespaceUris => m_namespaceUris; + + /// + /// + /// Populates the NodeManager by loading the standard nodes from an XML file stored as an embedded resource. + /// + public void CreateAddressSpace(IDictionary> externalReferences) + { + // TBD + } + + /// + /// + /// Disposes all of the nodes. + /// + public void DeleteAddressSpace() + { + var nodesToDispose = new List(); + + lock (DataLock) + { + // collect nodes to dispose. + foreach (INode node in m_nodes) + { + if (node is IDisposable disposable) + { + nodesToDispose.Add(disposable); + } + } + + m_nodes.Clear(); + } + + // dispose of the nodes. + foreach (IDisposable disposable in nodesToDispose) + { + try + { + disposable.Dispose(); + } + catch (Exception e) + { + m_logger.LogError(e, "Unexpected error disposing a Node object."); + } + } + } + + /// + public object GetManagerHandle(NodeId nodeId) + { + lock (DataLock) + { + if (nodeId.IsNull) + { + return null; + } + + return GetLocalNode(nodeId); + } + } + + /// + public void TranslateBrowsePath( + OperationContext context, + object sourceHandle, + RelativePathElement relativePath, + IList targetIds, + IList unresolvedTargetIds) + { + if (sourceHandle == null) + { + throw new ArgumentNullException(nameof(sourceHandle)); + } + + if (relativePath == null) + { + throw new ArgumentNullException(nameof(relativePath)); + } + + if (targetIds == null) + { + throw new ArgumentNullException(nameof(targetIds)); + } + + if (unresolvedTargetIds == null) + { + throw new ArgumentNullException(nameof(unresolvedTargetIds)); + } + + // check for valid handle. + if (sourceHandle is not ILocalNode source) + { + return; + } + + lock (DataLock) + { + // find the references that meet the filter criteria. + IList references = source.References.Find( + relativePath.ReferenceTypeId, + relativePath.IsInverse, + relativePath.IncludeSubtypes, + Server.TypeTree); + + // nothing more to do. + if (references == null || references.Count == 0) + { + return; + } + + // find targets with matching browse names. + foreach (IReference reference in references) + { + INode target = GetLocalNode(reference.TargetId); + + // target is not known to the node manager. + if (target == null) + { + // ignore unknown external references. + if (reference.TargetId.IsAbsolute) + { + continue; + } + + // caller must check the browse name. + unresolvedTargetIds.Add((NodeId)reference.TargetId); + continue; + } + + // check browse name. + if (target.BrowseName == relativePath.TargetName) + { + targetIds.Add(reference.TargetId); + } + } + } + } + + /// + public void Browse( + OperationContext context, + ref ContinuationPoint continuationPoint, + IList references) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (continuationPoint == null) + { + throw new ArgumentNullException(nameof(continuationPoint)); + } + + if (references == null) + { + throw new ArgumentNullException(nameof(references)); + } + + // check for valid handle. + if (continuationPoint.NodeToBrowse is not ILocalNode source) + { + throw new ServiceResultException(StatusCodes.BadNodeIdUnknown); + } + + // check for view. + if (!ViewDescription.IsDefault(continuationPoint.View)) + { + throw new ServiceResultException(StatusCodes.BadViewIdUnknown); + } + + lock (DataLock) + { + // construct list of references. + uint maxResultsToReturn = continuationPoint.MaxResultsToReturn; + + // get previous enumerator. + + // fetch a snapshot all references for node. + if (continuationPoint.Data is not IEnumerator enumerator) + { + enumerator = GetEnumerator(source.References); + enumerator.MoveNext(); + } + + static IEnumerator GetEnumerator(IReferenceCollection references) + { + var copy = new List(references); + return copy.GetEnumerator(); + } + + do + { + IReference reference = enumerator.Current; + + // silently ignore bad values. + if (reference == null || + reference.ReferenceTypeId.IsNull || + reference.TargetId.IsNull) + { + continue; + } + + // apply browse filters. + bool include = ApplyBrowseFilters( + reference, + continuationPoint.BrowseDirection, + continuationPoint.ReferenceTypeId, + continuationPoint.IncludeSubtypes); + + if (include) + { + var description = new ReferenceDescription { NodeId = reference.TargetId }; + description.SetReferenceType( + continuationPoint.ResultMask, + reference.ReferenceTypeId, + !reference.IsInverse); + + // only fetch the metadata if it is requested. + if (continuationPoint.TargetAttributesRequired) + { + // get the metadata for the node. + NodeMetadata metadata = GetNodeMetadata( + context, + GetManagerHandle(reference.TargetId), + continuationPoint.ResultMask); + + // update description with local node metadata. + if (metadata != null) + { + description.SetTargetAttributes( + continuationPoint.ResultMask, + metadata.NodeClass, + metadata.BrowseName, + metadata.DisplayName, + metadata.TypeDefinition); + + // check node class mask. + if (!CheckNodeClassMask( + continuationPoint.NodeClassMask, + description.NodeClass)) + { + continue; + } + } + // any target that is not remote must be owned by another node manager. + else if (!reference.TargetId.IsAbsolute) + { + description.Unfiltered = true; + } + } + + // add reference to list. + references.Add(description); + + // construct continuation point if max results reached. + if (maxResultsToReturn > 0 && references.Count >= maxResultsToReturn) + { + continuationPoint.Index = 0; + continuationPoint.Data = enumerator; + enumerator.MoveNext(); + return; + } + } + } while (enumerator.MoveNext()); + + // nothing more to browse if it exits from the loop normally. + continuationPoint.Dispose(); + continuationPoint = null; + } + } + + /// + /// Returns true if the target meets the filter criteria. + /// + private bool ApplyBrowseFilters( + IReference reference, + BrowseDirection browseDirection, + NodeId referenceTypeId, + bool includeSubtypes) + { + // check browse direction. + if (reference.IsInverse) + { + if (browseDirection == BrowseDirection.Forward) + { + return false; + } + } + else if (browseDirection == BrowseDirection.Inverse) + { + return false; + } + + // check reference type filter. + if (!referenceTypeId.IsNull && reference.ReferenceTypeId != referenceTypeId) + { + return includeSubtypes && + Server.TypeTree.IsTypeOf(reference.ReferenceTypeId, referenceTypeId); + } + + // include reference for now. + return true; + } + + /// + public NodeMetadata GetNodeMetadata( + OperationContext context, + object targetHandle, + BrowseResultMask resultMask) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // find target. + if (targetHandle is not ILocalNode target) + { + return null; + } + + lock (DataLock) + { + // copy the default metadata. + var metadata = new NodeMetadata(target, target.NodeId); + + // copy target attributes. + if (((int)resultMask & (int)BrowseResultMask.NodeClass) != 0) + { + metadata.NodeClass = target.NodeClass; + } + + if (((int)resultMask & (int)BrowseResultMask.BrowseName) != 0) + { + metadata.BrowseName = target.BrowseName; + } + + if (((int)resultMask & (int)BrowseResultMask.DisplayName) != 0) + { + metadata.DisplayName = target.DisplayName; + + // check if the display name can be localized. + if (!string.IsNullOrEmpty(metadata.DisplayName.TranslationInfo.Key)) + { + metadata.DisplayName = Server.ResourceManager.Translate( + context.PreferredLocales, + metadata.DisplayName); + } + } + + metadata.WriteMask = target.WriteMask; + + if (metadata.WriteMask != AttributeWriteMask.None) + { + var value = new DataValue((uint)(int)target.UserWriteMask); + ServiceResult result = target.Read(context, Attributes.UserWriteMask, value); + + if (ServiceResult.IsBad(result)) + { + metadata.WriteMask = AttributeWriteMask.None; + } + else + { + metadata.WriteMask = (AttributeWriteMask) + (int)((uint)(int)metadata.WriteMask & (uint)value.Value); + } + } + + metadata.EventNotifier = EventNotifiers.None; + metadata.AccessLevel = AccessLevels.None; + metadata.Executable = false; + + switch (target.NodeClass) + { + case NodeClass.Object: + metadata.EventNotifier = ((IObject)target).EventNotifier; + break; + case NodeClass.View: + metadata.EventNotifier = ((IView)target).EventNotifier; + break; + case NodeClass.Variable: + { + var variable = (IVariable)target; + metadata.DataType = variable.DataType; + metadata.ValueRank = variable.ValueRank; + metadata.ArrayDimensions = variable.ArrayDimensions; + metadata.AccessLevel = variable.AccessLevel; + + var value = new DataValue(variable.UserAccessLevel); + ServiceResult result = variable.Read( + context, + Attributes.UserAccessLevel, + value); + + if (ServiceResult.IsBad(result)) + { + metadata.AccessLevel = 0; + break; + } + + metadata.AccessLevel = (byte)(metadata.AccessLevel & (byte)value.Value); + break; + } + case NodeClass.Method: + { + var method = (IMethod)target; + metadata.Executable = method.Executable; + + if (metadata.Executable) + { + var value = new DataValue(method.UserExecutable); + ServiceResult result = method.Read( + context, + Attributes.UserExecutable, + value); + + if (ServiceResult.IsBad(result)) + { + metadata.Executable = false; + break; + } + + metadata.Executable = (bool)value.Value; + } + + break; + } + case NodeClass.Unspecified: + case NodeClass.ObjectType: + case NodeClass.VariableType: + case NodeClass.ReferenceType: + case NodeClass.DataType: + break; + default: + throw ServiceResultException.Unexpected( + $"Unexpected NodeClass {target.NodeClass}"); + } + + // look up type definition. + if (((int)resultMask & (int)BrowseResultMask.TypeDefinition) != 0 && + target.NodeClass is NodeClass.Variable or NodeClass.Object) + { + metadata.TypeDefinition = target.TypeDefinitionId; + } + + // Set AccessRestrictions and RolePermissions + var node = (Node)target; + metadata.AccessRestrictions = (AccessRestrictionType)node.AccessRestrictions; + metadata.RolePermissions = node.RolePermissions; + metadata.UserRolePermissions = node.UserRolePermissions; + + // check if NamespaceMetadata is defined for NamespaceUri + string namespaceUri = Server.NamespaceUris.GetString(target.NodeId.NamespaceIndex); + NamespaceMetadataState namespaceMetadataState = + Server.NodeManager.ConfigurationNodeManager + .GetNamespaceMetadataState(namespaceUri); + if (namespaceMetadataState != null) + { + metadata.DefaultAccessRestrictions = (AccessRestrictionType) + namespaceMetadataState.DefaultAccessRestrictions.Value; + metadata.DefaultRolePermissions = namespaceMetadataState.DefaultRolePermissions + .Value; + metadata.DefaultUserRolePermissions = namespaceMetadataState + .DefaultUserRolePermissions + .Value; + } + + // return metadata. + return metadata; + } + } + + /// + /// + /// This method must not be called without first acquiring + /// + public void AddReferences(IDictionary> references) + { + if (references == null) + { + throw new ArgumentNullException(nameof(references)); + } + + lock (DataLock) + { + IEnumerator>> enumerator = references + .GetEnumerator(); + + while (enumerator.MoveNext()) + { + ILocalNode actualNode = GetLocalNode(enumerator.Current.Key); + + if (actualNode != null) + { + foreach (IReference reference in enumerator.Current.Value) + { + AddReference( + actualNode, + reference.ReferenceTypeId, + reference.IsInverse, + reference.TargetId); + } + } + } + } + } + + /// + public void Read( + OperationContext context, + double maxAge, + IList nodesToRead, + IList values, + IList errors) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (nodesToRead == null) + { + throw new ArgumentNullException(nameof(nodesToRead)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + if (errors == null) + { + throw new ArgumentNullException(nameof(errors)); + } + + lock (DataLock) + { + for (int ii = 0; ii < nodesToRead.Count; ii++) + { + ReadValueId nodeToRead = nodesToRead[ii]; + + // skip items that have already been processed. + if (nodeToRead.Processed) + { + continue; + } + + // look up the node. + ILocalNode node = GetLocalNode(nodeToRead.NodeId); + + if (node == null) + { + continue; + } + + DataValue value = values[ii] = new DataValue(); + + value.Value = null; + value.ServerTimestamp = DateTime.MinValue; // Will be set later + value.SourceTimestamp = DateTime.MinValue; + value.StatusCode = StatusCodes.BadAttributeIdInvalid; + + // owned by this node manager. + nodeToRead.Processed = true; + + // read the default value (also verifies that the attribute id is valid for the node). + ServiceResult error = node.Read(context, nodeToRead.AttributeId, value); + + if (ServiceResult.IsBad(error)) + { + errors[ii] = error; + continue; + } + + // always use default value for base attributes. + bool useDefault; + switch (nodeToRead.AttributeId) + { + case Attributes.NodeId: + case Attributes.NodeClass: + case Attributes.BrowseName: + useDefault = true; + break; + default: + Attributes.ThrowIfOutOfRange(nodeToRead.AttributeId); + useDefault = false; + break; + } + + if (useDefault) + { + errors[ii] = error; + continue; + } + + // apply index range to value attributes. + if (nodeToRead.AttributeId == Attributes.Value) + { + object defaultValue = value.Value; + + error = nodeToRead.ParsedIndexRange.ApplyRange(ref defaultValue); + + if (ServiceResult.IsBad(error)) + { + value.Value = null; + errors[ii] = error; + continue; + } + + // apply data encoding. + if (!nodeToRead.DataEncoding.IsNull) + { + error = EncodeableObject.ApplyDataEncoding( + Server.MessageContext, + nodeToRead.DataEncoding, + ref defaultValue); + + if (ServiceResult.IsBad(error)) + { + value.Value = null; + errors[ii] = error; + continue; + } + } + + value.Value = defaultValue; + + // Set SourceTimestamp if not already set by the node + if (value.SourceTimestamp == DateTime.MinValue) + { + value.SourceTimestamp = DateTime.UtcNow; + } + + // Set ServerTimestamp to match SourceTimestamp for Value attributes + // This ensures ServerTimestamp and SourceTimestamp are equal, + // which is important for nodes like ServerStatus children where + // the node's read callback sets a specific timestamp + value.ServerTimestamp = value.SourceTimestamp; + } + } + } + } + + /// + public void HistoryRead( + OperationContext context, + HistoryReadDetails details, + TimestampsToReturn timestampsToReturn, + bool releaseContinuationPoints, + IList nodesToRead, + IList results, + IList errors) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (details == null) + { + throw new ArgumentNullException(nameof(details)); + } + + if (nodesToRead == null) + { + throw new ArgumentNullException(nameof(nodesToRead)); + } + + if (results == null) + { + throw new ArgumentNullException(nameof(results)); + } + + if (errors == null) + { + throw new ArgumentNullException(nameof(errors)); + } + + lock (DataLock) + { + for (int ii = 0; ii < nodesToRead.Count; ii++) + { + HistoryReadValueId nodeToRead = nodesToRead[ii]; + + // skip items that have already been processed. + if (nodeToRead.Processed) + { + continue; + } + + // look up the node. + ILocalNode node = GetLocalNode(nodeToRead.NodeId); + + if (node == null) + { + continue; + } + + // owned by this node manager. + nodeToRead.Processed = true; + + errors[ii] = StatusCodes.BadNotReadable; + } + } + } + + /// + public void Write( + OperationContext context, + IList nodesToWrite, + IList errors) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (nodesToWrite == null) + { + throw new ArgumentNullException(nameof(nodesToWrite)); + } + + if (errors == null) + { + throw new ArgumentNullException(nameof(errors)); + } + + lock (DataLock) + { + for (int ii = 0; ii < nodesToWrite.Count; ii++) + { + WriteValue nodeToWrite = nodesToWrite[ii]; + + // skip items that have already been processed. + if (nodeToWrite.Processed) + { + continue; + } + + // look up the node. + ILocalNode node = GetLocalNode(nodeToWrite.NodeId); + + if (node == null) + { + continue; + } + + // owned by this node manager. + nodeToWrite.Processed = true; + + if (!node.SupportsAttribute(nodeToWrite.AttributeId)) + { + errors[ii] = StatusCodes.BadAttributeIdInvalid; + continue; + } + + // fetch the node metadata. + NodeMetadata metadata = GetNodeMetadata(context, node, BrowseResultMask.All); + + // check access. + bool writeable = true; + ServiceResult error = null; + + // determine access rights. + switch (nodeToWrite.AttributeId) + { + case Attributes.NodeId: + case Attributes.NodeClass: + case Attributes.AccessLevel: + case Attributes.UserAccessLevel: + case Attributes.Executable: + case Attributes.UserExecutable: + case Attributes.EventNotifier: + writeable = false; + break; + case Attributes.Value: + writeable = (metadata.AccessLevel & AccessLevels.CurrentWrite) != 0; + break; + default: + writeable = (metadata.WriteMask & + Attributes.GetMask(nodeToWrite.AttributeId)) != 0; + break; + } + + // error if not writeable. + if (!writeable) + { + errors[ii] = StatusCodes.BadNotWritable; + continue; + } + + // determine expected datatype and value rank. + NodeId expectedDatatypeId = metadata.DataType; + int expectedValueRank = metadata.ValueRank; + + if (nodeToWrite.AttributeId != Attributes.Value) + { + expectedDatatypeId = Attributes.GetDataTypeId(nodeToWrite.AttributeId); + + DataValue value = nodeToWrite.Value; + + if (value.StatusCode != StatusCodes.Good || + value.ServerTimestamp != DateTime.MinValue || + value.SourceTimestamp != DateTime.MinValue) + { + errors[ii] = StatusCodes.BadWriteNotSupported; + continue; + } + + expectedValueRank = ValueRanks.Scalar; + + if (nodeToWrite.AttributeId == Attributes.ArrayDimensions) + { + expectedValueRank = ValueRanks.OneDimension; + } + } + + // check whether value being written is an instance of the expected data type. + object valueToWrite = nodeToWrite.Value.Value; + + var typeInfo = TypeInfo.IsInstanceOfDataType( + valueToWrite, + expectedDatatypeId, + expectedValueRank, + Server.NamespaceUris, + Server.TypeTree); + + if (typeInfo.IsUnknown) + { + errors[ii] = StatusCodes.BadTypeMismatch; + continue; + } + + // check index range. + if (nodeToWrite.ParsedIndexRange.Count > 0) + { + // check index range for scalars. + if (typeInfo.ValueRank < 0) + { + errors[ii] = StatusCodes.BadIndexRangeInvalid; + continue; + } + var array = (Array)valueToWrite; + + if (nodeToWrite.ParsedIndexRange.Count != array.Length) + { + errors[ii] = StatusCodes.BadIndexRangeInvalid; + continue; + } + } + + // write the default value. + error = node.Write(nodeToWrite.AttributeId, nodeToWrite.Value); + + if (ServiceResult.IsBad(error)) + { + errors[ii] = error; + } + } + } + } + + /// + public void HistoryUpdate( + OperationContext context, + Type detailsType, + IList nodesToUpdate, + IList results, + IList errors) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (nodesToUpdate == null) + { + throw new ArgumentNullException(nameof(nodesToUpdate)); + } + + if (results == null) + { + throw new ArgumentNullException(nameof(results)); + } + + if (errors == null) + { + throw new ArgumentNullException(nameof(errors)); + } + + lock (DataLock) + { + for (int ii = 0; ii < nodesToUpdate.Count; ii++) + { + HistoryUpdateDetails nodeToUpdate = nodesToUpdate[ii]; + + // skip items that have already been processed. + if (nodeToUpdate.Processed) + { + continue; + } + + // look up the node. + ILocalNode node = GetLocalNode(nodeToUpdate.NodeId); + + if (node == null) + { + continue; + } + + // owned by this node manager. + nodeToUpdate.Processed = true; + + errors[ii] = StatusCodes.BadNotWritable; + } + } + } + + /// + public void Call( + OperationContext context, + IList methodsToCall, + IList results, + IList errors) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (methodsToCall == null) + { + throw new ArgumentNullException(nameof(methodsToCall)); + } + + if (results == null) + { + throw new ArgumentNullException(nameof(results)); + } + + if (errors == null) + { + throw new ArgumentNullException(nameof(errors)); + } + + lock (DataLock) + { + for (int ii = 0; ii < methodsToCall.Count; ii++) + { + CallMethodRequest methodToCall = methodsToCall[ii]; + + // skip items that have already been processed. + if (methodToCall.Processed) + { + continue; + } + + // look up the node. + ILocalNode node = GetLocalNode(methodToCall.ObjectId); + + if (node == null) + { + continue; + } + + methodToCall.Processed = true; + + // look up the method. + ILocalNode method = GetLocalNode(methodToCall.MethodId); + + if (method == null) + { + errors[ii] = ServiceResult.Create( + StatusCodes.BadMethodInvalid, + "Method is not in the address space."); + continue; + } + + // check that the method is defined for the object. + if (!node.References.Exists( + ReferenceTypeIds.HasComponent, + false, + methodToCall.MethodId, + true, + Server.TypeTree)) + { + errors[ii] = ServiceResult.Create( + StatusCodes.BadMethodInvalid, + "Method is not a component of the Object."); + continue; + } + + errors[ii] = StatusCodes.BadNotImplemented; + } + } + } + + /// + public ServiceResult SubscribeToEvents( + OperationContext context, + object sourceId, + uint subscriptionId, + IEventMonitoredItem monitoredItem, + bool unsubscribe) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (sourceId == null) + { + throw new ArgumentNullException(nameof(sourceId)); + } + + if (monitoredItem == null) + { + throw new ArgumentNullException(nameof(monitoredItem)); + } + + lock (DataLock) + { + // validate the node. + NodeMetadata metadata = GetNodeMetadata( + context, + sourceId, + BrowseResultMask.NodeClass); + + if (metadata == null) + { + return StatusCodes.BadNodeIdUnknown; + } + + // validate the node class. + if ((((int)metadata.NodeClass & (int)NodeClass.Object) | (int)NodeClass.View) == 0) + { + return StatusCodes.BadNotSupported; + } + + // check that it supports events. + if ((metadata.EventNotifier & EventNotifiers.SubscribeToEvents) == 0) + { + return StatusCodes.BadNotSupported; + } + + return ServiceResult.Good; + } + } + + /// + public ServiceResult SubscribeToAllEvents( + OperationContext context, + uint subscriptionId, + IEventMonitoredItem monitoredItem, + bool unsubscribe) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (monitoredItem == null) + { + throw new ArgumentNullException(nameof(monitoredItem)); + } + + return ServiceResult.Good; + } + + /// + public ServiceResult ConditionRefresh( + OperationContext context, + IList monitoredItems) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return ServiceResult.Good; + } + + /// + /// Creates a set of monitored items. + /// + /// is null. + public void CreateMonitoredItems( + OperationContext context, + uint subscriptionId, + double publishingInterval, + TimestampsToReturn timestampsToReturn, + IList itemsToCreate, + IList errors, + IList filterErrors, + IList monitoredItems, + bool createDurable, + MonitoredItemIdFactory monitoredItemIdFactory) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (itemsToCreate == null) + { + throw new ArgumentNullException(nameof(itemsToCreate)); + } + + if (errors == null) + { + throw new ArgumentNullException(nameof(errors)); + } + + if (monitoredItems == null) + { + throw new ArgumentNullException(nameof(monitoredItems)); + } + + lock (DataLock) + { + for (int ii = 0; ii < errors.Count; ii++) + { + MonitoredItemCreateRequest itemToCreate = itemsToCreate[ii]; + + // skip items that have already been processed. + if (itemToCreate.Processed) + { + continue; + } + + // look up the node. + ILocalNode node = GetLocalNode(itemToCreate.ItemToMonitor.NodeId); + + if (node == null) + { + continue; + } + + // owned by this node manager. + itemToCreate.Processed = true; + + if (!node.SupportsAttribute(itemToCreate.ItemToMonitor.AttributeId)) + { + errors[ii] = StatusCodes.BadAttributeIdInvalid; + continue; + } + + // fetch the metadata for the node. + NodeMetadata metadata = GetNodeMetadata(context, node, BrowseResultMask.All); + + if (itemToCreate.ItemToMonitor.AttributeId == Attributes.Value && + (metadata.AccessLevel & AccessLevels.CurrentRead) == 0) + { + errors[ii] = StatusCodes.BadNotReadable; + continue; + } + + // check value rank against index range. + if (itemToCreate.ItemToMonitor.ParsedIndexRange != NumericRange.Empty) + { + int valueRank = metadata.ValueRank; + + if (itemToCreate.ItemToMonitor.AttributeId != Attributes.Value) + { + valueRank = Attributes.GetValueRank( + itemToCreate.ItemToMonitor.AttributeId); + } + + if (valueRank == ValueRanks.Scalar) + { + errors[ii] = StatusCodes.BadIndexRangeInvalid; + continue; + } + } + + // validate the filter against the node/attribute being monitored. + errors[ii] = ValidateFilter( + metadata, + itemToCreate.ItemToMonitor.AttributeId, + itemToCreate.RequestedParameters.Filter, + out bool rangeRequired); + + if (ServiceResult.IsBad(errors[ii])) + { + continue; + } + + // lookup EU range if required. + Range range = null; + + if (rangeRequired) + { + errors[ii] = ReadEURange(context, node, out range); + + if (ServiceResult.IsBad(errors[ii])) + { + continue; + } + } + + // limit the sampling rate for non-value attributes. + double minimumSamplingInterval = m_defaultMinimumSamplingInterval; + + if (itemToCreate.ItemToMonitor.AttributeId == Attributes.Value) + { + // use the MinimumSamplingInterval attribute to limit the sampling rate for value attributes. + + if (node is IVariable variableNode) + { + minimumSamplingInterval = variableNode.MinimumSamplingInterval; + + // use the default if the node does not specify one. + if (minimumSamplingInterval < 0) + { + minimumSamplingInterval = m_defaultMinimumSamplingInterval; + } + } + } + + // Allocate the monitored item id + uint monitoredItemId; + do + { + monitoredItemId = monitoredItemIdFactory.GetNextId(); + } while (!m_monitoredItems.TryAdd(monitoredItemId, null)); + + // create monitored item. + ISampledDataChangeMonitoredItem monitoredItem = + m_samplingGroupManager.CreateMonitoredItem( + context, + subscriptionId, + publishingInterval, + timestampsToReturn, + monitoredItemId, + node, + itemToCreate, + range, + minimumSamplingInterval, + createDurable); + + // final check for initial value + ServiceResult error = ReadInitialValue(context, node, monitoredItem); + if (ServiceResult.IsBad(error) && + (error.StatusCode == StatusCodes.BadAttributeIdInvalid || + error.StatusCode == StatusCodes.BadDataEncodingInvalid || + error.StatusCode == StatusCodes.BadDataEncodingUnsupported)) + { + errors[ii] = error; + continue; + } + + // now save monitored item. + Debug.Assert(m_monitoredItems[monitoredItemId] == null); + m_monitoredItems[monitoredItemId] = monitoredItem; + + // update monitored item list. + monitoredItems[ii] = monitoredItem; + + // errors updating the monitoring groups will be reported in notifications. + errors[ii] = StatusCodes.Good; + } + } + + // update all groups with any new items. + m_samplingGroupManager.ApplyChanges(); + } + + /// + /// Restore a set of monitored items after a restart. + /// + /// is null. + /// + public 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"); + } + + lock (DataLock) + { + for (int ii = 0; ii < itemsToRestore.Count; ii++) + { + IStoredMonitoredItem item = itemsToRestore[ii]; + + // skip items that have already been processed. + if (item.IsRestored) + { + continue; + } + + // look up the node. + ILocalNode node = GetLocalNode(item.NodeId); + + if (node == null) + { + continue; + } + + // owned by this node manager. + item.IsRestored = true; + + // create monitored item. + ISampledDataChangeMonitoredItem monitoredItem = m_samplingGroupManager + .RestoreMonitoredItem( + node, + item, + savedOwnerIdentity); + + // save monitored item. + m_monitoredItems.Add(monitoredItem.Id, monitoredItem); + + // update monitored item list. + monitoredItems[ii] = monitoredItem; + } + } + + // update all groups with any new items. + m_samplingGroupManager.ApplyChanges(); + } + + /// + /// Reads the initial value for a monitored item. + /// + /// The context. + /// The node to read. + /// The monitored item. + protected virtual ServiceResult ReadInitialValue( + OperationContext context, + ILocalNode node, + IDataChangeMonitoredItem2 monitoredItem) + { + var initialValue = new DataValue + { + Value = null, + ServerTimestamp = DateTime.UtcNow, + SourceTimestamp = DateTime.MinValue, + StatusCode = StatusCodes.BadWaitingForInitialData + }; + + ServiceResult error = node.Read(context, monitoredItem.AttributeId, initialValue); + + if (ServiceResult.IsBad(error)) + { + initialValue.Value = null; + initialValue.StatusCode = error.StatusCode; + } + + monitoredItem.QueueValue(initialValue, error, true); + + return error; + } + + /// + /// Modifies a set of monitored items. + /// + /// is null. + public void ModifyMonitoredItems( + OperationContext context, + TimestampsToReturn timestampsToReturn, + IList monitoredItems, + IList itemsToModify, + IList errors, + IList filterErrors) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (monitoredItems == null) + { + throw new ArgumentNullException(nameof(monitoredItems)); + } + + if (itemsToModify == null) + { + throw new ArgumentNullException(nameof(itemsToModify)); + } + + if (errors == null) + { + throw new ArgumentNullException(nameof(errors)); + } + + lock (DataLock) + { + for (int ii = 0; ii < errors.Count; ii++) + { + MonitoredItemModifyRequest itemToModify = itemsToModify[ii]; + + // skip items that have already been processed. + if (itemToModify.Processed || monitoredItems[ii] == null) + { + continue; + } + + // check if the node manager created the item. + if (!ReferenceEquals(this, monitoredItems[ii].NodeManager)) + { + continue; + } + + // owned by this node manager. + itemToModify.Processed = true; + + // validate monitored item. + + if (!m_monitoredItems.TryGetValue( + monitoredItems[ii].Id, + out ISampledDataChangeMonitoredItem monitoredItem)) + { + errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; + continue; + } + + if (!ReferenceEquals(monitoredItem, monitoredItems[ii])) + { + errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; + continue; + } + + // find the node being monitored. + + if (monitoredItem.ManagerHandle is not ILocalNode node) + { + errors[ii] = StatusCodes.BadNodeIdUnknown; + continue; + } + + // fetch the metadata for the node. + NodeMetadata metadata = GetNodeMetadata( + context, + monitoredItem.ManagerHandle, + BrowseResultMask.All); + + // validate the filter against the node/attribute being monitored. + errors[ii] = ValidateFilter( + metadata, + monitoredItem.AttributeId, + itemToModify.RequestedParameters.Filter, + out bool rangeRequired); + + if (ServiceResult.IsBad(errors[ii])) + { + continue; + } + + // lookup EU range if required. + Range range = null; + + if (rangeRequired) + { + // look up EU range. + errors[ii] = ReadEURange(context, node, out range); + + if (ServiceResult.IsBad(errors[ii])) + { + continue; + } + } + + // update sampling. + errors[ii] = m_samplingGroupManager.ModifyMonitoredItem( + context, + timestampsToReturn, + monitoredItem, + itemToModify, + range); + + // state of item did not change if an error returned here. + if (ServiceResult.IsBad(errors[ii])) + { + continue; + } + + // item has been modified successfully. + // errors updating the sampling groups will be reported in notifications. + errors[ii] = StatusCodes.Good; + } + } + + // update all sampling groups. + m_samplingGroupManager.ApplyChanges(); + } + + /// + /// Deletes a set of monitored items. + /// + /// is null. + public void DeleteMonitoredItems( + OperationContext context, + IList monitoredItems, + IList processedItems, + IList errors) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (monitoredItems == null) + { + throw new ArgumentNullException(nameof(monitoredItems)); + } + + if (errors == null) + { + throw new ArgumentNullException(nameof(errors)); + } + + lock (DataLock) + { + for (int ii = 0; ii < errors.Count; ii++) + { + // skip items that have already been processed. + if (processedItems[ii] || monitoredItems[ii] == null) + { + continue; + } + + // check if the node manager created the item. + if (!ReferenceEquals(this, monitoredItems[ii].NodeManager)) + { + continue; + } + + // owned by this node manager. + processedItems[ii] = true; + + // validate monitored item. + + if (!m_monitoredItems.TryGetValue( + monitoredItems[ii].Id, + out ISampledDataChangeMonitoredItem monitoredItem)) + { + errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; + continue; + } + + if (!ReferenceEquals(monitoredItem, monitoredItems[ii])) + { + errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; + continue; + } + + // remove item. + m_samplingGroupManager.StopMonitoring(monitoredItem); + + // remove association with the group. + m_monitoredItems.Remove(monitoredItem.Id); + + // delete successful. + errors[ii] = StatusCodes.Good; + } + } + + // remove all items from groups. + m_samplingGroupManager.ApplyChanges(); + } + + /// + /// 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 set of processed items. + /// Any errors. + /// is null. + public virtual void TransferMonitoredItems( + OperationContext context, + bool sendInitialValues, + IList monitoredItems, + IList processedItems, + IList errors) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (monitoredItems == null) + { + throw new ArgumentNullException(nameof(monitoredItems)); + } + + if (processedItems == null) + { + throw new ArgumentNullException(nameof(processedItems)); + } + + lock (DataLock) + { + for (int ii = 0; ii < monitoredItems.Count; ii++) + { + // skip items that have already been processed. + if (processedItems[ii] || monitoredItems[ii] == null) + { + continue; + } + + // check if the node manager created the item. + if (!ReferenceEquals(this, monitoredItems[ii].NodeManager)) + { + continue; + } + + // owned by this node manager. + processedItems[ii] = true; + + // validate monitored item. + IMonitoredItem monitoredItem = monitoredItems[ii]; + + // find the node being monitored. + if (monitoredItem.ManagerHandle is not ILocalNode node) + { + continue; + } + + if (sendInitialValues) + { + monitoredItem.SetupResendDataTrigger(); + } + + errors[ii] = StatusCodes.Good; + } + } + } + + /// + /// Changes the monitoring mode for a set of monitored items. + /// + /// is null. + public void SetMonitoringMode( + OperationContext context, + MonitoringMode monitoringMode, + IList monitoredItems, + IList processedItems, + IList errors) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (monitoredItems == null) + { + throw new ArgumentNullException(nameof(monitoredItems)); + } + + if (errors == null) + { + throw new ArgumentNullException(nameof(errors)); + } + + lock (DataLock) + { + for (int ii = 0; ii < errors.Count; ii++) + { + // skip items that have already been processed. + if (processedItems[ii] || monitoredItems[ii] == null) + { + continue; + } + + // check if the node manager created the item. + if (!ReferenceEquals(this, monitoredItems[ii].NodeManager)) + { + continue; + } + + // owned by this node manager. + processedItems[ii] = true; + + // validate monitored item. + + if (!m_monitoredItems.TryGetValue( + monitoredItems[ii].Id, + out ISampledDataChangeMonitoredItem monitoredItem)) + { + errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; + continue; + } + + if (!ReferenceEquals(monitoredItem, monitoredItems[ii])) + { + errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; + continue; + } + + // update monitoring mode. + MonitoringMode previousMode = monitoredItem.SetMonitoringMode(monitoringMode); + + // need to provide an immediate update after enabling. + if (previousMode == MonitoringMode.Disabled && + monitoringMode != MonitoringMode.Disabled) + { + var initialValue = new DataValue + { + ServerTimestamp = DateTime.UtcNow, + StatusCode = StatusCodes.BadWaitingForInitialData + }; + + // read the initial value. + + if (monitoredItem.ManagerHandle is Node node) + { + ServiceResult error = node.Read( + context, + monitoredItem.AttributeId, + initialValue); + + if (ServiceResult.IsBad(error)) + { + initialValue.Value = null; + initialValue.StatusCode = error.StatusCode; + } + } + + monitoredItem.QueueValue(initialValue, null); + } + + // modify the item attributes. + m_samplingGroupManager.ModifyMonitoring(context, monitoredItem); + + // item has been modified successfully. + // errors updating the sampling groups will be reported in notifications. + errors[ii] = StatusCodes.Good; + } + } + + // update all sampling groups. + m_samplingGroupManager.ApplyChanges(); + } + + /// + /// Returns true if the node class matches the node class mask. + /// + public static bool CheckNodeClassMask(uint nodeClassMask, NodeClass nodeClass) + { + if (nodeClassMask != 0) + { + return ((uint)nodeClass & nodeClassMask) != 0; + } + + return true; + } + + /// + /// The server that the node manager belongs to. + /// + protected IServerInternal Server { get; } + + /// + /// Returns an index for the NamespaceURI (Adds it to the server namespace table if it does not already exist). + /// + /// + /// Returns the server's default index (1) if the namespaceUri is empty or null. + /// + public ushort GetNamespaceIndex(string namespaceUri) + { + int namespaceIndex = 1; + + if (!string.IsNullOrEmpty(namespaceUri)) + { + namespaceIndex = Server.NamespaceUris.GetIndex(namespaceUri); + + if (namespaceIndex == -1) + { + namespaceIndex = Server.NamespaceUris.Append(namespaceUri); + } + } + + return (ushort)namespaceIndex; + } + + /// + /// Returns all targets of the specified reference. + /// + /// is null. + public NodeIdCollection FindLocalNodes( + NodeId sourceId, + NodeId referenceTypeId, + bool isInverse) + { + if (sourceId.IsNull) + { + throw new ArgumentNullException(nameof(sourceId)); + } + + if (referenceTypeId.IsNull) + { + throw new ArgumentNullException(nameof(referenceTypeId)); + } + + lock (DataLock) + { + if (GetManagerHandle(sourceId) is not ILocalNode source) + { + return null; + } + + var targets = new NodeIdCollection(); + + foreach (IReference reference in source.References) + { + if (reference.IsInverse != isInverse || + !Server.TypeTree.IsTypeOf(reference.ReferenceTypeId, referenceTypeId)) + { + continue; + } + + ExpandedNodeId targetId = reference.TargetId; + + if (targetId.IsAbsolute) + { + continue; + } + + targets.Add((NodeId)targetId); + } + + return targets; + } + } + + /// + /// Returns the id the first node with the specified browse name if it exists. null otherwise + /// + /// is null. + public NodeId FindTargetId( + NodeId sourceId, + NodeId referenceTypeId, + bool isInverse, + QualifiedName browseName) + { + if (sourceId.IsNull) + { + throw new ArgumentNullException(nameof(sourceId)); + } + + if (referenceTypeId.IsNull) + { + throw new ArgumentNullException(nameof(referenceTypeId)); + } + + lock (DataLock) + { + if (GetManagerHandle(sourceId) is not ILocalNode source) + { + return default; + } + + foreach (ReferenceNode reference in source.References.OfType()) + { + if (reference.IsInverse != isInverse || + !Server.TypeTree.IsTypeOf(reference.ReferenceTypeId, referenceTypeId)) + { + continue; + } + + ExpandedNodeId targetId = reference.TargetId; + + if (targetId.IsAbsolute) + { + continue; + } + + if (GetManagerHandle((NodeId)targetId) is not ILocalNode target) + { + continue; + } + + if (browseName.IsNull || target.BrowseName == browseName) + { + return (NodeId)targetId; + } + } + + return default; + } + } + + /// + /// Returns the first target that matches the browse path. + /// + public NodeId Find(NodeId sourceId, string browsePath) + { + IList targets = TranslateBrowsePath(sourceId, browsePath); + + if (targets.Count > 0) + { + return targets[0]; + } + + return default; + } + + /// + /// Returns a list of targets the match the browse path. + /// + public IList TranslateBrowsePath( + OperationContext context, + NodeId sourceId, + string browsePath) + { + return TranslateBrowsePath( + context, + sourceId, + RelativePath.Parse(browsePath, Server.TypeTree)); + } + + /// + /// Returns a list of targets the match the browse path. + /// + public IList TranslateBrowsePath(NodeId sourceId, string browsePath) + { + return TranslateBrowsePath( + null, + sourceId, + RelativePath.Parse(browsePath, Server.TypeTree)); + } + + /// + /// Returns a list of targets the match the browse path. + /// + public IList TranslateBrowsePath(NodeId sourceId, RelativePath relativePath) + { + return TranslateBrowsePath(null, sourceId, relativePath); + } + + /// + /// Returns a list of targets the match the browse path. + /// + public IList TranslateBrowsePath( + OperationContext context, + NodeId sourceId, + RelativePath relativePath) + { + var targets = new List(); + + if (relativePath == null || relativePath.Elements.Count == 0) + { + targets.Add(sourceId); + return targets; + } + + lock (DataLock) + { + // look up source in this node manager. + ILocalNode source = GetLocalNode(sourceId); + if (source == null) + { + return targets; + } + } + + // return the set of matching targets. + return targets; + } + + /// + /// Registers a source for a node. + /// + /// + /// The source could be one or more of IDataSource, IEventSource, ICallable, IHistorian or IViewManager + /// + /// is null. + public void RegisterSource(NodeId nodeId, object source, object handle, bool isEventSource) + { + if (nodeId.IsNull) + { + throw new ArgumentNullException(nameof(nodeId)); + } + + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + } + + /// + /// Called when the source is no longer used. + /// + /// + /// When a source disappears it must either delete all of its nodes from the address space + /// or unregister itself their source by calling RegisterSource with source == null. + /// After doing that the source must call this method. + /// + public void UnregisterSource(object source) + { + } + + /// + /// Applys the modelling rules to any existing instance. + /// + /// is null. + public void ApplyModellingRules( + ILocalNode instance, + ILocalNode typeDefinition, + ILocalNode templateDeclaration, + ushort namespaceIndex) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + if (typeDefinition == null) + { + throw new ArgumentNullException(nameof(typeDefinition)); + } + + // check existing type definition. + UpdateTypeDefinition(instance, typeDefinition.NodeId); + + // create list of declarations for the type definition (recursively collects definitions from supertypes). + var declarations = new List(); + BuildDeclarationList(typeDefinition, declarations); + + // add instance declaration if provided. + if (templateDeclaration != null) + { + var declaration = new DeclarationNode + { + Node = templateDeclaration, + BrowsePath = string.Empty + }; + + declarations.Add(declaration); + + BuildDeclarationList(templateDeclaration, declarations); + } + + // build list of instances to create. + var typeDefinitions = new List(); + var instanceDeclarations = new SortedDictionary(); + var possibleTargets = new SortedDictionary(); + + // create instances from declarations. + // subtypes appear in list last so traversing the list backwards find the overridden nodes first. + for (int ii = declarations.Count - 1; ii >= 0; ii--) + { + DeclarationNode declaration = declarations[ii]; + + // update type definition list. + if (string.IsNullOrEmpty(declaration.BrowsePath)) + { + typeDefinitions.Add(declaration.Node); + continue; + } + + // skip declaration if instance already exists. + // (i.e. the declaration was overridden). + if (instanceDeclarations.ContainsKey(declaration.BrowsePath)) + { + continue; + } + + // update instance declaration list. + instanceDeclarations[declaration.BrowsePath] = declaration.Node; + + // save the node as a possible target of references. + possibleTargets[declaration.Node.NodeId] = declaration.Node; + } + + // build list of instances that already exist. + var existingInstances = new SortedDictionary(); + BuildInstanceList(instance, string.Empty, existingInstances); + + // maps the instance declaration onto an instance node. + var instancesToCreate = new Dictionary(); + + // apply modelling rules to instance declarations. + foreach (KeyValuePair current in instanceDeclarations) + { + string browsePath = current.Key; + ILocalNode instanceDeclaration = current.Value; + + // check if the same instance has multiple browse paths to it. + if (instancesToCreate.TryGetValue(instanceDeclaration.NodeId, out _)) + { + continue; + } + + // check for an existing instance. + if (existingInstances.TryGetValue(browsePath, out ILocalNode newInstance)) + { + continue; + } + + // apply modelling rule to determine whether to create a new instance. + NodeId modellingRule = instanceDeclaration.ModellingRule; + + // always create a new instance if one does not already exist. + if (modellingRule == Objects.ModellingRule_Mandatory) + { + if (newInstance == null) + { + newInstance = instanceDeclaration.CreateCopy(CreateUniqueNodeId()); + AddNode(newInstance); + } + } + // ignore optional instances unless one has been specified in the existing tree. + else if (modellingRule == Objects.ModellingRule_Optional) + { + if (newInstance == null) + { + continue; + } + } + // ignore any unknown modelling rules. + else + { + continue; + } + + // save the mapping between the instance declaration and the new instance. + instancesToCreate[instanceDeclaration.NodeId] = newInstance; + } + + // add references from type definitions to top level. + foreach (ILocalNode type in typeDefinitions) + { + foreach (IReference reference in type.References) + { + // ignore external references from type. + if (reference.TargetId.IsAbsolute) + { + continue; + } + + // ignore subtype references. + if (m_nodes.TypeTree + .IsTypeOf(reference.ReferenceTypeId, ReferenceTypeIds.HasSubtype)) + { + continue; + } + + // ignore targets that are not in the instance tree. + if (!instancesToCreate.TryGetValue( + (NodeId)reference.TargetId, + out ILocalNode target)) + { + continue; + } + + // add forward and backward reference. + AddReference( + instance, + reference.ReferenceTypeId, + reference.IsInverse, + target, + true); + } + } + + // add references between instance declarations. + foreach (ILocalNode instanceDeclaration in instanceDeclarations.Values) + { + // find the source for the references. + + if (!instancesToCreate.TryGetValue( + instanceDeclaration.NodeId, + out ILocalNode source)) + { + continue; + } + + // check if the source is a shared node. + bool sharedNode = ReferenceEquals(instanceDeclaration, source); + + foreach (IReference reference in instanceDeclaration.References) + { + // add external reference. + if (reference.TargetId.IsAbsolute) + { + if (!sharedNode) + { + AddReference( + source, + reference.ReferenceTypeId, + reference.IsInverse, + reference.TargetId); + } + + continue; + } + + // check for modelling rule. + if (reference.ReferenceTypeId == ReferenceTypeIds.HasModellingRule) + { + if (!source.References.Exists( + ReferenceTypeIds.HasModellingRule, + false, + reference.TargetId, + false, + null)) + { + AddReference( + source, + reference.ReferenceTypeId, + false, + reference.TargetId); + } + + continue; + } + + // check for type definition. + if (reference.ReferenceTypeId == ReferenceTypeIds.HasTypeDefinition) + { + if (!sharedNode) + { + UpdateTypeDefinition(source, instanceDeclaration.TypeDefinitionId); + } + + continue; + } + + // add targets that are not in the instance tree. + if (!instancesToCreate.TryGetValue( + (NodeId)reference.TargetId, + out ILocalNode target)) + { + // don't update shared nodes because the reference should already exist. + if (sharedNode) + { + continue; + } + + // top level references to the type definition node were already added. + if (reference.TargetId == typeDefinition.NodeId) + { + continue; + } + + // see if a reference is allowed. + if (!IsExternalReferenceAllowed(reference.ReferenceTypeId)) + { + continue; + } + + // add one way reference. + source.References.Add( + reference.ReferenceTypeId, + reference.IsInverse, + reference.TargetId); + continue; + } + + // add forward and backward reference. + AddReference( + source, + reference.ReferenceTypeId, + reference.IsInverse, + target, + true); + } + } + } + + /// + /// Returns true if a one-way reference to external nodes is permitted. + /// + private bool IsExternalReferenceAllowed(NodeId referenceTypeId) + { + // always exclude hierarchial references. + if (m_nodes.TypeTree.IsTypeOf(referenceTypeId, ReferenceTypeIds.HierarchicalReferences)) + { + return false; + } + + // allow one way reference to event. + if (m_nodes.TypeTree.IsTypeOf(referenceTypeId, ReferenceTypes.GeneratesEvent)) + { + return true; + } + + // all other references not permitted. + return false; + } + + /// + /// Updates the type definition for a node. + /// + /// + private void UpdateTypeDefinition(ILocalNode instance, ExpandedNodeId typeDefinitionId) + { + // check existing type definition. + ExpandedNodeId existingTypeId = instance.TypeDefinitionId; + + if (existingTypeId == typeDefinitionId) + { + return; + } + + if (!existingTypeId.IsNull) + { + if (m_nodes.TypeTree.IsTypeOf(existingTypeId, typeDefinitionId)) + { + throw ServiceResultException.Create( + StatusCodes.BadTypeDefinitionInvalid, + "Type definition {0} is not a subtype of the existing type definition {1}.", + existingTypeId, + typeDefinitionId); + } + + DeleteReference( + instance, + ReferenceTypeIds.HasTypeDefinition, + false, + existingTypeId, + false); + } + + AddReference(instance, ReferenceTypeIds.HasTypeDefinition, false, typeDefinitionId); + } + + /// + /// A node in the type system that is used to instantiate objects or variables. + /// + private class DeclarationNode + { + public ILocalNode Node; + public string BrowsePath; + } + + /// + /// Builds the list of declaration nodes for a type definition. + /// + /// is null. + private void BuildDeclarationList( + ILocalNode typeDefinition, + List declarations) + { + if (typeDefinition == null) + { + throw new ArgumentNullException(nameof(typeDefinition)); + } + + if (declarations == null) + { + throw new ArgumentNullException(nameof(declarations)); + } + + // guard against loops (i.e. common grandparents). + for (int ii = 0; ii < declarations.Count; ii++) + { + if (ReferenceEquals(declarations[ii].Node, typeDefinition)) + { + return; + } + } + + // create the root declaration for the type. + var declaration = new DeclarationNode + { + Node = typeDefinition, + BrowsePath = string.Empty + }; + + declarations.Add(declaration); + + // follow references to supertypes first. + foreach ( + IReference reference in typeDefinition.References + .Find(ReferenceTypeIds.HasSubtype, true, false, null)) + { + ILocalNode supertype = GetLocalNode(reference.TargetId); + + if (supertype == null) + { + continue; + } + + BuildDeclarationList(supertype, declarations); + } + + // add children of type. + BuildDeclarationList(declaration, declarations); + } + + /// + /// Builds a list of declarations from the nodes aggregated by a parent. + /// + /// is null. + private void BuildDeclarationList( + DeclarationNode parent, + List declarations) + { + if (parent == null) + { + throw new ArgumentNullException(nameof(parent)); + } + + if (declarations == null) + { + throw new ArgumentNullException(nameof(declarations)); + } + + // get list of children. + foreach ( + IReference reference in parent.Node.References.Find( + ReferenceTypeIds.HierarchicalReferences, + false, + true, + m_nodes.TypeTree)) + { + // do not follow sub-type references. + if (m_nodes.TypeTree + .IsTypeOf(reference.ReferenceTypeId, ReferenceTypeIds.HasSubtype)) + { + continue; + } + + // find child (ignore children that are not in the node table). + ILocalNode child = GetLocalNode(reference.TargetId); + + if (child == null) + { + continue; + } + + // create the declartion node. + var declaration = new DeclarationNode + { + Node = child, + BrowsePath = Utils.Format("{0}.{1}", parent.BrowsePath, child.BrowseName) + }; + + declarations.Add(declaration); + + // recursively include aggregated children. + NodeId modellingRule = child.ModellingRule; + + if (modellingRule == ObjectIds.ModellingRule_Mandatory || + modellingRule == ObjectIds.ModellingRule_Optional) + { + BuildDeclarationList(declaration, declarations); + } + } + } + + /// + /// Builds a table of instances indexed by browse path from the nodes aggregated by a parent + /// + /// is null. + private void BuildInstanceList( + ILocalNode parent, + string browsePath, + IDictionary instances) + { + if (instances == null) + { + throw new ArgumentNullException(nameof(instances)); + } + + // guard against loops. + if (instances.ContainsKey(browsePath)) + { + return; + } + + // index parent by browse path. + instances[browsePath] = parent ?? throw new ArgumentNullException(nameof(parent)); + + // get list of children. + foreach ( + IReference reference in parent.References.Find( + ReferenceTypeIds.HierarchicalReferences, + false, + true, + m_nodes.TypeTree)) + { + // find child (ignore children that are not in the node table). + ILocalNode child = GetLocalNode(reference.TargetId); + + if (child == null) + { + continue; + } + + // recursively include aggregated children. + BuildInstanceList( + child, + Utils.Format("{0}.{1}", browsePath, child.BrowseName), + instances); + } + } + + /// + /// Exports a node to a nodeset. + /// + /// + public void ExportNode(NodeId nodeId, NodeSet nodeSet) + { + lock (DataLock) + { + ILocalNode node = + GetLocalNode(nodeId) + ?? throw ServiceResultException.Create( + StatusCodes.BadNodeIdUnknown, + "NodeId ({0}) does not exist.", + nodeId); + + ExportNode( + node, + nodeSet, + ((int)node.NodeClass & ((int)NodeClass.Object | (int)NodeClass.Variable)) != 0); + } + } + + /// + /// Exports a node to a nodeset. + /// + public void ExportNode(ILocalNode node, NodeSet nodeSet, bool instance) + { + lock (DataLock) + { + // check if the node has already been added. + NodeId exportedId = nodeSet.Export(node.NodeId, m_nodes.NamespaceUris); + + if (nodeSet.Contains(exportedId)) + { + return; + } + + // add to nodeset. + Node nodeToExport = nodeSet.Add(node, m_nodes.NamespaceUris, m_nodes.ServerUris); + + // follow children. + foreach (ReferenceNode reference in node.References.OfType()) + { + // export all references. + bool export = true; + + // unless it is a subtype reference. + if (Server.TypeTree + .IsTypeOf(reference.ReferenceTypeId, ReferenceTypeIds.HasSubtype)) + { + export = false; + } + + if (export) + { + nodeSet.AddReference( + nodeToExport, + reference, + m_nodes.NamespaceUris, + m_nodes.ServerUris); + } + + if (reference.IsInverse || + Server.TypeTree + .IsTypeOf(reference.ReferenceTypeId, ReferenceTypeIds.HasSubtype)) + { + nodeSet.AddReference( + nodeToExport, + reference, + m_nodes.NamespaceUris, + m_nodes.ServerUris); + } + + if (Server.TypeTree + .IsTypeOf(reference.ReferenceTypeId, ReferenceTypeIds.Aggregates)) + { + if (reference.IsInverse) + { + continue; + } + + ILocalNode child = GetLocalNode(reference.TargetId); + + if (child != null) + { + if (instance) + { + NodeId modellingRule = child.ModellingRule; + + if (modellingRule != Objects.ModellingRule_Mandatory) + { + continue; + } + } + + ExportNode(child, nodeSet, instance); + } + } + } + } + } + +#if XXX + /// + /// Changes the type definition for an instance. + /// + public void ChangeTypeDefinition(NodeId instanceId, NodeId typeDefinitionId) + { + try + { + m_lock.Enter(); + + // find the instance. + ILocalNode instance = GetLocalNode(instanceId) as ILocalNode; + + if (instance == null) + { + throw ServiceResultException.Create( + StatusCodes.BadNodeIdUnknown, + "NodeId ({0}) does not exist.", + instanceId); + } + + // check node class. + if (instance.NodeClass != NodeClass.Object && instance.NodeClass != NodeClass.Variable) + { + throw ServiceResultException.Create( + StatusCodes.BadNodeClassInvalid, + "Node (NodeClass={0}) cannot have a type definition.", + instance.NodeClass); + } + + // get current type definition. + ExpandedNodeId existingTypeId = instance.TypeDefinitionId; + + if (existingTypeId == typeDefinitionId) + { + return; + } + + // can only change to a subtype of the existing type definition. + if (!m_server.TypeTree.IsTypeOf(typeDefinitionId, existingTypeId)) + { + throw ServiceResultException.Create( + StatusCodes.BadTypeDefinitionInvalid, + "Type definition ({0}) must be a must subtype of the existing type definition ({1}).", + typeDefinitionId, + existingTypeId); + } + + // find the type definition node. + ILocalNode typeDefinition = GetLocalNode(typeDefinitionId) as ILocalNode; + + if (typeDefinition == null) + { + throw ServiceResultException.Create( + StatusCodes.BadTypeDefinitionInvalid, + "TypeDefinitionId ({0}) does not exist.", + typeDefinitionId); + } + + // apply modelling rules. + NodeFactory factory = new NodeFactory(m_nodes); + IList nodesToAdd = factory.ApplyModellingRules( + instance, + typeDefinition.NodeId, + ref m_lastId, + 1); + + // add the nodes. + foreach (Node nodeToAdd in nodesToAdd) + { + AddNode(nodeToAdd); + } + } + finally + { + m_lock.Exit(); + } + } +#endif + + /// + /// Deletes a node from the address sapce. + /// + /// is null. + /// + public void DeleteNode(NodeId nodeId, bool deleteChildren, bool silent) + { + if (nodeId.IsNull) + { + throw new ArgumentNullException(nameof(nodeId)); + } + + // find the node to delete. + + if (GetManagerHandle(nodeId) is not ILocalNode node) + { + if (!silent) + { + throw ServiceResultException.Create( + StatusCodes.BadSourceNodeIdInvalid, + "Node '{NodeId}' does not exist.", + nodeId); + } + + return; + } + + bool instance = ((int)node.NodeClass & + ((int)NodeClass.Object | (int)NodeClass.Variable)) != 0; + + var referencesToDelete = new Dictionary>(); + + if (silent) + { + try + { + DeleteNode(node, deleteChildren, instance, referencesToDelete); + } + catch (Exception e) + { + m_logger.LogError(e, "Error deleting node: {NodeId}", nodeId); + } + } + else + { + DeleteNode(node, deleteChildren, instance, referencesToDelete); + } + + if (referencesToDelete.Count > 0) + { + _ = Task.Run(() => OnDeleteReferencesAsync(referencesToDelete)); + } + } + + /// + /// Deletes a node from the address sapce. + /// + /// is null. + private void DeleteNode( + ILocalNode node, + bool deleteChildren, + bool instance, + Dictionary> referencesToDelete) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + var nodesToDelete = new List(); + var referencesForNode = new List(); + + lock (DataLock) + { + // remove the node. + m_nodes.Remove(node.NodeId); + + // check need to connect subtypes to the supertype if they are being deleted. + ExpandedNodeId supertypeId = Server.TypeTree.FindSuperType(node.NodeId); + + if (!supertypeId.IsNull) + { + Server.TypeTree.Remove(node.NodeId); + } + + // remove any references to the node. + foreach (IReference reference in node.References) + { + // ignore remote references. + if (reference.TargetId.IsAbsolute) + { + continue; + } + + // find the target. + + if (GetManagerHandle(reference.TargetId) is not ILocalNode target) + { + referencesForNode.Add(reference); + continue; + } + + // delete the backward reference. + target.References + .Remove(reference.ReferenceTypeId, !reference.IsInverse, node.NodeId); + + // check for children that need to be deleted. + if (deleteChildren && + Server.TypeTree + .IsTypeOf(reference.ReferenceTypeId, ReferenceTypeIds.Aggregates) && + !reference.IsInverse) + { + nodesToDelete.Add(target); + } + } + + if (referencesForNode.Count > 0) + { + referencesToDelete[node.NodeId] = referencesForNode; + } + } + + // delete the child nodes. + foreach (ILocalNode nodeToDelete in nodesToDelete) + { + DeleteNode(nodeToDelete, deleteChildren, instance, referencesToDelete); + } + } + + /// + /// Deletes the external references to a node in a background thread. + /// + private async ValueTask OnDeleteReferencesAsync(Dictionary> referencesToDelete) + { + foreach (KeyValuePair> current in referencesToDelete) + { + try + { + await Server.NodeManager.DeleteReferencesAsync(current.Key, current.Value) + .ConfigureAwait(false); + } + catch (Exception e) + { + m_logger.LogError(e, "Error deleting references for node: {NodeId}", current.Key); + } + } + } + + /// + /// Verifies that the source and the target meet the restrictions imposed by the reference type. + /// + /// + private void ValidateReference( + ILocalNode source, + NodeId referenceTypeId, + bool isInverse, + NodeClass targetNodeClass) + { + // find reference type. + if (GetLocalNode(referenceTypeId) is not IReferenceType) + { + throw ServiceResultException.Create( + StatusCodes.BadReferenceTypeIdInvalid, + "Reference type '{0}' does not exist.", + referenceTypeId); + } + + // swap the source and target for inverse references. + NodeClass sourceNodeClass = source.NodeClass; + + if (isInverse) + { + sourceNodeClass = targetNodeClass; + targetNodeClass = source.NodeClass; + } + + // check HasComponent references. + if (Server.TypeTree.IsTypeOf(referenceTypeId, ReferenceTypeIds.HasComponent)) + { + if (( + (int)sourceNodeClass & + ( + (int)NodeClass.Object | + (int)NodeClass.Variable | + (int)NodeClass.ObjectType | + (int)NodeClass.VariableType) + ) == 0) + { + throw ServiceResultException.Create( + StatusCodes.BadReferenceNotAllowed, + "Source node cannot be used with HasComponent references."); + } + + if (((int)targetNodeClass & + ((int)NodeClass.Object | + (int)NodeClass.Variable | + (int)NodeClass.Method)) == + 0) + { + throw ServiceResultException.Create( + StatusCodes.BadReferenceNotAllowed, + "Target node cannot be used with HasComponent references."); + } + + if (targetNodeClass == NodeClass.Variable && + ((int)targetNodeClass & + ((int)NodeClass.Variable | (int)NodeClass.VariableType)) == 0) + { + throw ServiceResultException.Create( + StatusCodes.BadReferenceNotAllowed, + "A Variable must be a component of an Variable or VariableType."); + } + + if (targetNodeClass == NodeClass.Method && + ((int)sourceNodeClass & + ((int)NodeClass.Object | (int)NodeClass.ObjectType)) == 0) + { + throw ServiceResultException.Create( + StatusCodes.BadReferenceNotAllowed, + "A Method must be a component of an Object or ObjectType."); + } + } + + // check HasProperty references. + if (Server.TypeTree.IsTypeOf(referenceTypeId, ReferenceTypes.HasProperty) && + targetNodeClass != NodeClass.Variable) + { + throw ServiceResultException.Create( + StatusCodes.BadReferenceNotAllowed, + "Targets of HasProperty references must be Variables."); + } + + // check HasSubtype references. + if (Server.TypeTree.IsTypeOf(referenceTypeId, ReferenceTypeIds.HasSubtype)) + { + if (( + (int)sourceNodeClass & + ( + (int)NodeClass.DataType | + (int)NodeClass.ReferenceType | + (int)NodeClass.ObjectType | + (int)NodeClass.VariableType) + ) == 0) + { + throw ServiceResultException.Create( + StatusCodes.BadReferenceNotAllowed, + "Source node cannot be used with HasSubtype references."); + } + + if (targetNodeClass != sourceNodeClass) + { + throw ServiceResultException.Create( + StatusCodes.BadReferenceNotAllowed, + "The source and target cannot be connected by a HasSubtype reference."); + } + } + + // TBD - check rules for other reference types. + } + + /// + /// Adds a reference between two existing nodes. + /// + public ServiceResult AddReference( + NodeId sourceId, + NodeId referenceTypeId, + bool isInverse, + NodeId targetId, + bool bidirectional) + { + lock (DataLock) + { + // find source. + if (GetManagerHandle(sourceId) is not ILocalNode source) + { + return StatusCodes.BadParentNodeIdInvalid; + } + + // add reference from target to source. + if (bidirectional) + { + // find target. + if (GetManagerHandle(targetId) is not ILocalNode target) + { + return StatusCodes.BadNodeIdUnknown; + } + + // ensure the reference is valid. + ValidateReference(source, referenceTypeId, isInverse, target.NodeClass); + + // add reference from target to source. + AddReferenceToLocalNode(target, referenceTypeId, !isInverse, sourceId, false); + } + + // add reference from source to target. + AddReferenceToLocalNode(source, referenceTypeId, isInverse, targetId, false); + + return null; + } + } + + /// + /// Ensures any changes to built-in nodes are reflected in the diagnostics node manager. + /// + private void AddReferenceToLocalNode( + ILocalNode source, + NodeId referenceTypeId, + bool isInverse, + ExpandedNodeId targetId, + bool isInternal) + { + source.References.Add(referenceTypeId, isInverse, targetId); + + if (!isInternal && source.NodeId.NamespaceIndex == 0) + { + lock (Server.DiagnosticsLock) + { + NodeState state = Server.DiagnosticsNodeManager + .FindPredefinedNode(source.NodeId); + + if (state != null) + { + INodeBrowser browser = state.CreateBrowser( + Server.DefaultSystemContext, + null, + referenceTypeId, + true, + isInverse ? BrowseDirection.Inverse : BrowseDirection.Forward, + default, + null, + true); + + bool found = false; + + for (IReference reference = browser.Next(); + reference != null; + reference = browser.Next()) + { + if (reference.TargetId == targetId) + { + found = true; + break; + } + } + + if (!found) + { + state.AddReference(referenceTypeId, isInverse, targetId); + } + } + } + } + } + + /// + /// Adds a reference between two existing nodes. + /// + /// + public void CreateReference( + NodeId sourceId, + NodeId referenceTypeId, + bool isInverse, + NodeId targetId, + bool bidirectional) + { + lock (DataLock) + { + ServiceResult result = AddReference( + sourceId, + referenceTypeId, + isInverse, + targetId, + bidirectional); + + if (ServiceResult.IsBad(result)) + { + throw new ServiceResultException(result); + } + } + } + + /// + /// Adds a reference to the address space. + /// + private void AddReference( + ILocalNode source, + NodeId referenceTypeId, + bool isInverse, + ILocalNode target, + bool bidirectional) + { + AddReferenceToLocalNode(source, referenceTypeId, isInverse, target.NodeId, false); + + if (bidirectional) + { + AddReferenceToLocalNode(target, referenceTypeId, !isInverse, source.NodeId, false); + } + } + + /// + /// Adds a reference to the address space. + /// + private void AddReference( + ILocalNode source, + NodeId referenceTypeId, + bool isInverse, + ExpandedNodeId targetId) + { + AddReferenceToLocalNode(source, referenceTypeId, isInverse, targetId, false); + } + + /// + /// Deletes a reference. + /// + /// is null. + public ServiceResult DeleteReference( + object sourceHandle, + NodeId referenceTypeId, + bool isInverse, + ExpandedNodeId targetId, + bool deleteBidirectional) + { + if (sourceHandle == null) + { + throw new ArgumentNullException(nameof(sourceHandle)); + } + + if (referenceTypeId.IsNull) + { + throw new ArgumentNullException(nameof(referenceTypeId)); + } + + if (targetId.IsNull) + { + throw new ArgumentNullException(nameof(targetId)); + } + + lock (DataLock) + { + if (sourceHandle is not ILocalNode source) + { + return StatusCodes.BadSourceNodeIdInvalid; + } + + source.References.Remove(referenceTypeId, isInverse, targetId); + + if (deleteBidirectional) + { + var target = GetManagerHandle(targetId) as ILocalNode; + + target?.References.Remove(referenceTypeId, !isInverse, source.NodeId); + } + + return ServiceResult.Good; + } + } + + /// + /// Deletes a reference. + /// + /// + public void DeleteReference( + NodeId sourceId, + NodeId referenceTypeId, + bool isInverse, + ExpandedNodeId targetId, + bool deleteBidirectional) + { + ServiceResult result = DeleteReference( + GetManagerHandle(sourceId) as ILocalNode, + referenceTypeId, + isInverse, + targetId, + deleteBidirectional); + + if (ServiceResult.IsBad(result)) + { + throw new ServiceResultException(result); + } + } + + /// + /// Adds a node to the address space. + /// + private void AddNode(ILocalNode node) + { + m_nodes.Attach(node); + } + + /// + /// Returns a node managed by the manager with the specified node id. + /// + public ILocalNode GetLocalNode(ExpandedNodeId nodeId) + { + if (nodeId.IsNull) + { + return null; + } + + // check for absolute declarations of local nodes. + if (nodeId.IsAbsolute) + { + if (nodeId.ServerIndex != 0) + { + return null; + } + + int namespaceIndex = Server.NamespaceUris.GetIndex(nodeId.NamespaceUri); + + if (namespaceIndex < 0 || nodeId.NamespaceIndex >= Server.NamespaceUris.Count) + { + return null; + } + + return GetLocalNode(nodeId.InnerNodeId.WithNamespaceIndex((ushort)namespaceIndex)); + } + + return GetLocalNode((NodeId)nodeId); + } + + /// + /// Returns a node managed by the manager with the specified node id. + /// + public ILocalNode GetLocalNode( + NodeId nodeId, + NodeId referenceTypeId, + bool isInverse, + bool includeSubtypes, + QualifiedName browseName) + { + lock (DataLock) + { + return m_nodes.Find( + nodeId, + referenceTypeId, + isInverse, + includeSubtypes, + browseName) as ILocalNode; + } + } + + /// + /// Returns a node managed by the manager with the specified node id. + /// + public ILocalNode GetLocalNode(NodeId nodeId) + { + lock (DataLock) + { + return m_nodes.Find(nodeId) as ILocalNode; + } + } + + /// + /// Returns a list of nodes which are targets of the specified references. + /// + public IList GetLocalNodes( + NodeId sourceId, + NodeId referenceTypeId, + bool isInverse, + bool includeSubtypes) + { + lock (DataLock) + { + var targets = new List(); + + ILocalNode source = GetLocalNode(sourceId); + + if (source == null) + { + return targets; + } + + foreach ( + IReference reference in source.References + .Find(referenceTypeId, isInverse, true, m_nodes.TypeTree)) + { + ILocalNode target = GetLocalNode(reference.TargetId); + + if (target != null) + { + targets.Add(target); + } + } + + return targets; + } + } + + /// + /// Returns a node managed by the manager that has the specified browse name. + /// + public ILocalNode GetTargetNode( + NodeId sourceId, + NodeId referenceTypeId, + bool isInverse, + bool includeSubtypes, + QualifiedName browseName) + { + lock (DataLock) + { + ILocalNode source = GetLocalNode(sourceId); + + if (source == null) + { + return null; + } + + return GetTargetNode( + source, + referenceTypeId, + isInverse, + includeSubtypes, + browseName); + } + } + + /// + /// Returns a node managed by the manager that has the specified browse name. + /// + private ILocalNode GetTargetNode( + ILocalNode source, + NodeId referenceTypeId, + bool isInverse, + bool includeSubtypes, + QualifiedName browseName) + { + foreach ( + IReference reference in source.References.Find( + referenceTypeId, + isInverse, + includeSubtypes, + Server.TypeTree)) + { + ILocalNode target = GetLocalNode(reference.TargetId); + + if (target == null) + { + continue; + } + + if (browseName.IsNull || browseName == target.BrowseName) + { + return target; + } + } + + return null; + } + + /// + /// Attaches a node to the address space. + /// + public void AttachNode(ILocalNode node) + { + AttachNode(node, false); + } + + /// + /// Attaches a node to the address space. + /// + /// is null. + /// + private void AttachNode(ILocalNode node, bool isInternal) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + lock (DataLock) + { + // check if node exists. + if (m_nodes.Exists(node.NodeId)) + { + throw ServiceResultException.Create( + StatusCodes.BadNodeIdExists, + "A node with the same node id already exists: {0}", + node.NodeId); + } + + // ensure reverse references exist. + foreach (IReference reference in node.References) + { + // ignore references that are always one way. + if (reference.ReferenceTypeId == ReferenceTypeIds.HasTypeDefinition || + reference.ReferenceTypeId == ReferenceTypeIds.HasModellingRule) + { + continue; + } + + // find target. + ILocalNode target = GetLocalNode(reference.TargetId); + + if (target != null) + { + AddReferenceToLocalNode( + target, + reference.ReferenceTypeId, + !reference.IsInverse, + node.NodeId, + isInternal); + } + } + + // must generate a model change event. + AddNode(node); + } + } + + /// + /// Creates a unique node identifier. + /// + public NodeId CreateUniqueNodeId() + { + return CreateUniqueNodeId(m_dynamicNamespaceIndex); + } + + /// + private object GetManagerHandle(ExpandedNodeId nodeId) + { + lock (DataLock) + { + if (nodeId.IsNull || nodeId.IsAbsolute) + { + return null; + } + + return GetLocalNode(nodeId); + } + } + + /// + /// Reads the EU Range for a variable. + /// + private ServiceResult ReadEURange( + OperationContext context, + ILocalNode node, + out Range range) + { + range = null; + + if (GetTargetNode( + node, + ReferenceTypes.HasProperty, + false, + true, + QualifiedName.From(BrowseNames.EURange)) is not IVariable target) + { + return StatusCodes.BadNodeIdUnknown; + } + + range = target.Value as Range; + + if (range == null) + { + return StatusCodes.BadTypeMismatch; + } + + return ServiceResult.Good; + } + + /// + /// Validates a filter for a monitored item. + /// + private ServiceResult ValidateFilter( + NodeMetadata metadata, + uint attributeId, + ExtensionObject filter, + out bool rangeRequired) + { + rangeRequired = false; + + // check filter. + DataChangeFilter datachangeFilter = null; + + if (!filter.IsNull) + { + datachangeFilter = filter.Body as DataChangeFilter; + } + + if (datachangeFilter != null) + { + // get the datatype of the node. + NodeId datatypeId = metadata.DataType; + + // check that filter is valid. + ServiceResult error = datachangeFilter.Validate(); + + if (ServiceResult.IsBad(error)) + { + return error; + } + + // check datatype of the variable. + if (!Server.TypeTree.IsTypeOf(datatypeId, DataTypes.Number)) + { + return StatusCodes.BadDeadbandFilterInvalid; + } + + // percent deadbands only allowed for analog data items. + if (datachangeFilter.DeadbandType == (int)DeadbandType.Percent) + { + ExpandedNodeId typeDefinitionId = metadata.TypeDefinition; + + if (typeDefinitionId.IsNull) + { + return StatusCodes.BadDeadbandFilterInvalid; + } + + // percent deadbands only allowed for analog data items. + if (!Server.TypeTree.IsTypeOf(typeDefinitionId, VariableTypes.AnalogItemType)) + { + return StatusCodes.BadDeadbandFilterInvalid; + } + + // the EURange property is required to use the filter. + rangeRequired = true; + } + } + + // filter is valid + return ServiceResult.Good; + } + + /// + /// Creates a new unique identifier for a node. + /// + private NodeId CreateUniqueNodeId(ushort namespaceIndex) + { + return new NodeId(Utils.IncrementIdentifier(ref m_lastId), namespaceIndex); + } + + private readonly NodeTable m_nodes; + private uint m_lastId; + private readonly SamplingGroupManager m_samplingGroupManager; + private readonly Dictionary m_monitoredItems; + private readonly double m_defaultMinimumSamplingInterval; + private readonly List m_namespaceUris; + private readonly ushort m_dynamicNamespaceIndex; + private readonly ILogger m_logger; + } +} diff --git a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs index 1de31f43e..fb55106c1 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs @@ -27,97 +27,42 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ -using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; namespace Opc.Ua.Server { - /// - public class CoreNodeManager : INodeManager, IDisposable, ICoreNodeManager + /// + /// A node manager which implements the ICoreNodeManager interface using the CustomNodeManager2 base class. + /// Every Server has one instance of this NodeManager. + /// It manages the built-in OPC UA nodes and provides core functionality. + /// + public class CoreNodeManager2 : CustomNodeManager2, ICoreNodeManager { /// /// Initializes the object with default values. /// - public CoreNodeManager( + public CoreNodeManager2( IServerInternal server, - ApplicationConfiguration configuration, - ushort dynamicNamespaceIndex) - { - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - Server = server ?? throw new ArgumentNullException(nameof(server)); - m_logger = server.Telemetry.CreateLogger(); - - m_nodes = new NodeTable(server.NamespaceUris, server.ServerUris, server.TypeTree); - m_monitoredItems = []; - m_defaultMinimumSamplingInterval = 1000; - m_namespaceUris = []; - m_dynamicNamespaceIndex = dynamicNamespaceIndex; - - // use namespace 1 if out of range. - if (m_dynamicNamespaceIndex == 0 || - m_dynamicNamespaceIndex >= server.NamespaceUris.Count) - { - m_dynamicNamespaceIndex = 1; - } - - m_samplingGroupManager = new SamplingGroupManager( - server, - this, - (uint)configuration.ServerConfiguration.MaxNotificationQueueSize, - (uint)configuration.ServerConfiguration.MaxDurableNotificationQueueSize, - configuration.ServerConfiguration.AvailableSamplingRates); - } - - /// - /// Frees any unmanaged resources. - /// - public void Dispose() + ApplicationConfiguration configuration) + : base(server, configuration) { - Dispose(true); - GC.SuppressFinalize(this); } /// - /// An overrideable version of the Dispose. + /// Initializes the object with default values. /// - protected virtual void Dispose(bool disposing) + public CoreNodeManager2( + IServerInternal server, + ApplicationConfiguration configuration, + ushort dynamicNamespaceIndex) + : base(server, configuration, server.NamespaceUris.GetString(dynamicNamespaceIndex)) { - if (disposing) - { - List nodes = null; - - lock (DataLock) - { - nodes = [.. m_nodes]; - m_nodes.Clear(); - - m_monitoredItems.Clear(); - } - - foreach (INode node in nodes) - { - Utils.SilentDispose(node); - } - - Utils.SilentDispose(m_samplingGroupManager); - } } - /// - /// Acquires the lock on the node manager. - /// - public object DataLock { get; } = new object(); - /// - public void ImportNodes(ISystemContext context, IEnumerable predefinedNodes) + public void ImportNodes( + ISystemContext context, + IEnumerable predefinedNodes) { ImportNodes(context, predefinedNodes, false); } @@ -128,3554 +73,74 @@ public void ImportNodes( IEnumerable predefinedNodes, bool isInternal) { - var nodesToExport = new NodeTable( - Server.NamespaceUris, - Server.ServerUris, - Server.TypeTree); - foreach (NodeState node in predefinedNodes) { - node.Export(context, nodesToExport); - } - - lock (DataLock) - { - foreach (ILocalNode nodeToExport in nodesToExport.OfType()) - { - AttachNode(nodeToExport, isInternal); - } - } - } - - /// - public IEnumerable NamespaceUris => m_namespaceUris; - - /// - /// - /// Populates the NodeManager by loading the standard nodes from an XML file stored as an embedded resource. - /// - public void CreateAddressSpace(IDictionary> externalReferences) - { - // TBD - } + AddPredefinedNode(context, node); - /// - /// - /// Disposes all of the nodes. - /// - public void DeleteAddressSpace() - { - var nodesToDispose = new List(); - - lock (DataLock) - { - // collect nodes to dispose. - foreach (INode node in m_nodes) + if (!isInternal) { - if (node is IDisposable disposable) + lock (Server.DiagnosticsLock) { - nodesToDispose.Add(disposable); + UpdateDiagnostics(context, node); } } - - m_nodes.Clear(); - } - - // dispose of the nodes. - foreach (IDisposable disposable in nodesToDispose) - { - try - { - disposable.Dispose(); - } - catch (Exception e) - { - m_logger.LogError(e, "Unexpected error disposing a Node object."); - } - } - } - - /// - public object GetManagerHandle(NodeId nodeId) - { - lock (DataLock) - { - if (nodeId.IsNull) - { - return null; - } - - return GetLocalNode(nodeId); } } - /// - public void TranslateBrowsePath( - OperationContext context, - object sourceHandle, - RelativePathElement relativePath, - IList targetIds, - IList unresolvedTargetIds) + /// + /// Updates the diagnostics node manager with the nodes that were imported. + /// + private void UpdateDiagnostics(ISystemContext context, NodeState node) { - if (sourceHandle == null) - { - throw new ArgumentNullException(nameof(sourceHandle)); - } - - if (relativePath == null) - { - throw new ArgumentNullException(nameof(relativePath)); - } - - if (targetIds == null) - { - throw new ArgumentNullException(nameof(targetIds)); - } - - if (unresolvedTargetIds == null) - { - throw new ArgumentNullException(nameof(unresolvedTargetIds)); - } - - // check for valid handle. - if (sourceHandle is not ILocalNode source) + if (node.NodeId.NamespaceIndex == 0) { - return; - } - - lock (DataLock) - { - // find the references that meet the filter criteria. - IList references = source.References.Find( - relativePath.ReferenceTypeId, - relativePath.IsInverse, - relativePath.IncludeSubtypes, - Server.TypeTree); - - // nothing more to do. - if (references == null || references.Count == 0) - { - return; - } + NodeState diagNode = Server.DiagnosticsNodeManager.FindPredefinedNode(node.NodeId); - // find targets with matching browse names. - foreach (IReference reference in references) + if (diagNode != null) { - INode target = GetLocalNode(reference.TargetId); - - // target is not known to the node manager. - if (target == null) - { - // ignore unknown external references. - if (reference.TargetId.IsAbsolute) - { - continue; - } - - // caller must check the browse name. - unresolvedTargetIds.Add((NodeId)reference.TargetId); - continue; - } + var references = new List(); + node.GetReferences(context, references); - // check browse name. - if (target.BrowseName == relativePath.TargetName) + foreach (IReference reference in references) { - targetIds.Add(reference.TargetId); + AddReferenceToDiagnostics(context, diagNode, reference); } } } - } - - /// - public void Browse( - OperationContext context, - ref ContinuationPoint continuationPoint, - IList references) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (continuationPoint == null) - { - throw new ArgumentNullException(nameof(continuationPoint)); - } - if (references == null) - { - throw new ArgumentNullException(nameof(references)); - } - - // check for valid handle. - if (continuationPoint.NodeToBrowse is not ILocalNode source) - { - throw new ServiceResultException(StatusCodes.BadNodeIdUnknown); - } - - // check for view. - if (!ViewDescription.IsDefault(continuationPoint.View)) - { - throw new ServiceResultException(StatusCodes.BadViewIdUnknown); - } + var children = new List(); + node.GetChildren(context, children); - lock (DataLock) + foreach (BaseInstanceState child in children) { - // construct list of references. - uint maxResultsToReturn = continuationPoint.MaxResultsToReturn; - - // get previous enumerator. - - // fetch a snapshot all references for node. - if (continuationPoint.Data is not IEnumerator enumerator) - { - enumerator = GetEnumerator(source.References); - enumerator.MoveNext(); - } - - static IEnumerator GetEnumerator(IReferenceCollection references) - { - var copy = new List(references); - return copy.GetEnumerator(); - } - - do - { - IReference reference = enumerator.Current; - - // silently ignore bad values. - if (reference == null || - reference.ReferenceTypeId.IsNull || - reference.TargetId.IsNull) - { - continue; - } - - // apply browse filters. - bool include = ApplyBrowseFilters( - reference, - continuationPoint.BrowseDirection, - continuationPoint.ReferenceTypeId, - continuationPoint.IncludeSubtypes); - - if (include) - { - var description = new ReferenceDescription { NodeId = reference.TargetId }; - description.SetReferenceType( - continuationPoint.ResultMask, - reference.ReferenceTypeId, - !reference.IsInverse); - - // only fetch the metadata if it is requested. - if (continuationPoint.TargetAttributesRequired) - { - // get the metadata for the node. - NodeMetadata metadata = GetNodeMetadata( - context, - GetManagerHandle(reference.TargetId), - continuationPoint.ResultMask); - - // update description with local node metadata. - if (metadata != null) - { - description.SetTargetAttributes( - continuationPoint.ResultMask, - metadata.NodeClass, - metadata.BrowseName, - metadata.DisplayName, - metadata.TypeDefinition); - - // check node class mask. - if (!CheckNodeClassMask( - continuationPoint.NodeClassMask, - description.NodeClass)) - { - continue; - } - } - // any target that is not remote must be owned by another node manager. - else if (!reference.TargetId.IsAbsolute) - { - description.Unfiltered = true; - } - } - - // add reference to list. - references.Add(description); - - // construct continuation point if max results reached. - if (maxResultsToReturn > 0 && references.Count >= maxResultsToReturn) - { - continuationPoint.Index = 0; - continuationPoint.Data = enumerator; - enumerator.MoveNext(); - return; - } - } - } while (enumerator.MoveNext()); - - // nothing more to browse if it exits from the loop normally. - continuationPoint.Dispose(); - continuationPoint = null; + UpdateDiagnostics(context, child); } } /// - /// Returns true if the target meets the filter criteria. + /// Adds a reference to the diagnostics node. /// - private bool ApplyBrowseFilters( - IReference reference, - BrowseDirection browseDirection, - NodeId referenceTypeId, - bool includeSubtypes) - { - // check browse direction. - if (reference.IsInverse) - { - if (browseDirection == BrowseDirection.Forward) - { - return false; - } - } - else if (browseDirection == BrowseDirection.Inverse) - { - return false; - } - - // check reference type filter. - if (!referenceTypeId.IsNull && reference.ReferenceTypeId != referenceTypeId) - { - return includeSubtypes && - Server.TypeTree.IsTypeOf(reference.ReferenceTypeId, referenceTypeId); - } - - // include reference for now. - return true; - } - - /// - public NodeMetadata GetNodeMetadata( - OperationContext context, - object targetHandle, - BrowseResultMask resultMask) + private static void AddReferenceToDiagnostics(ISystemContext context, NodeState diagNode, IReference reference) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - // find target. - if (targetHandle is not ILocalNode target) - { - return null; - } + INodeBrowser browser = diagNode.CreateBrowser( + context, + null, + reference.ReferenceTypeId, + true, + reference.IsInverse ? BrowseDirection.Inverse : BrowseDirection.Forward, + default, + null, + true); - lock (DataLock) + for (IReference existing = browser.Next(); existing != null; existing = browser.Next()) { - // copy the default metadata. - var metadata = new NodeMetadata(target, target.NodeId); - - // copy target attributes. - if (((int)resultMask & (int)BrowseResultMask.NodeClass) != 0) - { - metadata.NodeClass = target.NodeClass; - } - - if (((int)resultMask & (int)BrowseResultMask.BrowseName) != 0) - { - metadata.BrowseName = target.BrowseName; - } - - if (((int)resultMask & (int)BrowseResultMask.DisplayName) != 0) - { - metadata.DisplayName = target.DisplayName; - - // check if the display name can be localized. - if (!string.IsNullOrEmpty(metadata.DisplayName.TranslationInfo.Key)) - { - metadata.DisplayName = Server.ResourceManager.Translate( - context.PreferredLocales, - metadata.DisplayName); - } - } - - metadata.WriteMask = target.WriteMask; - - if (metadata.WriteMask != AttributeWriteMask.None) - { - var value = new DataValue((uint)(int)target.UserWriteMask); - ServiceResult result = target.Read(context, Attributes.UserWriteMask, value); - - if (ServiceResult.IsBad(result)) - { - metadata.WriteMask = AttributeWriteMask.None; - } - else - { - metadata.WriteMask = (AttributeWriteMask) - (int)((uint)(int)metadata.WriteMask & (uint)value.Value); - } - } - - metadata.EventNotifier = EventNotifiers.None; - metadata.AccessLevel = AccessLevels.None; - metadata.Executable = false; - - switch (target.NodeClass) - { - case NodeClass.Object: - metadata.EventNotifier = ((IObject)target).EventNotifier; - break; - case NodeClass.View: - metadata.EventNotifier = ((IView)target).EventNotifier; - break; - case NodeClass.Variable: - { - var variable = (IVariable)target; - metadata.DataType = variable.DataType; - metadata.ValueRank = variable.ValueRank; - metadata.ArrayDimensions = variable.ArrayDimensions; - metadata.AccessLevel = variable.AccessLevel; - - var value = new DataValue(variable.UserAccessLevel); - ServiceResult result = variable.Read( - context, - Attributes.UserAccessLevel, - value); - - if (ServiceResult.IsBad(result)) - { - metadata.AccessLevel = 0; - break; - } - - metadata.AccessLevel = (byte)(metadata.AccessLevel & (byte)value.Value); - break; - } - case NodeClass.Method: - { - var method = (IMethod)target; - metadata.Executable = method.Executable; - - if (metadata.Executable) - { - var value = new DataValue(method.UserExecutable); - ServiceResult result = method.Read( - context, - Attributes.UserExecutable, - value); - - if (ServiceResult.IsBad(result)) - { - metadata.Executable = false; - break; - } - - metadata.Executable = (bool)value.Value; - } - - break; - } - case NodeClass.Unspecified: - case NodeClass.ObjectType: - case NodeClass.VariableType: - case NodeClass.ReferenceType: - case NodeClass.DataType: - break; - default: - throw ServiceResultException.Unexpected( - $"Unexpected NodeClass {target.NodeClass}"); - } - - // look up type definition. - if (((int)resultMask & (int)BrowseResultMask.TypeDefinition) != 0 && - target.NodeClass is NodeClass.Variable or NodeClass.Object) - { - metadata.TypeDefinition = target.TypeDefinitionId; - } - - // Set AccessRestrictions and RolePermissions - var node = (Node)target; - metadata.AccessRestrictions = (AccessRestrictionType)node.AccessRestrictions; - metadata.RolePermissions = node.RolePermissions; - metadata.UserRolePermissions = node.UserRolePermissions; - - // check if NamespaceMetadata is defined for NamespaceUri - string namespaceUri = Server.NamespaceUris.GetString(target.NodeId.NamespaceIndex); - NamespaceMetadataState namespaceMetadataState = - Server.NodeManager.ConfigurationNodeManager - .GetNamespaceMetadataState(namespaceUri); - if (namespaceMetadataState != null) + if (existing.TargetId == reference.TargetId) { - metadata.DefaultAccessRestrictions = (AccessRestrictionType) - namespaceMetadataState.DefaultAccessRestrictions.Value; - metadata.DefaultRolePermissions = namespaceMetadataState.DefaultRolePermissions - .Value; - metadata.DefaultUserRolePermissions = namespaceMetadataState - .DefaultUserRolePermissions - .Value; + return; } - - // return metadata. - return metadata; } - } - - /// - /// - /// This method must not be called without first acquiring - /// - public void AddReferences(IDictionary> references) - { - if (references == null) - { - throw new ArgumentNullException(nameof(references)); - } - - lock (DataLock) - { - IEnumerator>> enumerator = references - .GetEnumerator(); - while (enumerator.MoveNext()) - { - ILocalNode actualNode = GetLocalNode(enumerator.Current.Key); - - if (actualNode != null) - { - foreach (IReference reference in enumerator.Current.Value) - { - AddReference( - actualNode, - reference.ReferenceTypeId, - reference.IsInverse, - reference.TargetId); - } - } - } - } + diagNode.AddReference(reference.ReferenceTypeId, reference.IsInverse, reference.TargetId); } - - /// - public void Read( - OperationContext context, - double maxAge, - IList nodesToRead, - IList values, - IList errors) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (nodesToRead == null) - { - throw new ArgumentNullException(nameof(nodesToRead)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - if (errors == null) - { - throw new ArgumentNullException(nameof(errors)); - } - - lock (DataLock) - { - for (int ii = 0; ii < nodesToRead.Count; ii++) - { - ReadValueId nodeToRead = nodesToRead[ii]; - - // skip items that have already been processed. - if (nodeToRead.Processed) - { - continue; - } - - // look up the node. - ILocalNode node = GetLocalNode(nodeToRead.NodeId); - - if (node == null) - { - continue; - } - - DataValue value = values[ii] = new DataValue(); - - value.Value = null; - value.ServerTimestamp = DateTime.MinValue; // Will be set later - value.SourceTimestamp = DateTime.MinValue; - value.StatusCode = StatusCodes.BadAttributeIdInvalid; - - // owned by this node manager. - nodeToRead.Processed = true; - - // read the default value (also verifies that the attribute id is valid for the node). - ServiceResult error = node.Read(context, nodeToRead.AttributeId, value); - - if (ServiceResult.IsBad(error)) - { - errors[ii] = error; - continue; - } - - // always use default value for base attributes. - bool useDefault; - switch (nodeToRead.AttributeId) - { - case Attributes.NodeId: - case Attributes.NodeClass: - case Attributes.BrowseName: - useDefault = true; - break; - default: - Attributes.ThrowIfOutOfRange(nodeToRead.AttributeId); - useDefault = false; - break; - } - - if (useDefault) - { - errors[ii] = error; - continue; - } - - // apply index range to value attributes. - if (nodeToRead.AttributeId == Attributes.Value) - { - object defaultValue = value.Value; - - error = nodeToRead.ParsedIndexRange.ApplyRange(ref defaultValue); - - if (ServiceResult.IsBad(error)) - { - value.Value = null; - errors[ii] = error; - continue; - } - - // apply data encoding. - if (!nodeToRead.DataEncoding.IsNull) - { - error = EncodeableObject.ApplyDataEncoding( - Server.MessageContext, - nodeToRead.DataEncoding, - ref defaultValue); - - if (ServiceResult.IsBad(error)) - { - value.Value = null; - errors[ii] = error; - continue; - } - } - - value.Value = defaultValue; - - // Set SourceTimestamp if not already set by the node - if (value.SourceTimestamp == DateTime.MinValue) - { - value.SourceTimestamp = DateTime.UtcNow; - } - - // Set ServerTimestamp to match SourceTimestamp for Value attributes - // This ensures ServerTimestamp and SourceTimestamp are equal, - // which is important for nodes like ServerStatus children where - // the node's read callback sets a specific timestamp - value.ServerTimestamp = value.SourceTimestamp; - } - } - } - } - - /// - public void HistoryRead( - OperationContext context, - HistoryReadDetails details, - TimestampsToReturn timestampsToReturn, - bool releaseContinuationPoints, - IList nodesToRead, - IList results, - IList errors) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (details == null) - { - throw new ArgumentNullException(nameof(details)); - } - - if (nodesToRead == null) - { - throw new ArgumentNullException(nameof(nodesToRead)); - } - - if (results == null) - { - throw new ArgumentNullException(nameof(results)); - } - - if (errors == null) - { - throw new ArgumentNullException(nameof(errors)); - } - - lock (DataLock) - { - for (int ii = 0; ii < nodesToRead.Count; ii++) - { - HistoryReadValueId nodeToRead = nodesToRead[ii]; - - // skip items that have already been processed. - if (nodeToRead.Processed) - { - continue; - } - - // look up the node. - ILocalNode node = GetLocalNode(nodeToRead.NodeId); - - if (node == null) - { - continue; - } - - // owned by this node manager. - nodeToRead.Processed = true; - - errors[ii] = StatusCodes.BadNotReadable; - } - } - } - - /// - public void Write( - OperationContext context, - IList nodesToWrite, - IList errors) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (nodesToWrite == null) - { - throw new ArgumentNullException(nameof(nodesToWrite)); - } - - if (errors == null) - { - throw new ArgumentNullException(nameof(errors)); - } - - lock (DataLock) - { - for (int ii = 0; ii < nodesToWrite.Count; ii++) - { - WriteValue nodeToWrite = nodesToWrite[ii]; - - // skip items that have already been processed. - if (nodeToWrite.Processed) - { - continue; - } - - // look up the node. - ILocalNode node = GetLocalNode(nodeToWrite.NodeId); - - if (node == null) - { - continue; - } - - // owned by this node manager. - nodeToWrite.Processed = true; - - if (!node.SupportsAttribute(nodeToWrite.AttributeId)) - { - errors[ii] = StatusCodes.BadAttributeIdInvalid; - continue; - } - - // fetch the node metadata. - NodeMetadata metadata = GetNodeMetadata(context, node, BrowseResultMask.All); - - // check access. - bool writeable = true; - ServiceResult error = null; - - // determine access rights. - switch (nodeToWrite.AttributeId) - { - case Attributes.NodeId: - case Attributes.NodeClass: - case Attributes.AccessLevel: - case Attributes.UserAccessLevel: - case Attributes.Executable: - case Attributes.UserExecutable: - case Attributes.EventNotifier: - writeable = false; - break; - case Attributes.Value: - writeable = (metadata.AccessLevel & AccessLevels.CurrentWrite) != 0; - break; - default: - writeable = (metadata.WriteMask & - Attributes.GetMask(nodeToWrite.AttributeId)) != 0; - break; - } - - // error if not writeable. - if (!writeable) - { - errors[ii] = StatusCodes.BadNotWritable; - continue; - } - - // determine expected datatype and value rank. - NodeId expectedDatatypeId = metadata.DataType; - int expectedValueRank = metadata.ValueRank; - - if (nodeToWrite.AttributeId != Attributes.Value) - { - expectedDatatypeId = Attributes.GetDataTypeId(nodeToWrite.AttributeId); - - DataValue value = nodeToWrite.Value; - - if (value.StatusCode != StatusCodes.Good || - value.ServerTimestamp != DateTime.MinValue || - value.SourceTimestamp != DateTime.MinValue) - { - errors[ii] = StatusCodes.BadWriteNotSupported; - continue; - } - - expectedValueRank = ValueRanks.Scalar; - - if (nodeToWrite.AttributeId == Attributes.ArrayDimensions) - { - expectedValueRank = ValueRanks.OneDimension; - } - } - - // check whether value being written is an instance of the expected data type. - object valueToWrite = nodeToWrite.Value.Value; - - var typeInfo = TypeInfo.IsInstanceOfDataType( - valueToWrite, - expectedDatatypeId, - expectedValueRank, - Server.NamespaceUris, - Server.TypeTree); - - if (typeInfo.IsUnknown) - { - errors[ii] = StatusCodes.BadTypeMismatch; - continue; - } - - // check index range. - if (nodeToWrite.ParsedIndexRange.Count > 0) - { - // check index range for scalars. - if (typeInfo.ValueRank < 0) - { - errors[ii] = StatusCodes.BadIndexRangeInvalid; - continue; - } - var array = (Array)valueToWrite; - - if (nodeToWrite.ParsedIndexRange.Count != array.Length) - { - errors[ii] = StatusCodes.BadIndexRangeInvalid; - continue; - } - } - - // write the default value. - error = node.Write(nodeToWrite.AttributeId, nodeToWrite.Value); - - if (ServiceResult.IsBad(error)) - { - errors[ii] = error; - } - } - } - } - - /// - public void HistoryUpdate( - OperationContext context, - Type detailsType, - IList nodesToUpdate, - IList results, - IList errors) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (nodesToUpdate == null) - { - throw new ArgumentNullException(nameof(nodesToUpdate)); - } - - if (results == null) - { - throw new ArgumentNullException(nameof(results)); - } - - if (errors == null) - { - throw new ArgumentNullException(nameof(errors)); - } - - lock (DataLock) - { - for (int ii = 0; ii < nodesToUpdate.Count; ii++) - { - HistoryUpdateDetails nodeToUpdate = nodesToUpdate[ii]; - - // skip items that have already been processed. - if (nodeToUpdate.Processed) - { - continue; - } - - // look up the node. - ILocalNode node = GetLocalNode(nodeToUpdate.NodeId); - - if (node == null) - { - continue; - } - - // owned by this node manager. - nodeToUpdate.Processed = true; - - errors[ii] = StatusCodes.BadNotWritable; - } - } - } - - /// - public void Call( - OperationContext context, - IList methodsToCall, - IList results, - IList errors) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (methodsToCall == null) - { - throw new ArgumentNullException(nameof(methodsToCall)); - } - - if (results == null) - { - throw new ArgumentNullException(nameof(results)); - } - - if (errors == null) - { - throw new ArgumentNullException(nameof(errors)); - } - - lock (DataLock) - { - for (int ii = 0; ii < methodsToCall.Count; ii++) - { - CallMethodRequest methodToCall = methodsToCall[ii]; - - // skip items that have already been processed. - if (methodToCall.Processed) - { - continue; - } - - // look up the node. - ILocalNode node = GetLocalNode(methodToCall.ObjectId); - - if (node == null) - { - continue; - } - - methodToCall.Processed = true; - - // look up the method. - ILocalNode method = GetLocalNode(methodToCall.MethodId); - - if (method == null) - { - errors[ii] = ServiceResult.Create( - StatusCodes.BadMethodInvalid, - "Method is not in the address space."); - continue; - } - - // check that the method is defined for the object. - if (!node.References.Exists( - ReferenceTypeIds.HasComponent, - false, - methodToCall.MethodId, - true, - Server.TypeTree)) - { - errors[ii] = ServiceResult.Create( - StatusCodes.BadMethodInvalid, - "Method is not a component of the Object."); - continue; - } - - errors[ii] = StatusCodes.BadNotImplemented; - } - } - } - - /// - public ServiceResult SubscribeToEvents( - OperationContext context, - object sourceId, - uint subscriptionId, - IEventMonitoredItem monitoredItem, - bool unsubscribe) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (sourceId == null) - { - throw new ArgumentNullException(nameof(sourceId)); - } - - if (monitoredItem == null) - { - throw new ArgumentNullException(nameof(monitoredItem)); - } - - lock (DataLock) - { - // validate the node. - NodeMetadata metadata = GetNodeMetadata( - context, - sourceId, - BrowseResultMask.NodeClass); - - if (metadata == null) - { - return StatusCodes.BadNodeIdUnknown; - } - - // validate the node class. - if ((((int)metadata.NodeClass & (int)NodeClass.Object) | (int)NodeClass.View) == 0) - { - return StatusCodes.BadNotSupported; - } - - // check that it supports events. - if ((metadata.EventNotifier & EventNotifiers.SubscribeToEvents) == 0) - { - return StatusCodes.BadNotSupported; - } - - return ServiceResult.Good; - } - } - - /// - public ServiceResult SubscribeToAllEvents( - OperationContext context, - uint subscriptionId, - IEventMonitoredItem monitoredItem, - bool unsubscribe) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (monitoredItem == null) - { - throw new ArgumentNullException(nameof(monitoredItem)); - } - - return ServiceResult.Good; - } - - /// - public ServiceResult ConditionRefresh( - OperationContext context, - IList monitoredItems) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - return ServiceResult.Good; - } - - /// - /// Creates a set of monitored items. - /// - /// is null. - public void CreateMonitoredItems( - OperationContext context, - uint subscriptionId, - double publishingInterval, - TimestampsToReturn timestampsToReturn, - IList itemsToCreate, - IList errors, - IList filterErrors, - IList monitoredItems, - bool createDurable, - MonitoredItemIdFactory monitoredItemIdFactory) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (itemsToCreate == null) - { - throw new ArgumentNullException(nameof(itemsToCreate)); - } - - if (errors == null) - { - throw new ArgumentNullException(nameof(errors)); - } - - if (monitoredItems == null) - { - throw new ArgumentNullException(nameof(monitoredItems)); - } - - lock (DataLock) - { - for (int ii = 0; ii < errors.Count; ii++) - { - MonitoredItemCreateRequest itemToCreate = itemsToCreate[ii]; - - // skip items that have already been processed. - if (itemToCreate.Processed) - { - continue; - } - - // look up the node. - ILocalNode node = GetLocalNode(itemToCreate.ItemToMonitor.NodeId); - - if (node == null) - { - continue; - } - - // owned by this node manager. - itemToCreate.Processed = true; - - if (!node.SupportsAttribute(itemToCreate.ItemToMonitor.AttributeId)) - { - errors[ii] = StatusCodes.BadAttributeIdInvalid; - continue; - } - - // fetch the metadata for the node. - NodeMetadata metadata = GetNodeMetadata(context, node, BrowseResultMask.All); - - if (itemToCreate.ItemToMonitor.AttributeId == Attributes.Value && - (metadata.AccessLevel & AccessLevels.CurrentRead) == 0) - { - errors[ii] = StatusCodes.BadNotReadable; - continue; - } - - // check value rank against index range. - if (itemToCreate.ItemToMonitor.ParsedIndexRange != NumericRange.Empty) - { - int valueRank = metadata.ValueRank; - - if (itemToCreate.ItemToMonitor.AttributeId != Attributes.Value) - { - valueRank = Attributes.GetValueRank( - itemToCreate.ItemToMonitor.AttributeId); - } - - if (valueRank == ValueRanks.Scalar) - { - errors[ii] = StatusCodes.BadIndexRangeInvalid; - continue; - } - } - - // validate the filter against the node/attribute being monitored. - errors[ii] = ValidateFilter( - metadata, - itemToCreate.ItemToMonitor.AttributeId, - itemToCreate.RequestedParameters.Filter, - out bool rangeRequired); - - if (ServiceResult.IsBad(errors[ii])) - { - continue; - } - - // lookup EU range if required. - Range range = null; - - if (rangeRequired) - { - errors[ii] = ReadEURange(context, node, out range); - - if (ServiceResult.IsBad(errors[ii])) - { - continue; - } - } - - // limit the sampling rate for non-value attributes. - double minimumSamplingInterval = m_defaultMinimumSamplingInterval; - - if (itemToCreate.ItemToMonitor.AttributeId == Attributes.Value) - { - // use the MinimumSamplingInterval attribute to limit the sampling rate for value attributes. - - if (node is IVariable variableNode) - { - minimumSamplingInterval = variableNode.MinimumSamplingInterval; - - // use the default if the node does not specify one. - if (minimumSamplingInterval < 0) - { - minimumSamplingInterval = m_defaultMinimumSamplingInterval; - } - } - } - - // Allocate the monitored item id - uint monitoredItemId; - do - { - monitoredItemId = monitoredItemIdFactory.GetNextId(); - } while (!m_monitoredItems.TryAdd(monitoredItemId, null)); - - // create monitored item. - ISampledDataChangeMonitoredItem monitoredItem = - m_samplingGroupManager.CreateMonitoredItem( - context, - subscriptionId, - publishingInterval, - timestampsToReturn, - monitoredItemId, - node, - itemToCreate, - range, - minimumSamplingInterval, - createDurable); - - // final check for initial value - ServiceResult error = ReadInitialValue(context, node, monitoredItem); - if (ServiceResult.IsBad(error) && - (error.StatusCode == StatusCodes.BadAttributeIdInvalid || - error.StatusCode == StatusCodes.BadDataEncodingInvalid || - error.StatusCode == StatusCodes.BadDataEncodingUnsupported)) - { - errors[ii] = error; - continue; - } - - // now save monitored item. - Debug.Assert(m_monitoredItems[monitoredItemId] == null); - m_monitoredItems[monitoredItemId] = monitoredItem; - - // update monitored item list. - monitoredItems[ii] = monitoredItem; - - // errors updating the monitoring groups will be reported in notifications. - errors[ii] = StatusCodes.Good; - } - } - - // update all groups with any new items. - m_samplingGroupManager.ApplyChanges(); - } - - /// - /// Restore a set of monitored items after a restart. - /// - /// is null. - /// - public 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"); - } - - lock (DataLock) - { - for (int ii = 0; ii < itemsToRestore.Count; ii++) - { - IStoredMonitoredItem item = itemsToRestore[ii]; - - // skip items that have already been processed. - if (item.IsRestored) - { - continue; - } - - // look up the node. - ILocalNode node = GetLocalNode(item.NodeId); - - if (node == null) - { - continue; - } - - // owned by this node manager. - item.IsRestored = true; - - // create monitored item. - ISampledDataChangeMonitoredItem monitoredItem = m_samplingGroupManager - .RestoreMonitoredItem( - node, - item, - savedOwnerIdentity); - - // save monitored item. - m_monitoredItems.Add(monitoredItem.Id, monitoredItem); - - // update monitored item list. - monitoredItems[ii] = monitoredItem; - } - } - - // update all groups with any new items. - m_samplingGroupManager.ApplyChanges(); - } - - /// - /// Reads the initial value for a monitored item. - /// - /// The context. - /// The node to read. - /// The monitored item. - protected virtual ServiceResult ReadInitialValue( - OperationContext context, - ILocalNode node, - IDataChangeMonitoredItem2 monitoredItem) - { - var initialValue = new DataValue - { - Value = null, - ServerTimestamp = DateTime.UtcNow, - SourceTimestamp = DateTime.MinValue, - StatusCode = StatusCodes.BadWaitingForInitialData - }; - - ServiceResult error = node.Read(context, monitoredItem.AttributeId, initialValue); - - if (ServiceResult.IsBad(error)) - { - initialValue.Value = null; - initialValue.StatusCode = error.StatusCode; - } - - monitoredItem.QueueValue(initialValue, error, true); - - return error; - } - - /// - /// Modifies a set of monitored items. - /// - /// is null. - public void ModifyMonitoredItems( - OperationContext context, - TimestampsToReturn timestampsToReturn, - IList monitoredItems, - IList itemsToModify, - IList errors, - IList filterErrors) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (monitoredItems == null) - { - throw new ArgumentNullException(nameof(monitoredItems)); - } - - if (itemsToModify == null) - { - throw new ArgumentNullException(nameof(itemsToModify)); - } - - if (errors == null) - { - throw new ArgumentNullException(nameof(errors)); - } - - lock (DataLock) - { - for (int ii = 0; ii < errors.Count; ii++) - { - MonitoredItemModifyRequest itemToModify = itemsToModify[ii]; - - // skip items that have already been processed. - if (itemToModify.Processed || monitoredItems[ii] == null) - { - continue; - } - - // check if the node manager created the item. - if (!ReferenceEquals(this, monitoredItems[ii].NodeManager)) - { - continue; - } - - // owned by this node manager. - itemToModify.Processed = true; - - // validate monitored item. - - if (!m_monitoredItems.TryGetValue( - monitoredItems[ii].Id, - out ISampledDataChangeMonitoredItem monitoredItem)) - { - errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; - continue; - } - - if (!ReferenceEquals(monitoredItem, monitoredItems[ii])) - { - errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; - continue; - } - - // find the node being monitored. - - if (monitoredItem.ManagerHandle is not ILocalNode node) - { - errors[ii] = StatusCodes.BadNodeIdUnknown; - continue; - } - - // fetch the metadata for the node. - NodeMetadata metadata = GetNodeMetadata( - context, - monitoredItem.ManagerHandle, - BrowseResultMask.All); - - // validate the filter against the node/attribute being monitored. - errors[ii] = ValidateFilter( - metadata, - monitoredItem.AttributeId, - itemToModify.RequestedParameters.Filter, - out bool rangeRequired); - - if (ServiceResult.IsBad(errors[ii])) - { - continue; - } - - // lookup EU range if required. - Range range = null; - - if (rangeRequired) - { - // look up EU range. - errors[ii] = ReadEURange(context, node, out range); - - if (ServiceResult.IsBad(errors[ii])) - { - continue; - } - } - - // update sampling. - errors[ii] = m_samplingGroupManager.ModifyMonitoredItem( - context, - timestampsToReturn, - monitoredItem, - itemToModify, - range); - - // state of item did not change if an error returned here. - if (ServiceResult.IsBad(errors[ii])) - { - continue; - } - - // item has been modified successfully. - // errors updating the sampling groups will be reported in notifications. - errors[ii] = StatusCodes.Good; - } - } - - // update all sampling groups. - m_samplingGroupManager.ApplyChanges(); - } - - /// - /// Deletes a set of monitored items. - /// - /// is null. - public void DeleteMonitoredItems( - OperationContext context, - IList monitoredItems, - IList processedItems, - IList errors) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (monitoredItems == null) - { - throw new ArgumentNullException(nameof(monitoredItems)); - } - - if (errors == null) - { - throw new ArgumentNullException(nameof(errors)); - } - - lock (DataLock) - { - for (int ii = 0; ii < errors.Count; ii++) - { - // skip items that have already been processed. - if (processedItems[ii] || monitoredItems[ii] == null) - { - continue; - } - - // check if the node manager created the item. - if (!ReferenceEquals(this, monitoredItems[ii].NodeManager)) - { - continue; - } - - // owned by this node manager. - processedItems[ii] = true; - - // validate monitored item. - - if (!m_monitoredItems.TryGetValue( - monitoredItems[ii].Id, - out ISampledDataChangeMonitoredItem monitoredItem)) - { - errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; - continue; - } - - if (!ReferenceEquals(monitoredItem, monitoredItems[ii])) - { - errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; - continue; - } - - // remove item. - m_samplingGroupManager.StopMonitoring(monitoredItem); - - // remove association with the group. - m_monitoredItems.Remove(monitoredItem.Id); - - // delete successful. - errors[ii] = StatusCodes.Good; - } - } - - // remove all items from groups. - m_samplingGroupManager.ApplyChanges(); - } - - /// - /// 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 set of processed items. - /// Any errors. - /// is null. - public virtual void TransferMonitoredItems( - OperationContext context, - bool sendInitialValues, - IList monitoredItems, - IList processedItems, - IList errors) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (monitoredItems == null) - { - throw new ArgumentNullException(nameof(monitoredItems)); - } - - if (processedItems == null) - { - throw new ArgumentNullException(nameof(processedItems)); - } - - lock (DataLock) - { - for (int ii = 0; ii < monitoredItems.Count; ii++) - { - // skip items that have already been processed. - if (processedItems[ii] || monitoredItems[ii] == null) - { - continue; - } - - // check if the node manager created the item. - if (!ReferenceEquals(this, monitoredItems[ii].NodeManager)) - { - continue; - } - - // owned by this node manager. - processedItems[ii] = true; - - // validate monitored item. - IMonitoredItem monitoredItem = monitoredItems[ii]; - - // find the node being monitored. - if (monitoredItem.ManagerHandle is not ILocalNode node) - { - continue; - } - - if (sendInitialValues) - { - monitoredItem.SetupResendDataTrigger(); - } - - errors[ii] = StatusCodes.Good; - } - } - } - - /// - /// Changes the monitoring mode for a set of monitored items. - /// - /// is null. - public void SetMonitoringMode( - OperationContext context, - MonitoringMode monitoringMode, - IList monitoredItems, - IList processedItems, - IList errors) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (monitoredItems == null) - { - throw new ArgumentNullException(nameof(monitoredItems)); - } - - if (errors == null) - { - throw new ArgumentNullException(nameof(errors)); - } - - lock (DataLock) - { - for (int ii = 0; ii < errors.Count; ii++) - { - // skip items that have already been processed. - if (processedItems[ii] || monitoredItems[ii] == null) - { - continue; - } - - // check if the node manager created the item. - if (!ReferenceEquals(this, monitoredItems[ii].NodeManager)) - { - continue; - } - - // owned by this node manager. - processedItems[ii] = true; - - // validate monitored item. - - if (!m_monitoredItems.TryGetValue( - monitoredItems[ii].Id, - out ISampledDataChangeMonitoredItem monitoredItem)) - { - errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; - continue; - } - - if (!ReferenceEquals(monitoredItem, monitoredItems[ii])) - { - errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; - continue; - } - - // update monitoring mode. - MonitoringMode previousMode = monitoredItem.SetMonitoringMode(monitoringMode); - - // need to provide an immediate update after enabling. - if (previousMode == MonitoringMode.Disabled && - monitoringMode != MonitoringMode.Disabled) - { - var initialValue = new DataValue - { - ServerTimestamp = DateTime.UtcNow, - StatusCode = StatusCodes.BadWaitingForInitialData - }; - - // read the initial value. - - if (monitoredItem.ManagerHandle is Node node) - { - ServiceResult error = node.Read( - context, - monitoredItem.AttributeId, - initialValue); - - if (ServiceResult.IsBad(error)) - { - initialValue.Value = null; - initialValue.StatusCode = error.StatusCode; - } - } - - monitoredItem.QueueValue(initialValue, null); - } - - // modify the item attributes. - m_samplingGroupManager.ModifyMonitoring(context, monitoredItem); - - // item has been modified successfully. - // errors updating the sampling groups will be reported in notifications. - errors[ii] = StatusCodes.Good; - } - } - - // update all sampling groups. - m_samplingGroupManager.ApplyChanges(); - } - - /// - /// Returns true if the node class matches the node class mask. - /// - public static bool CheckNodeClassMask(uint nodeClassMask, NodeClass nodeClass) - { - if (nodeClassMask != 0) - { - return ((uint)nodeClass & nodeClassMask) != 0; - } - - return true; - } - - /// - /// The server that the node manager belongs to. - /// - protected IServerInternal Server { get; } - - /// - /// Returns an index for the NamespaceURI (Adds it to the server namespace table if it does not already exist). - /// - /// - /// Returns the server's default index (1) if the namespaceUri is empty or null. - /// - public ushort GetNamespaceIndex(string namespaceUri) - { - int namespaceIndex = 1; - - if (!string.IsNullOrEmpty(namespaceUri)) - { - namespaceIndex = Server.NamespaceUris.GetIndex(namespaceUri); - - if (namespaceIndex == -1) - { - namespaceIndex = Server.NamespaceUris.Append(namespaceUri); - } - } - - return (ushort)namespaceIndex; - } - - /// - /// Returns all targets of the specified reference. - /// - /// is null. - public NodeIdCollection FindLocalNodes( - NodeId sourceId, - NodeId referenceTypeId, - bool isInverse) - { - if (sourceId.IsNull) - { - throw new ArgumentNullException(nameof(sourceId)); - } - - if (referenceTypeId.IsNull) - { - throw new ArgumentNullException(nameof(referenceTypeId)); - } - - lock (DataLock) - { - if (GetManagerHandle(sourceId) is not ILocalNode source) - { - return null; - } - - var targets = new NodeIdCollection(); - - foreach (IReference reference in source.References) - { - if (reference.IsInverse != isInverse || - !Server.TypeTree.IsTypeOf(reference.ReferenceTypeId, referenceTypeId)) - { - continue; - } - - ExpandedNodeId targetId = reference.TargetId; - - if (targetId.IsAbsolute) - { - continue; - } - - targets.Add((NodeId)targetId); - } - - return targets; - } - } - - /// - /// Returns the id the first node with the specified browse name if it exists. null otherwise - /// - /// is null. - public NodeId FindTargetId( - NodeId sourceId, - NodeId referenceTypeId, - bool isInverse, - QualifiedName browseName) - { - if (sourceId.IsNull) - { - throw new ArgumentNullException(nameof(sourceId)); - } - - if (referenceTypeId.IsNull) - { - throw new ArgumentNullException(nameof(referenceTypeId)); - } - - lock (DataLock) - { - if (GetManagerHandle(sourceId) is not ILocalNode source) - { - return default; - } - - foreach (ReferenceNode reference in source.References.OfType()) - { - if (reference.IsInverse != isInverse || - !Server.TypeTree.IsTypeOf(reference.ReferenceTypeId, referenceTypeId)) - { - continue; - } - - ExpandedNodeId targetId = reference.TargetId; - - if (targetId.IsAbsolute) - { - continue; - } - - if (GetManagerHandle((NodeId)targetId) is not ILocalNode target) - { - continue; - } - - if (browseName.IsNull || target.BrowseName == browseName) - { - return (NodeId)targetId; - } - } - - return default; - } - } - - /// - /// Returns the first target that matches the browse path. - /// - public NodeId Find(NodeId sourceId, string browsePath) - { - IList targets = TranslateBrowsePath(sourceId, browsePath); - - if (targets.Count > 0) - { - return targets[0]; - } - - return default; - } - - /// - /// Returns a list of targets the match the browse path. - /// - public IList TranslateBrowsePath( - OperationContext context, - NodeId sourceId, - string browsePath) - { - return TranslateBrowsePath( - context, - sourceId, - RelativePath.Parse(browsePath, Server.TypeTree)); - } - - /// - /// Returns a list of targets the match the browse path. - /// - public IList TranslateBrowsePath(NodeId sourceId, string browsePath) - { - return TranslateBrowsePath( - null, - sourceId, - RelativePath.Parse(browsePath, Server.TypeTree)); - } - - /// - /// Returns a list of targets the match the browse path. - /// - public IList TranslateBrowsePath(NodeId sourceId, RelativePath relativePath) - { - return TranslateBrowsePath(null, sourceId, relativePath); - } - - /// - /// Returns a list of targets the match the browse path. - /// - public IList TranslateBrowsePath( - OperationContext context, - NodeId sourceId, - RelativePath relativePath) - { - var targets = new List(); - - if (relativePath == null || relativePath.Elements.Count == 0) - { - targets.Add(sourceId); - return targets; - } - - lock (DataLock) - { - // look up source in this node manager. - ILocalNode source = GetLocalNode(sourceId); - if (source == null) - { - return targets; - } - } - - // return the set of matching targets. - return targets; - } - - /// - /// Registers a source for a node. - /// - /// - /// The source could be one or more of IDataSource, IEventSource, ICallable, IHistorian or IViewManager - /// - /// is null. - public void RegisterSource(NodeId nodeId, object source, object handle, bool isEventSource) - { - if (nodeId.IsNull) - { - throw new ArgumentNullException(nameof(nodeId)); - } - - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - } - - /// - /// Called when the source is no longer used. - /// - /// - /// When a source disappears it must either delete all of its nodes from the address space - /// or unregister itself their source by calling RegisterSource with source == null. - /// After doing that the source must call this method. - /// - public void UnregisterSource(object source) - { - } - - /// - /// Applys the modelling rules to any existing instance. - /// - /// is null. - public void ApplyModellingRules( - ILocalNode instance, - ILocalNode typeDefinition, - ILocalNode templateDeclaration, - ushort namespaceIndex) - { - if (instance == null) - { - throw new ArgumentNullException(nameof(instance)); - } - - if (typeDefinition == null) - { - throw new ArgumentNullException(nameof(typeDefinition)); - } - - // check existing type definition. - UpdateTypeDefinition(instance, typeDefinition.NodeId); - - // create list of declarations for the type definition (recursively collects definitions from supertypes). - var declarations = new List(); - BuildDeclarationList(typeDefinition, declarations); - - // add instance declaration if provided. - if (templateDeclaration != null) - { - var declaration = new DeclarationNode - { - Node = templateDeclaration, - BrowsePath = string.Empty - }; - - declarations.Add(declaration); - - BuildDeclarationList(templateDeclaration, declarations); - } - - // build list of instances to create. - var typeDefinitions = new List(); - var instanceDeclarations = new SortedDictionary(); - var possibleTargets = new SortedDictionary(); - - // create instances from declarations. - // subtypes appear in list last so traversing the list backwards find the overridden nodes first. - for (int ii = declarations.Count - 1; ii >= 0; ii--) - { - DeclarationNode declaration = declarations[ii]; - - // update type definition list. - if (string.IsNullOrEmpty(declaration.BrowsePath)) - { - typeDefinitions.Add(declaration.Node); - continue; - } - - // skip declaration if instance already exists. - // (i.e. the declaration was overridden). - if (instanceDeclarations.ContainsKey(declaration.BrowsePath)) - { - continue; - } - - // update instance declaration list. - instanceDeclarations[declaration.BrowsePath] = declaration.Node; - - // save the node as a possible target of references. - possibleTargets[declaration.Node.NodeId] = declaration.Node; - } - - // build list of instances that already exist. - var existingInstances = new SortedDictionary(); - BuildInstanceList(instance, string.Empty, existingInstances); - - // maps the instance declaration onto an instance node. - var instancesToCreate = new Dictionary(); - - // apply modelling rules to instance declarations. - foreach (KeyValuePair current in instanceDeclarations) - { - string browsePath = current.Key; - ILocalNode instanceDeclaration = current.Value; - - // check if the same instance has multiple browse paths to it. - if (instancesToCreate.TryGetValue(instanceDeclaration.NodeId, out _)) - { - continue; - } - - // check for an existing instance. - if (existingInstances.TryGetValue(browsePath, out ILocalNode newInstance)) - { - continue; - } - - // apply modelling rule to determine whether to create a new instance. - NodeId modellingRule = instanceDeclaration.ModellingRule; - - // always create a new instance if one does not already exist. - if (modellingRule == Objects.ModellingRule_Mandatory) - { - if (newInstance == null) - { - newInstance = instanceDeclaration.CreateCopy(CreateUniqueNodeId()); - AddNode(newInstance); - } - } - // ignore optional instances unless one has been specified in the existing tree. - else if (modellingRule == Objects.ModellingRule_Optional) - { - if (newInstance == null) - { - continue; - } - } - // ignore any unknown modelling rules. - else - { - continue; - } - - // save the mapping between the instance declaration and the new instance. - instancesToCreate[instanceDeclaration.NodeId] = newInstance; - } - - // add references from type definitions to top level. - foreach (ILocalNode type in typeDefinitions) - { - foreach (IReference reference in type.References) - { - // ignore external references from type. - if (reference.TargetId.IsAbsolute) - { - continue; - } - - // ignore subtype references. - if (m_nodes.TypeTree - .IsTypeOf(reference.ReferenceTypeId, ReferenceTypeIds.HasSubtype)) - { - continue; - } - - // ignore targets that are not in the instance tree. - if (!instancesToCreate.TryGetValue( - (NodeId)reference.TargetId, - out ILocalNode target)) - { - continue; - } - - // add forward and backward reference. - AddReference( - instance, - reference.ReferenceTypeId, - reference.IsInverse, - target, - true); - } - } - - // add references between instance declarations. - foreach (ILocalNode instanceDeclaration in instanceDeclarations.Values) - { - // find the source for the references. - - if (!instancesToCreate.TryGetValue( - instanceDeclaration.NodeId, - out ILocalNode source)) - { - continue; - } - - // check if the source is a shared node. - bool sharedNode = ReferenceEquals(instanceDeclaration, source); - - foreach (IReference reference in instanceDeclaration.References) - { - // add external reference. - if (reference.TargetId.IsAbsolute) - { - if (!sharedNode) - { - AddReference( - source, - reference.ReferenceTypeId, - reference.IsInverse, - reference.TargetId); - } - - continue; - } - - // check for modelling rule. - if (reference.ReferenceTypeId == ReferenceTypeIds.HasModellingRule) - { - if (!source.References.Exists( - ReferenceTypeIds.HasModellingRule, - false, - reference.TargetId, - false, - null)) - { - AddReference( - source, - reference.ReferenceTypeId, - false, - reference.TargetId); - } - - continue; - } - - // check for type definition. - if (reference.ReferenceTypeId == ReferenceTypeIds.HasTypeDefinition) - { - if (!sharedNode) - { - UpdateTypeDefinition(source, instanceDeclaration.TypeDefinitionId); - } - - continue; - } - - // add targets that are not in the instance tree. - if (!instancesToCreate.TryGetValue( - (NodeId)reference.TargetId, - out ILocalNode target)) - { - // don't update shared nodes because the reference should already exist. - if (sharedNode) - { - continue; - } - - // top level references to the type definition node were already added. - if (reference.TargetId == typeDefinition.NodeId) - { - continue; - } - - // see if a reference is allowed. - if (!IsExternalReferenceAllowed(reference.ReferenceTypeId)) - { - continue; - } - - // add one way reference. - source.References.Add( - reference.ReferenceTypeId, - reference.IsInverse, - reference.TargetId); - continue; - } - - // add forward and backward reference. - AddReference( - source, - reference.ReferenceTypeId, - reference.IsInverse, - target, - true); - } - } - } - - /// - /// Returns true if a one-way reference to external nodes is permitted. - /// - private bool IsExternalReferenceAllowed(NodeId referenceTypeId) - { - // always exclude hierarchial references. - if (m_nodes.TypeTree.IsTypeOf(referenceTypeId, ReferenceTypeIds.HierarchicalReferences)) - { - return false; - } - - // allow one way reference to event. - if (m_nodes.TypeTree.IsTypeOf(referenceTypeId, ReferenceTypes.GeneratesEvent)) - { - return true; - } - - // all other references not permitted. - return false; - } - - /// - /// Updates the type definition for a node. - /// - /// - private void UpdateTypeDefinition(ILocalNode instance, ExpandedNodeId typeDefinitionId) - { - // check existing type definition. - ExpandedNodeId existingTypeId = instance.TypeDefinitionId; - - if (existingTypeId == typeDefinitionId) - { - return; - } - - if (!existingTypeId.IsNull) - { - if (m_nodes.TypeTree.IsTypeOf(existingTypeId, typeDefinitionId)) - { - throw ServiceResultException.Create( - StatusCodes.BadTypeDefinitionInvalid, - "Type definition {0} is not a subtype of the existing type definition {1}.", - existingTypeId, - typeDefinitionId); - } - - DeleteReference( - instance, - ReferenceTypeIds.HasTypeDefinition, - false, - existingTypeId, - false); - } - - AddReference(instance, ReferenceTypeIds.HasTypeDefinition, false, typeDefinitionId); - } - - /// - /// A node in the type system that is used to instantiate objects or variables. - /// - private class DeclarationNode - { - public ILocalNode Node; - public string BrowsePath; - } - - /// - /// Builds the list of declaration nodes for a type definition. - /// - /// is null. - private void BuildDeclarationList( - ILocalNode typeDefinition, - List declarations) - { - if (typeDefinition == null) - { - throw new ArgumentNullException(nameof(typeDefinition)); - } - - if (declarations == null) - { - throw new ArgumentNullException(nameof(declarations)); - } - - // guard against loops (i.e. common grandparents). - for (int ii = 0; ii < declarations.Count; ii++) - { - if (ReferenceEquals(declarations[ii].Node, typeDefinition)) - { - return; - } - } - - // create the root declaration for the type. - var declaration = new DeclarationNode - { - Node = typeDefinition, - BrowsePath = string.Empty - }; - - declarations.Add(declaration); - - // follow references to supertypes first. - foreach ( - IReference reference in typeDefinition.References - .Find(ReferenceTypeIds.HasSubtype, true, false, null)) - { - ILocalNode supertype = GetLocalNode(reference.TargetId); - - if (supertype == null) - { - continue; - } - - BuildDeclarationList(supertype, declarations); - } - - // add children of type. - BuildDeclarationList(declaration, declarations); - } - - /// - /// Builds a list of declarations from the nodes aggregated by a parent. - /// - /// is null. - private void BuildDeclarationList( - DeclarationNode parent, - List declarations) - { - if (parent == null) - { - throw new ArgumentNullException(nameof(parent)); - } - - if (declarations == null) - { - throw new ArgumentNullException(nameof(declarations)); - } - - // get list of children. - foreach ( - IReference reference in parent.Node.References.Find( - ReferenceTypeIds.HierarchicalReferences, - false, - true, - m_nodes.TypeTree)) - { - // do not follow sub-type references. - if (m_nodes.TypeTree - .IsTypeOf(reference.ReferenceTypeId, ReferenceTypeIds.HasSubtype)) - { - continue; - } - - // find child (ignore children that are not in the node table). - ILocalNode child = GetLocalNode(reference.TargetId); - - if (child == null) - { - continue; - } - - // create the declartion node. - var declaration = new DeclarationNode - { - Node = child, - BrowsePath = Utils.Format("{0}.{1}", parent.BrowsePath, child.BrowseName) - }; - - declarations.Add(declaration); - - // recursively include aggregated children. - NodeId modellingRule = child.ModellingRule; - - if (modellingRule == ObjectIds.ModellingRule_Mandatory || - modellingRule == ObjectIds.ModellingRule_Optional) - { - BuildDeclarationList(declaration, declarations); - } - } - } - - /// - /// Builds a table of instances indexed by browse path from the nodes aggregated by a parent - /// - /// is null. - private void BuildInstanceList( - ILocalNode parent, - string browsePath, - IDictionary instances) - { - if (instances == null) - { - throw new ArgumentNullException(nameof(instances)); - } - - // guard against loops. - if (instances.ContainsKey(browsePath)) - { - return; - } - - // index parent by browse path. - instances[browsePath] = parent ?? throw new ArgumentNullException(nameof(parent)); - - // get list of children. - foreach ( - IReference reference in parent.References.Find( - ReferenceTypeIds.HierarchicalReferences, - false, - true, - m_nodes.TypeTree)) - { - // find child (ignore children that are not in the node table). - ILocalNode child = GetLocalNode(reference.TargetId); - - if (child == null) - { - continue; - } - - // recursively include aggregated children. - BuildInstanceList( - child, - Utils.Format("{0}.{1}", browsePath, child.BrowseName), - instances); - } - } - - /// - /// Exports a node to a nodeset. - /// - /// - public void ExportNode(NodeId nodeId, NodeSet nodeSet) - { - lock (DataLock) - { - ILocalNode node = - GetLocalNode(nodeId) - ?? throw ServiceResultException.Create( - StatusCodes.BadNodeIdUnknown, - "NodeId ({0}) does not exist.", - nodeId); - - ExportNode( - node, - nodeSet, - ((int)node.NodeClass & ((int)NodeClass.Object | (int)NodeClass.Variable)) != 0); - } - } - - /// - /// Exports a node to a nodeset. - /// - public void ExportNode(ILocalNode node, NodeSet nodeSet, bool instance) - { - lock (DataLock) - { - // check if the node has already been added. - NodeId exportedId = nodeSet.Export(node.NodeId, m_nodes.NamespaceUris); - - if (nodeSet.Contains(exportedId)) - { - return; - } - - // add to nodeset. - Node nodeToExport = nodeSet.Add(node, m_nodes.NamespaceUris, m_nodes.ServerUris); - - // follow children. - foreach (ReferenceNode reference in node.References.OfType()) - { - // export all references. - bool export = true; - - // unless it is a subtype reference. - if (Server.TypeTree - .IsTypeOf(reference.ReferenceTypeId, ReferenceTypeIds.HasSubtype)) - { - export = false; - } - - if (export) - { - nodeSet.AddReference( - nodeToExport, - reference, - m_nodes.NamespaceUris, - m_nodes.ServerUris); - } - - if (reference.IsInverse || - Server.TypeTree - .IsTypeOf(reference.ReferenceTypeId, ReferenceTypeIds.HasSubtype)) - { - nodeSet.AddReference( - nodeToExport, - reference, - m_nodes.NamespaceUris, - m_nodes.ServerUris); - } - - if (Server.TypeTree - .IsTypeOf(reference.ReferenceTypeId, ReferenceTypeIds.Aggregates)) - { - if (reference.IsInverse) - { - continue; - } - - ILocalNode child = GetLocalNode(reference.TargetId); - - if (child != null) - { - if (instance) - { - NodeId modellingRule = child.ModellingRule; - - if (modellingRule != Objects.ModellingRule_Mandatory) - { - continue; - } - } - - ExportNode(child, nodeSet, instance); - } - } - } - } - } - -#if XXX - /// - /// Changes the type definition for an instance. - /// - public void ChangeTypeDefinition(NodeId instanceId, NodeId typeDefinitionId) - { - try - { - m_lock.Enter(); - - // find the instance. - ILocalNode instance = GetLocalNode(instanceId) as ILocalNode; - - if (instance == null) - { - throw ServiceResultException.Create( - StatusCodes.BadNodeIdUnknown, - "NodeId ({0}) does not exist.", - instanceId); - } - - // check node class. - if (instance.NodeClass != NodeClass.Object && instance.NodeClass != NodeClass.Variable) - { - throw ServiceResultException.Create( - StatusCodes.BadNodeClassInvalid, - "Node (NodeClass={0}) cannot have a type definition.", - instance.NodeClass); - } - - // get current type definition. - ExpandedNodeId existingTypeId = instance.TypeDefinitionId; - - if (existingTypeId == typeDefinitionId) - { - return; - } - - // can only change to a subtype of the existing type definition. - if (!m_server.TypeTree.IsTypeOf(typeDefinitionId, existingTypeId)) - { - throw ServiceResultException.Create( - StatusCodes.BadTypeDefinitionInvalid, - "Type definition ({0}) must be a must subtype of the existing type definition ({1}).", - typeDefinitionId, - existingTypeId); - } - - // find the type definition node. - ILocalNode typeDefinition = GetLocalNode(typeDefinitionId) as ILocalNode; - - if (typeDefinition == null) - { - throw ServiceResultException.Create( - StatusCodes.BadTypeDefinitionInvalid, - "TypeDefinitionId ({0}) does not exist.", - typeDefinitionId); - } - - // apply modelling rules. - NodeFactory factory = new NodeFactory(m_nodes); - IList nodesToAdd = factory.ApplyModellingRules( - instance, - typeDefinition.NodeId, - ref m_lastId, - 1); - - // add the nodes. - foreach (Node nodeToAdd in nodesToAdd) - { - AddNode(nodeToAdd); - } - } - finally - { - m_lock.Exit(); - } - } -#endif - - /// - /// Deletes a node from the address sapce. - /// - /// is null. - /// - public void DeleteNode(NodeId nodeId, bool deleteChildren, bool silent) - { - if (nodeId.IsNull) - { - throw new ArgumentNullException(nameof(nodeId)); - } - - // find the node to delete. - - if (GetManagerHandle(nodeId) is not ILocalNode node) - { - if (!silent) - { - throw ServiceResultException.Create( - StatusCodes.BadSourceNodeIdInvalid, - "Node '{NodeId}' does not exist.", - nodeId); - } - - return; - } - - bool instance = ((int)node.NodeClass & - ((int)NodeClass.Object | (int)NodeClass.Variable)) != 0; - - var referencesToDelete = new Dictionary>(); - - if (silent) - { - try - { - DeleteNode(node, deleteChildren, instance, referencesToDelete); - } - catch (Exception e) - { - m_logger.LogError(e, "Error deleting node: {NodeId}", nodeId); - } - } - else - { - DeleteNode(node, deleteChildren, instance, referencesToDelete); - } - - if (referencesToDelete.Count > 0) - { - _ = Task.Run(() => OnDeleteReferencesAsync(referencesToDelete)); - } - } - - /// - /// Deletes a node from the address sapce. - /// - /// is null. - private void DeleteNode( - ILocalNode node, - bool deleteChildren, - bool instance, - Dictionary> referencesToDelete) - { - if (node == null) - { - throw new ArgumentNullException(nameof(node)); - } - - var nodesToDelete = new List(); - var referencesForNode = new List(); - - lock (DataLock) - { - // remove the node. - m_nodes.Remove(node.NodeId); - - // check need to connect subtypes to the supertype if they are being deleted. - ExpandedNodeId supertypeId = Server.TypeTree.FindSuperType(node.NodeId); - - if (!supertypeId.IsNull) - { - Server.TypeTree.Remove(node.NodeId); - } - - // remove any references to the node. - foreach (IReference reference in node.References) - { - // ignore remote references. - if (reference.TargetId.IsAbsolute) - { - continue; - } - - // find the target. - - if (GetManagerHandle(reference.TargetId) is not ILocalNode target) - { - referencesForNode.Add(reference); - continue; - } - - // delete the backward reference. - target.References - .Remove(reference.ReferenceTypeId, !reference.IsInverse, node.NodeId); - - // check for children that need to be deleted. - if (deleteChildren && - Server.TypeTree - .IsTypeOf(reference.ReferenceTypeId, ReferenceTypeIds.Aggregates) && - !reference.IsInverse) - { - nodesToDelete.Add(target); - } - } - - if (referencesForNode.Count > 0) - { - referencesToDelete[node.NodeId] = referencesForNode; - } - } - - // delete the child nodes. - foreach (ILocalNode nodeToDelete in nodesToDelete) - { - DeleteNode(nodeToDelete, deleteChildren, instance, referencesToDelete); - } - } - - /// - /// Deletes the external references to a node in a background thread. - /// - private async ValueTask OnDeleteReferencesAsync(Dictionary> referencesToDelete) - { - foreach (KeyValuePair> current in referencesToDelete) - { - try - { - await Server.NodeManager.DeleteReferencesAsync(current.Key, current.Value) - .ConfigureAwait(false); - } - catch (Exception e) - { - m_logger.LogError(e, "Error deleting references for node: {NodeId}", current.Key); - } - } - } - - /// - /// Verifies that the source and the target meet the restrictions imposed by the reference type. - /// - /// - private void ValidateReference( - ILocalNode source, - NodeId referenceTypeId, - bool isInverse, - NodeClass targetNodeClass) - { - // find reference type. - if (GetLocalNode(referenceTypeId) is not IReferenceType) - { - throw ServiceResultException.Create( - StatusCodes.BadReferenceTypeIdInvalid, - "Reference type '{0}' does not exist.", - referenceTypeId); - } - - // swap the source and target for inverse references. - NodeClass sourceNodeClass = source.NodeClass; - - if (isInverse) - { - sourceNodeClass = targetNodeClass; - targetNodeClass = source.NodeClass; - } - - // check HasComponent references. - if (Server.TypeTree.IsTypeOf(referenceTypeId, ReferenceTypeIds.HasComponent)) - { - if (( - (int)sourceNodeClass & - ( - (int)NodeClass.Object | - (int)NodeClass.Variable | - (int)NodeClass.ObjectType | - (int)NodeClass.VariableType) - ) == 0) - { - throw ServiceResultException.Create( - StatusCodes.BadReferenceNotAllowed, - "Source node cannot be used with HasComponent references."); - } - - if (((int)targetNodeClass & - ((int)NodeClass.Object | - (int)NodeClass.Variable | - (int)NodeClass.Method)) == - 0) - { - throw ServiceResultException.Create( - StatusCodes.BadReferenceNotAllowed, - "Target node cannot be used with HasComponent references."); - } - - if (targetNodeClass == NodeClass.Variable && - ((int)targetNodeClass & - ((int)NodeClass.Variable | (int)NodeClass.VariableType)) == 0) - { - throw ServiceResultException.Create( - StatusCodes.BadReferenceNotAllowed, - "A Variable must be a component of an Variable or VariableType."); - } - - if (targetNodeClass == NodeClass.Method && - ((int)sourceNodeClass & - ((int)NodeClass.Object | (int)NodeClass.ObjectType)) == 0) - { - throw ServiceResultException.Create( - StatusCodes.BadReferenceNotAllowed, - "A Method must be a component of an Object or ObjectType."); - } - } - - // check HasProperty references. - if (Server.TypeTree.IsTypeOf(referenceTypeId, ReferenceTypes.HasProperty) && - targetNodeClass != NodeClass.Variable) - { - throw ServiceResultException.Create( - StatusCodes.BadReferenceNotAllowed, - "Targets of HasProperty references must be Variables."); - } - - // check HasSubtype references. - if (Server.TypeTree.IsTypeOf(referenceTypeId, ReferenceTypeIds.HasSubtype)) - { - if (( - (int)sourceNodeClass & - ( - (int)NodeClass.DataType | - (int)NodeClass.ReferenceType | - (int)NodeClass.ObjectType | - (int)NodeClass.VariableType) - ) == 0) - { - throw ServiceResultException.Create( - StatusCodes.BadReferenceNotAllowed, - "Source node cannot be used with HasSubtype references."); - } - - if (targetNodeClass != sourceNodeClass) - { - throw ServiceResultException.Create( - StatusCodes.BadReferenceNotAllowed, - "The source and target cannot be connected by a HasSubtype reference."); - } - } - - // TBD - check rules for other reference types. - } - - /// - /// Adds a reference between two existing nodes. - /// - public ServiceResult AddReference( - NodeId sourceId, - NodeId referenceTypeId, - bool isInverse, - NodeId targetId, - bool bidirectional) - { - lock (DataLock) - { - // find source. - if (GetManagerHandle(sourceId) is not ILocalNode source) - { - return StatusCodes.BadParentNodeIdInvalid; - } - - // add reference from target to source. - if (bidirectional) - { - // find target. - if (GetManagerHandle(targetId) is not ILocalNode target) - { - return StatusCodes.BadNodeIdUnknown; - } - - // ensure the reference is valid. - ValidateReference(source, referenceTypeId, isInverse, target.NodeClass); - - // add reference from target to source. - AddReferenceToLocalNode(target, referenceTypeId, !isInverse, sourceId, false); - } - - // add reference from source to target. - AddReferenceToLocalNode(source, referenceTypeId, isInverse, targetId, false); - - return null; - } - } - - /// - /// Ensures any changes to built-in nodes are reflected in the diagnostics node manager. - /// - private void AddReferenceToLocalNode( - ILocalNode source, - NodeId referenceTypeId, - bool isInverse, - ExpandedNodeId targetId, - bool isInternal) - { - source.References.Add(referenceTypeId, isInverse, targetId); - - if (!isInternal && source.NodeId.NamespaceIndex == 0) - { - lock (Server.DiagnosticsLock) - { - NodeState state = Server.DiagnosticsNodeManager - .FindPredefinedNode(source.NodeId); - - if (state != null) - { - INodeBrowser browser = state.CreateBrowser( - Server.DefaultSystemContext, - null, - referenceTypeId, - true, - isInverse ? BrowseDirection.Inverse : BrowseDirection.Forward, - default, - null, - true); - - bool found = false; - - for (IReference reference = browser.Next(); - reference != null; - reference = browser.Next()) - { - if (reference.TargetId == targetId) - { - found = true; - break; - } - } - - if (!found) - { - state.AddReference(referenceTypeId, isInverse, targetId); - } - } - } - } - } - - /// - /// Adds a reference between two existing nodes. - /// - /// - public void CreateReference( - NodeId sourceId, - NodeId referenceTypeId, - bool isInverse, - NodeId targetId, - bool bidirectional) - { - lock (DataLock) - { - ServiceResult result = AddReference( - sourceId, - referenceTypeId, - isInverse, - targetId, - bidirectional); - - if (ServiceResult.IsBad(result)) - { - throw new ServiceResultException(result); - } - } - } - - /// - /// Adds a reference to the address space. - /// - private void AddReference( - ILocalNode source, - NodeId referenceTypeId, - bool isInverse, - ILocalNode target, - bool bidirectional) - { - AddReferenceToLocalNode(source, referenceTypeId, isInverse, target.NodeId, false); - - if (bidirectional) - { - AddReferenceToLocalNode(target, referenceTypeId, !isInverse, source.NodeId, false); - } - } - - /// - /// Adds a reference to the address space. - /// - private void AddReference( - ILocalNode source, - NodeId referenceTypeId, - bool isInverse, - ExpandedNodeId targetId) - { - AddReferenceToLocalNode(source, referenceTypeId, isInverse, targetId, false); - } - - /// - /// Deletes a reference. - /// - /// is null. - public ServiceResult DeleteReference( - object sourceHandle, - NodeId referenceTypeId, - bool isInverse, - ExpandedNodeId targetId, - bool deleteBidirectional) - { - if (sourceHandle == null) - { - throw new ArgumentNullException(nameof(sourceHandle)); - } - - if (referenceTypeId.IsNull) - { - throw new ArgumentNullException(nameof(referenceTypeId)); - } - - if (targetId.IsNull) - { - throw new ArgumentNullException(nameof(targetId)); - } - - lock (DataLock) - { - if (sourceHandle is not ILocalNode source) - { - return StatusCodes.BadSourceNodeIdInvalid; - } - - source.References.Remove(referenceTypeId, isInverse, targetId); - - if (deleteBidirectional) - { - var target = GetManagerHandle(targetId) as ILocalNode; - - target?.References.Remove(referenceTypeId, !isInverse, source.NodeId); - } - - return ServiceResult.Good; - } - } - - /// - /// Deletes a reference. - /// - /// - public void DeleteReference( - NodeId sourceId, - NodeId referenceTypeId, - bool isInverse, - ExpandedNodeId targetId, - bool deleteBidirectional) - { - ServiceResult result = DeleteReference( - GetManagerHandle(sourceId) as ILocalNode, - referenceTypeId, - isInverse, - targetId, - deleteBidirectional); - - if (ServiceResult.IsBad(result)) - { - throw new ServiceResultException(result); - } - } - - /// - /// Adds a node to the address space. - /// - private void AddNode(ILocalNode node) - { - m_nodes.Attach(node); - } - - /// - /// Returns a node managed by the manager with the specified node id. - /// - public ILocalNode GetLocalNode(ExpandedNodeId nodeId) - { - if (nodeId.IsNull) - { - return null; - } - - // check for absolute declarations of local nodes. - if (nodeId.IsAbsolute) - { - if (nodeId.ServerIndex != 0) - { - return null; - } - - int namespaceIndex = Server.NamespaceUris.GetIndex(nodeId.NamespaceUri); - - if (namespaceIndex < 0 || nodeId.NamespaceIndex >= Server.NamespaceUris.Count) - { - return null; - } - - return GetLocalNode(nodeId.InnerNodeId.WithNamespaceIndex((ushort)namespaceIndex)); - } - - return GetLocalNode((NodeId)nodeId); - } - - /// - /// Returns a node managed by the manager with the specified node id. - /// - public ILocalNode GetLocalNode( - NodeId nodeId, - NodeId referenceTypeId, - bool isInverse, - bool includeSubtypes, - QualifiedName browseName) - { - lock (DataLock) - { - return m_nodes.Find( - nodeId, - referenceTypeId, - isInverse, - includeSubtypes, - browseName) as ILocalNode; - } - } - - /// - /// Returns a node managed by the manager with the specified node id. - /// - public ILocalNode GetLocalNode(NodeId nodeId) - { - lock (DataLock) - { - return m_nodes.Find(nodeId) as ILocalNode; - } - } - - /// - /// Returns a list of nodes which are targets of the specified references. - /// - public IList GetLocalNodes( - NodeId sourceId, - NodeId referenceTypeId, - bool isInverse, - bool includeSubtypes) - { - lock (DataLock) - { - var targets = new List(); - - ILocalNode source = GetLocalNode(sourceId); - - if (source == null) - { - return targets; - } - - foreach ( - IReference reference in source.References - .Find(referenceTypeId, isInverse, true, m_nodes.TypeTree)) - { - ILocalNode target = GetLocalNode(reference.TargetId); - - if (target != null) - { - targets.Add(target); - } - } - - return targets; - } - } - - /// - /// Returns a node managed by the manager that has the specified browse name. - /// - public ILocalNode GetTargetNode( - NodeId sourceId, - NodeId referenceTypeId, - bool isInverse, - bool includeSubtypes, - QualifiedName browseName) - { - lock (DataLock) - { - ILocalNode source = GetLocalNode(sourceId); - - if (source == null) - { - return null; - } - - return GetTargetNode( - source, - referenceTypeId, - isInverse, - includeSubtypes, - browseName); - } - } - - /// - /// Returns a node managed by the manager that has the specified browse name. - /// - private ILocalNode GetTargetNode( - ILocalNode source, - NodeId referenceTypeId, - bool isInverse, - bool includeSubtypes, - QualifiedName browseName) - { - foreach ( - IReference reference in source.References.Find( - referenceTypeId, - isInverse, - includeSubtypes, - Server.TypeTree)) - { - ILocalNode target = GetLocalNode(reference.TargetId); - - if (target == null) - { - continue; - } - - if (browseName.IsNull || browseName == target.BrowseName) - { - return target; - } - } - - return null; - } - - /// - /// Attaches a node to the address space. - /// - public void AttachNode(ILocalNode node) - { - AttachNode(node, false); - } - - /// - /// Attaches a node to the address space. - /// - /// is null. - /// - private void AttachNode(ILocalNode node, bool isInternal) - { - if (node == null) - { - throw new ArgumentNullException(nameof(node)); - } - - lock (DataLock) - { - // check if node exists. - if (m_nodes.Exists(node.NodeId)) - { - throw ServiceResultException.Create( - StatusCodes.BadNodeIdExists, - "A node with the same node id already exists: {0}", - node.NodeId); - } - - // ensure reverse references exist. - foreach (IReference reference in node.References) - { - // ignore references that are always one way. - if (reference.ReferenceTypeId == ReferenceTypeIds.HasTypeDefinition || - reference.ReferenceTypeId == ReferenceTypeIds.HasModellingRule) - { - continue; - } - - // find target. - ILocalNode target = GetLocalNode(reference.TargetId); - - if (target != null) - { - AddReferenceToLocalNode( - target, - reference.ReferenceTypeId, - !reference.IsInverse, - node.NodeId, - isInternal); - } - } - - // must generate a model change event. - AddNode(node); - } - } - - /// - /// Creates a unique node identifier. - /// - public NodeId CreateUniqueNodeId() - { - return CreateUniqueNodeId(m_dynamicNamespaceIndex); - } - - /// - private object GetManagerHandle(ExpandedNodeId nodeId) - { - lock (DataLock) - { - if (nodeId.IsNull || nodeId.IsAbsolute) - { - return null; - } - - return GetLocalNode(nodeId); - } - } - - /// - /// Reads the EU Range for a variable. - /// - private ServiceResult ReadEURange( - OperationContext context, - ILocalNode node, - out Range range) - { - range = null; - - if (GetTargetNode( - node, - ReferenceTypes.HasProperty, - false, - true, - QualifiedName.From(BrowseNames.EURange)) is not IVariable target) - { - return StatusCodes.BadNodeIdUnknown; - } - - range = target.Value as Range; - - if (range == null) - { - return StatusCodes.BadTypeMismatch; - } - - return ServiceResult.Good; - } - - /// - /// Validates a filter for a monitored item. - /// - private ServiceResult ValidateFilter( - NodeMetadata metadata, - uint attributeId, - ExtensionObject filter, - out bool rangeRequired) - { - rangeRequired = false; - - // check filter. - DataChangeFilter datachangeFilter = null; - - if (!filter.IsNull) - { - datachangeFilter = filter.Body as DataChangeFilter; - } - - if (datachangeFilter != null) - { - // get the datatype of the node. - NodeId datatypeId = metadata.DataType; - - // check that filter is valid. - ServiceResult error = datachangeFilter.Validate(); - - if (ServiceResult.IsBad(error)) - { - return error; - } - - // check datatype of the variable. - if (!Server.TypeTree.IsTypeOf(datatypeId, DataTypes.Number)) - { - return StatusCodes.BadDeadbandFilterInvalid; - } - - // percent deadbands only allowed for analog data items. - if (datachangeFilter.DeadbandType == (int)DeadbandType.Percent) - { - ExpandedNodeId typeDefinitionId = metadata.TypeDefinition; - - if (typeDefinitionId.IsNull) - { - return StatusCodes.BadDeadbandFilterInvalid; - } - - // percent deadbands only allowed for analog data items. - if (!Server.TypeTree.IsTypeOf(typeDefinitionId, VariableTypes.AnalogItemType)) - { - return StatusCodes.BadDeadbandFilterInvalid; - } - - // the EURange property is required to use the filter. - rangeRequired = true; - } - } - - // filter is valid - return ServiceResult.Good; - } - - /// - /// Creates a new unique identifier for a node. - /// - private NodeId CreateUniqueNodeId(ushort namespaceIndex) - { - return new NodeId(Utils.IncrementIdentifier(ref m_lastId), namespaceIndex); - } - - private readonly NodeTable m_nodes; - private uint m_lastId; - private readonly SamplingGroupManager m_samplingGroupManager; - private readonly Dictionary m_monitoredItems; - private readonly double m_defaultMinimumSamplingInterval; - private readonly List m_namespaceUris; - private readonly ushort m_dynamicNamespaceIndex; - private readonly ILogger m_logger; } } diff --git a/Libraries/Opc.Ua.Server/NodeManager/MainNodeManagerFactory.cs b/Libraries/Opc.Ua.Server/NodeManager/MainNodeManagerFactory.cs index 2626f188b..24a6e677f 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/MainNodeManagerFactory.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/MainNodeManagerFactory.cs @@ -55,7 +55,7 @@ public IConfigurationNodeManager CreateConfigurationNodeManager() /// public ICoreNodeManager CreateCoreNodeManager(ushort dynamicNamespaceIndex) { - return new CoreNodeManager(m_server, m_applicationConfiguration, dynamicNamespaceIndex); + return new CoreNodeManager2(m_server, m_applicationConfiguration, dynamicNamespaceIndex); } private readonly ApplicationConfiguration m_applicationConfiguration; diff --git a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs index 8c272fd59..415935fd9 100644 --- a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs +++ b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs @@ -642,6 +642,10 @@ .. m_configuration.ServerConfiguration.ServerProfileArray m_configuration.TransportQuotas.MaxStringLength; serverObject.ServerCapabilities.MaxByteStringLength.Value = (uint) m_configuration.TransportQuotas.MaxByteStringLength; + serverObject.ServerCapabilities.MaxSessions.Value = (uint) + m_configuration.ServerConfiguration.MaxSessionCount; + serverObject.ServerCapabilities.MaxSubscriptions.Value = (uint) + m_configuration.ServerConfiguration.MaxSubscriptionCount; // Any operational limits Property that is provided shall have a non zero value. OperationLimitsState operationLimits = serverObject.ServerCapabilities From 2aec6a63fd691048690d4148b33bede2efcd8b97 Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Sun, 15 Feb 2026 13:30:31 +0100 Subject: [PATCH 3/6] Add Test --- .../Diagnostics/DiagnosticsNodeManager.cs | 2 +- .../NodeManager/CoreNodeManager.cs | 4 +- .../CoreNodeManager2Tests.cs | 113 ++++++++++++++++++ 3 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 Tests/Opc.Ua.Server.Tests/CoreNodeManager2Tests.cs diff --git a/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs b/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs index 96964374f..02ce9a9fc 100644 --- a/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs @@ -151,7 +151,7 @@ public override void CreateAddressSpace( // The nodes are now loaded by the DiagnosticsNodeManager from the file // output by the ModelDesigner V2. These nodes are added to the CoreNodeManager - // via the AttachNode() method when the DiagnosticsNodeManager starts. + // via the ImportNodes() method when the DiagnosticsNodeManager starts. Server.CoreNodeManager.ImportNodes(SystemContext, PredefinedNodes.Values, true); // hook up the server GetMonitoredItems method. diff --git a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs index fb55106c1..d14ef8c59 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs @@ -44,7 +44,7 @@ public class CoreNodeManager2 : CustomNodeManager2, ICoreNodeManager public CoreNodeManager2( IServerInternal server, ApplicationConfiguration configuration) - : base(server, configuration) + : base(server, configuration, useSamplingGroups: true) { } @@ -55,7 +55,7 @@ public CoreNodeManager2( IServerInternal server, ApplicationConfiguration configuration, ushort dynamicNamespaceIndex) - : base(server, configuration, server.NamespaceUris.GetString(dynamicNamespaceIndex)) + : base(server, configuration, useSamplingGroups: true, server.NamespaceUris.GetString(dynamicNamespaceIndex)) { } diff --git a/Tests/Opc.Ua.Server.Tests/CoreNodeManager2Tests.cs b/Tests/Opc.Ua.Server.Tests/CoreNodeManager2Tests.cs new file mode 100644 index 000000000..d1c1fc919 --- /dev/null +++ b/Tests/Opc.Ua.Server.Tests/CoreNodeManager2Tests.cs @@ -0,0 +1,113 @@ +/* ======================================================================== + * 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.Collections.Generic; +using System.Threading.Tasks; +using NUnit.Framework; +using Opc.Ua.Tests; +using Quickstarts.ReferenceServer; + +namespace Opc.Ua.Server.Tests +{ + [TestFixture] + [Category("CoreNodeManager2")] + [Parallelizable] + public class CoreNodeManager2Tests + { + [Test] + public async Task ImportNodes_IsInternal_UpdatesDiagnosticsAsync() + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + var fixture = new ServerFixture(t => new ReferenceServer(t)); + + try + { + StandardServer server = await fixture.StartAsync().ConfigureAwait(false); + + // Create a CoreNodeManager2 + var config = new ApplicationConfiguration + { + ServerConfiguration = new ServerConfiguration + { + MaxNotificationQueueSize = 100, + MaxDurableNotificationQueueSize = 100 + } + }; + var nodeManager = new CoreNodeManager2(server.CurrentInstance, config); + nodeManager.CreateAddressSpace(new Dictionary>()); + + // Create a node in Namespace 0 that also exists in DiagnosticsNodeManager (e.g. Server Object) + // Note: We need a node that exists in DiagnosticsNodeManager. StandardServer populates it with BaseNodes. + // Let's use ObjectIds.Server. + var serverNode = new BaseObjectState(null) + { + NodeId = ObjectIds.Server, + BrowseName = new QualifiedName(BrowseNames.Server, 0), + DisplayName = new LocalizedText("Server") + }; + + // Add a reference that we want to check + var targetNodeId = new NodeId(1234, 1); // Some random target + serverNode.AddReference(ReferenceTypeIds.HasComponent, false, targetNodeId); + + // Act - isInternal = false + nodeManager.ImportNodes(server.CurrentInstance.DefaultSystemContext, [serverNode], false); + + // Assert + // Check if DiagnosticsNodeManager has the reference + NodeState diagNode = server.CurrentInstance.DiagnosticsNodeManager.FindPredefinedNode(ObjectIds.Server); + Assert.That(diagNode, Is.Not.Null, "Diagnostics node should exist"); + Assert.That(diagNode.ReferenceExists(ReferenceTypeIds.HasComponent, false, targetNodeId), Is.True, "Reference should be added to diagnostics"); + + // Cleanup reference to verify isInternal = true + diagNode.RemoveReference(ReferenceTypeIds.HasComponent, false, targetNodeId); + Assert.That(diagNode.ReferenceExists(ReferenceTypeIds.HasComponent, false, targetNodeId), Is.False, "Reference should be removed"); + + // Act - isInternal = true + var serverNode2 = new BaseObjectState(null) + { + NodeId = ObjectIds.Server, + BrowseName = new QualifiedName(BrowseNames.Server, 0), + DisplayName = new LocalizedText("Server") + }; + serverNode2.AddReference(ReferenceTypeIds.HasComponent, false, targetNodeId); + + nodeManager.ImportNodes(server.CurrentInstance.DefaultSystemContext, [serverNode2], true); + + // Assert + Assert.That(diagNode.ReferenceExists(ReferenceTypeIds.HasComponent, false, targetNodeId), Is.False, + "Reference should NOT be added to diagnostics when isInternal=true"); + } + finally + { + await fixture.StopAsync().ConfigureAwait(false); + } + } + } +} From 8db003265b234b5cd0591f8585ccf6040a650a49 Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Sun, 15 Feb 2026 16:11:48 +0100 Subject: [PATCH 4/6] Add INodeManager3 with role permission validation Introduce INodeManager3 interface extending INodeManager2, adding synchronous role permission validation methods. Update all node manager interfaces and classes to use INodeManager3. Extend IAsyncNodeManager with async permission validation methods and update adapters for sync/async bridging. Deprecate FindNodeInAddressSpace in favor of new async FindNodeInAddressSpaceAsync. Refactor MonitoredNode2 to use thread-safe collections and locking for monitored items. Enhance event reporting with security and permission checks. Update constructors and documentation to reflect new interfaces and best practices. --- .../Configuration/ConfigurationNodeManager.cs | 6 +- .../IConfigurationNodeManager.cs | 2 +- .../Diagnostics/IDiagnosticsNodeManager.cs | 2 +- .../Adapters/AsyncNodeManagerAdapter.cs | 32 ++++ .../Adapters/SyncNodeManagerAdapter.cs | 18 +- .../NodeManager/CustomNodeManager.cs | 20 +-- .../NodeManager/IMasterNodeManager.cs | 6 + .../Opc.Ua.Server/NodeManager/INodeManager.cs | 65 ++++++-- .../NodeManager/MasterNodeManager.cs | 20 +++ .../MonitoredItem/MonitoredNode.cs | 157 +++++++----------- .../MonitoredNodeMonitoredItemManager.cs | 16 +- .../SamplingGroupMonitoredItemManager.cs | 10 +- 12 files changed, 206 insertions(+), 148 deletions(-) diff --git a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index f4aad62e1..dc5990cc1 100644 --- a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -184,7 +184,7 @@ protected override NodeState AddBehaviourToPredefinedNode( } else { - NodeState serverNode = FindNodeInAddressSpace(ObjectIds.Server); + NodeState serverNode = Server.NodeManager.FindNodeInAddressSpaceAsync(ObjectIds.Server).AsTask().GetAwaiter().GetResult(); serverNode?.ReplaceChild(context, activeNode); } // remove the reference to server node because it is set as parent @@ -1192,8 +1192,8 @@ private NamespaceMetadataState FindNamespaceMetadataState(string namespaceUri) var nameSpaceNodeId = ExpandedNodeId.ToNodeId( serverNamespacesReference.TargetId, Server.NamespaceUris); - if (FindNodeInAddressSpace( - nameSpaceNodeId) is not NamespaceMetadataState namespaceMetadata) + if (Server.NodeManager.FindNodeInAddressSpaceAsync( + nameSpaceNodeId).AsTask().GetAwaiter().GetResult() is not NamespaceMetadataState namespaceMetadata) { continue; } diff --git a/Libraries/Opc.Ua.Server/Configuration/IConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/IConfigurationNodeManager.cs index b7b73916c..c65b31abf 100644 --- a/Libraries/Opc.Ua.Server/Configuration/IConfigurationNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Configuration/IConfigurationNodeManager.cs @@ -32,7 +32,7 @@ namespace Opc.Ua.Server /// /// The Server Configuration Node Manager. /// - public interface IConfigurationNodeManager : INodeManager2 + public interface IConfigurationNodeManager : INodeManager3 { /// /// Gets or creates the node for the specified NamespaceUri. diff --git a/Libraries/Opc.Ua.Server/Diagnostics/IDiagnosticsNodeManager.cs b/Libraries/Opc.Ua.Server/Diagnostics/IDiagnosticsNodeManager.cs index 3c1d370ea..bac24d3ac 100644 --- a/Libraries/Opc.Ua.Server/Diagnostics/IDiagnosticsNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Diagnostics/IDiagnosticsNodeManager.cs @@ -32,7 +32,7 @@ namespace Opc.Ua.Server /// /// A node manager the diagnostic information exposed by the server. /// - public interface IDiagnosticsNodeManager : INodeManager2, INodeIdFactory + public interface IDiagnosticsNodeManager : INodeManager3, INodeIdFactory { /// /// True if diagnostics are currently enabled. diff --git a/Libraries/Opc.Ua.Server/NodeManager/Adapters/AsyncNodeManagerAdapter.cs b/Libraries/Opc.Ua.Server/NodeManager/Adapters/AsyncNodeManagerAdapter.cs index 28c0b35e2..d457c82fe 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/Adapters/AsyncNodeManagerAdapter.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/Adapters/AsyncNodeManagerAdapter.cs @@ -606,6 +606,38 @@ public ValueTask WriteAsync( return default; } + /// + public ValueTask ValidateEventRolePermissionsAsync(IEventMonitoredItem monitoredItem, IFilterTarget filterTarget) + { + if (SyncNodeManager is IAsyncNodeManager asyncNodeManager) + { + return asyncNodeManager.ValidateEventRolePermissionsAsync(monitoredItem, filterTarget); + } + + if (SyncNodeManager is INodeManager3 nodeManager2) + { + return new ValueTask(nodeManager2.ValidateEventRolePermissions(monitoredItem, filterTarget)); + } + + return new ValueTask(ServiceResult.Good); + } + + /// + public ValueTask ValidateRolePermissionsAsync(OperationContext operationContext, NodeId nodeId, PermissionType requestedPermission) + { + if (SyncNodeManager is IAsyncNodeManager asyncNodeManager) + { + return asyncNodeManager.ValidateRolePermissionsAsync(operationContext, nodeId, requestedPermission); + } + + if (SyncNodeManager is INodeManager3 nodeManager2) + { + return new ValueTask(nodeManager2.ValidateRolePermissions(operationContext, nodeId, requestedPermission)); + } + + return new ValueTask(ServiceResult.Good); + } + /// /// Frees any unmanaged resources. /// diff --git a/Libraries/Opc.Ua.Server/NodeManager/Adapters/SyncNodeManagerAdapter.cs b/Libraries/Opc.Ua.Server/NodeManager/Adapters/SyncNodeManagerAdapter.cs index 4d3559af7..5b70fe8ae 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/Adapters/SyncNodeManagerAdapter.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/Adapters/SyncNodeManagerAdapter.cs @@ -42,9 +42,9 @@ public static class SyncNodeManagerAdapterFactory /// if the NodeManager does not implement the interface uses the /// to create an ISyncNodeManager compatible object /// - public static INodeManager2 ToSyncNodeManager(this IAsyncNodeManager nodeManager) + public static INodeManager3 ToSyncNodeManager(this IAsyncNodeManager nodeManager) { - if (nodeManager is INodeManager2 syncNodeManager) + if (nodeManager is INodeManager3 syncNodeManager) { return syncNodeManager; } @@ -59,7 +59,7 @@ public static INodeManager2 ToSyncNodeManager(this IAsyncNodeManager nodeManager /// This allows asynchronous nodeManagers to be treated as synchronous, which can help /// compatibility with existing code. /// - public class SyncNodeManagerAdapter : INodeManager2 + public class SyncNodeManagerAdapter : INodeManager3 { /// /// Initializes a new instance of the class. @@ -288,6 +288,18 @@ public NodeMetadata GetPermissionMetadata( .AsTask().GetAwaiter().GetResult(); } + /// + public ServiceResult ValidateEventRolePermissions(IEventMonitoredItem monitoredItem, IFilterTarget filterTarget) + { + return m_nodeManager.ValidateEventRolePermissionsAsync(monitoredItem, filterTarget).AsTask().GetAwaiter().GetResult(); + } + + /// + public ServiceResult ValidateRolePermissions(OperationContext operationContext, NodeId nodeId, PermissionType requestedPermission) + { + return m_nodeManager.ValidateRolePermissionsAsync(operationContext, nodeId, requestedPermission).AsTask().GetAwaiter().GetResult(); + } + private readonly IAsyncNodeManager m_nodeManager; } } diff --git a/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs index 2f862a899..926d77d40 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs @@ -48,7 +48,7 @@ namespace Opc.Ua.Server /// 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 : INodeManager2, INodeIdFactory, IDisposable + public partial class CustomNodeManager2 : INodeManager3, INodeIdFactory, IDisposable { /// /// Initializes the node manager. @@ -169,7 +169,7 @@ protected CustomNodeManager2( } else { - m_monitoredItemManager = new MonitoredNodeMonitoredItemManager(this); + m_monitoredItemManager = new MonitoredNodeMonitoredItemManager(this, server); } PredefinedNodes = []; @@ -445,22 +445,10 @@ public bool DeleteNode(ServerSystemContext context, NodeId nodeId) /// /// Searches the node id in all node managers /// + [Obsolete("Use IServerInteral.IMasterNodeManager.FindNodeInAddressSpaceAsync instead.")] public NodeState FindNodeInAddressSpace(NodeId nodeId) { - if (nodeId.IsNull) - { - return null; - } - // search node id in all node managers - foreach (INodeManager nodeManager in Server.NodeManager.NodeManagers) - { - if (nodeManager.GetManagerHandle(nodeId) is not NodeHandle handle) - { - continue; - } - return handle.Node; - } - return null; + return Server.NodeManager.FindNodeInAddressSpaceAsync(nodeId).AsTask().GetAwaiter().GetResult(); } /// diff --git a/Libraries/Opc.Ua.Server/NodeManager/IMasterNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/IMasterNodeManager.cs index 76a364663..1b211f4fd 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/IMasterNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/IMasterNodeManager.cs @@ -141,6 +141,12 @@ ValueTask DeleteMonitoredItemsAsync( /// ValueTask DeleteReferencesAsync(NodeId targetId, IList references, CancellationToken cancellationToken = default); + /// + /// Searches the node id in all node managers, + /// returns the node state if found (and node Manager supports it), otherwise returns null. + /// + ValueTask FindNodeInAddressSpaceAsync(NodeId nodeId); + /// /// Returns node handle and its node manager. /// diff --git a/Libraries/Opc.Ua.Server/NodeManager/INodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/INodeManager.cs index 5f2a9f2a1..898b2cc05 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/INodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/INodeManager.cs @@ -376,7 +376,28 @@ NodeMetadata GetPermissionMetadata( } /// - /// An asynchronous version of the "Call" method defined on the interface. + /// An interface to an object that manages a set of nodes in the address space. + /// + public interface INodeManager3 : INodeManager2 + { + /// + /// Validates if the specified event monitored item has enough permissions to receive the specified event + /// + ServiceResult ValidateEventRolePermissions( + IEventMonitoredItem monitoredItem, + IFilterTarget filterTarget); + + /// + /// Validates Role permissions for the specified NodeId + /// + ServiceResult ValidateRolePermissions( + OperationContext operationContext, + NodeId nodeId, + PermissionType requestedPermission); + } + + /// + /// An asynchronous version of the "Call" method defined on the interface. /// public interface ICallAsyncNodeManager { @@ -392,7 +413,7 @@ ValueTask CallAsync( } /// - /// An asynchronous version of the "Read" method defined on the interface. + /// An asynchronous version of the "Read" method defined on the interface. /// public interface IReadAsyncNodeManager { @@ -427,7 +448,7 @@ ValueTask ReadAsync( } /// - /// An asynchronous version of the "Write" method defined on the interface. + /// An asynchronous version of the "Write" method defined on the interface. /// public interface IWriteAsyncNodeManager { @@ -446,7 +467,7 @@ ValueTask WriteAsync( } /// - /// An asynchronous version of the "HistoryRead" method defined on the interface. + /// An asynchronous version of the "HistoryRead" method defined on the interface. /// public interface IHistoryReadAsyncNodeManager { @@ -465,7 +486,7 @@ ValueTask HistoryReadAsync( } /// - /// An asynchronous version of the "HistoryUpdate" method defined on the interface. + /// An asynchronous version of the "HistoryUpdate" method defined on the interface. /// public interface IHistoryUpdateAsyncNodeManager { @@ -482,7 +503,7 @@ ValueTask HistoryUpdateAsync( } /// - /// An asynchronous version of the "ConditionRefresh" method defined on the interface. + /// An asynchronous version of the "ConditionRefresh" method defined on the interface. /// public interface IConditionRefreshAsyncNodeManager { @@ -496,7 +517,7 @@ ValueTask ConditionRefreshAsync( } /// - /// An asynchronous version of the "TranslateBrowsePath" method defined on the interface. + /// An asynchronous version of the "TranslateBrowsePath" method defined on the interface. /// public interface ITranslateBrowsePathAsyncNodeManager { @@ -527,7 +548,7 @@ ValueTask TranslateBrowsePathAsync( } /// - /// An asynchronous version of the "Browse" method defined on the interface. + /// An asynchronous version of the "Browse" method defined on the interface. /// public interface IBrowseAsyncNodeManager { @@ -557,7 +578,7 @@ ValueTask BrowseAsync( } /// - /// An asynchronous version of the "SetMonitoringMode" method defined on the interface. + /// An asynchronous version of the "SetMonitoringMode" method defined on the interface. /// public interface ISetMonitoringModeAsyncNodeManager { @@ -574,7 +595,7 @@ ValueTask SetMonitoringModeAsync( } /// - /// An asynchronous version of the "TransferMonitoredItems" method defined on the interface. + /// An asynchronous version of the "TransferMonitoredItems" method defined on the interface. /// public interface ITransferMonitoredItemsAsyncNodeManager { @@ -594,7 +615,7 @@ ValueTask TransferMonitoredItemsAsync( } /// - /// An asynchronous version of the "DeleteMonitoredItems" method defined on the interface. + /// An asynchronous version of the "DeleteMonitoredItems" method defined on the interface. /// public interface IDeleteMonitoredItemsAsyncNodeManager { @@ -610,7 +631,7 @@ ValueTask DeleteMonitoredItemsAsync( } /// - /// An asynchronous version of the "ModifyMonitoredItems" method defined on the interface. + /// An asynchronous version of the "ModifyMonitoredItems" method defined on the interface. /// public interface IModifyMonitoredItemsAsyncNodeManager { @@ -628,7 +649,7 @@ ValueTask ModifyMonitoredItemsAsync( } /// - /// An asynchronous version of the "CreateMonitoredItems" method defined on the interface. + /// An asynchronous version of the "CreateMonitoredItems" method defined on the interface. /// public interface ICreateMonitoredItemsAsyncNodeManager { @@ -672,11 +693,10 @@ ValueTask CreateAsync( } /// - /// An asynchronous verison of the interface. + /// An asynchronous verison of the interface. /// This interface is in active development and will be extended in future releases. /// Please use the sub interfaces to implement async support for specific service calls. /// - [Experimental("UA_NETStandard_1")] public interface IAsyncNodeManager : ICallAsyncNodeManager, IReadAsyncNodeManager, @@ -847,6 +867,21 @@ ValueTask RestoreMonitoredItemsAsync( IList monitoredItems, IUserIdentity savedOwnerIdentity, CancellationToken cancellationToken = default); + + /// + /// Validates if the specified event monitored item has enough permissions to receive the specified event + /// + ValueTask ValidateEventRolePermissionsAsync( + IEventMonitoredItem monitoredItem, + IFilterTarget filterTarget); + + /// + /// Validates Role permissions for the specified NodeId + /// + ValueTask ValidateRolePermissionsAsync( + OperationContext operationContext, + NodeId nodeId, + PermissionType requestedPermission); } /// diff --git a/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs index cd01a0eec..4af1063ab 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs @@ -1794,6 +1794,26 @@ private async ValueTask UpdateReferenceDescriptionAsync( return true; } + /// + public async ValueTask FindNodeInAddressSpaceAsync(NodeId nodeId) + { + if (nodeId.IsNull) + { + return null; + } + // search node id in all node managers + foreach (IAsyncNodeManager nodeManager in AsyncNodeManagers) + { + if ((await nodeManager.GetManagerHandleAsync(nodeId).ConfigureAwait(false)) + is not NodeHandle handle) + { + continue; + } + return handle.Node; + } + return null; + } + /// public virtual async ValueTask<(DataValueCollection values, DiagnosticInfoCollection diagnosticInfos)> ReadAsync( OperationContext context, diff --git a/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNode.cs b/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNode.cs index 7a33e9d16..7360fd3f6 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNode.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNode.cs @@ -30,6 +30,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Threading; namespace Opc.Ua.Server { @@ -47,17 +48,19 @@ public class MonitoredNode2 /// Initializes a new instance of the class. /// /// The node manager. + /// The server. /// The node. - public MonitoredNode2(CustomNodeManager2 nodeManager, NodeState node) + public MonitoredNode2(INodeManager3 nodeManager, IServerInternal server, NodeState node) { NodeManager = nodeManager; + m_server = server; Node = node; } /// /// Gets or sets the NodeManager which the MonitoredNode belongs to. /// - public CustomNodeManager2 NodeManager { get; set; } + public INodeManager3 NodeManager { get; set; } /// /// Gets or sets the Node being monitored. @@ -67,12 +70,12 @@ public MonitoredNode2(CustomNodeManager2 nodeManager, NodeState node) /// /// Gets the current list of data change MonitoredItems. /// - public List DataChangeMonitoredItems { get; private set; } + public ConcurrentDictionary DataChangeMonitoredItems { get; } = new(); /// /// Gets the current list of event MonitoredItems. /// - public List EventMonitoredItems { get; private set; } + public ConcurrentDictionary EventMonitoredItems { get; } = new(); /// /// Gets a value indicating whether this instance has monitored items. @@ -84,12 +87,12 @@ public bool HasMonitoredItems { get { - if (DataChangeMonitoredItems != null && DataChangeMonitoredItems.Count > 0) + if (DataChangeMonitoredItems != null && !DataChangeMonitoredItems.IsEmpty) { return true; } - if (EventMonitoredItems != null && EventMonitoredItems.Count > 0) + if (EventMonitoredItems != null && !EventMonitoredItems.IsEmpty) { return true; } @@ -104,13 +107,9 @@ public bool HasMonitoredItems /// The monitored item. public void Add(IDataChangeMonitoredItem2 datachangeItem) { - if (DataChangeMonitoredItems == null) - { - DataChangeMonitoredItems = []; - Node.OnStateChanged = OnMonitoredNodeChanged; - } + DataChangeMonitoredItems.TryAdd(datachangeItem.Id, datachangeItem); - DataChangeMonitoredItems.Add(datachangeItem); + Node.OnStateChanged = OnMonitoredNodeChanged; } /// @@ -119,22 +118,14 @@ public void Add(IDataChangeMonitoredItem2 datachangeItem) /// The monitored item. public void Remove(IDataChangeMonitoredItem2 datachangeItem) { - for (int ii = 0; ii < DataChangeMonitoredItems.Count; ii++) + if (DataChangeMonitoredItems.TryRemove(datachangeItem.Id, out _)) { - if (ReferenceEquals(DataChangeMonitoredItems[ii], datachangeItem)) - { - DataChangeMonitoredItems.RemoveAt(ii); - - // Remove the cached context for the monitored item - m_contextCache.TryRemove(datachangeItem.Id, out _); - - break; - } + // Remove the cached context for the monitored item + m_contextCache.TryRemove(datachangeItem.Id, out _); } - if (DataChangeMonitoredItems.Count == 0) + if (DataChangeMonitoredItems.IsEmpty) { - DataChangeMonitoredItems = null; Node.OnStateChanged = null; } } @@ -145,13 +136,9 @@ public void Remove(IDataChangeMonitoredItem2 datachangeItem) /// The monitored item. public void Add(IEventMonitoredItem eventItem) { - if (EventMonitoredItems == null) - { - EventMonitoredItems = []; - Node.OnReportEvent = OnReportEvent; - } + EventMonitoredItems.TryAdd(eventItem.Id, eventItem); - EventMonitoredItems.Add(eventItem); + Node.OnReportEvent = OnReportEvent; } /// @@ -160,18 +147,10 @@ public void Add(IEventMonitoredItem eventItem) /// The monitored item. public void Remove(IEventMonitoredItem eventItem) { - for (int ii = 0; ii < EventMonitoredItems.Count; ii++) - { - if (ReferenceEquals(EventMonitoredItems[ii], eventItem)) - { - EventMonitoredItems.RemoveAt(ii); - break; - } - } + EventMonitoredItems.TryRemove(eventItem.Id, out _); - if (EventMonitoredItems.Count == 0) + if (EventMonitoredItems.IsEmpty) { - EventMonitoredItems = null; Node.OnReportEvent = null; } } @@ -184,76 +163,53 @@ public void Remove(IEventMonitoredItem eventItem) /// The event. public void OnReportEvent(ISystemContext context, NodeState node, IFilterTarget e) { - var eventMonitoredItems = new List(); - - lock (NodeManager.Lock) + // make sure to process events in the order they are received and avoid concurrent processing of events for the same node + lock (m_eventLock) { - if (EventMonitoredItems == null) - { - return; - } - - for (int ii = 0; ii < EventMonitoredItems.Count; ii++) + foreach (KeyValuePair kvp in EventMonitoredItems) { - IEventMonitoredItem monitoredItem = EventMonitoredItems[ii]; - // enqueue event for role permission validation - eventMonitoredItems.Add(monitoredItem); - } - } - - for (int ii = 0; ii < eventMonitoredItems.Count; ii++) - { - IEventMonitoredItem monitoredItem = eventMonitoredItems[ii]; + IEventMonitoredItem monitoredItem = kvp.Value; - if (e is AuditEventState) - { - // check Server.Auditing flag and skip if false - if (!NodeManager.Server.Auditing) + if (e is AuditEventState) { - continue; + // check Server.Auditing flag and skip if false + if (!m_server.Auditing) + { + continue; + } + // check if channel is not encrypted and skip if so + if (monitoredItem?.Session?.EndpointDescription?.SecurityMode != + MessageSecurityMode.SignAndEncrypt && + monitoredItem?.Session?.EndpointDescription?.TransportProfileUri != + Profiles.HttpsBinaryTransport) + { + continue; + } } - // check if channel is not encrypted and skip if so - if (monitoredItem?.Session?.EndpointDescription?.SecurityMode != - MessageSecurityMode.SignAndEncrypt && - monitoredItem?.Session?.EndpointDescription?.TransportProfileUri != - Profiles.HttpsBinaryTransport) + + // validate if the monitored item has the required role permissions to receive the event + ServiceResult validationResult = NodeManager.ValidateEventRolePermissions( + monitoredItem, + e); + + if (ServiceResult.IsBad(validationResult)) { + // skip event reporting for EventType without permissions continue; } - } - - // validate if the monitored item has the required role permissions to receive the event - ServiceResult validationResult = NodeManager.ValidateEventRolePermissions( - monitoredItem, - e); - - if (ServiceResult.IsBad(validationResult)) - { - // skip event reporting for EventType without permissions - continue; - } - lock (NodeManager.Lock) - { // enqueue event if (context is ISessionSystemContext session && !session.SessionId.IsNull && monitoredItem?.Session != null && - !monitoredItem.Session.Id.IsNull) + !monitoredItem.Session.Id.IsNull && + !monitoredItem.Session.Id.Equals(session.SessionId)) { - if (monitoredItem.Session.Id.Equals(session.SessionId)) - { - monitoredItem?.QueueEvent(e); - } - else - { - continue; - } - } - else - { - monitoredItem?.QueueEvent(e); + // skip if the event does not belong to the same session as the monitored item + continue; } + + monitoredItem?.QueueEvent(e); } } } @@ -269,16 +225,17 @@ public void OnMonitoredNodeChanged( NodeState node, NodeStateChangeMasks changes) { - lock (NodeManager.Lock) + //make shure to process Invocations in the order they are received and avoid concurrent processing of value changes for the same node + lock (m_dataChangelock) { if (DataChangeMonitoredItems == null) { return; } - for (int ii = 0; ii < DataChangeMonitoredItems.Count; ii++) + foreach (KeyValuePair kvp in DataChangeMonitoredItems) { - IDataChangeMonitoredItem2 monitoredItem = DataChangeMonitoredItems[ii]; + IDataChangeMonitoredItem2 monitoredItem = kvp.Value; if (monitoredItem.AttributeId == Attributes.Value && (changes & NodeStateChangeMasks.Value) != 0) @@ -391,5 +348,9 @@ private ServerSystemContext GetOrCreateContext( new(); private readonly int m_cacheLifetimeTicks = (int)TimeSpan.FromMinutes(5).TotalMilliseconds; + + private readonly Lock m_dataChangelock = new(); + private readonly Lock m_eventLock = new(); + private readonly IServerInternal m_server; } } diff --git a/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNodeMonitoredItemManager.cs b/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNodeMonitoredItemManager.cs index 811d7e4a6..91f34812d 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNodeMonitoredItemManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNodeMonitoredItemManager.cs @@ -39,9 +39,10 @@ namespace Opc.Ua.Server public class MonitoredNodeMonitoredItemManager : IMonitoredItemManager { /// - public MonitoredNodeMonitoredItemManager(CustomNodeManager2 nodeManager) + public MonitoredNodeMonitoredItemManager(INodeManager3 nodeManager, IServerInternal server) { m_nodeManager = nodeManager; + m_server = server; MonitoredNodes = []; MonitoredItems = new ConcurrentDictionary(); } @@ -77,7 +78,7 @@ public ISampledDataChangeMonitoredItem CreateMonitoredItem( { NodeState cachedNode = addNodeToComponentCache(context, handle, handle.Node); MonitoredNodes[handle.Node.NodeId] - = monitoredNode = new MonitoredNode2(m_nodeManager, cachedNode); + = monitoredNode = new MonitoredNode2(m_nodeManager, m_server, cachedNode); } handle.Node = monitoredNode.Node; @@ -201,7 +202,7 @@ public bool RestoreMonitoredItem( { NodeState cachedNode = addNodeToComponentCache(context, handle, handle.Node); MonitoredNodes[handle.Node.NodeId] - = monitoredNode = new MonitoredNode2(m_nodeManager, cachedNode); + = monitoredNode = new MonitoredNode2(m_nodeManager, m_server, cachedNode); } handle.Node = monitoredNode.Node; @@ -255,7 +256,7 @@ public ServiceResult ModifyMonitoredItem( IEventMonitoredItem monitoredItem, bool unsubscribe) { - MonitoredNode2 monitoredNode = null; + MonitoredNode2 monitoredNode; // handle unsubscribe. if (unsubscribe) { @@ -292,12 +293,12 @@ public ServiceResult ModifyMonitoredItem( if (!MonitoredNodes.TryGetValue(source.NodeId, out monitoredNode)) { MonitoredNodes[source.NodeId] - = monitoredNode = new MonitoredNode2(m_nodeManager, source); + = monitoredNode = new MonitoredNode2(m_nodeManager, m_server, source); } // remove existing monitored items with the same Id prior to insertion in order to avoid duplicates // this is necessary since the SubscribeToEvents method is called also from ModifyMonitoredItemsForEvents - monitoredNode.EventMonitoredItems?.RemoveAll(e => e.Id == monitoredItem.Id); + monitoredNode.EventMonitoredItems.TryRemove(monitoredItem.Id, out _); // this links the node to specified monitored item and ensures all events // reported by the node are added to the monitored item's queue. @@ -310,6 +311,7 @@ public ServiceResult ModifyMonitoredItem( return (monitoredNode, ServiceResult.Good); } - private readonly CustomNodeManager2 m_nodeManager; + private readonly INodeManager3 m_nodeManager; + private readonly IServerInternal m_server; } } diff --git a/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/SamplingGroupMonitoredItemManager.cs b/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/SamplingGroupMonitoredItemManager.cs index dab4a0316..edafa9f77 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/SamplingGroupMonitoredItemManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/SamplingGroupMonitoredItemManager.cs @@ -39,7 +39,7 @@ public class SamplingGroupMonitoredItemManager : IMonitoredItemManager { /// public SamplingGroupMonitoredItemManager( - CustomNodeManager2 nodeManager, + INodeManager3 nodeManager, IServerInternal server, ApplicationConfiguration configuration) { @@ -51,6 +51,7 @@ public SamplingGroupMonitoredItemManager( configuration.ServerConfiguration.AvailableSamplingRates); m_nodeManager = nodeManager; + m_server = server; MonitoredNodes = []; MonitoredItems = new ConcurrentDictionary(); } @@ -319,12 +320,12 @@ public bool RestoreMonitoredItem( if (!MonitoredNodes.TryGetValue(source.NodeId, out monitoredNode)) { MonitoredNodes[source.NodeId] - = monitoredNode = new MonitoredNode2(m_nodeManager, source); + = monitoredNode = new MonitoredNode2(m_nodeManager, m_server, source); } // remove existing monitored items with the same Id prior to insertion in order to avoid duplicates // this is necessary since the SubscribeToEvents method is called also from ModifyMonitoredItemsForEvents - monitoredNode.EventMonitoredItems?.RemoveAll(e => e.Id == monitoredItem.Id); + monitoredNode.EventMonitoredItems.TryRemove(monitoredItem.Id, out _); // this links the node to specified monitored item and ensures all events // reported by the node are added to the monitored item's queue. @@ -334,7 +335,8 @@ public bool RestoreMonitoredItem( return (monitoredNode, ServiceResult.Good); } - private readonly CustomNodeManager2 m_nodeManager; + private readonly INodeManager3 m_nodeManager; + private readonly IServerInternal m_server; private readonly SamplingGroupManager m_samplingGroupManager; } } From a6ae8abd31007c9c414ef1901e07319dd87df259 Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Mon, 16 Feb 2026 13:19:34 +0100 Subject: [PATCH 5/6] fix comment --- .../Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNode.cs b/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNode.cs index 7360fd3f6..f87fda512 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNode.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNode.cs @@ -225,7 +225,7 @@ public void OnMonitoredNodeChanged( NodeState node, NodeStateChangeMasks changes) { - //make shure to process Invocations in the order they are received and avoid concurrent processing of value changes for the same node + //make sure to process data change notifications in the order they are received and avoid concurrent processing of value changes for the same node lock (m_dataChangelock) { if (DataChangeMonitoredItems == null) From c5b71fc8fd3c8672b123770ad0c6a5eee6191ad5 Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Mon, 16 Feb 2026 21:32:43 +0100 Subject: [PATCH 6/6] fix --- .../NodeManager/CoreNodeManager.Obsolete.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.Obsolete.cs b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.Obsolete.cs index cc529ff13..d25563623 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.Obsolete.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.Obsolete.cs @@ -748,7 +748,7 @@ public void Read( // apply index range to value attributes. if (nodeToRead.AttributeId == Attributes.Value) { - object defaultValue = value.Value; + Variant defaultValue = value.WrappedValue; error = nodeToRead.ParsedIndexRange.ApplyRange(ref defaultValue); @@ -968,7 +968,7 @@ public void Write( } // check whether value being written is an instance of the expected data type. - object valueToWrite = nodeToWrite.Value.Value; + Variant valueToWrite = nodeToWrite.Value.WrappedValue; var typeInfo = TypeInfo.IsInstanceOfDataType( valueToWrite, @@ -992,7 +992,7 @@ public void Write( errors[ii] = StatusCodes.BadIndexRangeInvalid; continue; } - var array = (Array)valueToWrite; + var array = (Array)valueToWrite.AsBoxedObject(); if (nodeToWrite.ParsedIndexRange.Count != array.Length) { @@ -3589,9 +3589,7 @@ private ServiceResult ReadEURange( return StatusCodes.BadNodeIdUnknown; } - range = target.Value as Range; - - if (range == null) + if (!target.Value.TryGetStructure(out range)) { return StatusCodes.BadTypeMismatch; }