/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Opc.Ua.Server
{
///
public class MasterNodeManager : IDisposable, IMasterNodeManager
{
///
/// Initializes the object with default values.
///
public MasterNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
string? dynamicNamespaceUri,
params INodeManager[] additionalManagers)
: this(server, configuration, dynamicNamespaceUri, null, additionalManagers)
{
}
///
/// Initializes the object with default values.
///
public MasterNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
string? dynamicNamespaceUri,
params IAsyncNodeManager[] additionalManagers)
: this(server, configuration, dynamicNamespaceUri, additionalManagers, null)
{
}
///
/// Initializes the object with default values.
///
public MasterNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
string? dynamicNamespaceUri,
IEnumerable? additionalManagers,
IEnumerable? additionalSyncManagers)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Server = server ?? throw new ArgumentNullException(nameof(server));
m_logger = server.Telemetry.CreateLogger();
m_nodeManagers = [];
m_maxContinuationPointsPerBrowse = (uint)configuration.ServerConfiguration!
.MaxBrowseContinuationPoints;
// ensure the dynamic namespace uris.
int dynamicNamespaceIndex = 1;
if (!string.IsNullOrEmpty(dynamicNamespaceUri))
{
dynamicNamespaceIndex = server.NamespaceUris.GetIndex(dynamicNamespaceUri!);
if (dynamicNamespaceIndex == -1)
{
dynamicNamespaceIndex = server.NamespaceUris.Append(dynamicNamespaceUri!);
}
}
// need to build a table of NamespaceIndexes and their NodeManagers.
List registeredManagers;
var namespaceManagers = new Dictionary>
{
[0] = [],
[1] = registeredManagers = []
};
// always add the diagnostics and configuration node manager to the start of the list.
IConfigurationNodeManager configurationAndDiagnosticsManager
= server.MainNodeManagerFactory.CreateConfigurationNodeManager();
RegisterNodeManager(
configurationAndDiagnosticsManager,
registeredManagers,
namespaceManagers);
// 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.
ICoreNodeManager coreNodeManager = server.MainNodeManagerFactory.CreateCoreNodeManager((ushort)dynamicNamespaceIndex);
m_nodeManagers.Add(coreNodeManager);
// register core node manager for default UA namespace.
namespaceManagers[0].Add(m_nodeManagers[1]);
// register core node manager for built-in server namespace.
namespaceManagers[1].Add(m_nodeManagers[1]);
// add the custom NodeManagers provided by the application.
if (additionalManagers != null)
{
foreach (IAsyncNodeManager nodeManager in additionalManagers)
{
RegisterNodeManager(nodeManager, registeredManagers, namespaceManagers);
}
}
if (additionalSyncManagers != null)
{
foreach (INodeManager nodeManager in additionalSyncManagers)
{
RegisterNodeManager(nodeManager.ToAsyncNodeManager(), registeredManagers, namespaceManagers);
}
}
// build NamespaceManagersDictionary from local dictionary.
foreach (KeyValuePair> namespaceManager in namespaceManagers)
{
NamespaceManagers.TryAdd(namespaceManager.Key, namespaceManager.Value.AsReadOnly());
}
}
///
/// Registers the node manager with the master node manager.
///
private void RegisterNodeManager(
IAsyncNodeManager nodeManager,
List registeredManagers,
Dictionary> namespaceManagers)
{
m_nodeManagers.Add(nodeManager);
// ensure the NamespaceUris supported by the NodeManager are in the Server's NamespaceTable.
if (nodeManager.NamespaceUris != null)
{
foreach (string namespaceUri in nodeManager.NamespaceUris)
{
// look up the namespace uri.
int index = Server.NamespaceUris.GetIndex(namespaceUri);
if (index == -1)
{
index = Server.NamespaceUris.Append(namespaceUri);
}
// add manager to list for the namespace.
if (!namespaceManagers.TryGetValue(index, out registeredManagers!))
{
namespaceManagers[index] = registeredManagers = [];
}
registeredManagers.Add(nodeManager);
}
}
}
///
/// Frees any unmanaged resources.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// An overrideable version of the Dispose.
///
protected virtual void Dispose(bool disposing)
{
if (disposing && !m_disposed)
{
m_disposed = true;
m_namespaceManagersSemaphoreSlim.Dispose();
m_startupShutdownSemaphoreSlim.Wait();
List nodeManagers = [.. m_nodeManagers];
m_nodeManagers.Clear();
m_startupShutdownSemaphoreSlim.Dispose();
foreach (IAsyncNodeManager nodeManager in nodeManagers)
{
(nodeManager as IDisposable)?.Dispose();
}
}
}
///
/// Adds a reference to the table of external references.
///
///
/// This is a convenience function used by custom NodeManagers.
///
public static void CreateExternalReference(
IDictionary> externalReferences,
NodeId sourceId,
NodeId referenceTypeId,
bool isInverse,
NodeId targetId)
{
var reference = new ReferenceNode
{
ReferenceTypeId = referenceTypeId,
IsInverse = isInverse,
TargetId = targetId
};
if (!externalReferences.TryGetValue(sourceId, out IList? references))
{
externalReferences[sourceId] = references = [];
}
references!.Add(reference);
}
///
/// Determine the required history access permission depending on the HistoryUpdateDetails
///
/// The HistoryUpdateDetails passed in
/// The corresponding history access permission
protected static PermissionType DetermineHistoryAccessPermission(
HistoryUpdateDetails historyUpdateDetails)
{
Type detailsType = historyUpdateDetails.GetType();
if (detailsType == typeof(UpdateDataDetails))
{
var updateDataDetails = (UpdateDataDetails)historyUpdateDetails;
return GetHistoryPermissionType(updateDataDetails.PerformInsertReplace);
}
else if (detailsType == typeof(UpdateStructureDataDetails))
{
var updateStructureDataDetails = (UpdateStructureDataDetails)historyUpdateDetails;
return GetHistoryPermissionType(updateStructureDataDetails.PerformInsertReplace);
}
else if (detailsType == typeof(UpdateEventDetails))
{
var updateEventDetails = (UpdateEventDetails)historyUpdateDetails;
return GetHistoryPermissionType(updateEventDetails.PerformInsertReplace);
}
else if (detailsType == typeof(DeleteRawModifiedDetails) ||
detailsType == typeof(DeleteAtTimeDetails) ||
detailsType == typeof(DeleteEventDetails))
{
return PermissionType.DeleteHistory;
}
return PermissionType.ModifyHistory;
}
///
/// Determine the History PermissionType depending on PerformUpdateType
///
/// The corresponding PermissionType
protected static PermissionType GetHistoryPermissionType(PerformUpdateType updateType)
{
switch (updateType)
{
case PerformUpdateType.Insert:
return PermissionType.InsertHistory;
case PerformUpdateType.Update:
return PermissionType.InsertHistory | PermissionType.ModifyHistory;
case PerformUpdateType.Replace:
case PerformUpdateType.Remove:
return PermissionType.ModifyHistory;
default:
Debug.Fail($"Unexpected update type {updateType}");
return PermissionType.ModifyHistory;
}
}
///
public ICoreNodeManager? CoreNodeManager => m_nodeManagers[1] as ICoreNodeManager;
///
public IDiagnosticsNodeManager? DiagnosticsNodeManager
=> m_nodeManagers[0] as IDiagnosticsNodeManager;
///
public IConfigurationNodeManager? ConfigurationNodeManager
=> m_nodeManagers[0] as IConfigurationNodeManager;
///
public virtual async ValueTask StartupAsync(CancellationToken cancellationToken = default)
{
await m_startupShutdownSemaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
m_logger.LogInformation(
Utils.TraceMasks.StartStop,
"MasterNodeManager.Startup - NodeManagers={Count}",
m_nodeManagers.Count);
// create the address spaces.
var externalReferences = new Dictionary>();
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
try
{
await nodeManager.CreateAddressSpaceAsync(externalReferences, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception e)
{
m_logger.LogError(
e,
"Unexpected error creating address space for NodeManager ={NodeManager}.",
nodeManager);
throw;
}
}
// update external references.
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
try
{
await nodeManager.AddReferencesAsync(externalReferences, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception e)
{
m_logger.LogError(
e,
"Unexpected error adding references for NodeManager ={NodeManager}.",
nodeManager);
throw;
}
}
}
finally
{
m_startupShutdownSemaphoreSlim.Release();
}
}
///
public virtual async ValueTask SessionClosingAsync(
OperationContext context,
NodeId sessionId,
bool deleteSubscriptions,
CancellationToken cancellationToken = default)
{
await m_startupShutdownSemaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
try
{
await nodeManager.SessionClosingAsync(context, sessionId, deleteSubscriptions, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception e)
{
m_logger.LogError(
e,
"Unexpected error closing session for NodeManager ={NodeManager}.",
nodeManager);
}
}
}
finally
{
m_startupShutdownSemaphoreSlim.Release();
}
}
///
public virtual async ValueTask SessionActivatedAsync(
OperationContext context,
NodeId sessionId,
CancellationToken cancellationToken = default)
{
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
try
{
await nodeManager.SessionActivatedAsync(context, sessionId, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception e)
{
m_logger.LogError(
e,
"Unexpected error notifying node manager of session activation for NodeManager={NodeManager}.",
nodeManager);
}
}
}
///
public virtual async ValueTask ShutdownAsync(CancellationToken cancellationToken = default)
{
await m_startupShutdownSemaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
m_logger.LogInformation(
Utils.TraceMasks.StartStop,
"MasterNodeManager.Shutdown - NodeManagers={Count}",
m_nodeManagers.Count);
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.DeleteAddressSpaceAsync(cancellationToken)
.ConfigureAwait(false);
}
}
finally
{
m_startupShutdownSemaphoreSlim.Release();
}
}
///
public void RegisterNamespaceManager(string namespaceUri, INodeManager nodeManager)
{
RegisterNamespaceManager(namespaceUri, nodeManager.ToAsyncNodeManager());
}
///
public void RegisterNamespaceManager(string namespaceUri, IAsyncNodeManager nodeManager)
{
if (string.IsNullOrEmpty(namespaceUri))
{
throw new ArgumentNullException(nameof(namespaceUri));
}
if (nodeManager == null)
{
throw new ArgumentNullException(nameof(nodeManager));
}
// look up the namespace uri.
int index = Server.NamespaceUris.GetIndex(namespaceUri);
if (index < 0)
{
index = Server.NamespaceUris.Append(namespaceUri);
}
m_namespaceManagersSemaphoreSlim.Wait();
try
{
NamespaceManagers.AddOrUpdate(
index,
[nodeManager],
(key, existingNodeManagers) =>
{
var nodeManagers = existingNodeManagers.ToList();
nodeManagers.Add(nodeManager);
return nodeManagers.AsReadOnly();
});
}
finally
{
m_namespaceManagersSemaphoreSlim.Release();
}
}
///
public bool UnregisterNamespaceManager(string namespaceUri, INodeManager nodeManager)
{
return UnregisterNamespaceManager(namespaceUri, null, nodeManager);
}
///
public bool UnregisterNamespaceManager(string namespaceUri, IAsyncNodeManager nodeManager)
{
return UnregisterNamespaceManager(namespaceUri, nodeManager, null);
}
private bool UnregisterNamespaceManager(string namespaceUri, IAsyncNodeManager? asyncNodeManager, INodeManager? nodeManager)
{
if (string.IsNullOrEmpty(namespaceUri))
{
throw new ArgumentNullException(nameof(namespaceUri));
}
if (nodeManager == null && asyncNodeManager == null)
{
throw new ArgumentNullException(nameof(nodeManager));
}
// look up the namespace uri.
int namespaceIndex = Server.NamespaceUris.GetIndex(namespaceUri);
if (namespaceIndex < 0)
{
return false;
}
m_namespaceManagersSemaphoreSlim.Wait();
try
{
if (!NamespaceManagers.TryGetValue(namespaceIndex, out IReadOnlyList? readOnlyNodeManagers))
{
return false;
}
var nodeManagers = readOnlyNodeManagers!.ToList();
int nodeManagersFound;
IAsyncNodeManager? nodeManagerToRemove = asyncNodeManager;
if (nodeManagerToRemove is null)
{
nodeManagersFound = nodeManagers.RemoveAll(manager => manager.SyncNodeManager == nodeManager);
}
else
{
nodeManagersFound = nodeManagers.Remove(nodeManagerToRemove) ? 1 : 0;
}
if (nodeManagers.Count == 0)
{
NamespaceManagers.TryRemove(namespaceIndex, out _);
}
else
{
NamespaceManagers[namespaceIndex] = nodeManagers.AsReadOnly();
}
return nodeManagersFound > 0;
}
finally
{
m_namespaceManagersSemaphoreSlim.Release();
}
}
///
public virtual object? GetManagerHandle(NodeId nodeId, out INodeManager? nodeManager)
{
object? handle;
nodeManager = null;
// null node ids have no manager.
if (nodeId.IsNull)
{
return null;
}
// use the namespace index to select the node manager.
int index = nodeId.NamespaceIndex;
// check if node managers are registered - use the core node manager if unknown.
if (!NamespaceManagers.TryGetValue(index, out IReadOnlyList? nodeManagers))
{
handle = m_nodeManagers[1].SyncNodeManager.GetManagerHandle(nodeId);
if (handle != null)
{
nodeManager = m_nodeManagers[1].SyncNodeManager;
return handle;
}
return null;
}
foreach (IAsyncNodeManager asyncNodeManager in nodeManagers!)
{
handle = asyncNodeManager.SyncNodeManager.GetManagerHandle(nodeId);
if (handle != null)
{
nodeManager = asyncNodeManager.SyncNodeManager;
return handle;
}
}
// node not recognized.
return null;
}
///
[Obsolete("Use GetManagerHandleAsync instead.")]
public virtual object? GetManagerHandle(NodeId nodeId, out IAsyncNodeManager? nodeManager)
{
(object? handle, IAsyncNodeManager? nodeManager) result =
GetManagerHandleAsync(nodeId).AsTask().GetAwaiter().GetResult();
nodeManager = result.nodeManager;
return result.handle;
}
///
public virtual async ValueTask<(object? handle, IAsyncNodeManager? nodeManager)>
GetManagerHandleAsync(NodeId nodeId, CancellationToken cancellationToken = default)
{
object? handle;
// null node ids have no manager.
if (nodeId.IsNull)
{
return (null, null);
}
// use the namespace index to select the node manager.
int index = nodeId.NamespaceIndex;
// check if node managers are registered - use the core node manager if unknown.
if (!NamespaceManagers.TryGetValue(index, out IReadOnlyList? nodeManagers))
{
handle = await m_nodeManagers[1].GetManagerHandleAsync(nodeId, cancellationToken)
.ConfigureAwait(false);
if (handle != null)
{
return (handle, m_nodeManagers[1]);
}
return (null, null);
}
foreach (IAsyncNodeManager asyncNodeManager in nodeManagers!)
{
handle = await asyncNodeManager.GetManagerHandleAsync(nodeId, cancellationToken)
.ConfigureAwait(false);
if (handle != null)
{
return (handle, asyncNodeManager);
}
}
// node not recognized.
return (null, null);
}
///
/// Adds the references to the target.
///
[Obsolete("Use AddReferencesAsync instead.")]
public virtual void AddReferences(NodeId sourceId, IList references)
{
AddReferencesAsync(sourceId, references).AsTask().GetAwaiter().GetResult();
}
///
public virtual async ValueTask AddReferencesAsync(NodeId sourceId,
IList references,
CancellationToken cancellationToken = default)
{
// find source node.
(object? sourceHandle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(sourceId, cancellationToken)
.ConfigureAwait(false);
if (sourceHandle == null)
{
return;
}
var map = new Dictionary> { { sourceId, references } };
await nodeManager!.AddReferencesAsync(map, cancellationToken)
.ConfigureAwait(false);
}
///
/// Deletes the references to the target.
///
[Obsolete("Use DeleteReferencesAsync")]
public virtual void DeleteReferences(NodeId targetId, IList references)
{
DeleteReferencesAsync(targetId, references).AsTask().GetAwaiter().GetResult();
}
///
public virtual async ValueTask DeleteReferencesAsync(NodeId targetId,
IList references,
CancellationToken cancellationToken = default)
{
foreach (ReferenceNode reference in references.OfType())
{
var sourceId = ExpandedNodeId.ToNodeId(reference.TargetId, Server.NamespaceUris);
// find source node.
(object? sourceHandle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(sourceId, cancellationToken)
.ConfigureAwait(false);
if (sourceHandle == null)
{
continue;
}
// delete the reference.
await nodeManager!.DeleteReferenceAsync(
sourceHandle,
reference.ReferenceTypeId,
!reference.IsInverse,
targetId,
false,
cancellationToken)
.ConfigureAwait(false);
}
}
///
[Obsolete("Use RemoveReferencesAsync instead.")]
public void RemoveReferences(List referencesToRemove)
{
RemoveReferencesAsync(referencesToRemove).AsTask().GetAwaiter().GetResult();
}
///
public async ValueTask RemoveReferencesAsync(List referencesToRemove, CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < referencesToRemove.Count; ii++)
{
LocalReference reference = referencesToRemove[ii];
// find source node.
(object? sourceHandle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(
reference.SourceId,
cancellationToken)
.ConfigureAwait(false);
if (sourceHandle == null)
{
continue;
}
// delete the reference.
await nodeManager!.DeleteReferenceAsync(
sourceHandle,
reference.ReferenceTypeId,
reference.IsInverse,
reference.TargetId,
false,
cancellationToken)
.ConfigureAwait(false);
}
}
///
public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)>
AddNodesAsync(
OperationContext context,
ArrayOf nodesToAdd,
CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var results = new AddNodesResult[nodesToAdd.Count];
var diagnosticInfos = new DiagnosticInfo[nodesToAdd.Count];
bool anyDiagnostics = false;
for (int ii = 0; ii < nodesToAdd.Count; ii++)
{
AddNodesItem item = nodesToAdd[ii];
(ServiceResult result, NodeId addedNodeId) = await DispatchAddNodeAsync(
context,
item,
cancellationToken).ConfigureAwait(false);
results[ii] = new AddNodesResult
{
StatusCode = result.StatusCode,
AddedNodeId = addedNodeId
};
if (ServiceResult.IsBad(result))
{
anyDiagnostics = true;
diagnosticInfos[ii] = new DiagnosticInfo(
result,
context.DiagnosticsMask,
false,
context.StringTable,
m_logger);
}
}
return (results.ToArrayOf(), anyDiagnostics ? diagnosticInfos.ToArrayOf() : default);
}
///
public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)>
DeleteNodesAsync(
OperationContext context,
ArrayOf nodesToDelete,
CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var results = new StatusCode[nodesToDelete.Count];
var diagnosticInfos = new DiagnosticInfo[nodesToDelete.Count];
bool anyDiagnostics = false;
for (int ii = 0; ii < nodesToDelete.Count; ii++)
{
DeleteNodesItem item = nodesToDelete[ii];
ServiceResult result = await DispatchDeleteNodeAsync(
context,
item,
cancellationToken).ConfigureAwait(false);
results[ii] = result.StatusCode;
if (ServiceResult.IsBad(result))
{
anyDiagnostics = true;
diagnosticInfos[ii] = new DiagnosticInfo(
result,
context.DiagnosticsMask,
false,
context.StringTable,
m_logger);
}
}
return (results.ToArrayOf(), anyDiagnostics ? diagnosticInfos.ToArrayOf() : default);
}
///
public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)>
AddReferencesAsync(
OperationContext context,
ArrayOf referencesToAdd,
CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var results = new StatusCode[referencesToAdd.Count];
var diagnosticInfos = new DiagnosticInfo[referencesToAdd.Count];
bool anyDiagnostics = false;
for (int ii = 0; ii < referencesToAdd.Count; ii++)
{
AddReferencesItem item = referencesToAdd[ii];
ServiceResult result = await DispatchAddReferenceAsync(
context,
item,
cancellationToken).ConfigureAwait(false);
results[ii] = result.StatusCode;
if (ServiceResult.IsBad(result))
{
anyDiagnostics = true;
diagnosticInfos[ii] = new DiagnosticInfo(
result,
context.DiagnosticsMask,
false,
context.StringTable,
m_logger);
}
}
return (results.ToArrayOf(), anyDiagnostics ? diagnosticInfos.ToArrayOf() : default);
}
///
public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)>
DeleteReferencesAsync(
OperationContext context,
ArrayOf referencesToDelete,
CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var results = new StatusCode[referencesToDelete.Count];
var diagnosticInfos = new DiagnosticInfo[referencesToDelete.Count];
bool anyDiagnostics = false;
for (int ii = 0; ii < referencesToDelete.Count; ii++)
{
DeleteReferencesItem item = referencesToDelete[ii];
ServiceResult result = await DispatchDeleteReferenceAsync(
context,
item,
cancellationToken).ConfigureAwait(false);
results[ii] = result.StatusCode;
if (ServiceResult.IsBad(result))
{
anyDiagnostics = true;
diagnosticInfos[ii] = new DiagnosticInfo(
result,
context.DiagnosticsMask,
false,
context.StringTable,
m_logger);
}
}
return (results.ToArrayOf(), anyDiagnostics ? diagnosticInfos.ToArrayOf() : default);
}
private async ValueTask<(ServiceResult result, NodeId addedNodeId)> DispatchAddNodeAsync(
OperationContext context,
AddNodesItem item,
CancellationToken cancellationToken)
{
if (item == null)
{
return (new ServiceResult(StatusCodes.BadNothingToDo), NodeId.Null);
}
if (item.BrowseName.IsNull)
{
return (new ServiceResult(StatusCodes.BadBrowseNameInvalid), NodeId.Null);
}
if (item.ParentNodeId.IsNull)
{
return (new ServiceResult(StatusCodes.BadParentNodeIdInvalid), NodeId.Null);
}
if (item.ReferenceTypeId.IsNull ||
!Server.TypeTree.IsKnown(item.ReferenceTypeId))
{
return (new ServiceResult(StatusCodes.BadReferenceTypeIdInvalid), NodeId.Null);
}
if (!Server.TypeTree.IsTypeOf(
item.ReferenceTypeId,
ReferenceTypeIds.HierarchicalReferences))
{
return (new ServiceResult(StatusCodes.BadReferenceNotAllowed), NodeId.Null);
}
var parentNodeId = ExpandedNodeId.ToNodeId(item.ParentNodeId, Server.NamespaceUris);
if (parentNodeId.IsNull)
{
return (new ServiceResult(StatusCodes.BadParentNodeIdInvalid), NodeId.Null);
}
// Routing per spec:
// - When RequestedNewNodeId is provided, dispatch to that namespace's
// owner (the namespace dictates the responsible NodeManager).
// - Otherwise, prefer the parent's owner. If the parent's owner has
// not opted in to NodeManagement, fall back to the first
// opted-in NodeManager so cross-NodeManager AddNodes under a
// read-only parent (e.g. ObjectsFolder) is still serviceable.
INodeManagementAsyncNodeManager? nodeManagement = null;
if (!item.RequestedNewNodeId.IsNull)
{
var requestedNodeId = ExpandedNodeId.ToNodeId(
item.RequestedNewNodeId, Server.NamespaceUris);
if (requestedNodeId.IsNull)
{
return (new ServiceResult(StatusCodes.BadNodeIdRejected), NodeId.Null);
}
nodeManagement = FindNodeManagementOwner(requestedNodeId.NamespaceIndex);
if (nodeManagement == null || !nodeManagement.AllowNodeManagement)
{
return (new ServiceResult(StatusCodes.BadUserAccessDenied), NodeId.Null);
}
}
else
{
(object? parentHandle, IAsyncNodeManager? parentOwner) =
await GetManagerHandleAsync(parentNodeId, cancellationToken).ConfigureAwait(false);
if (parentHandle == null || parentOwner == null)
{
return (new ServiceResult(StatusCodes.BadParentNodeIdInvalid), NodeId.Null);
}
if (parentOwner.AllowNodeManagement)
{
nodeManagement = parentOwner;
}
else
{
nodeManagement = FindFirstOptedInNodeManager();
if (nodeManagement == null)
{
return (new ServiceResult(StatusCodes.BadUserAccessDenied), NodeId.Null);
}
}
}
try
{
return await nodeManagement.AddNodeAsync(context, item, cancellationToken)
.ConfigureAwait(false);
}
catch (ServiceResultException ex)
{
return (new ServiceResult(ex), NodeId.Null);
}
}
private async ValueTask DispatchDeleteNodeAsync(
OperationContext context,
DeleteNodesItem item,
CancellationToken cancellationToken)
{
if (item == null)
{
return new ServiceResult(StatusCodes.BadNothingToDo);
}
if (item.NodeId.IsNull)
{
return new ServiceResult(StatusCodes.BadNodeIdInvalid);
}
(object? handle, IAsyncNodeManager? owner) =
await GetManagerHandleAsync(item.NodeId, cancellationToken).ConfigureAwait(false);
if (handle == null || owner == null)
{
return new ServiceResult(StatusCodes.BadNodeIdUnknown);
}
if (!owner.AllowNodeManagement)
{
return new ServiceResult(StatusCodes.BadUserAccessDenied);
}
try
{
return await owner.DeleteNodeAsync(context, item, cancellationToken)
.ConfigureAwait(false);
}
catch (ServiceResultException ex)
{
return new ServiceResult(ex);
}
}
private async ValueTask DispatchAddReferenceAsync(
OperationContext context,
AddReferencesItem item,
CancellationToken cancellationToken)
{
if (item == null)
{
return new ServiceResult(StatusCodes.BadNothingToDo);
}
if (item.SourceNodeId.IsNull)
{
return new ServiceResult(StatusCodes.BadSourceNodeIdInvalid);
}
if (item.TargetNodeId.IsNull)
{
return new ServiceResult(StatusCodes.BadTargetNodeIdInvalid);
}
if (item.ReferenceTypeId.IsNull ||
!Server.TypeTree.IsKnown(item.ReferenceTypeId))
{
return new ServiceResult(StatusCodes.BadReferenceTypeIdInvalid);
}
(object? sourceHandle, IAsyncNodeManager? sourceOwner) =
await GetManagerHandleAsync(item.SourceNodeId, cancellationToken).ConfigureAwait(false);
if (sourceHandle == null || sourceOwner == null)
{
return new ServiceResult(StatusCodes.BadSourceNodeIdInvalid);
}
if (!sourceOwner.AllowNodeManagement)
{
return new ServiceResult(StatusCodes.BadUserAccessDenied);
}
ServiceResult sourceResult;
try
{
sourceResult = await sourceOwner.AddReferenceAsync(context, item, cancellationToken)
.ConfigureAwait(false);
}
catch (ServiceResultException ex)
{
return new ServiceResult(ex);
}
if (ServiceResult.IsBad(sourceResult))
{
return sourceResult;
}
// Best-effort: write the complementary edge into the target's owning manager
// when the target is local. Failures here are logged but do not change the
// source-side success so the source half stays consistent.
var targetNodeId = ExpandedNodeId.ToNodeId(item.TargetNodeId, Server.NamespaceUris);
if (!targetNodeId.IsNull)
{
(object? targetHandle, IAsyncNodeManager? targetOwner) = await GetManagerHandleAsync(
targetNodeId, cancellationToken).ConfigureAwait(false);
if (targetHandle != null &&
targetOwner != null &&
!ReferenceEquals(targetOwner, sourceOwner) &&
targetOwner.AllowNodeManagement)
{
var inverseItem = new AddReferencesItem
{
SourceNodeId = targetNodeId,
ReferenceTypeId = item.ReferenceTypeId,
IsForward = !item.IsForward,
TargetServerUri = item.TargetServerUri,
TargetNodeId = item.SourceNodeId,
TargetNodeClass = item.TargetNodeClass
};
try
{
ServiceResult inverseResult = await targetOwner.AddReferenceAsync(
context, inverseItem, cancellationToken).ConfigureAwait(false);
if (ServiceResult.IsBad(inverseResult))
{
m_logger.LogWarning(
"AddReferences: failed to mirror inverse edge {RefType} from {Source} to {Target} on owning NodeManager: {Status}",
item.ReferenceTypeId,
item.SourceNodeId,
item.TargetNodeId,
inverseResult.StatusCode);
}
}
catch (ServiceResultException ex)
{
m_logger.LogWarning(
ex,
"AddReferences: failed to mirror inverse edge {RefType} from {Source} to {Target} on owning NodeManager.",
item.ReferenceTypeId,
item.SourceNodeId,
item.TargetNodeId);
}
}
}
return sourceResult;
}
private async ValueTask DispatchDeleteReferenceAsync(
OperationContext context,
DeleteReferencesItem item,
CancellationToken cancellationToken)
{
if (item == null)
{
return new ServiceResult(StatusCodes.BadNothingToDo);
}
if (item.SourceNodeId.IsNull)
{
return new ServiceResult(StatusCodes.BadSourceNodeIdInvalid);
}
if (item.ReferenceTypeId.IsNull ||
!Server.TypeTree.IsKnown(item.ReferenceTypeId))
{
return new ServiceResult(StatusCodes.BadReferenceTypeIdInvalid);
}
(object? sourceHandle, IAsyncNodeManager? sourceOwner) =
await GetManagerHandleAsync(item.SourceNodeId, cancellationToken).ConfigureAwait(false);
if (sourceHandle == null || sourceOwner == null)
{
return new ServiceResult(StatusCodes.BadSourceNodeIdInvalid);
}
if (!sourceOwner.AllowNodeManagement)
{
return new ServiceResult(StatusCodes.BadUserAccessDenied);
}
ServiceResult sourceResult;
try
{
sourceResult = await sourceOwner.DeleteReferenceAsync(context, item, cancellationToken)
.ConfigureAwait(false);
}
catch (ServiceResultException ex)
{
return new ServiceResult(ex);
}
if (ServiceResult.IsBad(sourceResult))
{
return sourceResult;
}
if (!item.DeleteBidirectional)
{
return sourceResult;
}
// Cross-manager: remove the complementary edge on the target's owner.
var targetNodeId = ExpandedNodeId.ToNodeId(item.TargetNodeId, Server.NamespaceUris);
if (targetNodeId.IsNull)
{
return sourceResult;
}
(object? targetHandle2, IAsyncNodeManager? targetOwner2) = await GetManagerHandleAsync(
targetNodeId, cancellationToken).ConfigureAwait(false);
if (targetHandle2 != null &&
targetOwner2 != null &&
!ReferenceEquals(targetOwner2, sourceOwner) &&
targetOwner2.AllowNodeManagement)
{
var inverseItem = new DeleteReferencesItem
{
SourceNodeId = targetNodeId,
ReferenceTypeId = item.ReferenceTypeId,
IsForward = !item.IsForward,
TargetNodeId = item.SourceNodeId,
DeleteBidirectional = false
};
try
{
ServiceResult inverseResult = await targetOwner2.DeleteReferenceAsync(
context, inverseItem, cancellationToken).ConfigureAwait(false);
if (ServiceResult.IsBad(inverseResult))
{
m_logger.LogWarning(
"DeleteReferences: failed to mirror inverse delete {RefType} from {Source} to {Target} on owning NodeManager: {Status}",
item.ReferenceTypeId,
item.SourceNodeId,
item.TargetNodeId,
inverseResult.StatusCode);
}
}
catch (ServiceResultException ex)
{
m_logger.LogWarning(
ex,
"DeleteReferences: failed to mirror inverse delete {RefType} from {Source} to {Target} on owning NodeManager.",
item.ReferenceTypeId,
item.SourceNodeId,
item.TargetNodeId);
}
}
return sourceResult;
}
///
/// Returns the first NodeManager registered against the given namespace
/// index, or null when no NodeManager owns the namespace. Since
/// is aggregated into
/// every owner exposes the surface;
/// callers should still check .
///
private INodeManagementAsyncNodeManager? FindNodeManagementOwner(ushort namespaceIndex)
{
if (!NamespaceManagers.TryGetValue(namespaceIndex, out IReadOnlyList? nodeManagers))
{
return null;
}
return nodeManagers.Count > 0 ? nodeManagers[0] : null;
}
///
/// Returns the first registered NodeManager that has opted in to
/// NodeManagement, or null if no NodeManager has done so. Used
/// as the cross-NodeManager AddNodes fallback when the parent's
/// owning NodeManager does not allow NodeManagement.
///
private INodeManagementAsyncNodeManager? FindFirstOptedInNodeManager()
{
for (int ii = 0; ii < m_nodeManagers.Count; ii++)
{
if (m_nodeManagers[ii].AllowNodeManagement)
{
return m_nodeManagers[ii];
}
}
return null;
}
///
public virtual void RegisterNodes(
OperationContext context,
ArrayOf nodesToRegister,
out ArrayOf registeredNodeIds)
{
// return the node id provided.
registeredNodeIds = nodesToRegister;
m_logger.LogTrace(
Utils.TraceMasks.ServiceDetail,
"MasterNodeManager.RegisterNodes - Count={Count}",
nodesToRegister.Count);
// it is up to the node managers to assign the handles.
/*
List processedNodes = new List(new bool[itemsToDelete.Count]);
for (int ii = 0; ii < m_nodeManagers.Count; ii++)
{
m_nodeManagers[ii].RegisterNodes(
context,
nodesToRegister,
registeredNodeIds,
processedNodes);
}
*/
}
///
public virtual void UnregisterNodes(
OperationContext context,
ArrayOf nodesToUnregister)
{
m_logger.LogTrace(
Utils.TraceMasks.ServiceDetail,
"MasterNodeManager.UnregisterNodes - Count={Count}",
nodesToUnregister.Count);
// it is up to the node managers to assign the handles.
/*
List processedNodes = new List(new bool[itemsToDelete.Count]);
for (int ii = 0; ii < m_nodeManagers.Count; ii++)
{
m_nodeManagers[ii].RegisterNodes(
context,
nodesToUnregister,
processedNodes);
}
*/
}
///
/// Translates a start node id plus a relative paths into a node id.
///
/// is null.
///
[Obsolete("Use TranslateBrowsePathsToNodeIdsAsync instead.")]
public virtual void TranslateBrowsePathsToNodeIds(
OperationContext context,
ArrayOf browsePaths,
out ArrayOf results,
out ArrayOf diagnosticInfos)
{
(results, diagnosticInfos) = TranslateBrowsePathsToNodeIdsAsync(
context,
browsePaths).AsTask().GetAwaiter().GetResult();
}
///
public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)>
TranslateBrowsePathsToNodeIdsAsync(
OperationContext context,
ArrayOf browsePaths,
CancellationToken cancellationToken = default)
{
bool diagnosticsExist = false;
var results = new List(browsePaths.Count);
var diagnosticInfos = new List(browsePaths.Count);
for (int ii = 0; ii < browsePaths.Count; ii++)
{
// check if request has timed out or been cancelled.
if (StatusCode.IsBad(context.OperationStatus))
{
throw new ServiceResultException(context.OperationStatus);
}
BrowsePath browsePath = browsePaths[ii];
var result = new BrowsePathResult { StatusCode = StatusCodes.Good };
results.Add(result);
ServiceResult error;
// need to trap unexpected exceptions to handle bugs in the node managers.
try
{
error = await TranslateBrowsePathAsync(context, browsePath, result, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception e)
{
error = ServiceResult.Create(
e,
StatusCodes.BadUnexpectedError,
"Unexpected error translating browse path.");
}
if (ServiceResult.IsGood(error))
{
// check for no match.
if (result.Targets.Count == 0)
{
error = StatusCodes.BadNoMatch;
}
// put a placeholder for diagnostics.
else if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
diagnosticInfos.Add(null!);
}
}
// check for error.
if (error != null && error.Code != StatusCodes.Good)
{
result.StatusCode = error.StatusCode;
if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
DiagnosticInfo? diagnosticInfo = ServerUtils.CreateDiagnosticInfo(
Server,
context,
error,
m_logger);
diagnosticInfos.Add(diagnosticInfo!);
diagnosticsExist = true;
}
}
}
// clear the diagnostics array if no diagnostics requested or no errors occurred.
UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos);
return (results, diagnosticInfos);
}
///
/// Updates the diagnostics return parameter.
///
protected void UpdateDiagnostics(
OperationContext context,
bool diagnosticsExist,
ref List diagnosticInfos)
{
if (diagnosticsExist && context.StringTable.Count == 0)
{
diagnosticsExist = false;
for (int ii = 0; !diagnosticsExist && ii < diagnosticInfos.Count; ii++)
{
DiagnosticInfo? diagnosticInfo = diagnosticInfos[ii];
int depth = 0;
while (diagnosticInfo != null && depth++ < DiagnosticInfo.MaxInnerDepth)
{
if (!string.IsNullOrEmpty(diagnosticInfo.AdditionalInfo))
{
diagnosticsExist = true;
break;
}
diagnosticInfo = diagnosticInfo.InnerDiagnosticInfo;
}
}
}
if (!diagnosticsExist)
{
diagnosticInfos = null!;
}
}
///
/// Translates a browse path.
///
protected async ValueTask TranslateBrowsePathAsync(
OperationContext context,
BrowsePath browsePath,
BrowsePathResult result,
CancellationToken cancellationToken)
{
Debug.Assert(browsePath != null);
Debug.Assert(result != null);
// check for valid start node.
(object? sourceHandle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(
browsePath!.StartingNode,
cancellationToken)
.ConfigureAwait(false);
if (sourceHandle == null)
{
return StatusCodes.BadNodeIdUnknown;
}
// check the relative path.
RelativePath relativePath = browsePath.RelativePath;
if (relativePath.Elements.IsEmpty)
{
return StatusCodes.BadNothingToDo;
}
for (int ii = 0; ii < relativePath.Elements.Count; ii++)
{
RelativePathElement element = relativePath.Elements[ii];
if (element == null || relativePath.Elements[ii].TargetName.IsNull)
{
return StatusCodes.BadBrowseNameInvalid;
}
if (element.ReferenceTypeId.IsNull)
{
element.ReferenceTypeId = ReferenceTypeIds.References;
element.IncludeSubtypes = true;
}
}
// validate access rights and role permissions
ServiceResult serviceResult = await ValidatePermissionsAsync(
context,
nodeManager,
sourceHandle,
PermissionType.Browse,
null,
true,
cancellationToken)
.ConfigureAwait(false);
if (ServiceResult.IsGood(serviceResult))
{
List targets = [];
// translate path only if validation is passing
await TranslateBrowsePathAsync(
context,
nodeManager,
sourceHandle,
relativePath,
targets,
0,
cancellationToken)
.ConfigureAwait(false);
result!.Targets = targets;
}
return serviceResult;
}
///
/// Recursively processes the elements in the RelativePath starting at the specified index.
///
///
private async ValueTask TranslateBrowsePathAsync(
OperationContext context,
IAsyncNodeManager? nodeManager,
object? sourceHandle,
RelativePath relativePath,
List targets,
int index,
CancellationToken cancellationToken)
{
Debug.Assert(nodeManager != null);
Debug.Assert(sourceHandle != null);
Debug.Assert(relativePath != null);
Debug.Assert(targets != null);
// check for end of list.
if (index < 0 || index >= relativePath!.Elements.Count)
{
return;
}
// follow the next hop.
RelativePathElement element = relativePath.Elements[index];
// check for valid reference type.
if (!element.IncludeSubtypes && element.ReferenceTypeId.IsNull)
{
return;
}
// check for valid target name.
if (element.TargetName.IsNull)
{
throw new ServiceResultException(StatusCodes.BadBrowseNameInvalid);
}
var targetIds = new List();
var externalTargetIds = new List();
try
{
await nodeManager!.TranslateBrowsePathAsync(
context,
sourceHandle!,
element,
targetIds,
externalTargetIds,
cancellationToken)
.ConfigureAwait(false);
}
catch (Exception e)
{
m_logger.LogError(e, "Unexpected error translating browse path.");
return;
}
// must check the browse name on all external targets.
for (int ii = 0; ii < externalTargetIds.Count; ii++)
{
// get the browse name from another node manager.
var description = new ReferenceDescription();
await UpdateReferenceDescriptionAsync(
context,
externalTargetIds[ii],
NodeClass.Unspecified,
BrowseResultMask.BrowseName,
description,
cancellationToken)
.ConfigureAwait(false);
// add to list if target name matches.
if (description.BrowseName == element.TargetName)
{
bool found = false;
for (int jj = 0; jj < targetIds.Count; jj++)
{
if (targetIds[jj] == externalTargetIds[ii])
{
found = true;
break;
}
}
if (!found)
{
targetIds.Add(externalTargetIds[ii]);
}
}
}
// check if done after a final hop.
if (index == relativePath.Elements.Count - 1)
{
for (int ii = 0; ii < targetIds.Count; ii++)
{
// Check the role permissions for target nodes
(object? targetHandle, IAsyncNodeManager? targetNodeManager) =
await GetManagerHandleAsync(
ExpandedNodeId.ToNodeId(targetIds[ii], Server.NamespaceUris),
cancellationToken)
.ConfigureAwait(false);
if (targetHandle != null && targetNodeManager != null)
{
NodeMetadata nodeMetadata = await targetNodeManager.GetNodeMetadataAsync(
context,
targetHandle,
BrowseResultMask.All,
cancellationToken)
.ConfigureAwait(false);
ServiceResult serviceResult = ValidateRolePermissions(
context,
nodeMetadata,
PermissionType.Browse,
m_logger);
if (ServiceResult.IsBad(serviceResult))
{
// Remove target node without role permissions.
continue;
}
}
var target = new BrowsePathTarget
{
TargetId = targetIds[ii],
RemainingPathIndex = uint.MaxValue
};
targets!.Add(target);
}
return;
}
// process next hops.
for (int ii = 0; ii < targetIds.Count; ii++)
{
ExpandedNodeId targetId = targetIds[ii];
// check for external reference.
if (targetId.IsAbsolute)
{
var target = new BrowsePathTarget
{
TargetId = targetId,
RemainingPathIndex = (uint)(index + 1)
};
targets!.Add(target);
continue;
}
// check for valid start node.
(sourceHandle, nodeManager) = await GetManagerHandleAsync((NodeId)targetId, cancellationToken)
.ConfigureAwait(false);
if (sourceHandle == null)
{
continue;
}
// recursively follow hops.
await TranslateBrowsePathAsync(
context,
nodeManager,
sourceHandle,
relativePath,
targets!,
index + 1,
cancellationToken)
.ConfigureAwait(false);
}
}
///
public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)> BrowseAsync(
OperationContext context,
ViewDescription view,
uint maxReferencesPerNode,
ArrayOf nodesToBrowse,
CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (view != null && !view.ViewId.IsNull)
{
(object? viewHandle, IAsyncNodeManager? viewManager) =
await GetManagerHandleAsync(view.ViewId, cancellationToken)
.ConfigureAwait(false);
if (viewHandle == null || viewManager == null)
{
throw new ServiceResultException(StatusCodes.BadViewIdUnknown);
}
NodeMetadata metadata = await viewManager.GetNodeMetadataAsync(
context,
viewHandle,
BrowseResultMask.NodeClass,
cancellationToken)
.ConfigureAwait(false);
if (metadata == null || metadata.NodeClass != NodeClass.View)
{
throw new ServiceResultException(StatusCodes.BadViewIdUnknown);
}
// validate access rights and role permissions
ServiceResult validationResult = await ValidatePermissionsAsync(
context,
viewManager,
viewHandle,
PermissionType.Browse,
null,
true,
cancellationToken)
.ConfigureAwait(false);
if (ServiceResult.IsBad(validationResult))
{
throw new ServiceResultException(validationResult);
}
view.Handle = viewHandle;
}
bool diagnosticsExist = false;
var results = new List(nodesToBrowse.Count);
var diagnosticInfos = new List(nodesToBrowse.Count);
uint continuationPointsAssigned = 0;
for (int ii = 0; ii < nodesToBrowse.Count; ii++)
{
// check if request has timed out or been cancelled.
if (StatusCode.IsBad(context.OperationStatus))
{
// release all allocated continuation points.
foreach (BrowseResult current in results)
{
if (current != null && !current.ContinuationPoint.IsEmpty)
{
ContinuationPoint? cp = context.Session
.RestoreContinuationPoint(current.ContinuationPoint);
cp?.Dispose();
}
}
throw new ServiceResultException(context.OperationStatus);
}
BrowseDescription nodeToBrowse = nodesToBrowse[ii];
// initialize result.
var result = new BrowseResult { StatusCode = StatusCodes.Good };
results.Add(result);
ServiceResult error;
// need to trap unexpected exceptions to handle bugs in the node managers.
try
{
error = await BrowseAsync(
context,
view,
maxReferencesPerNode,
continuationPointsAssigned < m_maxContinuationPointsPerBrowse,
nodeToBrowse,
result,
cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
error = ServiceResult.Create(
e,
StatusCodes.BadUnexpectedError,
"Unexpected error browsing node.");
}
// check for continuation point.
if (!result.ContinuationPoint.IsEmpty)
{
continuationPointsAssigned++;
}
// check for error.
result.StatusCode = error.StatusCode;
if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
DiagnosticInfo? diagnosticInfo = null;
if (error != null && error.Code != StatusCodes.Good)
{
diagnosticInfo = ServerUtils.CreateDiagnosticInfo(Server, context, error, m_logger);
diagnosticsExist = true;
}
diagnosticInfos.Add(diagnosticInfo!);
}
}
// clear the diagnostics array if no diagnostics requested or no errors occurred.
UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos);
return (results, diagnosticInfos);
}
///
/// Prepare a cache per NodeManager and unique NodeId that holds the attributes needed to validate the AccessRestrictions and RolePermissions.
/// This cache is then used in subsequenct calls to avoid triggering unnecessary time consuming callbacks.
/// The current services that benefit from this are the Read service
///
/// One of the following types used in the service calls:
/// ReadValueId used in the Read service
/// The collection of nodes on which the service operates uppon
/// The resulting cache that holds the values of the AccessRestrictions and RolePermissions attributes needed for Read service
///
private static void PrepareValidationCache(
ArrayOf nodesList,
out Dictionary uniqueNodesServiceAttributes)
{
var uniqueNodes = new HashSet();
Type listType = typeof(T);
if (listType != typeof(ReadValueId) &&
listType != typeof(WriteValue) &&
listType != typeof(BrowseDescription) &&
listType != typeof(CallMethodRequest))
{
throw new ArgumentException(
"Provided List nodesCollection is of wrong type, T should be type BrowseDescription, ReadValueId or CallMethodRequest",
nameof(nodesList));
}
for (int i = 0; i < nodesList.Count; i++)
{
NodeId nodeId = default;
if (listType == typeof(ReadValueId))
{
nodeId = (nodesList[i] as ReadValueId)?.NodeId ?? default;
}
else if (listType == typeof(BrowseDescription))
{
nodeId = (nodesList[i] as BrowseDescription)?.NodeId ?? default;
}
else if (listType == typeof(CallMethodRequest))
{
nodeId = (nodesList[i] as CallMethodRequest)?.ObjectId ?? default;
}
if (!nodeId.IsNull)
{
uniqueNodes.Add(nodeId);
}
}
// uniqueNodesReadAttributes is the place where the attributes for each unique nodeId are kept on the services
uniqueNodesServiceAttributes = [];
foreach (NodeId uniqueNode in uniqueNodes)
{
uniqueNodesServiceAttributes.Add(uniqueNode, []);
}
}
///
public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)>
BrowseNextAsync(
OperationContext context,
bool releaseContinuationPoints,
ArrayOf continuationPoints,
CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
bool diagnosticsExist = false;
var results = new List(continuationPoints.Count);
var diagnosticInfos = new List(continuationPoints.Count);
uint continuationPointsAssigned = 0;
for (int ii = 0; ii < continuationPoints.Count; ii++)
{
ContinuationPoint? cp;
// check if request has timed out or been canceled.
if (StatusCode.IsBad(context.OperationStatus))
{
// release all allocated continuation points.
foreach (BrowseResult current in results)
{
if (current != null && !current.ContinuationPoint.IsEmpty)
{
cp = context.Session
.RestoreContinuationPoint(current.ContinuationPoint);
cp?.Dispose();
}
}
throw new ServiceResultException(context.OperationStatus);
}
// find the continuation point.
cp = context.Session.RestoreContinuationPoint(continuationPoints[ii]);
// validate access rights and role permissions
if (cp != null)
{
ServiceResult validationResult = await ValidatePermissionsAsync(
context,
cp.Manager,
cp.NodeToBrowse,
PermissionType.Browse,
null,
true,
cancellationToken)
.ConfigureAwait(false);
if (ServiceResult.IsBad(validationResult))
{
var badResult = new BrowseResult { StatusCode = validationResult.Code };
results.Add(badResult);
// put placeholder for diagnostics
diagnosticInfos.Add(null!);
continue;
}
}
// initialize result.
var result = new BrowseResult { StatusCode = StatusCodes.Good };
results.Add(result);
// check if simply releasing the continuation point.
if (releaseContinuationPoints)
{
cp?.Dispose();
continue;
}
ServiceResult? error = null;
// check if continuation point has expired.
if (cp == null)
{
error = StatusCodes.BadContinuationPointInvalid;
}
if (cp != null)
{
// need to trap unexpected exceptions to handle bugs in the node managers.
try
{
ArrayOf references = result.References;
(error, cp, references) = await FetchReferencesAsync(
context,
continuationPointsAssigned < m_maxContinuationPointsPerBrowse,
cp!,
references,
cancellationToken)
.ConfigureAwait(false);
result.References = references;
}
catch (Exception e)
{
error = ServiceResult.Create(
e,
StatusCodes.BadUnexpectedError,
"Unexpected error browsing node.");
}
// check for continuation point.
if (!result.ContinuationPoint.IsEmpty)
{
continuationPointsAssigned++;
}
}
// check for error.
result.StatusCode = error!.StatusCode;
if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
DiagnosticInfo? diagnosticInfo = null;
if (error != null && error.Code != StatusCodes.Good)
{
diagnosticInfo = ServerUtils.CreateDiagnosticInfo(
Server,
context,
error,
m_logger);
diagnosticsExist = true;
}
diagnosticInfos.Add(diagnosticInfo!);
}
// check for continuation point.
if (cp != null)
{
result.StatusCode = StatusCodes.Good;
result.ContinuationPoint = cp.Id.ToByteArray().ToByteString();
}
}
// clear the diagnostics array if no diagnostics requested or no errors occurred.
UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos);
return (results, diagnosticInfos);
}
///
/// Returns the set of references that meet the filter criteria.
///
protected async ValueTask BrowseAsync(
OperationContext context,
ViewDescription? view,
uint maxReferencesPerNode,
bool assignContinuationPoint,
BrowseDescription nodeToBrowse,
BrowseResult result,
CancellationToken cancellationToken = default)
{
Debug.Assert(context != null);
Debug.Assert(nodeToBrowse != null);
Debug.Assert(result != null);
// find node manager that owns the node.
(object? handle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(nodeToBrowse!.NodeId, cancellationToken)
.ConfigureAwait(false);
if (handle == null)
{
return StatusCodes.BadNodeIdUnknown;
}
if (!nodeToBrowse.ReferenceTypeId.IsNull &&
!Server.TypeTree.IsKnown(nodeToBrowse.ReferenceTypeId))
{
return StatusCodes.BadReferenceTypeIdInvalid;
}
if (nodeToBrowse.BrowseDirection is < BrowseDirection.Forward or > BrowseDirection.Both)
{
return StatusCodes.BadBrowseDirectionInvalid;
}
// validate access rights and role permissions
ServiceResult validationResult = await ValidatePermissionsAsync(
context!,
nodeManager,
handle,
PermissionType.Browse,
null,
true,
cancellationToken)
.ConfigureAwait(false);
if (ServiceResult.IsBad(validationResult))
{
return validationResult;
}
// create a continuation point.
ContinuationPoint? tempCp = null;
try
{
tempCp = new ContinuationPoint
{
Manager = nodeManager!,
View = view,
NodeToBrowse = handle,
MaxResultsToReturn = maxReferencesPerNode,
BrowseDirection = nodeToBrowse.BrowseDirection,
ReferenceTypeId = nodeToBrowse.ReferenceTypeId,
IncludeSubtypes = nodeToBrowse.IncludeSubtypes,
NodeClassMask = nodeToBrowse.NodeClassMask,
ResultMask = (BrowseResultMask)nodeToBrowse.ResultMask,
Index = 0,
Data = null
};
ContinuationPoint? cp = tempCp;
// check if reference type left unspecified.
if (cp.ReferenceTypeId.IsNull)
{
cp.ReferenceTypeId = ReferenceTypeIds.References;
cp.IncludeSubtypes = true;
}
// loop until browse is complete or max results.
ArrayOf references = result!.References;
ServiceResult error;
(error, cp, references) = await FetchReferencesAsync(
context!,
assignContinuationPoint,
cp!,
references,
cancellationToken)
.ConfigureAwait(false);
tempCp = null; // ownership transferred to FetchReferencesAsync
result.References = references;
// save continuation point.
if (cp != null)
{
result.StatusCode = StatusCodes.Good;
result.ContinuationPoint = cp.Id.ToByteArray().ToByteString();
}
// all is good.
return error;
}
finally
{
tempCp?.Dispose();
}
}
///
/// Loops until browse is complete for max results reached.
///
protected async ValueTask<(
ServiceResult serviceResult,
ContinuationPoint? cp,
ArrayOf references
)> FetchReferencesAsync(
OperationContext context,
bool assignContinuationPoint,
ContinuationPoint cp,
ArrayOf references,
CancellationToken cancellationToken = default)
{
Debug.Assert(context != null);
Debug.Assert(cp != null);
IAsyncNodeManager nodeManager = cp!.Manager;
var nodeClassMask = (NodeClass)cp.NodeClassMask;
BrowseResultMask resultMask = cp.ResultMask;
var referenceList = references.ToList();
ContinuationPoint? currentCp = cp;
// loop until browse is complete or max results.
while (currentCp != null)
{
currentCp = await nodeManager.BrowseAsync(context!, currentCp, referenceList, cancellationToken)
.ConfigureAwait(false);
var referencesToKeep = new List(referenceList.Count);
// check for incomplete reference descriptions.
for (int ii = 0; ii < referenceList.Count; ii++)
{
ReferenceDescription reference = referenceList[ii];
// check if filtering must be applied.
if (reference.Unfiltered)
{
// ignore unknown external references.
if (reference.NodeId.IsAbsolute)
{
continue;
}
// update the description.
bool include = await UpdateReferenceDescriptionAsync(
context!,
(NodeId)reference.NodeId,
nodeClassMask,
resultMask,
reference,
cancellationToken)
.ConfigureAwait(false);
if (!include)
{
continue;
}
}
// add to list.
referencesToKeep.Add(reference);
}
// replace list.
referenceList = referencesToKeep;
// check if browse limit reached.
if (currentCp != null && referenceList.Count >= currentCp.MaxResultsToReturn)
{
if (!assignContinuationPoint)
{
return (StatusCodes.BadNoContinuationPoints, currentCp, referenceList);
}
currentCp.Id = Guid.NewGuid();
context!.Session!.SaveContinuationPoint(currentCp);
break;
}
}
// all is good.
return (ServiceResult.Good, currentCp, referenceList);
}
///
/// Updates the reference description with the node attributes.
///
/// is null.
private async ValueTask UpdateReferenceDescriptionAsync(
OperationContext context,
NodeId targetId,
NodeClass nodeClassMask,
BrowseResultMask resultMask,
ReferenceDescription description,
CancellationToken cancellationToken = default)
{
if (targetId.IsNull)
{
throw new ArgumentNullException(nameof(targetId));
}
if (description == null)
{
throw new ArgumentNullException(nameof(description));
}
// find node manager that owns the node.
(object? handle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(targetId, cancellationToken)
.ConfigureAwait(false);
// dangling reference - nothing more to do.
if (handle == null)
{
return false;
}
// fetch the node attributes.
NodeMetadata metadata = await nodeManager!.GetNodeMetadataAsync(context, handle, resultMask, cancellationToken)
.ConfigureAwait(false);
if (metadata == null)
{
return false;
}
// check nodeclass filter.
if (nodeClassMask != NodeClass.Unspecified &&
((int)metadata.NodeClass & (int)nodeClassMask) == 0)
{
return false;
}
// update attributes.
description.NodeId = metadata.NodeId;
description.SetTargetAttributes(
resultMask,
metadata.NodeClass,
metadata.BrowseName,
metadata.DisplayName,
metadata.TypeDefinition);
description.Unfiltered = false;
return true;
}
///
public async ValueTask FindNodeInAddressSpaceAsync(NodeId nodeId, CancellationToken cancellationToken = default)
{
if (nodeId.IsNull)
{
return null;
}
// search node id in all node managers
foreach (IAsyncNodeManager nodeManager in AsyncNodeManagers)
{
if ((await nodeManager.GetManagerHandleAsync(nodeId, cancellationToken).ConfigureAwait(false))
is not NodeHandle handle)
{
continue;
}
return handle.Node;
}
return null;
}
///
public virtual async ValueTask<(ArrayOf values, ArrayOf diagnosticInfos)> ReadAsync(
OperationContext context,
double maxAge,
TimestampsToReturn timestampsToReturn,
ArrayOf nodesToRead,
CancellationToken cancellationToken = default)
{
if (maxAge < 0)
{
throw new ServiceResultException(StatusCodes.BadMaxAgeInvalid);
}
if (timestampsToReturn is < TimestampsToReturn.Source or > TimestampsToReturn.Neither)
{
throw new ServiceResultException(StatusCodes.BadTimestampsToReturnInvalid);
}
bool diagnosticsExist = false;
var values = new List(nodesToRead.Count);
var diagnosticInfos = new List(nodesToRead.Count);
// create empty list of errors.
var errors = new List(values.Count);
for (int ii = 0; ii < nodesToRead.Count; ii++)
{
errors.Add(null!);
}
// add placeholder for each result.
bool validItems = false;
m_logger.LogTrace(
Utils.TraceMasks.ServiceDetail,
"MasterNodeManager.Read - Count={Count}",
nodesToRead.Count);
PrepareValidationCache(
nodesToRead,
out Dictionary uniqueNodesReadAttributes);
for (int ii = 0; ii < nodesToRead.Count; ii++)
{
// add default value to values collection
values.Add(default);
// add placeholder for diagnostics
diagnosticInfos.Add(null!);
// pre-validate and pre-parse parameter.
errors[ii] = (await ValidateReadRequestAsync(
context,
nodesToRead[ii],
uniqueNodesReadAttributes,
false,
cancellationToken)
.ConfigureAwait(false))!;
// return error status.
if (ServiceResult.IsBad(errors[ii]))
{
nodesToRead[ii].Processed = true;
}
// found at least one valid item.
else
{
nodesToRead[ii].Processed = false;
validItems = true;
}
}
// call each node manager.
if (validItems)
{
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.ReadAsync(
context,
maxAge,
nodesToRead,
values,
errors,
cancellationToken).ConfigureAwait(false);
}
}
// process results.
for (int ii = 0; ii < nodesToRead.Count; ii++)
{
DataValue value = values[ii];
// set an error code for nodes that were not handled by any node manager.
if (!nodesToRead[ii].Processed)
{
value = values[ii] = DataValue.FromStatusCode(
StatusCodes.BadNodeIdUnknown,
DateTime.UtcNow);
errors[ii] = new ServiceResult(values[ii].StatusCode);
}
// update the diagnostic info and ensure the status code in the data value is the same as the error code.
if (errors[ii] != null && errors[ii].Code != StatusCodes.Good)
{
if (value.IsNull)
{
value = values[ii] = DataValue.FromStatusCode(errors[ii].Code, DateTime.UtcNow);
}
value = values[ii] = value.WithStatus(errors[ii].Code);
if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
diagnosticInfos[ii] = ServerUtils.CreateDiagnosticInfo(
Server,
context,
errors[ii],
m_logger)!;
diagnosticsExist = true;
}
}
// apply the timestamp filters.
if (timestampsToReturn is not TimestampsToReturn.Server and not TimestampsToReturn.Both)
{
value = values[ii] = value.WithServerTimestamp(DateTimeUtc.MinValue);
}
if (timestampsToReturn is not TimestampsToReturn.Source and not TimestampsToReturn.Both)
{
_ = values[ii] = value.WithSourceTimestamp(DateTimeUtc.MinValue);
}
}
// clear the diagnostics array if no diagnostics requested or no errors occurred.
UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos);
return (values, diagnosticInfos);
}
///
public virtual async ValueTask<(ArrayOf values, ArrayOf diagnosticInfos)> HistoryReadAsync(
OperationContext context,
ExtensionObject historyReadDetails,
TimestampsToReturn timestampsToReturn,
bool releaseContinuationPoints,
ArrayOf nodesToRead,
CancellationToken cancellationToken = default)
{
// validate history details parameter.
if (historyReadDetails.IsNull)
{
throw new ServiceResultException(StatusCodes.BadHistoryOperationInvalid);
}
if (!historyReadDetails.TryGetValue(out HistoryReadDetails? details))
{
throw new ServiceResultException(StatusCodes.BadHistoryOperationInvalid);
}
// create result lists.
bool diagnosticsExist = false;
var results = new List(nodesToRead.Count);
var diagnosticInfos = new List(nodesToRead.Count);
// pre-validate items.
bool validItems = false;
// create empty list of errors.
var errors = new List(results.Count);
for (int ii = 0; ii < nodesToRead.Count; ii++)
{
errors.Add(null!);
}
for (int ii = 0; ii < nodesToRead.Count; ii++)
{
// Limit permission restrictions to Client initiated service call
HistoryReadResult? result = null;
DiagnosticInfo? diagnosticInfo = null;
// pre-validate and pre-parse parameter.
errors[ii] = (await ValidateHistoryReadRequestAsync(context, nodesToRead[ii], cancellationToken)
.ConfigureAwait(false))!;
// return error status.
if (ServiceResult.IsBad(errors[ii]))
{
nodesToRead[ii].Processed = true;
result = new HistoryReadResult { StatusCode = errors[ii].Code };
// add diagnostics if requested.
if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
diagnosticInfo = ServerUtils.CreateDiagnosticInfo(
Server,
context,
errors[ii],
m_logger);
diagnosticsExist = true;
}
}
// found at least one valid item.
else
{
nodesToRead[ii].Processed = false;
validItems = true;
}
results.Add(result!);
diagnosticInfos.Add(diagnosticInfo!);
}
// call each node manager.
if (validItems)
{
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.HistoryReadAsync(
context,
details!,
timestampsToReturn,
releaseContinuationPoints,
nodesToRead,
results,
errors,
cancellationToken).ConfigureAwait(false);
}
for (int ii = 0; ii < nodesToRead.Count; ii++)
{
HistoryReadResult result = results[ii];
// set an error code for nodes that were not handled by any node manager.
if (!nodesToRead[ii].Processed)
{
nodesToRead[ii].Processed = true;
result = results[ii] = new HistoryReadResult();
result.StatusCode = StatusCodes.BadNodeIdUnknown;
errors[ii] = results[ii].StatusCode;
}
// update the diagnostic info and ensure the status code in the result is the same as the error code.
if (errors[ii] != null && errors[ii].Code != StatusCodes.Good)
{
result ??= results[ii] = new HistoryReadResult();
result.StatusCode = errors[ii].Code;
// add diagnostics if requested.
if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
diagnosticInfos[ii] = ServerUtils.CreateDiagnosticInfo(
Server,
context,
errors[ii],
m_logger)!;
diagnosticsExist = true;
}
}
}
}
// clear the diagnostics array if no diagnostics requested or no errors occurred.
UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos);
return (results, diagnosticInfos);
}
///
public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)> WriteAsync(
OperationContext context,
ArrayOf nodesToWrite,
CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
int count = nodesToWrite.Count;
bool diagnosticsExist = false;
var results = new List(count);
var diagnosticInfos = new List(count);
PrepareValidationCache(
nodesToWrite,
out Dictionary uniqueNodesReadAttributes);
// add placeholder for each result.
bool validItems = false;
for (int ii = 0; ii < count; ii++)
{
StatusCode result = StatusCodes.Good;
DiagnosticInfo? diagnosticInfo = null;
// pre-validate and pre-parse parameter. Validate also access rights and role permissions
ServiceResult? error = await ValidateWriteRequestAsync(
context,
nodesToWrite[ii],
uniqueNodesReadAttributes,
false,
cancellationToken)
.ConfigureAwait(false);
// return error status.
if (ServiceResult.IsBad(error))
{
nodesToWrite[ii].Processed = true;
result = error!.Code;
// add diagnostics if requested.
if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
diagnosticInfo = ServerUtils.CreateDiagnosticInfo(Server, context, error, m_logger);
diagnosticsExist = true;
}
}
// found at least one valid item.
else
{
nodesToWrite[ii].Processed = false;
validItems = true;
}
results.Add(result);
diagnosticInfos.Add(diagnosticInfo!);
}
// call each node manager.
if (validItems)
{
var errors = new List(count);
errors.AddRange(new ServiceResult[count]);
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.WriteAsync(
context,
nodesToWrite,
errors,
cancellationToken).ConfigureAwait(false);
}
for (int ii = 0; ii < nodesToWrite.Count; ii++)
{
if (!nodesToWrite[ii].Processed)
{
errors[ii] = StatusCodes.BadNodeIdUnknown;
}
if (errors[ii] != null && errors[ii].Code != StatusCodes.Good)
{
results[ii] = errors[ii].Code;
// add diagnostics if requested.
if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
diagnosticInfos[ii] = ServerUtils.CreateDiagnosticInfo(
Server,
context,
errors[ii],
m_logger)!;
diagnosticsExist = true;
}
}
ServerUtils.ReportWriteValue(
nodesToWrite[ii].NodeId,
nodesToWrite[ii].Value,
results[ii]);
}
}
// clear the diagnostics array if no diagnostics requested or no errors occurred.
UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos);
return (results, diagnosticInfos);
}
///
public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)>
HistoryUpdateAsync(
OperationContext context,
ArrayOf historyUpdateDetails,
CancellationToken cancellationToken = default)
{
Type? detailsType = null;
var nodesToUpdate = new List();
// verify that all extension objects in the list have the same type.
foreach (ExtensionObject details in historyUpdateDetails)
{
if (details.IsNull)
{
continue;
}
if (!details.TryGetValue(out HistoryUpdateDetails? historyUpdateDetail))
{
nodesToUpdate.Add(null!); // Retain old behavior
continue;
}
detailsType = historyUpdateDetail!.GetType();
nodesToUpdate.Add(historyUpdateDetail);
}
// create result lists.
bool diagnosticsExist = false;
var results = new List(nodesToUpdate.Count);
var diagnosticInfos = new List(nodesToUpdate.Count);
// pre-validate items.
bool validItems = false;
// create empty list of errors.
var errors = new List(results.Count);
for (int ii = 0; ii < nodesToUpdate.Count; ii++)
{
errors.Add(null!);
}
for (int ii = 0; ii < nodesToUpdate.Count; ii++)
{
HistoryUpdateResult? result = null;
DiagnosticInfo? diagnosticInfo = null;
// check the type of details parameter.
ServiceResult? error;
if (nodesToUpdate[ii].GetType() != detailsType)
{
error = StatusCodes.BadHistoryOperationInvalid;
}
// pre-validate and pre-parse parameter.
else
{
error = await ValidateHistoryUpdateRequestAsync(context, nodesToUpdate[ii], cancellationToken)
.ConfigureAwait(false);
}
// return error status.
if (ServiceResult.IsBad(error))
{
nodesToUpdate[ii].Processed = true;
result = new HistoryUpdateResult { StatusCode = error!.Code };
// add diagnostics if requested.
if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
diagnosticInfo = ServerUtils.CreateDiagnosticInfo(Server, context, error, m_logger);
diagnosticsExist = true;
}
}
// found at least one valid item.
else
{
nodesToUpdate[ii].Processed = false;
validItems = true;
}
results.Add(result!);
diagnosticInfos.Add(diagnosticInfo!);
}
// call each node manager.
if (validItems)
{
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.HistoryUpdateAsync(
context,
detailsType!,
nodesToUpdate,
results,
errors,
cancellationToken).ConfigureAwait(false);
}
for (int ii = 0; ii < nodesToUpdate.Count; ii++)
{
HistoryUpdateResult result = results[ii];
// set an error code for nodes that were not handled by any node manager.
if (!nodesToUpdate[ii].Processed)
{
nodesToUpdate[ii].Processed = true;
result = results[ii] = new HistoryUpdateResult();
result.StatusCode = StatusCodes.BadNodeIdUnknown;
errors[ii] = result.StatusCode;
}
// update the diagnostic info and ensure the status code in the result is the same as the error code.
if (errors[ii] != null && errors[ii].Code != StatusCodes.Good)
{
result ??= results[ii] = new HistoryUpdateResult();
result.StatusCode = errors[ii].Code;
// add diagnostics if requested.
if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
diagnosticInfos[ii] = ServerUtils.CreateDiagnosticInfo(
Server,
context,
errors[ii],
m_logger)!;
diagnosticsExist = true;
}
}
}
}
// clear the diagnostics array if no diagnostics requested or no errors occurred.
UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos);
return (results, diagnosticInfos);
}
///
public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)>
CallAsync(
OperationContext context,
ArrayOf methodsToCall,
CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
bool diagnosticsExist = false;
var results = new List(methodsToCall.Count);
var diagnosticInfos = new List(methodsToCall.Count);
var errors = new List(methodsToCall.Count);
PrepareValidationCache(
methodsToCall,
out Dictionary uniqueNodesReadAttributes);
// add placeholder for each result.
bool validItems = false;
for (int ii = 0; ii < methodsToCall.Count; ii++)
{
results.Add(null!);
errors.Add(null!);
if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
diagnosticInfos.Add(null!);
}
// validate request parameters.
errors[ii] = await ValidateCallRequestItemAsync(
context,
methodsToCall[ii],
uniqueNodesReadAttributes,
// With resolver-based method lookup and an attribute cache, GetPermissionMetadataAsync
// reads and caches the required permission attributes without the permissionsOnly shortcut.
false,
cancellationToken).ConfigureAwait(false);
if (ServiceResult.IsBad(errors[ii]))
{
methodsToCall[ii].Processed = true;
// add diagnostics if requested.
if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
diagnosticInfos[ii] = ServerUtils.CreateDiagnosticInfo(
Server,
context,
errors[ii],
m_logger)!;
diagnosticsExist = true;
}
continue;
}
// found at least one valid item.
validItems = true;
methodsToCall[ii].Processed = false;
}
// call each node manager.
if (validItems)
{
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.CallAsync(
context,
methodsToCall,
results,
errors,
cancellationToken).ConfigureAwait(false);
}
}
for (int ii = 0; ii < methodsToCall.Count; ii++)
{
// set an error code for calls that were not handled by any node manager.
if (!methodsToCall[ii].Processed)
{
results[ii] = new CallMethodResult();
errors[ii] = StatusCodes.BadNodeIdUnknown;
}
// update the diagnostic info and ensure the status code in the result is the same as the error code.
if (errors[ii] != null && errors[ii].Code != StatusCodes.Good)
{
if (results[ii] == null)
{
results[ii] = new CallMethodResult();
}
results[ii].StatusCode = errors[ii].Code;
// add diagnostics if requested.
if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0)
{
diagnosticInfos[ii] = ServerUtils.CreateDiagnosticInfo(
Server,
context,
errors[ii],
m_logger)!;
diagnosticsExist = true;
}
}
}
// clear the diagnostics array if no diagnostics requested or no errors occurred.
UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos);
return (results, diagnosticInfos);
}
///
public virtual async ValueTask ConditionRefreshAsync(
OperationContext context,
IList monitoredItems,
CancellationToken cancellationToken = default)
{
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
try
{
await nodeManager.ConditionRefreshAsync(context, monitoredItems, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception e)
{
m_logger.LogError(e, "Error calling ConditionRefreshAsync on AsyncNodeManager.");
}
}
}
///
/// Creates a set of monitored items.
///
/// is null.
///
///
[Obsolete("Use CreateMonitoredItemsAsync")]
public virtual void CreateMonitoredItems(
OperationContext context,
uint subscriptionId,
double publishingInterval,
TimestampsToReturn timestampsToReturn,
ArrayOf itemsToCreate,
IList errors,
IList filterResults,
IList monitoredItems,
bool createDurable)
{
CreateMonitoredItemsAsync(
context,
subscriptionId,
publishingInterval,
timestampsToReturn,
itemsToCreate,
errors,
filterResults,
monitoredItems,
createDurable).AsTask().GetAwaiter().GetResult();
}
///
public virtual async ValueTask CreateMonitoredItemsAsync(
OperationContext context,
uint subscriptionId,
double publishingInterval,
TimestampsToReturn timestampsToReturn,
ArrayOf itemsToCreate,
IList errors,
IList filterResults,
IList monitoredItems,
bool createDurable,
CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (errors == null)
{
throw new ArgumentNullException(nameof(errors));
}
if (filterResults == null)
{
throw new ArgumentNullException(nameof(filterResults));
}
if (monitoredItems == null)
{
throw new ArgumentNullException(nameof(monitoredItems));
}
if (publishingInterval < 0)
{
throw new ArgumentOutOfRangeException(nameof(publishingInterval));
}
if (timestampsToReturn is < TimestampsToReturn.Source or > TimestampsToReturn.Neither)
{
throw new ServiceResultException(StatusCodes.BadTimestampsToReturnInvalid);
}
// add placeholder for each result.
bool validItems = false;
for (int ii = 0; ii < itemsToCreate.Count; ii++)
{
// validate request parameters.
errors[ii] = (await ValidateMonitoredItemCreateRequestAsync(context, itemsToCreate[ii], cancellationToken)
.ConfigureAwait(false))!;
if (ServiceResult.IsBad(errors[ii]))
{
itemsToCreate[ii].Processed = true;
continue;
}
// found at least one valid item.
validItems = true;
itemsToCreate[ii].Processed = false;
}
// call each node manager.
if (validItems)
{
// create items for event filters.
await CreateMonitoredItemsForEventsAsync(
context,
subscriptionId,
publishingInterval,
timestampsToReturn,
itemsToCreate,
errors,
filterResults,
monitoredItems,
createDurable,
m_monitoredItemIdFactory,
cancellationToken)
.ConfigureAwait(false);
// create items for data access.
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.CreateMonitoredItemsAsync(
context,
subscriptionId,
publishingInterval,
timestampsToReturn,
itemsToCreate,
errors,
filterResults,
monitoredItems,
createDurable,
m_monitoredItemIdFactory,
cancellationToken)
.ConfigureAwait(false);
}
// fill results for unknown nodes.
for (int ii = 0; ii < errors.Count; ii++)
{
if (!itemsToCreate[ii].Processed)
{
errors[ii] = new ServiceResult(StatusCodes.BadNodeIdUnknown);
}
}
}
}
///
/// Create monitored items for event subscriptions.
///
private async ValueTask CreateMonitoredItemsForEventsAsync(
OperationContext context,
uint subscriptionId,
double publishingInterval,
TimestampsToReturn timestampsToReturn,
ArrayOf itemsToCreate,
IList errors,
IList filterResults,
IList monitoredItems,
bool createDurable,
MonitoredItemIdFactory monitoredItemIdFactory,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < itemsToCreate.Count; ii++)
{
MonitoredItemCreateRequest itemToCreate = itemsToCreate[ii];
if (!itemToCreate.Processed)
{
// all event subscriptions required an event filter.
if (!itemToCreate.RequestedParameters.Filter.TryGetValue(out EventFilter? filter))
{
continue;
}
itemToCreate.Processed = true;
// only the value attribute may be used with an event subscription.
if (itemToCreate.ItemToMonitor.AttributeId != Attributes.EventNotifier)
{
errors[ii] = StatusCodes.BadFilterNotAllowed;
continue;
}
// the index range parameter has no meaning for event subscriptions.
if (!string.IsNullOrEmpty(itemToCreate.ItemToMonitor.IndexRange))
{
errors[ii] = StatusCodes.BadIndexRangeInvalid;
continue;
}
// the data encoding has no meaning for event subscriptions.
if (!itemToCreate.ItemToMonitor.DataEncoding.IsNull)
{
errors[ii] = StatusCodes.BadDataEncodingInvalid;
continue;
}
// validate the event filter.
EventFilter.Result result = filter!.Validate(
new FilterContext(Server.NamespaceUris, Server.TypeTree, context, Server.Telemetry));
if (ServiceResult.IsBad(result.Status))
{
errors[ii] = result.Status;
filterResults[ii] = result.ToEventFilterResult(
context.DiagnosticsMask,
context.StringTable,
m_logger);
continue;
}
// check if a valid node.
(object? handle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(
itemToCreate.ItemToMonitor.NodeId, cancellationToken)
.ConfigureAwait(false);
if (handle == null)
{
errors[ii] = StatusCodes.BadNodeIdUnknown;
continue;
}
NodeMetadata nodeMetadata = await nodeManager!.GetNodeMetadataAsync(
context,
handle,
BrowseResultMask.All,
cancellationToken)
.ConfigureAwait(false);
errors[ii] = ValidateRolePermissions(
context,
nodeMetadata,
PermissionType.ReceiveEvents,
m_logger);
if (ServiceResult.IsBad(errors[ii]))
{
continue;
}
IEventMonitoredItem monitoredItem = Server.EventManager.CreateMonitoredItem(
context,
nodeManager,
handle,
subscriptionId,
monitoredItemIdFactory,
timestampsToReturn,
publishingInterval,
itemToCreate,
filter,
createDurable);
// subscribe to all node managers.
if (itemToCreate.ItemToMonitor.NodeId == Objects.Server)
{
foreach (IAsyncNodeManager manager in m_nodeManagers)
{
try
{
await manager.SubscribeToAllEventsAsync(
context,
subscriptionId,
monitoredItem,
false,
cancellationToken)
.ConfigureAwait(false);
}
catch (Exception e)
{
m_logger.LogError(
e,
"NodeManager threw an exception subscribing to all events. NodeManager={NodeManager}",
manager);
}
}
}
// only subscribe to the node manager that owns the node.
else
{
ServiceResult error = await nodeManager.SubscribeToEventsAsync(
context,
handle,
subscriptionId,
monitoredItem,
false,
cancellationToken)
.ConfigureAwait(false);
if (ServiceResult.IsBad(error))
{
Server.EventManager.DeleteMonitoredItem(monitoredItem.Id);
errors[ii] = error;
continue;
}
}
monitoredItems[ii] = monitoredItem;
errors[ii] = StatusCodes.Good;
}
}
}
///
public virtual async ValueTask RestoreMonitoredItemsAsync(
IList itemsToRestore,
IList monitoredItems,
IUserIdentity savedOwnerIdentity,
CancellationToken cancellationToken = default)
{
if (itemsToRestore == null)
{
throw new ArgumentNullException(nameof(itemsToRestore));
}
if (monitoredItems == null)
{
throw new ArgumentNullException(nameof(monitoredItems));
}
if (Server.IsRunning)
{
throw new InvalidOperationException(
"Subscription restore can only occur on startup");
}
// create items for event filters.
await RestoreMonitoredItemsForEventsAsync(itemsToRestore, monitoredItems, cancellationToken)
.ConfigureAwait(false);
// create items for data access.
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.RestoreMonitoredItemsAsync(
itemsToRestore,
monitoredItems,
savedOwnerIdentity,
cancellationToken)
.ConfigureAwait(false);
}
m_monitoredItemIdFactory.SetStartValue(itemsToRestore.Max(i => i.Id));
}
///
/// Restore monitored items for event subscriptions.
///
private async ValueTask RestoreMonitoredItemsForEventsAsync(
IList itemsToRestore,
IList monitoredItems,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < itemsToRestore.Count; ii++)
{
IStoredMonitoredItem item = itemsToRestore[ii];
if (!item.IsRestored)
{
// all event subscriptions required an event filter.
if (item.OriginalFilter is not EventFilter)
{
continue;
}
item.IsRestored = true;
// check if a valid node.
(object? handle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(item.NodeId, cancellationToken)
.ConfigureAwait(false);
if (handle == null)
{
continue;
}
IEventMonitoredItem monitoredItem = Server.EventManager.RestoreMonitoredItem(
nodeManager!,
handle,
item);
// subscribe to all node managers.
if (item.NodeId == Objects.Server)
{
foreach (IAsyncNodeManager manager in m_nodeManagers)
{
try
{
await manager.SubscribeToAllEventsAsync(
new OperationContext(monitoredItem),
monitoredItem.SubscriptionId,
monitoredItem,
false,
cancellationToken)
.ConfigureAwait(false);
}
catch (Exception e)
{
m_logger.LogError(
e,
"NodeManager threw an exception subscribing to all events. NodeManager={NodeManager}",
manager);
}
}
}
// only subscribe to the node manager that owns the node.
else
{
ServiceResult error = await nodeManager!.SubscribeToEventsAsync(
new OperationContext(monitoredItem),
handle,
monitoredItem.SubscriptionId,
monitoredItem,
false,
cancellationToken)
.ConfigureAwait(false);
if (ServiceResult.IsBad(error))
{
Server.EventManager.DeleteMonitoredItem(monitoredItem.Id);
continue;
}
}
monitoredItems[ii] = monitoredItem;
}
}
}
///
/// Modifies a set of monitored items.
///
/// is null.
///
[Obsolete("Use ModifyMonitoredItemsAsync")]
public virtual void ModifyMonitoredItems(
OperationContext context,
TimestampsToReturn timestampsToReturn,
IList monitoredItems,
ArrayOf itemsToModify,
IList errors,
IList filterResults)
{
ModifyMonitoredItemsAsync(
context,
timestampsToReturn,
monitoredItems,
itemsToModify,
errors,
filterResults).AsTask().GetAwaiter().GetResult();
}
///
public virtual async ValueTask ModifyMonitoredItemsAsync(
OperationContext context,
TimestampsToReturn timestampsToReturn,
IList monitoredItems,
ArrayOf itemsToModify,
IList errors,
IList filterResults,
CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (monitoredItems == null)
{
throw new ArgumentNullException(nameof(monitoredItems));
}
if (errors == null)
{
throw new ArgumentNullException(nameof(errors));
}
if (filterResults == null)
{
throw new ArgumentNullException(nameof(filterResults));
}
if (timestampsToReturn is < TimestampsToReturn.Source or > TimestampsToReturn.Neither)
{
throw new ServiceResultException(StatusCodes.BadTimestampsToReturnInvalid);
}
bool validItems = false;
for (int ii = 0; ii < itemsToModify.Count; ii++)
{
// check for errors.
if (ServiceResult.IsBad(errors[ii]) || monitoredItems[ii] == null)
{
itemsToModify[ii].Processed = true;
continue;
}
// validate request parameters.
errors[ii] = ValidateMonitoredItemModifyRequest(itemsToModify[ii])!;
if (ServiceResult.IsBad(errors[ii]))
{
itemsToModify[ii].Processed = true;
continue;
}
// found at least one valid item.
validItems = true;
itemsToModify[ii].Processed = false;
}
// call each node manager.
if (validItems)
{
// modify items for event filters.
await ModifyMonitoredItemsForEventsAsync(
context,
timestampsToReturn,
monitoredItems,
itemsToModify,
errors,
filterResults,
cancellationToken)
.ConfigureAwait(false);
// let each node manager figure out which items it owns.
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.ModifyMonitoredItemsAsync(
context,
timestampsToReturn,
monitoredItems,
itemsToModify,
errors,
filterResults,
cancellationToken)
.ConfigureAwait(false);
}
// update results.
for (int ii = 0; ii < errors.Count; ii++)
{
if (!itemsToModify[ii].Processed)
{
errors[ii] = new ServiceResult(StatusCodes.BadMonitoredItemIdInvalid);
}
}
}
}
///
/// Modify monitored items for event subscriptions.
///
private async ValueTask ModifyMonitoredItemsForEventsAsync(
OperationContext context,
TimestampsToReturn timestampsToReturn,
IList monitoredItems,
ArrayOf itemsToModify,
IList errors,
IList filterResults,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < itemsToModify.Count; ii++)
{
// all event subscriptions are handled by the event manager.
if (monitoredItems[ii] is not IEventMonitoredItem monitoredItem ||
(monitoredItem.MonitoredItemType & MonitoredItemTypeMask.Events) == 0)
{
continue;
}
MonitoredItemModifyRequest itemToModify = itemsToModify[ii];
itemToModify.Processed = true;
// check for a valid filter.
if (itemToModify.RequestedParameters.Filter.IsNull)
{
errors[ii] = StatusCodes.BadEventFilterInvalid;
continue;
}
// all event subscriptions required an event filter.
if (!itemToModify.RequestedParameters.Filter.TryGetValue(out EventFilter? filter))
{
errors[ii] = StatusCodes.BadEventFilterInvalid;
continue;
}
// validate the event filter.
EventFilter.Result result = filter!.Validate(
new FilterContext(Server.NamespaceUris, Server.TypeTree, context, Server.Telemetry));
if (ServiceResult.IsBad(result.Status))
{
errors[ii] = result.Status;
filterResults[ii] = result.ToEventFilterResult(
context.DiagnosticsMask,
context.StringTable,
m_logger);
continue;
}
// modify the item.
Server.EventManager.ModifyMonitoredItem(
context,
monitoredItem,
timestampsToReturn,
itemToModify,
filter);
// subscribe to all node managers.
if ((monitoredItem.MonitoredItemType & MonitoredItemTypeMask.AllEvents) != 0)
{
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.SubscribeToAllEventsAsync(
context,
monitoredItem.SubscriptionId,
monitoredItem,
false,
cancellationToken)
.ConfigureAwait(false);
}
}
// only subscribe to the node manager that owns the node.
else
{
await monitoredItem.NodeManager.SubscribeToEventsAsync(
context,
monitoredItem.ManagerHandle,
monitoredItem.SubscriptionId,
monitoredItem,
false,
cancellationToken).ConfigureAwait(false);
}
errors[ii] = StatusCodes.Good;
}
}
///
/// Transfers a set of monitored items.
///
/// is null.
[Obsolete("User TransferMonitoredItemsAsync")]
public virtual void TransferMonitoredItems(
OperationContext context,
bool sendInitialValues,
IList monitoredItems,
IList errors)
{
TransferMonitoredItemsAsync(
context,
sendInitialValues,
monitoredItems,
errors).AsTask().GetAwaiter().GetResult();
}
///
public virtual async ValueTask TransferMonitoredItemsAsync(
OperationContext context,
bool sendInitialValues,
IList monitoredItems,
IList errors,
CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (monitoredItems == null)
{
throw new ArgumentNullException(nameof(monitoredItems));
}
if (errors == null)
{
throw new ArgumentNullException(nameof(errors));
}
var processedItems = new List(monitoredItems.Count);
// preset results for unknown nodes
for (int ii = 0; ii < monitoredItems.Count; ii++)
{
processedItems.Add(monitoredItems[ii] == null);
errors[ii] = StatusCodes.BadMonitoredItemIdInvalid;
}
// call each node manager.
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.TransferMonitoredItemsAsync(
context,
sendInitialValues,
monitoredItems,
processedItems,
errors,
cancellationToken)
.ConfigureAwait(false);
}
}
///
/// Deletes a set of monitored items.
///
/// is null.
[Obsolete("Use DeleteMonitoredItemsAsync")]
public virtual void DeleteMonitoredItems(
OperationContext context,
uint subscriptionId,
IList itemsToDelete,
IList errors)
{
DeleteMonitoredItemsAsync(
context,
subscriptionId,
itemsToDelete,
errors).AsTask().GetAwaiter().GetResult();
}
///
public virtual async ValueTask DeleteMonitoredItemsAsync(
OperationContext context,
uint subscriptionId,
IList itemsToDelete,
IList errors,
CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (itemsToDelete == null)
{
throw new ArgumentNullException(nameof(itemsToDelete));
}
if (errors == null)
{
throw new ArgumentNullException(nameof(errors));
}
var processedItems = new List(itemsToDelete.Count);
for (int ii = 0; ii < itemsToDelete.Count; ii++)
{
processedItems.Add(ServiceResult.IsBad(errors[ii]) || itemsToDelete[ii] == null);
}
// delete items for event filters.
await DeleteMonitoredItemsForEventsAsync(
context,
subscriptionId,
itemsToDelete,
processedItems,
errors,
cancellationToken)
.ConfigureAwait(false);
// call each node manager.
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.DeleteMonitoredItemsAsync(
context,
itemsToDelete,
processedItems,
errors,
cancellationToken)
.ConfigureAwait(false);
}
// fill results for unknown nodes.
for (int ii = 0; ii < errors.Count; ii++)
{
if (!processedItems[ii])
{
errors[ii] = StatusCodes.BadMonitoredItemIdInvalid;
}
}
}
///
/// Delete monitored items for event subscriptions.
///
private async ValueTask DeleteMonitoredItemsForEventsAsync(
OperationContext context,
uint subscriptionId,
IList monitoredItems,
List processedItems,
IList errors,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < monitoredItems.Count; ii++)
{
// all event subscriptions are handled by the event manager.
if (monitoredItems[ii] is not IEventMonitoredItem monitoredItem ||
(monitoredItem.MonitoredItemType & MonitoredItemTypeMask.Events) == 0)
{
continue;
}
processedItems[ii] = true;
// unsubscribe to all node managers.
if ((monitoredItem.MonitoredItemType & MonitoredItemTypeMask.AllEvents) != 0)
{
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.SubscribeToAllEventsAsync(
context,
subscriptionId,
monitoredItem,
true,
cancellationToken)
.ConfigureAwait(false);
}
}
// only unsubscribe to the node manager that owns the node.
else
{
await monitoredItem.NodeManager.SubscribeToEventsAsync(
context,
monitoredItem.ManagerHandle,
subscriptionId,
monitoredItem,
true,
cancellationToken).ConfigureAwait(false);
}
// delete the item.
Server.EventManager.DeleteMonitoredItem(monitoredItem.Id);
// success.
errors[ii] = StatusCodes.Good;
}
}
///
public virtual async ValueTask SetMonitoringModeAsync(
OperationContext context,
MonitoringMode monitoringMode,
IList itemsToModify,
IList errors,
CancellationToken cancellationToken = default)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (itemsToModify == null)
{
throw new ArgumentNullException(nameof(itemsToModify));
}
if (errors == null)
{
throw new ArgumentNullException(nameof(errors));
}
// call each node manager.
var processedItems = new List(itemsToModify.Count);
for (int ii = 0; ii < itemsToModify.Count; ii++)
{
processedItems.Add(ServiceResult.IsBad(errors[ii]) || itemsToModify[ii] == null);
}
// delete items for event filters.
SetMonitoringModeForEvents(
context,
monitoringMode,
itemsToModify,
processedItems,
errors);
foreach (IAsyncNodeManager nodeManager in m_nodeManagers)
{
await nodeManager.SetMonitoringModeAsync(
context,
monitoringMode,
itemsToModify,
processedItems,
errors,
cancellationToken)
.ConfigureAwait(false);
}
// fill results for unknown nodes.
for (int ii = 0; ii < errors.Count; ii++)
{
if (!processedItems[ii])
{
errors[ii] = StatusCodes.BadMonitoredItemIdInvalid;
}
}
}
///
/// Delete monitored items for event subscriptions.
///
private static void SetMonitoringModeForEvents(
OperationContext context,
MonitoringMode monitoringMode,
IList monitoredItems,
List processedItems,
IList errors)
{
for (int ii = 0; ii < monitoredItems.Count; ii++)
{
// all event subscriptions are handled by the event manager.
if (monitoredItems[ii] is not IEventMonitoredItem monitoredItem ||
(monitoredItem.MonitoredItemType & MonitoredItemTypeMask.Events) == 0)
{
continue;
}
processedItems[ii] = true;
// set the monitoring mode.
monitoredItem.SetMonitoringMode(monitoringMode);
// success.
errors[ii] = StatusCodes.Good;
}
}
///
/// The server that the node manager belongs to.
///
protected IServerInternal Server { get; }
///
public IReadOnlyList AsyncNodeManagers => m_nodeManagers;
///
public IReadOnlyList NodeManagers => m_nodeManagers.ConvertAll(m => m.SyncNodeManager);
///
/// The namespace managers being managed
///
internal ConcurrentDictionary> NamespaceManagers { get; } = [];
///
/// Validates a monitoring attributes parameter.
///
protected static ServiceResult? ValidateMonitoringAttributes(MonitoringParameters attributes)
{
// check for null structure.
if (attributes == null)
{
return new ServiceResult(StatusCodes.BadStructureMissing);
}
// If a filter was specified, it needs to be a known filter structure.
if (!attributes.Filter.IsNull &&
!attributes.Filter.TryGetValue(out MonitoringFilter? _))
{
return new ServiceResult(StatusCodes.BadMonitoredItemFilterInvalid);
}
// passed basic validation.
return null;
}
///
/// Validates a monitoring filter.
///
protected static ServiceResult? ValidateMonitoringFilter(ExtensionObject filter)
{
// check that no filter is specified for non-value attributes.
if (!filter.IsNull)
{
// validate data change filter.
if (filter.TryGetValue(out DataChangeFilter? datachangeFilter))
{
ServiceResult error = datachangeFilter.Validate();
if (ServiceResult.IsBad(error))
{
return error;
}
}
}
// passed basic validation.
return null;
}
///
/// Validates a monitored item create request parameter.
///
protected async ValueTask ValidateMonitoredItemCreateRequestAsync(
OperationContext operationContext,
MonitoredItemCreateRequest item,
CancellationToken cancellationToken = default)
{
// check for null structure.
if (item == null)
{
return new ServiceResult(StatusCodes.BadStructureMissing);
}
// validate read value id component. Validate also access rights and permissions
ServiceResult? error = await ValidateReadRequestAsync(
operationContext,
item.ItemToMonitor,
null,
true,
cancellationToken)
.ConfigureAwait(false);
if (ServiceResult.IsBad(error))
{
return error;
}
// check for valid monitoring mode.
if ((int)item.MonitoringMode is < 0 or > ((int)MonitoringMode.Reporting))
{
return new ServiceResult(StatusCodes.BadMonitoringModeInvalid);
}
// check for null structure.
MonitoringParameters attributes = item.RequestedParameters;
error = ValidateMonitoringAttributes(attributes);
if (ServiceResult.IsBad(error))
{
return error;
}
// check that no filter is specified for non-value attributes.
if (item.ItemToMonitor.AttributeId is not Attributes.Value and not Attributes
.EventNotifier)
{
if (!attributes.Filter.IsNull)
{
return new ServiceResult(StatusCodes.BadFilterNotAllowed);
}
}
else
{
error = ValidateMonitoringFilter(attributes.Filter);
if (ServiceResult.IsBad(error))
{
return error;
}
}
// passed basic validation.
return null;
}
///
/// Validates a monitored item modify request parameter.
///
protected static ServiceResult? ValidateMonitoredItemModifyRequest(
MonitoredItemModifyRequest item)
{
// check for null structure.
if (item == null)
{
return new ServiceResult(StatusCodes.BadStructureMissing);
}
// check for null structure.
MonitoringParameters attributes = item.RequestedParameters;
ServiceResult? error = ValidateMonitoringAttributes(attributes);
if (ServiceResult.IsBad(error))
{
return error;
}
// validate monitoring filter.
error = ValidateMonitoringFilter(attributes.Filter);
if (ServiceResult.IsBad(error))
{
return error;
}
// passed basic validation.
return null;
}
///
/// Validates a call request item parameter and checks access rights and role permissions.
///
protected async ValueTask ValidateCallRequestItemAsync(
OperationContext operationContext,
CallMethodRequest callMethodRequest,
Dictionary? uniqueNodesReadAttributes = null,
bool permissionsOnly = false,
CancellationToken cancellationToken = default)
{
// check for null structure.
if (callMethodRequest == null)
{
return StatusCodes.BadStructureMissing;
}
// check object id.
if (callMethodRequest.ObjectId.IsNull)
{
return StatusCodes.BadNodeIdInvalid;
}
// check method id.
if (callMethodRequest.MethodId.IsNull)
{
return StatusCodes.BadMethodInvalid;
}
(_, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(callMethodRequest.ObjectId, cancellationToken)
.ConfigureAwait(false);
if (nodeManager == null)
{
return StatusCodes.BadNodeIdUnknown;
}
// Method resolution fallback is provided by the IAsyncNodeManager adapter path.
MethodState method = await nodeManager.FindMethodStateAsync(
operationContext,
callMethodRequest,
cancellationToken).ConfigureAwait(false);
if (method != null)
{
// check access rights and role permissions
return await ValidatePermissionsAsync(
operationContext,
method.NodeId,
PermissionType.Call,
uniqueNodesReadAttributes,
permissionsOnly,
cancellationToken)
.ConfigureAwait(false);
}
return StatusCodes.Good;
}
///
/// Validates a Read or MonitoredItemCreate request. It validates also access rights and role permissions
///
protected async ValueTask ValidateReadRequestAsync(
OperationContext operationContext,
ReadValueId readValueId,
Dictionary? uniqueNodesReadAttributes = null,
bool permissionsOnly = false,
CancellationToken cancellationToken = default)
{
ServiceResult? serviceResult = ReadValueId.Validate(readValueId);
if (ServiceResult.IsGood(serviceResult))
{
//any attribute other than Value or RolePermissions
PermissionType requestedPermission = PermissionType.Browse;
if (readValueId.AttributeId == Attributes.RolePermissions)
{
requestedPermission = PermissionType.ReadRolePermissions;
}
else if (readValueId.AttributeId == Attributes.Value)
{
requestedPermission = PermissionType.Read;
}
// check access rights and role permissions
serviceResult = await ValidatePermissionsAsync(
operationContext,
readValueId.NodeId,
requestedPermission,
uniqueNodesReadAttributes,
permissionsOnly,
cancellationToken)
.ConfigureAwait(false);
}
return serviceResult;
}
///
/// Validates a Write request. It validates also access rights and role permissions
///
protected async ValueTask ValidateWriteRequestAsync(
OperationContext operationContext,
WriteValue writeValue,
Dictionary? uniqueNodesServiceAttributes = null,
bool permissionsOnly = false,
CancellationToken cancellationToken = default)
{
ServiceResult? serviceResult = WriteValue.Validate(writeValue);
if (ServiceResult.IsGood(serviceResult))
{
PermissionType requestedPermission = PermissionType.WriteAttribute; //any attribute other than Value, RolePermissions or Historizing
if (writeValue.AttributeId == Attributes.RolePermissions)
{
requestedPermission = PermissionType.WriteRolePermissions;
}
else if (writeValue.AttributeId == Attributes.Historizing)
{
requestedPermission = PermissionType.WriteHistorizing;
}
else if (writeValue.AttributeId == Attributes.Value)
{
requestedPermission = PermissionType.Write;
}
// check access rights and permissions
serviceResult = await ValidatePermissionsAsync(
operationContext,
writeValue.NodeId,
requestedPermission,
uniqueNodesServiceAttributes,
permissionsOnly,
cancellationToken)
.ConfigureAwait(false);
}
return serviceResult;
}
///
/// Validates a HistoryRead request. It validates also access rights and role permissions
///
protected async ValueTask ValidateHistoryReadRequestAsync(
OperationContext operationContext,
HistoryReadValueId historyReadValueId,
CancellationToken cancellationToken = default)
{
ServiceResult? serviceResult = HistoryReadValueId.Validate(historyReadValueId);
if (ServiceResult.IsGood(serviceResult))
{
// check access rights and permissions
serviceResult = await ValidatePermissionsAsync(
operationContext,
historyReadValueId.NodeId,
PermissionType.ReadHistory,
null,
true,
cancellationToken)
.ConfigureAwait(false);
}
return serviceResult;
}
///
/// Validates a HistoryUpdate request. It validates also access rights and role permissions
///
protected async ValueTask ValidateHistoryUpdateRequestAsync(
OperationContext operationContext,
HistoryUpdateDetails historyUpdateDetails,
CancellationToken cancellationToken = default)
{
ServiceResult? serviceResult = HistoryUpdateDetails.Validate(historyUpdateDetails);
if (ServiceResult.IsGood(serviceResult))
{
// check access rights and permissions
PermissionType requiredPermission = DetermineHistoryAccessPermission(
historyUpdateDetails);
serviceResult = await ValidatePermissionsAsync(
operationContext,
historyUpdateDetails.NodeId,
requiredPermission,
null,
true,
cancellationToken).ConfigureAwait(false);
}
return serviceResult;
}
///
/// Check if the Base NodeClass attributes and NameSpace meta-data attributes
/// are valid for the given operation context of the specified node.
///
/// The Operation Context
/// The node whose attributes are validated
/// The requested permission
/// The cache holding the values of the attributes neeeded to be used in subsequent calls
/// Only the AccessRestrictions and RolePermission attributes are read. Should be false if uniqueNodesServiceAttributes is not null
/// The Cancellation Token
/// StatusCode Good if permission is granted, BadUserAccessDenied if not granted
/// or a bad status code describing the validation process failure
protected async ValueTask ValidatePermissionsAsync(
OperationContext context,
NodeId nodeId,
PermissionType requestedPermision,
Dictionary? uniqueNodesServiceAttributes = null,
bool permissionsOnly = false,
CancellationToken cancellationToken = default)
{
if (context.Session != null)
{
(object? nodeHandle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(nodeId, cancellationToken)
.ConfigureAwait(false);
return await ValidatePermissionsAsync(
context,
nodeManager,
nodeHandle,
requestedPermision,
uniqueNodesServiceAttributes,
permissionsOnly,
cancellationToken)
.ConfigureAwait(false);
}
return StatusCodes.Good;
}
///
/// Check if the Base NodeClass attributes and NameSpace meta-data attributes
/// are valid for the given operation context of the specified node.
///
/// The Operation Context
/// The node manager handling the nodeHandle
/// The node handle of the node whose attributes are validated
/// The requested permission
/// The cache holding the values of the attributes neeeded to be used in subsequent calls
/// Only the AccessRestrictions and RolePermission attributes are read. Should be false if uniqueNodesServiceAttributes is not null
/// The Cancellation Token
/// StatusCode Good if permission is granted, BadUserAccessDenied if not granted
/// or a bad status code describing the validation process failure
protected async ValueTask ValidatePermissionsAsync(
OperationContext context,
IAsyncNodeManager? nodeManager,
object? nodeHandle,
PermissionType requestedPermision,
Dictionary? uniqueNodesServiceAttributes = null,
bool permissionsOnly = false,
CancellationToken cancellationToken = default)
{
ServiceResult serviceResult = StatusCodes.Good;
// check if validation is necessary
if (context.Session != null && nodeManager != null && nodeHandle != null)
{
// First attempt to retrieve just the Permission metadata with or without cache optimization
// If it happens that nodemanager does not fully implement GetPermissionMetadata,
// fallback to GetNodeMetadataAsync
NodeMetadata? nodeMetadata = await nodeManager!.GetPermissionMetadataAsync(context,
nodeHandle,
BrowseResultMask.NodeClass,
uniqueNodesServiceAttributes!,
permissionsOnly,
cancellationToken)
.ConfigureAwait(false);
// If not INodeManager2 or GetPermissionMetadata() returns null.
nodeMetadata ??= await nodeManager.GetNodeMetadataAsync(
context,
nodeHandle,
BrowseResultMask.NodeClass,
cancellationToken)
.ConfigureAwait(false);
if (nodeMetadata != null)
{
// check RolePermissions
serviceResult = ValidateRolePermissions(
context,
nodeMetadata,
requestedPermision,
m_logger);
if (ServiceResult.IsGood(serviceResult))
{
// check AccessRestrictions
serviceResult = ValidateAccessRestrictions(context, nodeMetadata);
}
}
}
return serviceResult;
}
///
/// Validate the AccessRestrictions attribute
///
/// The Operation Context
/// Metadata
/// Good if the AccessRestrictions passes the validation
protected static ServiceResult ValidateAccessRestrictions(
OperationContext context,
NodeMetadata nodeMetadata)
{
ServiceResult serviceResult = StatusCodes.Good;
// Type hierarchy nodes (ObjectType/VariableType and their children)
// are universally accessible regardless of AccessRestrictions.
if (nodeMetadata.IsPartOfTypeHierarchy)
{
return serviceResult;
}
AccessRestrictionType restrictions = AccessRestrictionType.None;
if (nodeMetadata.AccessRestrictions != AccessRestrictionType.None)
{
restrictions = nodeMetadata.AccessRestrictions;
}
else if (nodeMetadata.DefaultAccessRestrictions != AccessRestrictionType.None)
{
restrictions = nodeMetadata.DefaultAccessRestrictions;
}
if (restrictions != AccessRestrictionType.None)
{
bool encryptionRequired =
(restrictions & AccessRestrictionType.EncryptionRequired) ==
AccessRestrictionType.EncryptionRequired;
bool signingRequired =
(restrictions & AccessRestrictionType.SigningRequired) ==
AccessRestrictionType.SigningRequired;
bool sessionRequired =
(restrictions & AccessRestrictionType.SessionRequired) ==
AccessRestrictionType.SessionRequired;
bool applyRestrictionsToBrowse =
(restrictions & AccessRestrictionType.ApplyRestrictionsToBrowse) ==
AccessRestrictionType.ApplyRestrictionsToBrowse;
bool browseOperation =
context.RequestType
is RequestType.Browse
or RequestType.BrowseNext
or RequestType.TranslateBrowsePathsToNodeIds;
// Access restriction validation runs while a request is being processed,
// which always carries a channel context with an endpoint description.
EndpointDescription endpointDescription =
context.ChannelContext!.EndpointDescription!;
if ((
encryptionRequired &&
endpointDescription.SecurityMode != MessageSecurityMode.SignAndEncrypt &&
endpointDescription.TransportProfileUri !=
Profiles.HttpsBinaryTransport &&
((applyRestrictionsToBrowse && browseOperation) || !browseOperation)
) ||
(
signingRequired &&
endpointDescription.SecurityMode != MessageSecurityMode.Sign &&
endpointDescription.SecurityMode != MessageSecurityMode.SignAndEncrypt &&
endpointDescription.TransportProfileUri !=
Profiles.HttpsBinaryTransport &&
((applyRestrictionsToBrowse && browseOperation) || !browseOperation)
) ||
(sessionRequired && context.Session == null))
{
serviceResult = ServiceResult.Create(
StatusCodes.BadSecurityModeInsufficient,
"Access restricted to nodeId {0} due to insufficient security mode.",
nodeMetadata.NodeId);
}
}
return serviceResult;
}
///
/// Validates the role permissions
///
protected internal static ServiceResult ValidateRolePermissions(
OperationContext context,
NodeMetadata nodeMetadata,
PermissionType requestedPermission,
ILogger? logger = null)
{
if (nodeMetadata == null || requestedPermission == PermissionType.None)
{
// no permission is required hence the validation passes
return StatusCodes.Good;
}
// Type hierarchy nodes (ObjectType/VariableType and their children)
// are universally accessible regardless of RolePermissions.
if (nodeMetadata.IsPartOfTypeHierarchy)
{
return StatusCodes.Good;
}
// get the intersection of user role permissions and role permissions
ArrayOf userRolePermissions = default;
if (!nodeMetadata.UserRolePermissions.IsEmpty)
{
userRolePermissions = nodeMetadata.UserRolePermissions;
}
else if (!nodeMetadata.DefaultUserRolePermissions.IsEmpty)
{
userRolePermissions = nodeMetadata.DefaultUserRolePermissions;
}
ArrayOf rolePermissions;
if (!nodeMetadata.RolePermissions.IsEmpty)
{
rolePermissions = nodeMetadata.RolePermissions;
}
else
{
rolePermissions = nodeMetadata.DefaultRolePermissions;
}
if (userRolePermissions.IsEmpty && rolePermissions.IsEmpty)
{
// there is no restriction from role permissions
return StatusCodes.Good;
}
// group all permissions defined in rolePermissions by RoleId
var roleIdPermissions = new Dictionary();
if (!rolePermissions.IsEmpty)
{
foreach (RolePermissionType rolePermission in rolePermissions)
{
if (roleIdPermissions.ContainsKey(rolePermission.RoleId))
{
roleIdPermissions[rolePermission.RoleId] |= (PermissionType)rolePermission
.Permissions;
}
else
{
roleIdPermissions[rolePermission.RoleId] =
(PermissionType)rolePermission.Permissions;
}
}
}
// group all permissions defined in userRolePermissions by RoleId
var roleIdPermissionsDefinedForUser = new Dictionary();
if (!userRolePermissions.IsEmpty)
{
foreach (RolePermissionType rolePermission in userRolePermissions)
{
if (roleIdPermissionsDefinedForUser.ContainsKey(rolePermission.RoleId))
{
roleIdPermissionsDefinedForUser[rolePermission.RoleId] |= (PermissionType)
rolePermission.Permissions;
}
else
{
roleIdPermissionsDefinedForUser[rolePermission.RoleId] =
(PermissionType)rolePermission.Permissions;
}
}
}
Dictionary commonRoleIdPermissions;
if (rolePermissions.IsEmpty)
{
// there were no role permissions defined for this node only user role permissions
commonRoleIdPermissions = roleIdPermissionsDefinedForUser;
}
else if (userRolePermissions.IsEmpty)
{
// there were no user role permissions defined only role permissions for the node
commonRoleIdPermissions = roleIdPermissions;
}
else
{
commonRoleIdPermissions = [];
// intersect role permissions from node and user
foreach (NodeId roleId in roleIdPermissions.Keys)
{
if (roleIdPermissionsDefinedForUser.TryGetValue(
roleId,
out PermissionType value))
{
commonRoleIdPermissions[roleId] = roleIdPermissions[roleId] & value;
}
}
}
ArrayOf currentRoleIds = context?.UserIdentity?.GrantedRoleIds ?? default;
if (currentRoleIds.IsEmpty)
{
logger?.LogDebug("Current user has no granted role.");
return ServiceResult.Create(
StatusCodes.BadUserAccessDenied,
"Current user has no granted role.");
}
PermissionType userActualPermissions = PermissionType.None;
foreach (NodeId currentRoleId in currentRoleIds)
{
if (commonRoleIdPermissions.TryGetValue(currentRoleId, out PermissionType value))
{
userActualPermissions |= value;
if ((value & requestedPermission) != PermissionType.None)
{
// there is one role that current session has na is listed in requested role
return StatusCodes.Good;
}
}
}
logger?.LogDebug(
"Role permissions validation failed for node {NodeId}. Requested: {RequestedPermission}, User has: {UserPermissions}",
nodeMetadata.NodeId,
requestedPermission,
userActualPermissions);
return ServiceResult.Create(
StatusCodes.BadUserAccessDenied,
"The requested permission {0} is not granted for node id {1}.",
requestedPermission,
nodeMetadata.NodeId);
}
private readonly ILogger m_logger;
private readonly SemaphoreSlim m_startupShutdownSemaphoreSlim = new(1, 1);
private readonly List m_nodeManagers;
private readonly MonitoredItemIdFactory m_monitoredItemIdFactory = new();
private readonly uint m_maxContinuationPointsPerBrowse;
private readonly SemaphoreSlim m_namespaceManagersSemaphoreSlim = new(1, 1);
private bool m_disposed;
}
///
/// Stores a reference between NodeManagers that is needs to be created or deleted.
///
public class LocalReference
{
///
/// Initializes the reference.
///
public LocalReference(
NodeId sourceId,
NodeId referenceTypeId,
bool isInverse,
NodeId targetId)
{
SourceId = sourceId;
ReferenceTypeId = referenceTypeId;
IsInverse = isInverse;
TargetId = targetId;
}
///
/// The source of the reference.
///
public NodeId SourceId { get; }
///
/// The type of reference.
///
public NodeId ReferenceTypeId { get; }
///
/// True if the reference is an inverse reference.
///
public bool IsInverse { get; }
///
/// The target of the reference.
///
public NodeId TargetId { get; }
}
///
/// Represents a generator for unique monitored item ids.
/// Call next() to retrieve the next valid monitoredItemId.
///
/// This class provides a mechanism to generate sequential ids for monitored
/// items. It is designed to ensure thread-safe incrementation of the identifier.
public class MonitoredItemIdFactory
{
///
/// Initialize the MonitoredItemIdFactory with a new start value the ids start incrementing from.
///
///
public void SetStartValue(uint firstId)
{
Utils.SetIdentifier(ref m_lastMonitoredItemId, firstId);
}
///
/// Get the next unique monitored item id.
///
/// an uint that can be used as an id for a monitored item
public uint GetNextId()
{
return Utils.IncrementIdentifier(ref m_lastMonitoredItemId);
}
private uint m_lastMonitoredItemId;
}
}