diff --git a/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs b/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs index 0ed69b096..10fbb1f8e 100644 --- a/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs @@ -155,8 +155,8 @@ 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. - Server.CoreNodeManager.ImportNodes(SystemContext, PredefinedNodes.Values, true); + // via ImportNodes() method when the DiagnosticsNodeManager starts. + Server.CoreNodeManager.ImportNodes(SystemContext, PredefinedNodes.Values); // hook up the server GetMonitoredItems method. var getMonitoredItems = (GetMonitoredItemsMethodState)FindPredefinedNode( diff --git a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs index 0679ffda4..f0605499a 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs @@ -42,8 +42,10 @@ namespace Opc.Ua.Server /// /// Every Server has one instance of this NodeManager. /// It stores objects that implement ILocalNode and indexes them by NodeId. + /// This class is deprecated. Use instead. /// - public class CoreNodeManager : INodeManager, IDisposable + [Obsolete("Use CoreNodeManager2 instead. This class will be removed in a future version.")] + public class CoreNodeManager : INodeManager, ICoreNodeManager, IDisposable { /// /// Initializes the object with default values. @@ -148,11 +150,11 @@ internal void ImportNodes( node.Export(context, nodesToExport); } - lock (Server.CoreNodeManager.DataLock) + lock (DataLock) { foreach (ILocalNode nodeToExport in nodesToExport.OfType()) { - Server.CoreNodeManager.AttachNode(nodeToExport, isInternal); + AttachNode(nodeToExport, isInternal); } } } @@ -3571,6 +3573,11 @@ public NodeId CreateUniqueNodeId() return CreateUniqueNodeId(m_dynamicNamespaceIndex); } + /// + /// Returns the namespace index used for dynamically created nodes. + /// + public ushort DynamicNamespaceIndex => m_dynamicNamespaceIndex; + /// private object GetManagerHandle(ExpandedNodeId nodeId) { @@ -3682,6 +3689,37 @@ private NodeId CreateUniqueNodeId(ushort namespaceIndex) return new NodeId(Utils.IncrementIdentifier(ref m_lastId), namespaceIndex); } + /// + /// Called when the session is closed. + /// + public void SessionClosing(OperationContext context, NodeId sessionId, bool deleteSubscriptions) + { + // No special handling needed for session closing in the core node manager + } + + /// + /// Returns true if the node is in the view. + /// + public bool IsNodeInView(OperationContext context, NodeId viewId, object nodeHandle) + { + // Core node manager supports all views + return true; + } + + /// + /// Returns the metadata needed for validating permissions, associated with the node. + /// + public NodeMetadata GetPermissionMetadata( + OperationContext context, + object targetHandle, + BrowseResultMask resultMask, + Dictionary> uniqueNodesServiceAttributesCache, + bool permissionsOnly) + { + // Delegate to the standard GetNodeMetadata method + return GetNodeMetadata(context, targetHandle, resultMask); + } + private readonly NodeTable m_nodes; private uint m_lastId; private readonly SamplingGroupManager m_samplingGroupManager; diff --git a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager2.cs b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager2.cs new file mode 100644 index 000000000..351843201 --- /dev/null +++ b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager2.cs @@ -0,0 +1,181 @@ +/* ======================================================================== + * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace Opc.Ua.Server +{ + /// + /// The core node manager for the server based on CustomNodeManager2. + /// + /// + /// Every Server has one instance of this NodeManager. + /// It manages the built-in OPC UA nodes and provides core functionality. + /// This is a refactored version of CoreNodeManager that inherits from CustomNodeManager2 + /// to consolidate the NodeManager implementations in the server library. + /// + public class CoreNodeManager2 : CustomNodeManager2, ICoreNodeManager + { + /// + /// Initializes the node manager with default values. + /// + public CoreNodeManager2( + IServerInternal server, + ApplicationConfiguration configuration, + ushort dynamicNamespaceIndex) + : base( + server, + configuration, + true, // Enable SamplingGroups + server.Telemetry.CreateLogger(), + Array.Empty()) // CoreNodeManager manages namespaces 0 and 1 by default + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + // Store the dynamic namespace index (typically namespace index 1) + m_dynamicNamespaceIndex = dynamicNamespaceIndex; + + // Use namespace 1 if out of range + if (m_dynamicNamespaceIndex == 0 || + m_dynamicNamespaceIndex >= server.NamespaceUris.Count) + { + m_dynamicNamespaceIndex = 1; + } + + // Set up namespaces - CoreNodeManager handles namespace 0 (UA) and 1 (server namespace) + SetNamespaceIndexes(new ushort[] { 0, m_dynamicNamespaceIndex }); + } + + /// + /// Acquires the lock on the node manager. + /// + /// + /// This property provides compatibility with the old CoreNodeManager API. + /// It maps to the Lock property from CustomNodeManager2. + /// + public object DataLock => Lock; + + /// + /// Returns an opaque handle identifying the node to the node manager. + /// + public override object GetManagerHandle(NodeId nodeId) + { + lock (Lock) + { + if (NodeId.IsNull(nodeId)) + { + return null; + } + + // Check if it's in namespace 0 (UA standard namespace) or the dynamic namespace + if (nodeId.NamespaceIndex != 0 && nodeId.NamespaceIndex != m_dynamicNamespaceIndex) + { + return null; + } + + // Try to find the node in predefined nodes + NodeState node = Find(nodeId); + if (node != null) + { + return new NodeHandle(nodeId, node); + } + + // Return null if not found (will be handled by other node managers) + return null; + } + } + + /// + /// Creates a unique node identifier. + /// + public NodeId CreateUniqueNodeId() + { + return CreateUniqueNodeId(m_dynamicNamespaceIndex); + } + + /// + /// Creates a new unique identifier for a node in the specified namespace. + /// + private NodeId CreateUniqueNodeId(ushort namespaceIndex) + { + return new NodeId(Utils.IncrementIdentifier(ref m_lastId), namespaceIndex); + } + + /// + /// Imports the nodes from a dictionary of NodeState objects. + /// + public void ImportNodes(ISystemContext context, IEnumerable predefinedNodes) + { + ImportNodes(context, predefinedNodes, false); + } + + /// + /// Imports the nodes from a dictionary of NodeState objects. + /// + internal void ImportNodes( + ISystemContext context, + IEnumerable predefinedNodes, + bool isInternal) + { + lock (Lock) + { + foreach (NodeState node in predefinedNodes) + { + // Add the node to the predefined nodes dictionary + AddPredefinedNode(context, node); + } + } + } + + /// + /// Attaches a node to the address space. + /// + /// + /// This method is provided for compatibility with the old CoreNodeManager. + /// It maps to AddPredefinedNode from CustomNodeManager2. + /// + internal void AttachNode(NodeState node, bool isInternal) + { + AddPredefinedNode(SystemContext, node); + } + + /// + /// Returns the namespace index used for dynamically created nodes. + /// + public ushort DynamicNamespaceIndex => m_dynamicNamespaceIndex; + + private uint m_lastId; + private readonly ushort m_dynamicNamespaceIndex; + } +} diff --git a/Libraries/Opc.Ua.Server/NodeManager/ICoreNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/ICoreNodeManager.cs new file mode 100644 index 000000000..20fc54515 --- /dev/null +++ b/Libraries/Opc.Ua.Server/NodeManager/ICoreNodeManager.cs @@ -0,0 +1,79 @@ +/* ======================================================================== + * 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; + +namespace Opc.Ua.Server +{ + /// + /// The interface for the core node manager. + /// + /// + /// This interface defines the contract for the core node manager that handles + /// the built-in OPC UA nodes (namespace 0) and the server's dynamic namespace. + /// It extends INodeManager2 with additional methods specific to the core node manager. + /// + public interface ICoreNodeManager : INodeManager2 + { + /// + /// Acquires the lock on the node manager. + /// + /// + /// This lock should be used when accessing or modifying the node manager's internal state. + /// + object DataLock { get; } + + /// + /// Imports the nodes from a collection of NodeState objects. + /// + /// The system context. + /// The predefined nodes to import. + /// + /// This method is used to add nodes to the core node manager's address space. + /// It is typically called during initialization or when loading predefined nodes. + /// + void ImportNodes(ISystemContext context, IEnumerable predefinedNodes); + + /// + /// Creates a unique node identifier. + /// + /// A new unique NodeId in the dynamic namespace. + /// + /// This method generates unique node identifiers for dynamically created nodes. + /// The NodeIds are created in the server's dynamic namespace. + /// + NodeId CreateUniqueNodeId(); + + /// + /// Returns the namespace index used for dynamically created nodes. + /// + /// The dynamic namespace index. + ushort DynamicNamespaceIndex { get; } + } +} diff --git a/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs index 38ebb8fee..558443a10 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs @@ -121,7 +121,7 @@ public MasterNodeManager( // add the core node manager second because the diagnostics node manager takes priority. // always add the core node manager to the second of the list. - var coreNodeManager = new CoreNodeManager(Server, configuration, (ushort)dynamicNamespaceIndex); + var coreNodeManager = new CoreNodeManager2(Server, configuration, (ushort)dynamicNamespaceIndex); m_nodeManagers.Add(coreNodeManager.ToAsyncNodeManager()); // register core node manager for default UA namespace. @@ -306,7 +306,7 @@ protected static PermissionType GetHistoryPermissionType(PerformUpdateType updat /// /// Returns the core node manager. /// - public CoreNodeManager CoreNodeManager => m_nodeManagers[1].SyncNodeManager as CoreNodeManager; + public ICoreNodeManager CoreNodeManager => m_nodeManagers[1].SyncNodeManager as ICoreNodeManager; /// /// Returns the diagnostics node manager. diff --git a/Libraries/Opc.Ua.Server/Server/IServerInternal.cs b/Libraries/Opc.Ua.Server/Server/IServerInternal.cs index dee2635d4..08e7171e4 100644 --- a/Libraries/Opc.Ua.Server/Server/IServerInternal.cs +++ b/Libraries/Opc.Ua.Server/Server/IServerInternal.cs @@ -96,7 +96,7 @@ public interface IServerInternal : IAuditEventServer, IDisposable /// 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. diff --git a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs index 6032cb8b0..18f514827 100644 --- a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs +++ b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs @@ -281,7 +281,7 @@ 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. diff --git a/Tests/Opc.Ua.Server.Tests/CoreNodeManager2Tests.cs b/Tests/Opc.Ua.Server.Tests/CoreNodeManager2Tests.cs new file mode 100644 index 000000000..a90ad2fe0 --- /dev/null +++ b/Tests/Opc.Ua.Server.Tests/CoreNodeManager2Tests.cs @@ -0,0 +1,233 @@ +/* ======================================================================== + * 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.Linq; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace Opc.Ua.Server.Tests +{ + /// + /// Test + /// + [TestFixture] + [Category("CoreNodeManager2")] + [SetCulture("en-us")] + [SetUICulture("en-us")] + [Parallelizable] + public class CoreNodeManager2Tests + { + /// + /// Tests that CoreNodeManager2 is properly instantiated and accessible. + /// + [Test] + public async Task TestCoreNodeManager2Instantiation() + { + var fixture = new ServerFixture(); + + try + { + // Arrange & Act + StandardServer server = await fixture.StartAsync().ConfigureAwait(false); + + // Assert + Assert.That(server.CurrentInstance.CoreNodeManager, Is.Not.Null); + Assert.That(server.CurrentInstance.CoreNodeManager, Is.InstanceOf()); + } + finally + { + await fixture.StopAsync().ConfigureAwait(false); + } + } + + /// + /// Tests that CoreNodeManager2 inherits from CustomNodeManager2. + /// + [Test] + public async Task TestCoreNodeManager2InheritsFromCustomNodeManager2() + { + var fixture = new ServerFixture(); + + try + { + // Arrange & Act + StandardServer server = await fixture.StartAsync().ConfigureAwait(false); + + // Assert + Assert.That(server.CurrentInstance.CoreNodeManager, Is.InstanceOf()); + } + finally + { + await fixture.StopAsync().ConfigureAwait(false); + } + } + + /// + /// Tests that CoreNodeManager2 has DataLock property for compatibility. + /// + [Test] + public async Task TestCoreNodeManager2HasDataLockProperty() + { + var fixture = new ServerFixture(); + + try + { + // Arrange & Act + StandardServer server = await fixture.StartAsync().ConfigureAwait(false); + CoreNodeManager2 coreNodeManager = server.CurrentInstance.CoreNodeManager as CoreNodeManager2; + + // Assert + Assert.That(coreNodeManager.DataLock, Is.Not.Null); + Assert.That(coreNodeManager.DataLock, Is.EqualTo(coreNodeManager.Lock)); + } + finally + { + await fixture.StopAsync().ConfigureAwait(false); + } + } + + /// + /// Tests that CoreNodeManager2 can import nodes. + /// + [Test] + public async Task TestCoreNodeManager2ImportNodes() + { + var fixture = new ServerFixture(); + + try + { + // Arrange + StandardServer server = await fixture.StartAsync().ConfigureAwait(false); + CoreNodeManager2 coreNodeManager = server.CurrentInstance.CoreNodeManager as CoreNodeManager2; + + var testNode = new DataItemState(null) + { + NodeId = new NodeId(Guid.NewGuid(), coreNodeManager.DynamicNamespaceIndex), + BrowseName = new QualifiedName("TestNode", coreNodeManager.DynamicNamespaceIndex), + DisplayName = "Test Node" + }; + + // Act + coreNodeManager.ImportNodes(coreNodeManager.SystemContext, new NodeState[] { testNode }); + + // Assert + NodeState foundNode = coreNodeManager.Find(testNode.NodeId); + Assert.That(foundNode, Is.Not.Null); + Assert.That(foundNode.NodeId, Is.EqualTo(testNode.NodeId)); + Assert.That(foundNode.BrowseName, Is.EqualTo(testNode.BrowseName)); + } + finally + { + await fixture.StopAsync().ConfigureAwait(false); + } + } + + /// + /// Tests that CoreNodeManager2 can create unique node IDs. + /// + [Test] + public async Task TestCoreNodeManager2CreateUniqueNodeId() + { + var fixture = new ServerFixture(); + + try + { + // Arrange + StandardServer server = await fixture.StartAsync().ConfigureAwait(false); + CoreNodeManager2 coreNodeManager = server.CurrentInstance.CoreNodeManager as CoreNodeManager2; + + // Act + NodeId nodeId1 = coreNodeManager.CreateUniqueNodeId(); + NodeId nodeId2 = coreNodeManager.CreateUniqueNodeId(); + + // Assert + Assert.That(nodeId1, Is.Not.Null); + Assert.That(nodeId2, Is.Not.Null); + Assert.That(nodeId1, Is.Not.EqualTo(nodeId2)); + Assert.That(nodeId1.NamespaceIndex, Is.EqualTo(coreNodeManager.DynamicNamespaceIndex)); + Assert.That(nodeId2.NamespaceIndex, Is.EqualTo(coreNodeManager.DynamicNamespaceIndex)); + } + finally + { + await fixture.StopAsync().ConfigureAwait(false); + } + } + + /// + /// Tests that CoreNodeManager2 manages namespace 0 and 1. + /// + [Test] + public async Task TestCoreNodeManager2ManagesCorrectNamespaces() + { + var fixture = new ServerFixture(); + + try + { + // Arrange & Act + StandardServer server = await fixture.StartAsync().ConfigureAwait(false); + CoreNodeManager2 coreNodeManager = server.CurrentInstance.CoreNodeManager as CoreNodeManager2; + + // Assert + Assert.That(coreNodeManager.NamespaceIndexes, Is.Not.Null); + Assert.That(coreNodeManager.NamespaceIndexes, Does.Contain((ushort)0)); // UA namespace + Assert.That(coreNodeManager.NamespaceIndexes.Count, Is.EqualTo(2)); + } + finally + { + await fixture.StopAsync().ConfigureAwait(false); + } + } + + /// + /// Tests that CoreNodeManager2 uses SamplingGroups. + /// + [Test] + public async Task TestCoreNodeManager2UsesSamplingGroups() + { + var fixture = new ServerFixture(); + + try + { + // Arrange & Act + StandardServer server = await fixture.StartAsync().ConfigureAwait(false); + CoreNodeManager2 coreNodeManager = server.CurrentInstance.CoreNodeManager as CoreNodeManager2; + + // Assert that the node manager is properly initialized + // The SamplingGroups support is enabled in the constructor + Assert.That(coreNodeManager, Is.Not.Null); + Assert.That(coreNodeManager.Server, Is.Not.Null); + } + finally + { + await fixture.StopAsync().ConfigureAwait(false); + } + } + } +}