mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 08:41:51 +08:00
81b3c9b371
# Description Adds the OPC UA NodeManagement service set (AddNodes / DeleteNodes / AddReferences / DeleteReferences) to the server SDK and fixes a long-standing shutdown bug where `NodeState.Delete` callbacks never fired on address-space teardown. ### NodeManagement service set (#3736) - New optional `INodeManagementAsyncNodeManager` facet interface (added to `INodeManager.cs` alongside other facets, discovered via `as`, **not** part of the `IAsyncNodeManager` composite). Opt-in per NodeManager via `AllowNodeManagement` — default is `false` so existing NodeManagers preserve binary compatibility. - Dispatcher in `MasterNodeManager` routes: - **AddNodes** by `RequestedNewNodeId.NamespaceIndex` when supplied, otherwise to the parent's owning NodeManager. When the parent's owner is read-only (e.g. `ObjectsFolder` owned by `CoreNodeManager`), the dispatcher falls back to the first opted-in NodeManager so conformance flows work out of the box. - **DeleteNodes** / **AddReferences** / **DeleteReferences** to the owning NodeManager of the target or source node, returning `BadUserAccessDenied` when that NodeManager has not opted in. Cross-NodeManager `AddReference` / `DeleteReference` best-effort mirrors the inverse edge on the target's NodeManager. - `AsyncCustomNodeManager` provides a default implementation: per-item validation, `RequestedNewNodeId` honored, browse-name dedup for both local and cross-NodeManager parents, `DeleteTargetReferences` semantics, and a NodeId allocator fallback when an `INodeIdFactory.New` override returns a null `NodeId` (e.g. parent-based hierarchical naming with no parent attached). - `StandardServer` adds the four service overrides plumbed through `OperationLimits.MaxNodesPerNodeManagement`, audit events, and `OnRequestComplete`; new `ReportAuditAddReferencesEvent` / `ReportAuditDeleteReferencesEvent` helpers in `AuditEvents`. - Quickstarts `ReferenceNodeManager` opts in via `AllowNodeManagement => true`; the legacy partial-class override in `ReferenceServer.NodeManagement.cs` is removed. ### `NodeState.Delete` on shutdown (#3762) - `CustomNodeManager.DeleteAddressSpace` and `AsyncCustomNodeManager.DeleteAddressSpaceAsync` now snapshot root `PredefinedNodes` (skipping children whose parent is in the snapshot to prevent double-fire), invoke `NodeState.Delete(SystemContext)` on each root (which recurses into children and raises `OnBeforeDelete` / `OnAfterDelete` / `NodeStateChangeMasks.Deleted`), and then clear the map. - Derived overrides in Quickstarts `ReferenceNodeManager`, `SampleNodeManager`, and `FileSystemNodeManager` chain to `base` so the callback fires for their nodes too. ### Tests - **21 new dispatcher unit tests** in `Tests/Opc.Ua.Server.Tests/NodeManager/AsyncCustomNodeManagerNodeManagementTests.cs` covering `AllowNodeManagement` default/opt-in, AddNode validation (`BadBrowseNameInvalid`, `BadParentNodeIdInvalid`, `BadNodeIdRejected`, `BadNodeIdExists`, `BadBrowseNameDuplicated`, `BadNodeClassInvalid`), RequestedNewNodeId paths, DeleteNode + `DeleteTargetReferences`, AddReference duplicate detection (`BadDuplicateReferenceNotAllowed`), and DeleteReference (`BadNoMatch`). - **2 new DeleteAddressSpaceAsync tests** in `AsyncCustomNodeManagerTests` verifying `NodeState.Delete` invocation and no double-fire on children. - Updated InformationModel `NodeManagementAddRefTests.AddReferenceBetweenExistingNodesSucceedsAsync` to accept `BadUserAccessDenied` / `BadNotSupported` when the source NM has opted out. - **Full `Opc.Ua.Server.Tests` on net10.0: 1709 passed, 0 failed.** - **All 26 InformationModel `Category=NodeManagement` conformance tests pass** against the reference server (1 skipped where source NM opts out). ### Docs - New [`Docs/NodeManagement.md`](Docs/NodeManagement.md) with architecture, opt-in pattern, dispatch and routing rules, request-handling table, error-code table, audit events, and custom-behavior examples. - `Docs/MigrationGuide.md` gets two new sections — *NodeManagement service set* (downstream migration steps) and *DeleteAddressSpace invokes NodeState.Delete (#3762)* (shutdown behavior change) — with TOC entries. - `Docs/README.md` links the new `NodeManagement.md`. ## Related Issues - Fixes #3736 - Fixes #3762 ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received.
855 lines
32 KiB
C#
855 lines
32 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Opc.Ua.Server
|
|
{
|
|
/// <summary>
|
|
/// The factory for an <see cref="AsyncNodeManagerAdapter"/>
|
|
/// </summary>
|
|
public static class AsyncNodeManagerAdapterFactory
|
|
{
|
|
/// <summary>
|
|
/// returns an instance of <see cref="IAsyncNodeManager"/>
|
|
/// if the NodeManager does not implement the interface uses the <see cref="AsyncNodeManagerAdapter"/>
|
|
/// to create an IAsyncNodeManager compatible object
|
|
/// </summary>
|
|
public static IAsyncNodeManager ToAsyncNodeManager(this INodeManager nodeManager)
|
|
{
|
|
if (nodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager;
|
|
}
|
|
return new AsyncNodeManagerAdapter(nodeManager);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// An adapter that makes a synchronous INodeManager conform to the IAsyncNodeManager interface.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This allows synchronous, or only partially asynchronous node managers to be treated as asynchronous, which can help
|
|
/// unify the calling logic within the MasterNodeManager.
|
|
/// </remarks>
|
|
public class AsyncNodeManagerAdapter : IAsyncNodeManager, IDisposable
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="AsyncNodeManagerAdapter"/> class.
|
|
/// </summary>
|
|
/// <param name="nodeManager">The synchronous node manager to wrap.</param>
|
|
/// <exception cref="ArgumentNullException"><paramref name="nodeManager"/> is <c>null</c>.</exception>
|
|
public AsyncNodeManagerAdapter(INodeManager nodeManager)
|
|
{
|
|
SyncNodeManager = nodeManager ?? throw new ArgumentNullException(nameof(nodeManager));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public IEnumerable<string> NamespaceUris => SyncNodeManager.NamespaceUris;
|
|
|
|
/// <inheritdoc/>
|
|
public INodeManager SyncNodeManager { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask AddReferencesAsync(
|
|
IDictionary<NodeId, IList<IReference>> references,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.AddReferencesAsync(references, cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.AddReferences(references);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<ContinuationPoint?> BrowseAsync(
|
|
OperationContext context,
|
|
ContinuationPoint continuationPoint,
|
|
IList<ReferenceDescription> references,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IBrowseAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.BrowseAsync(context, continuationPoint, references, cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.Browse(context, ref continuationPoint, references);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return new ValueTask<ContinuationPoint?>(continuationPoint);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask CallAsync(
|
|
OperationContext context,
|
|
ArrayOf<CallMethodRequest> methodsToCall,
|
|
IList<CallMethodResult> results,
|
|
IList<ServiceResult> errors,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is ICallAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.CallAsync(context, methodsToCall, results, errors, cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.Call(context, methodsToCall, results, errors);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<MethodState> FindMethodStateAsync(
|
|
OperationContext context,
|
|
CallMethodRequest methodToCall,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is INodeManager3 nodeManager)
|
|
{
|
|
MethodState method = nodeManager.FindMethodState(context, methodToCall);
|
|
return new ValueTask<MethodState>(method);
|
|
}
|
|
|
|
return new ValueTask<MethodState>(result: null!);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<ServiceResult> ConditionRefreshAsync(
|
|
OperationContext context,
|
|
IList<IEventMonitoredItem> monitoredItems,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IConditionRefreshAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.ConditionRefreshAsync(context, monitoredItems, cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.ConditionRefresh(context, monitoredItems);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask CreateAddressSpaceAsync(
|
|
IDictionary<NodeId, IList<IReference>> externalReferences,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.CreateAddressSpaceAsync(externalReferences, cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.CreateAddressSpace(externalReferences);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask CreateMonitoredItemsAsync(
|
|
OperationContext context,
|
|
uint subscriptionId,
|
|
double publishingInterval,
|
|
TimestampsToReturn timestampsToReturn,
|
|
ArrayOf<MonitoredItemCreateRequest> itemsToCreate,
|
|
IList<ServiceResult> errors,
|
|
IList<MonitoringFilterResult> filterErrors,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
bool createDurable,
|
|
MonitoredItemIdFactory monitoredItemIdFactory,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is ICreateMonitoredItemsAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.CreateMonitoredItemsAsync(
|
|
context,
|
|
subscriptionId,
|
|
publishingInterval,
|
|
timestampsToReturn,
|
|
itemsToCreate,
|
|
errors,
|
|
filterErrors,
|
|
monitoredItems,
|
|
createDurable,
|
|
monitoredItemIdFactory,
|
|
cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.CreateMonitoredItems(
|
|
context,
|
|
subscriptionId,
|
|
publishingInterval,
|
|
timestampsToReturn,
|
|
itemsToCreate,
|
|
errors,
|
|
filterErrors,
|
|
monitoredItems,
|
|
createDurable,
|
|
monitoredItemIdFactory);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask DeleteAddressSpaceAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.DeleteAddressSpaceAsync(cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.DeleteAddressSpace();
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask DeleteMonitoredItemsAsync(
|
|
OperationContext context,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
IList<bool> processedItems,
|
|
IList<ServiceResult> errors,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IDeleteMonitoredItemsAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.DeleteMonitoredItemsAsync(
|
|
context,
|
|
monitoredItems,
|
|
processedItems,
|
|
errors,
|
|
cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.DeleteMonitoredItems(context, monitoredItems, processedItems, errors);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<ServiceResult> DeleteReferenceAsync(
|
|
object sourceHandle,
|
|
NodeId referenceTypeId,
|
|
bool isInverse,
|
|
ExpandedNodeId targetId,
|
|
bool deleteBidirectional,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.DeleteReferenceAsync(sourceHandle,
|
|
referenceTypeId,
|
|
isInverse,
|
|
targetId,
|
|
deleteBidirectional,
|
|
cancellationToken);
|
|
}
|
|
|
|
ServiceResult result = SyncNodeManager.DeleteReference(sourceHandle,
|
|
referenceTypeId,
|
|
isInverse,
|
|
targetId,
|
|
deleteBidirectional);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return new ValueTask<ServiceResult>(result);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<object> GetManagerHandleAsync(NodeId nodeId, CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.GetManagerHandleAsync(nodeId, cancellationToken);
|
|
}
|
|
|
|
object? handle = SyncNodeManager.GetManagerHandle(nodeId);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return new ValueTask<object>(handle!);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<NodeMetadata> GetNodeMetadataAsync(
|
|
OperationContext context,
|
|
object targetHandle,
|
|
BrowseResultMask resultMask,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.GetNodeMetadataAsync(context, targetHandle, resultMask, cancellationToken);
|
|
}
|
|
|
|
NodeMetadata? nodeMetadata = SyncNodeManager.GetNodeMetadata(context, targetHandle, resultMask);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return new ValueTask<NodeMetadata>(nodeMetadata!);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<NodeMetadata?> GetPermissionMetadataAsync(
|
|
OperationContext context,
|
|
object targetHandle,
|
|
BrowseResultMask resultMask,
|
|
Dictionary<NodeId, Variant[]> uniqueNodesServiceAttributesCache,
|
|
bool permissionsOnly,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.GetPermissionMetadataAsync(
|
|
context,
|
|
targetHandle,
|
|
resultMask,
|
|
uniqueNodesServiceAttributesCache,
|
|
permissionsOnly,
|
|
cancellationToken);
|
|
}
|
|
if (SyncNodeManager is INodeManager2 nodeManager2)
|
|
{
|
|
NodeMetadata? nodeMetadata = nodeManager2.GetPermissionMetadata(
|
|
context,
|
|
targetHandle,
|
|
resultMask,
|
|
uniqueNodesServiceAttributesCache,
|
|
permissionsOnly);
|
|
return new ValueTask<NodeMetadata?>(nodeMetadata);
|
|
}
|
|
|
|
return new ValueTask<NodeMetadata?>((NodeMetadata?)null);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask HistoryReadAsync(
|
|
OperationContext context,
|
|
HistoryReadDetails details,
|
|
TimestampsToReturn timestampsToReturn,
|
|
bool releaseContinuationPoints,
|
|
ArrayOf<HistoryReadValueId> nodesToRead,
|
|
IList<HistoryReadResult> results,
|
|
IList<ServiceResult> errors,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IHistoryReadAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.HistoryReadAsync(
|
|
context,
|
|
details,
|
|
timestampsToReturn,
|
|
releaseContinuationPoints,
|
|
nodesToRead,
|
|
results,
|
|
errors,
|
|
cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.HistoryRead(
|
|
context,
|
|
details,
|
|
timestampsToReturn,
|
|
releaseContinuationPoints,
|
|
nodesToRead,
|
|
results,
|
|
errors);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask HistoryUpdateAsync(
|
|
OperationContext context,
|
|
Type detailsType,
|
|
ArrayOf<HistoryUpdateDetails> nodesToUpdate,
|
|
IList<HistoryUpdateResult> results,
|
|
IList<ServiceResult> errors,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IHistoryUpdateAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.HistoryUpdateAsync(
|
|
context,
|
|
detailsType,
|
|
nodesToUpdate,
|
|
results,
|
|
errors,
|
|
cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.HistoryUpdate(
|
|
context,
|
|
detailsType,
|
|
nodesToUpdate,
|
|
results,
|
|
errors);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<bool> IsNodeInViewAsync(
|
|
OperationContext context,
|
|
NodeId viewId,
|
|
object nodeHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.IsNodeInViewAsync(context, viewId, nodeHandle, cancellationToken);
|
|
}
|
|
|
|
if (SyncNodeManager is INodeManager2 nodeManager2)
|
|
{
|
|
bool result = nodeManager2.IsNodeInView(context, viewId, nodeHandle);
|
|
return new ValueTask<bool>(result);
|
|
}
|
|
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask ModifyMonitoredItemsAsync(OperationContext context,
|
|
TimestampsToReturn timestampsToReturn,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
ArrayOf<MonitoredItemModifyRequest> itemsToModify,
|
|
IList<ServiceResult> errors,
|
|
IList<MonitoringFilterResult> filterErrors,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IModifyMonitoredItemsAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.ModifyMonitoredItemsAsync(
|
|
context,
|
|
timestampsToReturn,
|
|
monitoredItems,
|
|
itemsToModify,
|
|
errors,
|
|
filterErrors,
|
|
cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.ModifyMonitoredItems(
|
|
context,
|
|
timestampsToReturn,
|
|
monitoredItems,
|
|
itemsToModify,
|
|
errors,
|
|
filterErrors);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask ReadAsync(OperationContext context,
|
|
double maxAge,
|
|
ArrayOf<ReadValueId> nodesToRead,
|
|
IList<DataValue> values,
|
|
IList<ServiceResult> errors,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IReadAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.ReadAsync(
|
|
context,
|
|
maxAge,
|
|
nodesToRead,
|
|
values,
|
|
errors,
|
|
cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.Read(context, maxAge, nodesToRead, values, errors);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask RestoreMonitoredItemsAsync(
|
|
IList<IStoredMonitoredItem> itemsToRestore,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
IUserIdentity savedOwnerIdentity,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.RestoreMonitoredItemsAsync(itemsToRestore, monitoredItems, savedOwnerIdentity, cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.RestoreMonitoredItems(itemsToRestore, monitoredItems, savedOwnerIdentity);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask SessionClosingAsync(
|
|
OperationContext context,
|
|
NodeId sessionId,
|
|
bool deleteSubscriptions,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.SessionClosingAsync(context, sessionId, deleteSubscriptions, cancellationToken);
|
|
}
|
|
|
|
if (SyncNodeManager is INodeManager2 nodeManager2)
|
|
{
|
|
nodeManager2.SessionClosing(context, sessionId, deleteSubscriptions);
|
|
}
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask SessionActivatedAsync(
|
|
OperationContext context,
|
|
NodeId sessionId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.SessionActivatedAsync(context, sessionId, cancellationToken);
|
|
}
|
|
|
|
if (SyncNodeManager is INodeManager3 nodeManager3)
|
|
{
|
|
nodeManager3.SessionActivated(context, sessionId);
|
|
}
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask SetMonitoringModeAsync(
|
|
OperationContext context,
|
|
MonitoringMode monitoringMode,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
IList<bool> processedItems,
|
|
IList<ServiceResult> errors,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is ISetMonitoringModeAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.SetMonitoringModeAsync(
|
|
context,
|
|
monitoringMode,
|
|
monitoredItems,
|
|
processedItems,
|
|
errors,
|
|
cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.SetMonitoringMode(
|
|
context,
|
|
monitoringMode,
|
|
monitoredItems,
|
|
processedItems,
|
|
errors);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<ServiceResult> SubscribeToAllEventsAsync(
|
|
OperationContext context,
|
|
uint subscriptionId,
|
|
IEventMonitoredItem monitoredItem,
|
|
bool unsubscribe,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.SubscribeToAllEventsAsync(
|
|
context,
|
|
subscriptionId,
|
|
monitoredItem,
|
|
unsubscribe,
|
|
cancellationToken);
|
|
}
|
|
|
|
ServiceResult result = SyncNodeManager.SubscribeToAllEvents(
|
|
context,
|
|
subscriptionId,
|
|
monitoredItem,
|
|
unsubscribe);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return new ValueTask<ServiceResult>(result);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<ServiceResult> SubscribeToEventsAsync(
|
|
OperationContext context,
|
|
object sourceId,
|
|
uint subscriptionId,
|
|
IEventMonitoredItem monitoredItem,
|
|
bool unsubscribe,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.SubscribeToEventsAsync(
|
|
context,
|
|
sourceId,
|
|
subscriptionId,
|
|
monitoredItem,
|
|
unsubscribe,
|
|
cancellationToken);
|
|
}
|
|
|
|
ServiceResult result = SyncNodeManager.SubscribeToEvents(
|
|
context,
|
|
sourceId,
|
|
subscriptionId,
|
|
monitoredItem,
|
|
unsubscribe);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return new ValueTask<ServiceResult>(result);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask TransferMonitoredItemsAsync(
|
|
OperationContext context,
|
|
bool sendInitialValues,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
IList<bool> processedItems,
|
|
IList<ServiceResult> errors,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is ITransferMonitoredItemsAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.TransferMonitoredItemsAsync(
|
|
context,
|
|
sendInitialValues,
|
|
monitoredItems,
|
|
processedItems,
|
|
errors,
|
|
cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.TransferMonitoredItems(
|
|
context,
|
|
sendInitialValues,
|
|
monitoredItems,
|
|
processedItems,
|
|
errors);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask TranslateBrowsePathAsync(
|
|
OperationContext context,
|
|
object sourceHandle,
|
|
RelativePathElement relativePath,
|
|
IList<ExpandedNodeId> targetIds,
|
|
IList<NodeId> unresolvedTargetIds,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is ITranslateBrowsePathAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.TranslateBrowsePathAsync(context, sourceHandle, relativePath, targetIds, unresolvedTargetIds, cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.TranslateBrowsePath(context, sourceHandle, relativePath, targetIds, unresolvedTargetIds);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask WriteAsync(
|
|
OperationContext context,
|
|
ArrayOf<WriteValue> nodesToWrite,
|
|
IList<ServiceResult> errors,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IWriteAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.WriteAsync(context, nodesToWrite, errors, cancellationToken);
|
|
}
|
|
|
|
SyncNodeManager.Write(context, nodesToWrite, errors);
|
|
|
|
// Return a completed ValueTask since the underlying call is synchronous.
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<ServiceResult> ValidateEventRolePermissionsAsync(
|
|
IEventMonitoredItem monitoredItem,
|
|
IFilterTarget filterTarget,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.ValidateEventRolePermissionsAsync(monitoredItem, filterTarget, cancellationToken);
|
|
}
|
|
|
|
if (SyncNodeManager is INodeManager3 nodeManager2)
|
|
{
|
|
return new ValueTask<ServiceResult>(nodeManager2.ValidateEventRolePermissions(monitoredItem, filterTarget));
|
|
}
|
|
|
|
return new ValueTask<ServiceResult>(ServiceResult.Good);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<ServiceResult> ValidateRolePermissionsAsync(
|
|
OperationContext operationContext,
|
|
NodeId nodeId,
|
|
PermissionType requestedPermission,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
|
|
{
|
|
return asyncNodeManager.ValidateRolePermissionsAsync(operationContext, nodeId, requestedPermission, cancellationToken);
|
|
}
|
|
|
|
if (SyncNodeManager is INodeManager3 nodeManager2)
|
|
{
|
|
return new ValueTask<ServiceResult>(nodeManager2.ValidateRolePermissions(operationContext, nodeId, requestedPermission));
|
|
}
|
|
|
|
return new ValueTask<ServiceResult>(ServiceResult.Good);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public bool IsMultipleEventConsumerNode(NodeId nodeId)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
/// <remarks>
|
|
/// The legacy synchronous <see cref="INodeManager"/> contract does not
|
|
/// expose NodeManagement service hooks. Adapted managers therefore
|
|
/// opt out by returning <c>false</c>; the master dispatcher rejects
|
|
/// per-item requests with <see cref="StatusCodes.BadUserAccessDenied"/>.
|
|
/// </remarks>
|
|
public bool AllowNodeManagement
|
|
=> SyncNodeManager is INodeManagementAsyncNodeManager nm && nm.AllowNodeManagement;
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<(ServiceResult result, NodeId addedNodeId)> AddNodeAsync(
|
|
OperationContext context,
|
|
AddNodesItem item,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is INodeManagementAsyncNodeManager nm)
|
|
{
|
|
return nm.AddNodeAsync(context, item, cancellationToken);
|
|
}
|
|
return new ValueTask<(ServiceResult, NodeId)>(
|
|
(new ServiceResult(StatusCodes.BadServiceUnsupported), NodeId.Null));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<ServiceResult> DeleteNodeAsync(
|
|
OperationContext context,
|
|
DeleteNodesItem item,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is INodeManagementAsyncNodeManager nm)
|
|
{
|
|
return nm.DeleteNodeAsync(context, item, cancellationToken);
|
|
}
|
|
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadServiceUnsupported));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<ServiceResult> AddReferenceAsync(
|
|
OperationContext context,
|
|
AddReferencesItem item,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is INodeManagementAsyncNodeManager nm)
|
|
{
|
|
return nm.AddReferenceAsync(context, item, cancellationToken);
|
|
}
|
|
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadServiceUnsupported));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<ServiceResult> DeleteReferenceAsync(
|
|
OperationContext context,
|
|
DeleteReferencesItem item,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (SyncNodeManager is INodeManagementAsyncNodeManager nm)
|
|
{
|
|
return nm.DeleteReferenceAsync(context, item, cancellationToken);
|
|
}
|
|
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadServiceUnsupported));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Frees any unmanaged resources.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An overrideable version of the Dispose.
|
|
/// </summary>
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
(SyncNodeManager as IDisposable)?.Dispose();
|
|
}
|
|
}
|
|
}
|
|
}
|