mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 08:41:51 +08:00
a81d981755
# Description
Pure code-style sweep across the repo — no behavioral changes. Drives
`dotnet format` info-level diagnostics down on a large slice of the
codebase (Stack, Libraries, Tests, Tools).
Six commits, applied bottom-up in safety order so each batch leaves the
build green:
1. **`style: fix whitespace and final-newline formatting (WHITESPACE,
FINALNEWLINE)`**
197 hits across ~30 files; pure mechanical cleanup.
2. **`style: apply dotnet format style fixes (IDE rules)`**
IDE-rule batch via `dotnet format style --severity info` —
`var`/explicit type, simplify names, collection expressions, expression
bodies, unused usings, etc.
3. **`style: apply dotnet format analyzers fixes (RCS blank-line
formatting)`**
Roslynator formatting (RCS0xxx) blank-line and punctuation rules.
4. **`style: convert comments to doc comments and inline local vars
(RCS1181, RCS1124)`**
24 files. RCS1181 promotes `// summary`-style comments above
public/internal members to `/// <summary>` doc comments; RCS1124 inlines
locals used exactly once. Generated `Tools/` files were reverted to keep
the source-generator outputs deterministic.
5. **`docs(agent): correct --diagnostics flag form and verify Roslynator
4.x code fixers`**
Updates `.github/agents/opc-ua-codestyle-enforcer.agent.md` with two
corrections discovered while running this sweep:
- `dotnet format --diagnostics "RULE1 RULE2"` (single quoted,
space-separated) silently fixes nothing on `dotnet format` 10.x — it
parses as one unknown ID and exits 0. The correct form is `--diagnostics
RULE1 --diagnostics RULE2 …`, repeated per rule.
- Roslynator 4.15.0 **does** ship code fixers for
RCS1078/1118/1124/1140/1142/1181/1221/1260 (the previous "no auto-fixer"
list was outdated). RCS1140/1142 default to `Hidden` so need `--severity
info` to surface and fire; RCS1078 requires
`roslynator_empty_string_style = field` (already set in
`.editorconfig`). Only NUnit4002/2046/2010 genuinely lack auto-fixers in
this stack.
6. **`style: format new code from master merge (dotnet format sweep)`**
After merging master into the branch (commit `e3ebc8742`), re-ran the
same three-phase format sweep on the 151 .cs files brought in by the
merge, scoped per project via repeated `--include`. 81 files changed;
same rule families as commits 1–4.
- Hand-fixed `Tests/Opc.Ua.MigrationAnalyzer.Tests/AnalyzerHarness.cs`
for `RCS1174` (redundant `async`/`await` removal) because the auto-fixer
didn't apply for that specific case.
- Reverted files that the formatter would break:
- `Libraries/Opc.Ua.Server/Server/ServerInternalData.cs` — multi-TFM
analysis disagreement injected `<<<<<<<` / `=======` / `>>>>>>>`
markers.
-
`Tools/Opc.Ua.MigrationAnalyzer.Core/{Core/Stack/Client/ChannelBase.cs,
Core/Stack/Types/IUserIdentityTokenHandler.cs,
Client/Session/Session.cs}` — IDE0005 removed `using
Opc.Ua.Security.Certificates` that IS used (the files are 1.5.378
reference fixtures for the analyzer).
- `Tests/Opc.Ua.Gds.Tests/PushTest.cs` — IDE0002 over-simplified
`OpcUa.ObjectTypeIds` to `ObjectTypeIds`, which then resolves to a
conflicting type.
### What this PR does **not** touch
- No behavioral or semantic logic changes.
- No `.editorconfig` rule promotions (rules stay at their current
severities; this is a backlog-burndown pass, not a rule tightening).
- Generated source under `Tools/` is intentionally excluded.
- Higher-risk rule families (`RCS1090`/`CA2007` `ConfigureAwait`,
`RCS1249` null-forgiving cleanup, `IDE0270`/`IDE0059`) are left for
follow-up sweeps because they need per-TFM build verification or
semantic review.
## Related Issues
_No tracking issue — this is a non-functional style sweep and the
agent-doc correction is a follow-up to the same work. Happy to open one
if the maintainers prefer._
## Checklist
- [ ] 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. _(N/A — style-only, no
behavioral change)_
- [x] I have added all necessary documentation. _(agent doc updated with
the two corrections)_
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings. _(verified clean on `net10.0` and `net48`; only
pre-existing `NETSDK1005`/`NETSDK1207` failures on Tools/AOT projects)_
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. _(build
verified on net10 and net48 per batch; full test suite not yet run
locally — relying on CI)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
---------
Co-authored-by: agent <agent@local>
4728 lines
178 KiB
C#
4728 lines
178 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.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
|
|
{
|
|
/// <inheritdoc/>
|
|
public class MasterNodeManager : IDisposable, IMasterNodeManager
|
|
{
|
|
/// <summary>
|
|
/// Initializes the object with default values.
|
|
/// </summary>
|
|
public MasterNodeManager(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration,
|
|
string? dynamicNamespaceUri,
|
|
params INodeManager[] additionalManagers)
|
|
: this(server, configuration, dynamicNamespaceUri, null, additionalManagers)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the object with default values.
|
|
/// </summary>
|
|
public MasterNodeManager(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration,
|
|
string? dynamicNamespaceUri,
|
|
params IAsyncNodeManager[] additionalManagers)
|
|
: this(server, configuration, dynamicNamespaceUri, additionalManagers, null)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the object with default values.
|
|
/// </summary>
|
|
public MasterNodeManager(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration,
|
|
string? dynamicNamespaceUri,
|
|
IEnumerable<IAsyncNodeManager>? additionalManagers,
|
|
IEnumerable<INodeManager>? additionalSyncManagers)
|
|
{
|
|
if (configuration == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(configuration));
|
|
}
|
|
|
|
Server = server ?? throw new ArgumentNullException(nameof(server));
|
|
m_logger = server.Telemetry.CreateLogger<MasterNodeManager>();
|
|
|
|
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<IAsyncNodeManager> registeredManagers;
|
|
var namespaceManagers = new Dictionary<int, List<IAsyncNodeManager>>
|
|
{
|
|
[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<int, List<IAsyncNodeManager>> namespaceManager in namespaceManagers)
|
|
{
|
|
NamespaceManagers.TryAdd(namespaceManager.Key, namespaceManager.Value.AsReadOnly());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers the node manager with the master node manager.
|
|
/// </summary>
|
|
private void RegisterNodeManager(
|
|
IAsyncNodeManager nodeManager,
|
|
List<IAsyncNodeManager> registeredManagers,
|
|
Dictionary<int, List<IAsyncNodeManager>> 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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 && !m_disposed)
|
|
{
|
|
m_disposed = true;
|
|
|
|
m_namespaceManagersSemaphoreSlim.Dispose();
|
|
|
|
m_startupShutdownSemaphoreSlim.Wait();
|
|
|
|
List<IAsyncNodeManager> nodeManagers = [.. m_nodeManagers];
|
|
m_nodeManagers.Clear();
|
|
|
|
m_startupShutdownSemaphoreSlim.Dispose();
|
|
|
|
foreach (IAsyncNodeManager nodeManager in nodeManagers)
|
|
{
|
|
(nodeManager as IDisposable)?.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a reference to the table of external references.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is a convenience function used by custom NodeManagers.
|
|
/// </remarks>
|
|
public static void CreateExternalReference(
|
|
IDictionary<NodeId, IList<IReference>> 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<IReference>? references))
|
|
{
|
|
externalReferences[sourceId] = references = [];
|
|
}
|
|
|
|
references!.Add(reference);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determine the required history access permission depending on the HistoryUpdateDetails
|
|
/// </summary>
|
|
/// <param name="historyUpdateDetails">The HistoryUpdateDetails passed in</param>
|
|
/// <returns>The corresponding history access permission</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determine the History PermissionType depending on PerformUpdateType
|
|
/// </summary>
|
|
/// <returns>The corresponding PermissionType</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ICoreNodeManager? CoreNodeManager => m_nodeManagers[1] as ICoreNodeManager;
|
|
|
|
/// <inheritdoc/>
|
|
public IDiagnosticsNodeManager? DiagnosticsNodeManager
|
|
=> m_nodeManagers[0] as IDiagnosticsNodeManager;
|
|
|
|
/// <inheritdoc/>
|
|
public IConfigurationNodeManager? ConfigurationNodeManager
|
|
=> m_nodeManagers[0] as IConfigurationNodeManager;
|
|
|
|
/// <inheritdoc/>
|
|
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<NodeId, IList<IReference>>();
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void RegisterNamespaceManager(string namespaceUri, INodeManager nodeManager)
|
|
{
|
|
RegisterNamespaceManager(namespaceUri, nodeManager.ToAsyncNodeManager());
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public bool UnregisterNamespaceManager(string namespaceUri, INodeManager nodeManager)
|
|
{
|
|
return UnregisterNamespaceManager(namespaceUri, null, nodeManager);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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<IAsyncNodeManager>? 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();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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<IAsyncNodeManager>? 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;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
[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;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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<IAsyncNodeManager>? 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the references to the target.
|
|
/// </summary>
|
|
[Obsolete("Use AddReferencesAsync instead.")]
|
|
public virtual void AddReferences(NodeId sourceId, IList<IReference> references)
|
|
{
|
|
AddReferencesAsync(sourceId, references).AsTask().GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask AddReferencesAsync(NodeId sourceId,
|
|
IList<IReference> 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<NodeId, IList<IReference>> { { sourceId, references } };
|
|
await nodeManager!.AddReferencesAsync(map, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes the references to the target.
|
|
/// </summary>
|
|
[Obsolete("Use DeleteReferencesAsync")]
|
|
public virtual void DeleteReferences(NodeId targetId, IList<IReference> references)
|
|
{
|
|
DeleteReferencesAsync(targetId, references).AsTask().GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask DeleteReferencesAsync(NodeId targetId,
|
|
IList<IReference> references,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
foreach (ReferenceNode reference in references.OfType<ReferenceNode>())
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
[Obsolete("Use RemoveReferencesAsync instead.")]
|
|
public void RemoveReferences(List<LocalReference> referencesToRemove)
|
|
{
|
|
RemoveReferencesAsync(referencesToRemove).AsTask().GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask RemoveReferencesAsync(List<LocalReference> 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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask<(ArrayOf<AddNodesResult> results, ArrayOf<DiagnosticInfo> diagnosticInfos)>
|
|
AddNodesAsync(
|
|
OperationContext context,
|
|
ArrayOf<AddNodesItem> 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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask<(ArrayOf<StatusCode> results, ArrayOf<DiagnosticInfo> diagnosticInfos)>
|
|
DeleteNodesAsync(
|
|
OperationContext context,
|
|
ArrayOf<DeleteNodesItem> 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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask<(ArrayOf<StatusCode> results, ArrayOf<DiagnosticInfo> diagnosticInfos)>
|
|
AddReferencesAsync(
|
|
OperationContext context,
|
|
ArrayOf<AddReferencesItem> 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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask<(ArrayOf<StatusCode> results, ArrayOf<DiagnosticInfo> diagnosticInfos)>
|
|
DeleteReferencesAsync(
|
|
OperationContext context,
|
|
ArrayOf<DeleteReferencesItem> 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<ServiceResult> 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<ServiceResult> 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<ServiceResult> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the first NodeManager registered against the given namespace
|
|
/// index, or <c>null</c> when no NodeManager owns the namespace. Since
|
|
/// <see cref="INodeManagementAsyncNodeManager"/> is aggregated into
|
|
/// <see cref="IAsyncNodeManager"/> every owner exposes the surface;
|
|
/// callers should still check <see cref="INodeManagementAsyncNodeManager.AllowNodeManagement"/>.
|
|
/// </summary>
|
|
private INodeManagementAsyncNodeManager? FindNodeManagementOwner(ushort namespaceIndex)
|
|
{
|
|
if (!NamespaceManagers.TryGetValue(namespaceIndex, out IReadOnlyList<IAsyncNodeManager>? nodeManagers))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return nodeManagers.Count > 0 ? nodeManagers[0] : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the first registered NodeManager that has opted in to
|
|
/// NodeManagement, or <c>null</c> if no NodeManager has done so. Used
|
|
/// as the cross-NodeManager AddNodes fallback when the parent's
|
|
/// owning NodeManager does not allow NodeManagement.
|
|
/// </summary>
|
|
private INodeManagementAsyncNodeManager? FindFirstOptedInNodeManager()
|
|
{
|
|
for (int ii = 0; ii < m_nodeManagers.Count; ii++)
|
|
{
|
|
if (m_nodeManagers[ii].AllowNodeManagement)
|
|
{
|
|
return m_nodeManagers[ii];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual void RegisterNodes(
|
|
OperationContext context,
|
|
ArrayOf<NodeId> nodesToRegister,
|
|
out ArrayOf<NodeId> 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<bool> processedNodes = new List<bool>(new bool[itemsToDelete.Count]);
|
|
|
|
for (int ii = 0; ii < m_nodeManagers.Count; ii++)
|
|
{
|
|
m_nodeManagers[ii].RegisterNodes(
|
|
context,
|
|
nodesToRegister,
|
|
registeredNodeIds,
|
|
processedNodes);
|
|
}
|
|
*/
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual void UnregisterNodes(
|
|
OperationContext context,
|
|
ArrayOf<NodeId> 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<bool> processedNodes = new List<bool>(new bool[itemsToDelete.Count]);
|
|
|
|
for (int ii = 0; ii < m_nodeManagers.Count; ii++)
|
|
{
|
|
m_nodeManagers[ii].RegisterNodes(
|
|
context,
|
|
nodesToUnregister,
|
|
processedNodes);
|
|
}
|
|
*/
|
|
}
|
|
|
|
/// <summary>
|
|
/// Translates a start node id plus a relative paths into a node id.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"><paramref name="browsePaths"/> is <c>null</c>.</exception>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
[Obsolete("Use TranslateBrowsePathsToNodeIdsAsync instead.")]
|
|
public virtual void TranslateBrowsePathsToNodeIds(
|
|
OperationContext context,
|
|
ArrayOf<BrowsePath> browsePaths,
|
|
out ArrayOf<BrowsePathResult> results,
|
|
out ArrayOf<DiagnosticInfo> diagnosticInfos)
|
|
{
|
|
(results, diagnosticInfos) = TranslateBrowsePathsToNodeIdsAsync(
|
|
context,
|
|
browsePaths).AsTask().GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask<(ArrayOf<BrowsePathResult> results, ArrayOf<DiagnosticInfo> diagnosticInfos)>
|
|
TranslateBrowsePathsToNodeIdsAsync(
|
|
OperationContext context,
|
|
ArrayOf<BrowsePath> browsePaths,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
bool diagnosticsExist = false;
|
|
var results = new List<BrowsePathResult>(browsePaths.Count);
|
|
var diagnosticInfos = new List<DiagnosticInfo>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the diagnostics return parameter.
|
|
/// </summary>
|
|
protected void UpdateDiagnostics(
|
|
OperationContext context,
|
|
bool diagnosticsExist,
|
|
ref List<DiagnosticInfo> 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!;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Translates a browse path.
|
|
/// </summary>
|
|
protected async ValueTask<ServiceResult> 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<BrowsePathTarget> targets = [];
|
|
// translate path only if validation is passing
|
|
await TranslateBrowsePathAsync(
|
|
context,
|
|
nodeManager,
|
|
sourceHandle,
|
|
relativePath,
|
|
targets,
|
|
0,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
result!.Targets = targets;
|
|
}
|
|
|
|
return serviceResult;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recursively processes the elements in the RelativePath starting at the specified index.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
private async ValueTask TranslateBrowsePathAsync(
|
|
OperationContext context,
|
|
IAsyncNodeManager? nodeManager,
|
|
object? sourceHandle,
|
|
RelativePath relativePath,
|
|
List<BrowsePathTarget> 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<ExpandedNodeId>();
|
|
var externalTargetIds = new List<NodeId>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask<(ArrayOf<BrowseResult> results, ArrayOf<DiagnosticInfo> diagnosticInfos)> BrowseAsync(
|
|
OperationContext context,
|
|
ViewDescription view,
|
|
uint maxReferencesPerNode,
|
|
ArrayOf<BrowseDescription> 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<BrowseResult>(nodesToBrowse.Count);
|
|
var diagnosticInfos = new List<DiagnosticInfo>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
/// <typeparam name="T">One of the following types used in the service calls:
|
|
/// ReadValueId used in the Read service</typeparam>
|
|
/// <param name="nodesList">The collection of nodes on which the service operates uppon</param>
|
|
/// <param name="uniqueNodesServiceAttributes">The resulting cache that holds the values of the AccessRestrictions and RolePermissions attributes needed for Read service</param>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
private static void PrepareValidationCache<T>(
|
|
ArrayOf<T> nodesList,
|
|
out Dictionary<NodeId, Variant[]> uniqueNodesServiceAttributes)
|
|
{
|
|
var uniqueNodes = new HashSet<NodeId>();
|
|
Type listType = typeof(T);
|
|
|
|
if (listType != typeof(ReadValueId) &&
|
|
listType != typeof(WriteValue) &&
|
|
listType != typeof(BrowseDescription) &&
|
|
listType != typeof(CallMethodRequest))
|
|
{
|
|
throw new ArgumentException(
|
|
"Provided List<T> 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, []);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask<(ArrayOf<BrowseResult> results, ArrayOf<DiagnosticInfo> diagnosticInfos)>
|
|
BrowseNextAsync(
|
|
OperationContext context,
|
|
bool releaseContinuationPoints,
|
|
ArrayOf<ByteString> continuationPoints,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
bool diagnosticsExist = false;
|
|
var results = new List<BrowseResult>(continuationPoints.Count);
|
|
var diagnosticInfos = new List<DiagnosticInfo>(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<ReferenceDescription> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the set of references that meet the filter criteria.
|
|
/// </summary>
|
|
protected async ValueTask<ServiceResult> 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<ReferenceDescription> 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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loops until browse is complete for max results reached.
|
|
/// </summary>
|
|
protected async ValueTask<(
|
|
ServiceResult serviceResult,
|
|
ContinuationPoint? cp,
|
|
ArrayOf<ReferenceDescription> references
|
|
)> FetchReferencesAsync(
|
|
OperationContext context,
|
|
bool assignContinuationPoint,
|
|
ContinuationPoint cp,
|
|
ArrayOf<ReferenceDescription> 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<ReferenceDescription>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the reference description with the node attributes.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"><paramref name="targetId"/> is <c>null</c>.</exception>
|
|
private async ValueTask<bool> 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;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<NodeState?> 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;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask<(ArrayOf<DataValue> values, ArrayOf<DiagnosticInfo> diagnosticInfos)> ReadAsync(
|
|
OperationContext context,
|
|
double maxAge,
|
|
TimestampsToReturn timestampsToReturn,
|
|
ArrayOf<ReadValueId> 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<DataValue>(nodesToRead.Count);
|
|
var diagnosticInfos = new List<DiagnosticInfo>(nodesToRead.Count);
|
|
|
|
// create empty list of errors.
|
|
var errors = new List<ServiceResult>(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<NodeId, Variant[]> 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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask<(ArrayOf<HistoryReadResult> values, ArrayOf<DiagnosticInfo> diagnosticInfos)> HistoryReadAsync(
|
|
OperationContext context,
|
|
ExtensionObject historyReadDetails,
|
|
TimestampsToReturn timestampsToReturn,
|
|
bool releaseContinuationPoints,
|
|
ArrayOf<HistoryReadValueId> 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<HistoryReadResult>(nodesToRead.Count);
|
|
var diagnosticInfos = new List<DiagnosticInfo>(nodesToRead.Count);
|
|
|
|
// pre-validate items.
|
|
bool validItems = false;
|
|
// create empty list of errors.
|
|
var errors = new List<ServiceResult>(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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask<(ArrayOf<StatusCode> results, ArrayOf<DiagnosticInfo> diagnosticInfos)> WriteAsync(
|
|
OperationContext context,
|
|
ArrayOf<WriteValue> nodesToWrite,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
int count = nodesToWrite.Count;
|
|
|
|
bool diagnosticsExist = false;
|
|
var results = new List<StatusCode>(count);
|
|
var diagnosticInfos = new List<DiagnosticInfo>(count);
|
|
|
|
PrepareValidationCache(
|
|
nodesToWrite,
|
|
out Dictionary<NodeId, Variant[]> 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<ServiceResult>(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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask<(ArrayOf<HistoryUpdateResult> results, ArrayOf<DiagnosticInfo> diagnosticInfos)>
|
|
HistoryUpdateAsync(
|
|
OperationContext context,
|
|
ArrayOf<ExtensionObject> historyUpdateDetails,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
Type? detailsType = null;
|
|
var nodesToUpdate = new List<HistoryUpdateDetails>();
|
|
|
|
// 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<HistoryUpdateResult>(nodesToUpdate.Count);
|
|
var diagnosticInfos = new List<DiagnosticInfo>(nodesToUpdate.Count);
|
|
|
|
// pre-validate items.
|
|
bool validItems = false;
|
|
|
|
// create empty list of errors.
|
|
var errors = new List<ServiceResult>(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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask<(ArrayOf<CallMethodResult> results, ArrayOf<DiagnosticInfo> diagnosticInfos)>
|
|
CallAsync(
|
|
OperationContext context,
|
|
ArrayOf<CallMethodRequest> methodsToCall,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
bool diagnosticsExist = false;
|
|
var results = new List<CallMethodResult>(methodsToCall.Count);
|
|
var diagnosticInfos = new List<DiagnosticInfo>(methodsToCall.Count);
|
|
var errors = new List<ServiceResult>(methodsToCall.Count);
|
|
|
|
PrepareValidationCache(
|
|
methodsToCall,
|
|
out Dictionary<NodeId, Variant[]> 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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask ConditionRefreshAsync(
|
|
OperationContext context,
|
|
IList<IEventMonitoredItem> 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.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a set of monitored items.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"><paramref name="context"/> is <c>null</c>.</exception>
|
|
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
[Obsolete("Use CreateMonitoredItemsAsync")]
|
|
public virtual void CreateMonitoredItems(
|
|
OperationContext context,
|
|
uint subscriptionId,
|
|
double publishingInterval,
|
|
TimestampsToReturn timestampsToReturn,
|
|
ArrayOf<MonitoredItemCreateRequest> itemsToCreate,
|
|
IList<ServiceResult> errors,
|
|
IList<MonitoringFilterResult> filterResults,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
bool createDurable)
|
|
{
|
|
CreateMonitoredItemsAsync(
|
|
context,
|
|
subscriptionId,
|
|
publishingInterval,
|
|
timestampsToReturn,
|
|
itemsToCreate,
|
|
errors,
|
|
filterResults,
|
|
monitoredItems,
|
|
createDurable).AsTask().GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask CreateMonitoredItemsAsync(
|
|
OperationContext context,
|
|
uint subscriptionId,
|
|
double publishingInterval,
|
|
TimestampsToReturn timestampsToReturn,
|
|
ArrayOf<MonitoredItemCreateRequest> itemsToCreate,
|
|
IList<ServiceResult> errors,
|
|
IList<MonitoringFilterResult> filterResults,
|
|
IList<IMonitoredItem> 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create monitored items for event subscriptions.
|
|
/// </summary>
|
|
private async ValueTask CreateMonitoredItemsForEventsAsync(
|
|
OperationContext context,
|
|
uint subscriptionId,
|
|
double publishingInterval,
|
|
TimestampsToReturn timestampsToReturn,
|
|
ArrayOf<MonitoredItemCreateRequest> itemsToCreate,
|
|
IList<ServiceResult> errors,
|
|
IList<MonitoringFilterResult> filterResults,
|
|
IList<IMonitoredItem> 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask RestoreMonitoredItemsAsync(
|
|
IList<IStoredMonitoredItem> itemsToRestore,
|
|
IList<IMonitoredItem> 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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restore monitored items for event subscriptions.
|
|
/// </summary>
|
|
private async ValueTask RestoreMonitoredItemsForEventsAsync(
|
|
IList<IStoredMonitoredItem> itemsToRestore,
|
|
IList<IMonitoredItem> 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Modifies a set of monitored items.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"><paramref name="context"/> is <c>null</c>.</exception>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
[Obsolete("Use ModifyMonitoredItemsAsync")]
|
|
public virtual void ModifyMonitoredItems(
|
|
OperationContext context,
|
|
TimestampsToReturn timestampsToReturn,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
ArrayOf<MonitoredItemModifyRequest> itemsToModify,
|
|
IList<ServiceResult> errors,
|
|
IList<MonitoringFilterResult> filterResults)
|
|
{
|
|
ModifyMonitoredItemsAsync(
|
|
context,
|
|
timestampsToReturn,
|
|
monitoredItems,
|
|
itemsToModify,
|
|
errors,
|
|
filterResults).AsTask().GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask ModifyMonitoredItemsAsync(
|
|
OperationContext context,
|
|
TimestampsToReturn timestampsToReturn,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
ArrayOf<MonitoredItemModifyRequest> itemsToModify,
|
|
IList<ServiceResult> errors,
|
|
IList<MonitoringFilterResult> 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Modify monitored items for event subscriptions.
|
|
/// </summary>
|
|
private async ValueTask ModifyMonitoredItemsForEventsAsync(
|
|
OperationContext context,
|
|
TimestampsToReturn timestampsToReturn,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
ArrayOf<MonitoredItemModifyRequest> itemsToModify,
|
|
IList<ServiceResult> errors,
|
|
IList<MonitoringFilterResult> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Transfers a set of monitored items.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"><paramref name="context"/> is <c>null</c>.</exception>
|
|
[Obsolete("User TransferMonitoredItemsAsync")]
|
|
public virtual void TransferMonitoredItems(
|
|
OperationContext context,
|
|
bool sendInitialValues,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
IList<ServiceResult> errors)
|
|
{
|
|
TransferMonitoredItemsAsync(
|
|
context,
|
|
sendInitialValues,
|
|
monitoredItems,
|
|
errors).AsTask().GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask TransferMonitoredItemsAsync(
|
|
OperationContext context,
|
|
bool sendInitialValues,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
IList<ServiceResult> 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<bool>(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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a set of monitored items.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"><paramref name="context"/> is <c>null</c>.</exception>
|
|
[Obsolete("Use DeleteMonitoredItemsAsync")]
|
|
public virtual void DeleteMonitoredItems(
|
|
OperationContext context,
|
|
uint subscriptionId,
|
|
IList<IMonitoredItem> itemsToDelete,
|
|
IList<ServiceResult> errors)
|
|
{
|
|
DeleteMonitoredItemsAsync(
|
|
context,
|
|
subscriptionId,
|
|
itemsToDelete,
|
|
errors).AsTask().GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask DeleteMonitoredItemsAsync(
|
|
OperationContext context,
|
|
uint subscriptionId,
|
|
IList<IMonitoredItem> itemsToDelete,
|
|
IList<ServiceResult> 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<bool>(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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete monitored items for event subscriptions.
|
|
/// </summary>
|
|
private async ValueTask DeleteMonitoredItemsForEventsAsync(
|
|
OperationContext context,
|
|
uint subscriptionId,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
List<bool> processedItems,
|
|
IList<ServiceResult> 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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual async ValueTask SetMonitoringModeAsync(
|
|
OperationContext context,
|
|
MonitoringMode monitoringMode,
|
|
IList<IMonitoredItem> itemsToModify,
|
|
IList<ServiceResult> 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<bool>(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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete monitored items for event subscriptions.
|
|
/// </summary>
|
|
private static void SetMonitoringModeForEvents(
|
|
OperationContext context,
|
|
MonitoringMode monitoringMode,
|
|
IList<IMonitoredItem> monitoredItems,
|
|
List<bool> processedItems,
|
|
IList<ServiceResult> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The server that the node manager belongs to.
|
|
/// </summary>
|
|
protected IServerInternal Server { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public IReadOnlyList<IAsyncNodeManager> AsyncNodeManagers => m_nodeManagers;
|
|
|
|
/// <inheritdoc/>
|
|
public IReadOnlyList<INodeManager> NodeManagers => m_nodeManagers.ConvertAll(m => m.SyncNodeManager);
|
|
|
|
/// <summary>
|
|
/// The namespace managers being managed
|
|
/// </summary>
|
|
internal ConcurrentDictionary<int, IReadOnlyList<IAsyncNodeManager>> NamespaceManagers { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Validates a monitoring attributes parameter.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a monitoring filter.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a monitored item create request parameter.
|
|
/// </summary>
|
|
protected async ValueTask<ServiceResult?> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a monitored item modify request parameter.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a call request item parameter and checks access rights and role permissions.
|
|
/// </summary>
|
|
protected async ValueTask<ServiceResult> ValidateCallRequestItemAsync(
|
|
OperationContext operationContext,
|
|
CallMethodRequest callMethodRequest,
|
|
Dictionary<NodeId, Variant[]>? 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a Read or MonitoredItemCreate request. It validates also access rights and role permissions
|
|
/// </summary>
|
|
protected async ValueTask<ServiceResult?> ValidateReadRequestAsync(
|
|
OperationContext operationContext,
|
|
ReadValueId readValueId,
|
|
Dictionary<NodeId, Variant[]>? 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a Write request. It validates also access rights and role permissions
|
|
/// </summary>
|
|
protected async ValueTask<ServiceResult?> ValidateWriteRequestAsync(
|
|
OperationContext operationContext,
|
|
WriteValue writeValue,
|
|
Dictionary<NodeId, Variant[]>? 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a HistoryRead request. It validates also access rights and role permissions
|
|
/// </summary>
|
|
protected async ValueTask<ServiceResult?> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a HistoryUpdate request. It validates also access rights and role permissions
|
|
/// </summary>
|
|
protected async ValueTask<ServiceResult?> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the Base NodeClass attributes and NameSpace meta-data attributes
|
|
/// are valid for the given operation context of the specified node.
|
|
/// </summary>
|
|
/// <param name="context">The Operation Context</param>
|
|
/// <param name="nodeId">The node whose attributes are validated</param>
|
|
/// <param name="requestedPermision">The requested permission</param>
|
|
/// <param name="uniqueNodesServiceAttributes">The cache holding the values of the attributes neeeded to be used in subsequent calls</param>
|
|
/// <param name="permissionsOnly">Only the AccessRestrictions and RolePermission attributes are read. Should be false if uniqueNodesServiceAttributes is not null</param>
|
|
/// <param name="cancellationToken">The Cancellation Token</param>
|
|
/// <returns>StatusCode Good if permission is granted, BadUserAccessDenied if not granted
|
|
/// or a bad status code describing the validation process failure </returns>
|
|
protected async ValueTask<ServiceResult> ValidatePermissionsAsync(
|
|
OperationContext context,
|
|
NodeId nodeId,
|
|
PermissionType requestedPermision,
|
|
Dictionary<NodeId, Variant[]>? 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the Base NodeClass attributes and NameSpace meta-data attributes
|
|
/// are valid for the given operation context of the specified node.
|
|
/// </summary>
|
|
/// <param name="context">The Operation Context</param>
|
|
/// <param name="nodeManager">The node manager handling the nodeHandle</param>
|
|
/// <param name="nodeHandle">The node handle of the node whose attributes are validated</param>
|
|
/// <param name="requestedPermision">The requested permission</param>
|
|
/// <param name="uniqueNodesServiceAttributes">The cache holding the values of the attributes neeeded to be used in subsequent calls</param>
|
|
/// <param name="permissionsOnly">Only the AccessRestrictions and RolePermission attributes are read. Should be false if uniqueNodesServiceAttributes is not null</param>
|
|
/// <param name="cancellationToken">The Cancellation Token</param>
|
|
/// <returns>StatusCode Good if permission is granted, BadUserAccessDenied if not granted
|
|
/// or a bad status code describing the validation process failure </returns>
|
|
protected async ValueTask<ServiceResult> ValidatePermissionsAsync(
|
|
OperationContext context,
|
|
IAsyncNodeManager? nodeManager,
|
|
object? nodeHandle,
|
|
PermissionType requestedPermision,
|
|
Dictionary<NodeId, Variant[]>? 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate the AccessRestrictions attribute
|
|
/// </summary>
|
|
/// <param name="context">The Operation Context</param>
|
|
/// <param name="nodeMetadata">Metadata</param>
|
|
/// <returns>Good if the AccessRestrictions passes the validation</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates the role permissions
|
|
/// </summary>
|
|
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<RolePermissionType> userRolePermissions = default;
|
|
if (!nodeMetadata.UserRolePermissions.IsEmpty)
|
|
{
|
|
userRolePermissions = nodeMetadata.UserRolePermissions;
|
|
}
|
|
else if (!nodeMetadata.DefaultUserRolePermissions.IsEmpty)
|
|
{
|
|
userRolePermissions = nodeMetadata.DefaultUserRolePermissions;
|
|
}
|
|
|
|
ArrayOf<RolePermissionType> 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<NodeId, PermissionType>();
|
|
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<NodeId, PermissionType>();
|
|
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<NodeId, PermissionType> 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<NodeId> 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<IAsyncNodeManager> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores a reference between NodeManagers that is needs to be created or deleted.
|
|
/// </summary>
|
|
public class LocalReference
|
|
{
|
|
/// <summary>
|
|
/// Initializes the reference.
|
|
/// </summary>
|
|
public LocalReference(
|
|
NodeId sourceId,
|
|
NodeId referenceTypeId,
|
|
bool isInverse,
|
|
NodeId targetId)
|
|
{
|
|
SourceId = sourceId;
|
|
ReferenceTypeId = referenceTypeId;
|
|
IsInverse = isInverse;
|
|
TargetId = targetId;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The source of the reference.
|
|
/// </summary>
|
|
public NodeId SourceId { get; }
|
|
|
|
/// <summary>
|
|
/// The type of reference.
|
|
/// </summary>
|
|
public NodeId ReferenceTypeId { get; }
|
|
|
|
/// <summary>
|
|
/// True if the reference is an inverse reference.
|
|
/// </summary>
|
|
public bool IsInverse { get; }
|
|
|
|
/// <summary>
|
|
/// The target of the reference.
|
|
/// </summary>
|
|
public NodeId TargetId { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a generator for unique monitored item ids.
|
|
/// Call next() to retrieve the next valid monitoredItemId.
|
|
/// </summary>
|
|
/// <remarks>This class provides a mechanism to generate sequential ids for monitored
|
|
/// items. It is designed to ensure thread-safe incrementation of the identifier.</remarks>
|
|
public class MonitoredItemIdFactory
|
|
{
|
|
/// <summary>
|
|
/// Initialize the MonitoredItemIdFactory with a new start value the ids start incrementing from.
|
|
/// </summary>
|
|
/// <param name="firstId"></param>
|
|
public void SetStartValue(uint firstId)
|
|
{
|
|
Utils.SetIdentifier(ref m_lastMonitoredItemId, firstId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the next unique monitored item id.
|
|
/// </summary>
|
|
/// <returns>an uint that can be used as an id for a monitored item</returns>
|
|
public uint GetNextId()
|
|
{
|
|
return Utils.IncrementIdentifier(ref m_lastMonitoredItemId);
|
|
}
|
|
|
|
private uint m_lastMonitoredItemId;
|
|
}
|
|
}
|