mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-17 05:11:13 +08:00
2871355d18
## Summary Implements the **GDS AliasName Server facet** (OPC 10000-17 Annex C) in `Opc.Ua.Gds.Server`, so that the CTT conformance unit *GDS AliasName Discovery* passes apart from two CTT script defects (documented, Mantis drafts in `plans/ctt-issues.md`). Spec requirements: - **C.1:** aggregate the AliasNames of registered Servers that expose the `ALIAS` capability (OPC 10000-12 Annex D). Pull all AliasNameCategory instances and merge those whose BrowseNames have the same Name. Provide the ExpandedNodeId of every referenced node and the ServerUri of the Server that contains it. - **C.2:** on registration, merge the Server's AliasNames into a single tree for TagVariables/Topics, combine aliases that share a name, and add the ServerUri to `ServerArray`. - **C.3:** on unregistration, remove that Server's aliases and categories (but keep those other Servers still provide), remove its AliasFor references, and remove its ServerUri from `ServerArray`. ### Design - **`AliasNameAggregator`** (`Opc.Ua.Gds.Server.AliasNames`) holds one snapshot per registered application and builds the merged view: - Categories are keyed by their BrowseName path below `Aliases`. Aliases are merged by name within a category; their targets are the union over all sources. - Targets are stored as namespace-URI ExpandedNodeIds. The ServerIndex is resolved against the server's `ServerArray` each time the view is built, because removing a ServerUri renumbers the entries after it. - Only ServerUris the aggregator added itself are removed again. - Also implements `FindAlias`/`FindAliasVerbose`, recursive over sub-categories. - **`SessionAliasNameSourceReader`** reads a source over a client Session by browsing the `Aliases` (i=23470) hierarchy and the AliasFor references. - It browses instead of calling `FindAlias` because the category structure has to be aggregated too, and a source whose aliases come from a NodeSet (like the CTT's embedded server) does not implement `FindAlias`. - It connects anonymously, to the most secure endpoint first, falling back to SecurityMode None. It runs GetEndpoints once per DiscoveryUrl. - **`ApplicationsNodeManager`** (new partial `ApplicationsNodeManager.AliasNames.cs`): - Hooks RegisterApplication (now `OnCallAsync`), UpdateApplication (background re-read) and UnregisterApplication. - Materializes the merged view as `AliasNameType` and `AliasNameCategoryType` nodes in the GDS application-record namespace, organized by the well-known TagVariables/Topics/Aliases objects. Each category gets a `FindAlias` answered from the merged list. - Changes and node updates are serialized, so a read that finishes after a concurrent unregister cannot re-add the source. - RegisterApplication waits at most `AliasNameRegistrationWait` (5 s) for the read; a slower or unreachable source is finished in the background and never blocks clients past their call timeout. - Optional periodic re-read via `AliasNameRefreshInterval`; it also retries sources that could not be read. - **`IAliasNameStoreRegistry.RegisterContributor`/`UnregisterContributor`** (Opc.Ua.Server): a read-only contributor's aliases are added to the `FindAlias`/`FindAliasVerbose` results of the categories it reports, next to the owning store's. This lets the aggregated aliases share the well-known categories with the server's own aliases, which matters for the reference server, an AliasName Server and a GDS at once. Contributors never receive Add/Delete calls. - **Opt-in:** `GlobalDiscoveryServerConfiguration.EnableAliasNameAggregation` is off by default, because the GDS then opens client connections to the DiscoveryUrls of registered applications. `Ctt.ReferenceServer.Config.xml` enables it. - `Opc.Ua.Gds.Server` now references `Opc.Ua.Client`; the `Opc.Ua` meta-package already depends on both. ### CTT (1.05.513 with the patched scripts), p14-gds, reference server `--ctt`, embedded CTT source + 2 extra AliasName sources | GDS AliasName Discovery | before | after | after, with the C54/C55 script fixes | | --- | --- | --- | --- | | initialize | Skipped (no sources) | Pass | Pass | | 001 | Error | Error (C55) | Pass | | 002 | Error | Error (C54) | Warning (RegisterApplication > 100 ms) | | 003 | Pass | Pass | Pass | | 004 | Error | Error (C54) | Pass | | 005 | Skipped | Error (C55) | Pass | | 006, 007, 009, 011, 012, 014, 015 | Skipped | Error (C54) | Pass | | 008 | Skipped | Pass | Pass | | 010, 013 | Skipped | Error (C55) | Pass | - **C54:** the script compares the GDS's AliasFor target (ServerIndex > 0, namespace URI, as Annex C.1 and Part 4 §7.16 require) with the source's local NodeId using `equals()`, which can never match. - **C55:** "no remote aliases" is checked as "no aliases", so it fails on the reference server's own aliases. Both are documented with draft Mantis texts (not filed). The remaining script checks pass in 15/15 test cases. No regressions: - Application Directory and Query Applications are unchanged against the baseline. The only difference is one ActivateSession timing warning in one run. - p09 (AliasName CUs) passes 32/32, identical to the baseline. ## Tests - `AliasNameAggregatorTests` (unit) covers: - merging across Servers; - categories kept while still referenced and removed after the last source; - ServerArray add, remove and renumbering; - pre-existing ServerUris kept; - local targets; - FindAlias/FindAliasVerbose scope, pattern and reference-type filter. - `GdsAliasNameAggregationTests` (end-to-end) registers a second in-process reference server with `ALIAS` and reads it with the real session reader. It checks: - aggregated nodes and remote AliasFor targets; - the ServerArray entry; - FindAlias on TagVariables and on the aggregated category; - that unregistering removes everything; - that an unreachable source still registers; - that a Server without `ALIAS` is ignored. - Registry contributor tests in `AliasNameStoreRegistryTests`. - Opc.Ua.Gds.Tests: 1246 passed, 0 failed, 56 skipped. Opc.Ua.Server.Tests AliasNames: 198/198. InformationModel and Features alias tests pass. net48 compiles. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
4.8 KiB
C#
117 lines
4.8 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.IO;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Gds;
|
|
using Opc.Ua.Gds.Server;
|
|
using Opc.Ua.Gds.Server.Database.Linq;
|
|
using Opc.Ua.Server;
|
|
|
|
namespace Quickstarts.ReferenceServer
|
|
{
|
|
/// <summary>
|
|
/// Factory that creates the GDS ApplicationsNodeManager for the reference server.
|
|
/// The node manager automatically wires handlers for:
|
|
/// <list type="bullet">
|
|
/// <item><description>CertificateDirectoryType (Part 12 §7.6)</description></item>
|
|
/// <item><description>KeyCredentialServiceType (Part 12 §8)</description></item>
|
|
/// <item><description>AuthorizationServiceType (Part 12 §9)</description></item>
|
|
/// </list>
|
|
/// </summary>
|
|
public sealed class GdsNodeManagerFactory : IAsyncNodeManagerFactory
|
|
{
|
|
private readonly GlobalDiscoveryServerConfiguration m_gdsConfiguration;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="GdsNodeManagerFactory"/> class.
|
|
/// </summary>
|
|
/// <param name="gdsConfiguration">The GDS configuration.</param>
|
|
public GdsNodeManagerFactory(GlobalDiscoveryServerConfiguration gdsConfiguration)
|
|
{
|
|
m_gdsConfiguration = gdsConfiguration;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
/// <remarks>
|
|
/// Taken from the manager itself so the two cannot drift: the master
|
|
/// node manager routes a namespace to this manager only if it is
|
|
/// advertised here.
|
|
/// </remarks>
|
|
public ArrayOf<string> NamespacesUris
|
|
=> new(ApplicationsNodeManager.DefaultNamespaceUris());
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<IAsyncNodeManager> CreateAsync(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
// Ensure the GDS data types are registered in the message context
|
|
if (!server.MessageContext.Factory.ContainsEncodeableType(
|
|
Opc.Ua.Gds.DataTypeIds.ApplicationRecordDataType))
|
|
{
|
|
server.MessageContext.Factory.Builder
|
|
.AddOpcUaGds()
|
|
.Commit();
|
|
}
|
|
|
|
string? databaseStorePath = Utils.ReplaceSpecialFolderNames(
|
|
m_gdsConfiguration.DatabaseStorePath);
|
|
|
|
// Ensure the directory exists
|
|
string? databaseDir = Path.GetDirectoryName(databaseStorePath);
|
|
if (!string.IsNullOrEmpty(databaseDir))
|
|
{
|
|
Directory.CreateDirectory(databaseDir);
|
|
}
|
|
|
|
LinqApplicationsDatabase database = string.IsNullOrEmpty(databaseDir)
|
|
? new LinqApplicationsDatabase()
|
|
: JsonApplicationsDatabase.Load(databaseStorePath!);
|
|
|
|
#pragma warning disable CA2000 // Ownership is transferred to the server via returned node manager instance.
|
|
var nodeManager = new ApplicationsNodeManager(
|
|
server,
|
|
configuration,
|
|
database,
|
|
database,
|
|
new CertificateGroup(server.Telemetry),
|
|
autoApprove: true);
|
|
#pragma warning restore CA2000
|
|
// GDS AliasName Server facet (OPC 10000-17 Annex C), enabled in
|
|
// the CTT configuration.
|
|
nodeManager.AliasNameAggregationEnabled |= m_gdsConfiguration.EnableAliasNameAggregation;
|
|
return new ValueTask<IAsyncNodeManager>(nodeManager);
|
|
}
|
|
}
|
|
}
|