diff --git a/docs/MigrationGuide.md b/docs/MigrationGuide.md index 16db44b205..40033939c4 100644 --- a/docs/MigrationGuide.md +++ b/docs/MigrationGuide.md @@ -196,6 +196,27 @@ for the before/after and [Custom node types and assignment control](NodeManagers.md#custom-node-types-and-assignment-control) for the runtime rules. +## Removed members on IServerInternal + +The following members had no consumer anywhere in the stack, its samples or +its tests, and have been removed. Each has a direct replacement: + +| Removed | Use instead | +| --- | --- | +| `CloseSession(OperationContext, NodeId, bool)` | `CloseSessionAsync(…)` | +| `Status` | `CurrentState` to read, `UpdateServerStatus` to write | +| `ServerDiagnostics` | `UpdateServerDiagnostics(Action<…>)` | +| `DiagnosticsEnabled` | `IDiagnosticsNodeManager.DiagnosticsEnabled` | +| `ModellingRulesManager`, `ConformanceUnitsManager` | the concrete `ServerInternalData`, which owns them | + +`MessageContext`, `DefaultSystemContext`, `CurrentState`, `ServerObject`, +`ReportEventAsync`, `CloseSessionAsync`, `DeleteSubscriptionAsync` and +`UpdateServerDiagnostics` all moved *down* to `IServerContext`, the ambient +view of a running server that `IServerInternal` now derives from. They remain +reachable through `IServerInternal` unchanged, so no call site has to move. +`CurrentState` is read-only on the ambient interface; the server itself still +sets it. + ## Migrating servers that relied on unserved history advertisement Server startup now reconciles variables that advertise history with the diff --git a/src/Opc.Ua.Redundancy.Kubernetes/Health/KubernetesReadinessStartupTask.cs b/src/Opc.Ua.Redundancy.Kubernetes/Health/KubernetesReadinessStartupTask.cs index 1cd804c9b5..807e263cfc 100644 --- a/src/Opc.Ua.Redundancy.Kubernetes/Health/KubernetesReadinessStartupTask.cs +++ b/src/Opc.Ua.Redundancy.Kubernetes/Health/KubernetesReadinessStartupTask.cs @@ -50,7 +50,7 @@ namespace Opc.Ua.Redundancy.Kubernetes } /// - public ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default) + public ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default) { if (server == null) { diff --git a/src/Opc.Ua.Redundancy.Server/Discovery/PeerDiscoveryStartupTask.cs b/src/Opc.Ua.Redundancy.Server/Discovery/PeerDiscoveryStartupTask.cs index 75015c11a6..fc1b0afd00 100644 --- a/src/Opc.Ua.Redundancy.Server/Discovery/PeerDiscoveryStartupTask.cs +++ b/src/Opc.Ua.Redundancy.Server/Discovery/PeerDiscoveryStartupTask.cs @@ -64,14 +64,14 @@ namespace Opc.Ua.Redundancy.Server } /// - public ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default) + public ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default) { if (server == null) { throw new ArgumentNullException(nameof(server)); } - m_logger = server.Telemetry.CreateLogger(); + m_logger = server.DefaultSystemContext.Telemetry.CreateLogger(); m_discovery.PeersChanged += OnPeersChanged; m_loop = Task.Run(() => RefreshLoopAsync(m_cts.Token), CancellationToken.None); return default; diff --git a/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/LoadDirectionStartupTask.cs b/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/LoadDirectionStartupTask.cs index 7cb1e01155..9d15994238 100644 --- a/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/LoadDirectionStartupTask.cs +++ b/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/LoadDirectionStartupTask.cs @@ -66,18 +66,18 @@ namespace Opc.Ua.Redundancy.Server } /// - public ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default) + public ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default) { if (server == null) { throw new ArgumentNullException(nameof(server)); } - string[] serverUris = server.ServerUris.ToArray(); + string[] serverUris = server.DefaultSystemContext.ServerUris.ToArray(); string? localServerUri = serverUris.Length > 0 ? serverUris[0] : null; if (string.IsNullOrEmpty(localServerUri)) { - server.Telemetry + server.DefaultSystemContext.Telemetry .CreateLogger() .LoadDirectionDisabledLocalServerUriUnavailable(); return default; diff --git a/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/PeerDirectionPublishStartupTask.cs b/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/PeerDirectionPublishStartupTask.cs index a7b8c846ed..35d53a80f8 100644 --- a/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/PeerDirectionPublishStartupTask.cs +++ b/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/PeerDirectionPublishStartupTask.cs @@ -71,8 +71,7 @@ namespace Opc.Ua.Redundancy.Server } /// - public async ValueTask OnServerStartedAsync( - IServerInternal server, + public async ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default) { if (server == null) @@ -81,7 +80,7 @@ namespace Opc.Ua.Redundancy.Server } string? localServerUri = ResolveLocalServerUri(server); - m_logger = server.Telemetry.CreateLogger(); + m_logger = server.DefaultSystemContext.Telemetry.CreateLogger(); if (string.IsNullOrEmpty(localServerUri)) { m_logger.LoadDirectionDisabledLocalServerUriUnavailableForPeerDirection(); @@ -174,9 +173,9 @@ namespace Opc.Ua.Redundancy.Server } } - private static string? ResolveLocalServerUri(IServerInternal server) + private static string? ResolveLocalServerUri(IServerContext server) { - string[] serverUris = server.ServerUris.ToArray(); + string[] serverUris = server.DefaultSystemContext.ServerUris.ToArray(); return serverUris.Length > 0 ? serverUris[0] : null; } diff --git a/src/Opc.Ua.Redundancy.Server/Redundancy/RequestServerStateChangeStartupTask.cs b/src/Opc.Ua.Redundancy.Server/Redundancy/RequestServerStateChangeStartupTask.cs index dd4d04c58d..31ced16b38 100644 --- a/src/Opc.Ua.Redundancy.Server/Redundancy/RequestServerStateChangeStartupTask.cs +++ b/src/Opc.Ua.Redundancy.Server/Redundancy/RequestServerStateChangeStartupTask.cs @@ -28,6 +28,7 @@ * ======================================================================*/ using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Opc.Ua.Server; @@ -64,7 +65,7 @@ namespace Opc.Ua.Redundancy.Server } /// - public ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default) + public ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default) { if (server == null) { @@ -79,7 +80,7 @@ namespace Opc.Ua.Redundancy.Server m_server = server; RequestServerStateChangeMethodState? requestServerStateChange = - server.DiagnosticsNodeManager?.FindPredefinedNode( + server.FindPredefinedNode( MethodIds.Server_RequestServerStateChange) ?? serverObject.RequestServerStateChange; requestServerStateChange?.OnCall = OnRequestServerStateChange; @@ -101,7 +102,7 @@ namespace Opc.Ua.Redundancy.Server { ValidateAdminAccess(context); - IServerInternal? server = m_server; + IServerContext? server = m_server; ServerObjectState? serverObject = server?.ServerObject; if (server == null || serverObject == null) { @@ -128,7 +129,7 @@ namespace Opc.Ua.Redundancy.Server return; } - IConfigurationNodeManager? configurationNodeManager = (m_server?.ConfigurationNodeManager) ?? + IConfigurationNodeManager? configurationNodeManager = m_server?.FindNodeManagers().FirstOrDefault() ?? throw new ServiceResultException( StatusCodes.BadUserAccessDenied, "A configuration node manager is required to validate administrator access."); @@ -200,6 +201,6 @@ namespace Opc.Ua.Redundancy.Server private readonly RequestServerStateChangeOptions m_options; private readonly IServiceLevelController? m_serviceLevelController; - private IServerInternal? m_server; + private IServerContext? m_server; } } diff --git a/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyController.cs b/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyController.cs index 0f03ed9217..63cf1c6dfa 100644 --- a/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyController.cs +++ b/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyController.cs @@ -29,6 +29,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Opc.Ua.Server; @@ -131,7 +132,7 @@ namespace Opc.Ua.Redundancy.Server /// The cancellation token. /// is null. internal async ValueTask AttachAsync( - IServerInternal server, + IServerContext server, CancellationToken cancellationToken = default) { if (server == null) @@ -167,7 +168,7 @@ namespace Opc.Ua.Redundancy.Server /// configured mode and publishes the mode-specific values. /// private static async ValueTask ApplyAsync( - IServerInternal server, + IServerContext server, ServerRedundancyOptions options, CancellationToken cancellationToken) { @@ -187,7 +188,7 @@ namespace Opc.Ua.Redundancy.Server } NodeId targetTypeDefinition = GetTypeDefinitionId(options.Mode); - var replacer = server.DiagnosticsNodeManager as IPredefinedNodeSubtypeReplacer; + IPredefinedNodeSubtypeReplacer? replacer = server.FindNodeManagers().FirstOrDefault(); if (replacer != null && !redundancy.TypeDefinitionId.Equals(targetTypeDefinition)) { @@ -302,6 +303,6 @@ namespace Opc.Ua.Redundancy.Server private readonly ServerRedundancyOptions m_options; private readonly SemaphoreSlim m_gate = new(1, 1); - private IServerInternal? m_server; + private IServerContext? m_server; } } diff --git a/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyStartupTask.cs b/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyStartupTask.cs index a7cb5f9adc..01fcf38def 100644 --- a/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyStartupTask.cs +++ b/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyStartupTask.cs @@ -61,8 +61,7 @@ namespace Opc.Ua.Redundancy.Server } /// - public async ValueTask OnServerStartedAsync( - IServerInternal server, + public async ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default) { if (server == null) @@ -75,14 +74,14 @@ namespace Opc.Ua.Redundancy.Server await m_controller.AttachAsync(server, cancellationToken).ConfigureAwait(false); } - private void WarnIfServiceLevelProviderMissing(IServerInternal server) + private void WarnIfServiceLevelProviderMissing(IServerContext server) { if (!m_warnIfServiceLevelProviderMissing || !m_controller.Options.IsNonTransparentMode) { return; } - ILogger logger = server.Telemetry.CreateLogger(); + ILogger logger = server.DefaultSystemContext.Telemetry.CreateLogger(); logger.NonTransparentServerRedundancyModeWithoutServiceLevelProvider(m_controller.Mode); } diff --git a/src/Opc.Ua.Redundancy.Server/Redundancy/ServiceLevelStartupTask.cs b/src/Opc.Ua.Redundancy.Server/Redundancy/ServiceLevelStartupTask.cs index d5dbaa351d..a172504fa3 100644 --- a/src/Opc.Ua.Redundancy.Server/Redundancy/ServiceLevelStartupTask.cs +++ b/src/Opc.Ua.Redundancy.Server/Redundancy/ServiceLevelStartupTask.cs @@ -56,7 +56,7 @@ namespace Opc.Ua.Redundancy.Server } /// - public ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default) + public ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default) { if (server == null) { diff --git a/src/Opc.Ua.Redundancy.Server/State/DistributedAddressSpaceStartupTask.cs b/src/Opc.Ua.Redundancy.Server/State/DistributedAddressSpaceStartupTask.cs index 53ad7cb3b4..f14626f8a3 100644 --- a/src/Opc.Ua.Redundancy.Server/State/DistributedAddressSpaceStartupTask.cs +++ b/src/Opc.Ua.Redundancy.Server/State/DistributedAddressSpaceStartupTask.cs @@ -77,14 +77,14 @@ namespace Opc.Ua.Redundancy.Server public INodeStateStoreRegistry? NodeStateStoreRegistry { get; private set; } /// - public async ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default) + public async ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default) { if (server == null) { throw new ArgumentNullException(nameof(server)); } - ILogger logger = server.Telemetry.CreateLogger(); + ILogger logger = server.DefaultSystemContext.Telemetry.CreateLogger(); // Build the store with the server's populated message context so // NodeId namespace indices resolve correctly. Disposal ownership is @@ -97,7 +97,7 @@ namespace Opc.Ua.Redundancy.Server // Own the node state store registry; nothing in the core server // surface holds it. The default store is the fallback for every // node that does not have a more specific binding. - var registry = new NodeStateStoreRegistry(server.NamespaceUris); + var registry = new NodeStateStoreRegistry(server.DefaultSystemContext.NamespaceUris); registry.RegisterDefault(store); m_registry = registry; NodeStateStoreRegistry = registry; @@ -107,13 +107,9 @@ namespace Opc.Ua.Redundancy.Server await m_election.TryAcquireOrRenewAsync(cancellationToken).ConfigureAwait(false); m_election.Start(); - foreach (INodeManager nodeManager in server.NodeManager.NodeManagers) + foreach (ILocalAddressSpaceSource source in + server.FindNodeManagers()) { - if (nodeManager is not ILocalAddressSpaceSource source) - { - continue; - } - ILocalAddressSpace addressSpace = source.CreateLocalAddressSpace(); var synchronizer = new AddressSpaceSynchronizer( store, addressSpace, m_election, logger); diff --git a/src/Opc.Ua.Redundancy.Server/State/ReplicatedAddressSpaceStartupTask.cs b/src/Opc.Ua.Redundancy.Server/State/ReplicatedAddressSpaceStartupTask.cs index 169c40717f..4cc621ebe4 100644 --- a/src/Opc.Ua.Redundancy.Server/State/ReplicatedAddressSpaceStartupTask.cs +++ b/src/Opc.Ua.Redundancy.Server/State/ReplicatedAddressSpaceStartupTask.cs @@ -58,22 +58,18 @@ namespace Opc.Ua.Redundancy.Server } /// - public async ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default) + public async ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default) { if (server == null) { throw new ArgumentNullException(nameof(server)); } - ILogger logger = server.Telemetry.CreateLogger(); + ILogger logger = server.DefaultSystemContext.Telemetry.CreateLogger(); - foreach (INodeManager nodeManager in server.NodeManager.NodeManagers) + foreach (ILocalAddressSpaceSource source in + server.FindNodeManagers()) { - if (nodeManager is not ILocalAddressSpaceSource source) - { - continue; - } - ILocalAddressSpace addressSpace = source.CreateLocalAddressSpace(); ITransport transport = m_options.CreateTransport(m_services, out InMemoryNetwork? defaultNetwork); if (defaultNetwork != null) diff --git a/src/Opc.Ua.Robotics.Server/Hosting/RobotIntentPostSetup.cs b/src/Opc.Ua.Robotics.Server/Hosting/RobotIntentPostSetup.cs index 18a172279c..d72f2f8d8c 100644 --- a/src/Opc.Ua.Robotics.Server/Hosting/RobotIntentPostSetup.cs +++ b/src/Opc.Ua.Robotics.Server/Hosting/RobotIntentPostSetup.cs @@ -99,16 +99,13 @@ namespace Opc.Ua.Robotics.Server.Hosting internal sealed class RobotIntentHostStartupTask : IServerStartupTask { public ValueTask OnServerStartedAsync( - IServerInternal server, + IServerContext server, CancellationToken cancellationToken = default) { - IReadOnlyList nodeManagers = server.NodeManager.AsyncNodeManagers; - for (int ii = 0; ii < nodeManagers.Count; ii++) + foreach (RobotIntentNodeManager robotIntentNodeManager in + server.FindNodeManagers()) { - if (nodeManagers[ii] is RobotIntentNodeManager robotIntentNodeManager) - { - robotIntentNodeManager.StartIntentControllerHosts(); - } + robotIntentNodeManager.StartIntentControllerHosts(); } return default; } diff --git a/src/Opc.Ua.Server/Hosting/IServerStartupTask.cs b/src/Opc.Ua.Server/Hosting/IServerStartupTask.cs index 5897d4c4f6..06fe4d5340 100644 --- a/src/Opc.Ua.Server/Hosting/IServerStartupTask.cs +++ b/src/Opc.Ua.Server/Hosting/IServerStartupTask.cs @@ -34,20 +34,24 @@ namespace Opc.Ua.Server.Hosting { /// /// A task run by the hosted server immediately after the server has - /// started, with access to the live . Every + /// started, with access to the live . Every /// implementation registered in DI is invoked once. This is the seam /// features use to wire runtime behavior that needs the fully-initialized - /// server (loaded node managers, the populated message context, the - /// ServerObject, the session manager) without subclassing - /// . + /// server (a bound address space, the populated message context, the + /// ServerObject) without subclassing . /// + /// + /// A task that needs a particular subsystem takes it as a constructor + /// dependency, which is how every implementation in this repository already + /// obtains what it needs. The context carries only what is genuinely ambient. + /// public interface IServerStartupTask { /// /// Invoked once after the server has started. /// - /// The live server internals. + /// The live server context. /// Cancellation token. - ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default); + ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default); } } diff --git a/src/Opc.Ua.Server/Server/IServerContext.cs b/src/Opc.Ua.Server/Server/IServerContext.cs new file mode 100644 index 0000000000..6f71fb0187 --- /dev/null +++ b/src/Opc.Ua.Server/Server/IServerContext.cs @@ -0,0 +1,153 @@ +/* ======================================================================== + * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Opc.Ua.Server +{ + /// + /// The ambient view of a running server, handed to components that need to observe it + /// and act on it without owning any part of it. + /// + /// + /// + /// This is deliberately small. Every member either answers a question about the server + /// as a whole or performs a server-wide operation; none of them hands out a subsystem. + /// A component that needs a particular subsystem asks for that subsystem in its + /// constructor, which states the dependency instead of hiding it behind an ambient + /// handle. + /// + /// + /// The ambient tables - namespace URIs, server URIs, the type table, the encodeable + /// factory and telemetry - are reached through , + /// which already carries all of them. They are not repeated here. + /// + /// + public interface IServerContext + { + /// + /// The current state of the server. + /// + ServerState CurrentState { get; } + + /// + /// The Server object node. + /// + ServerObjectState ServerObject { get; } + + /// + /// The context to use when serializing and deserializing extension objects. + /// + /// + /// This is the server's own context and carries its configured decoding limits. + /// It is deliberately not derived from : + /// ISystemContext.AsMessageContext() produces a context with default + /// limits, which would silently widen what a component accepts. + /// + IServiceMessageContext MessageContext { get; } + + /// + /// The context to use for operations that are not attributable to a session. + /// Also carries the namespace URIs, server URIs, type table, encodeable factory + /// and telemetry context. + /// + ServerSystemContext DefaultSystemContext { get; } + + /// + /// Creates a context that attributes operations to the given session, carrying its + /// identity and preferred locales. + /// + /// The session to attribute operations to. + ServerSystemContext CreateSystemContext(ISession session); + + /// + /// Finds a node in the server's predefined address space. + /// + /// The expected node type. + /// The node to find. + /// + /// The node, or null when it is absent or is not of type + /// . + /// + T? FindPredefinedNode(NodeId nodeId) where T : NodeState; + + /// + /// Finds every registered node manager that provides the requested capability. + /// + /// + /// Node managers advertise optional capabilities by implementing marker interfaces. + /// This asks for the capability rather than for the registry, so callers do not + /// have to know how node managers are stored or filter the list themselves. + /// + /// The capability to look for. + IEnumerable FindNodeManagers() where T : class; + + /// + /// Reports a global event. + /// + /// The event. + /// The cancellation token. + ValueTask ReportEventAsync(IFilterTarget e, CancellationToken cancellationToken = default); + + /// + /// Closes the specified session. + /// + /// The context. + /// The session identifier. + /// if set to true subscriptions are to be deleted. + /// The cancellation token. + ValueTask CloseSessionAsync( + OperationContext context, + NodeId sessionId, + bool deleteSubscriptions, + CancellationToken cancellationToken = default); + + /// + /// Deletes the specified subscription. + /// + /// The subscription identifier. + /// The cancellation token. + ValueTask DeleteSubscriptionAsync(uint subscriptionId, CancellationToken cancellationToken = default); + + /// + /// Applies an update to the server diagnostics while holding the server's + /// diagnostics lock. + /// + /// + /// The server owns its lock and never exposes it, so callers cannot participate in + /// the server's locking order. The diagnostic nodes are marked dirty inside the + /// critical section. + /// + /// The mutation to apply to the diagnostics. + void UpdateServerDiagnostics(Action update); + } +} diff --git a/src/Opc.Ua.Server/Server/IServerInternal.cs b/src/Opc.Ua.Server/Server/IServerInternal.cs index 502f20fc70..e5cff3c12f 100644 --- a/src/Opc.Ua.Server/Server/IServerInternal.cs +++ b/src/Opc.Ua.Server/Server/IServerInternal.cs @@ -38,7 +38,13 @@ namespace Opc.Ua.Server /// /// The interface that a server exposes to objects that it contains. /// - public interface IServerInternal : IAuditEventServer, IDisposable + /// + /// Components that only need to observe the server and act on it should take + /// instead. This interface additionally hands out the + /// server's subsystems, which a component should ask for by constructor injection + /// rather than reach for through an ambient handle. + /// + public interface IServerInternal : IServerContext, IAuditEventServer, IDisposable { /// /// The endpoint addresses used by the server. @@ -46,18 +52,6 @@ namespace Opc.Ua.Server /// The endpoint addresses. IEnumerable EndpointAddresses { get; } - /// - /// The context to use when serializing/deserializing extension objects. - /// - /// The message context. - IServiceMessageContext MessageContext { get; } - - /// - /// The default system context for the server. - /// - /// The default system context. - ServerSystemContext DefaultSystemContext { get; } - /// /// The table of namespace uris known to the server. /// @@ -141,20 +135,6 @@ namespace Opc.Ua.Server /// The aggregate manager. AggregateManager AggregateManager { get; } - /// - /// A manager for modelling rules supported by the server. - /// - /// The modelling rules manager. - ModellingRulesManager ModellingRulesManager { get; } - - /// - /// A manager for the conformance units and server profiles the server - /// advertises, aggregated from the registered - /// node managers. - /// - /// The conformance units manager. - ConformanceUnitsManager ConformanceUnitsManager { get; } - /// /// The manager for active sessions. /// @@ -218,91 +198,6 @@ namespace Opc.Ua.Server /// bool IsRunning { get; } - /// - /// Returns the status object for the server. - /// - /// The status. - [Obsolete("No longer thread safe. To read the value use CurrentState, to write use UpdateServerStatus.")] - ServerStatusValue Status { get; } - - /// - /// Gets or sets the current state of the server. - /// - /// The state of the current. - ServerState CurrentState { get; set; } - - /// - /// Returns the Server object node - /// - /// The Server object node. - ServerObjectState ServerObject { get; } - - /// - /// Applies an update to the server diagnostics while holding the server's - /// diagnostics lock. - /// - /// - /// The server owns its lock and never exposes it, so callers cannot participate in - /// the server's locking order. The diagnostic nodes are marked dirty inside the - /// critical section. - /// - /// The mutation to apply to the diagnostics. - void UpdateServerDiagnostics(Action update); - - /// - /// Reads a value derived from the server diagnostics while holding the server's - /// diagnostics lock. - /// - /// - /// Do not let the diagnostics object escape the callback: once the lock is - /// released, any field read from it is unsynchronized. - /// - /// The type of the value produced. - /// The projection applied to the diagnostics. - TResult ReadServerDiagnostics( - Func read); - - /// - /// Returns the diagnostics structure for the server. - /// - /// The server diagnostics. - ServerDiagnosticsSummaryDataType ServerDiagnostics { get; } - - /// - /// Whether the server is collecting diagnostics. - /// - /// true if diagnostics is enabled; otherwise, false. - bool DiagnosticsEnabled { get; } - - /// - /// Closes the specified session. - /// - /// The context. - /// The session identifier. - /// if set to true subscriptions are to be deleted. - [Obsolete("Use CloseSessionAsync instead.")] - void CloseSession(OperationContext context, NodeId sessionId, bool deleteSubscriptions); - - /// - /// Closes the specified session. - /// - /// The context. - /// The session identifier. - /// if set to true subscriptions are to be deleted. - /// The cancellation token. - ValueTask CloseSessionAsync( - OperationContext context, - NodeId sessionId, - bool deleteSubscriptions, - CancellationToken cancellationToken = default); - - /// - /// Deletes the specified subscription. - /// - /// The subscription identifier. - /// The cancellation token. - ValueTask DeleteSubscriptionAsync(uint subscriptionId, CancellationToken cancellationToken = default); - /// /// Called by any component to report a global event. /// @@ -316,14 +211,6 @@ namespace Opc.Ua.Server /// The event. void ReportEvent(ISystemContext context, IFilterTarget e); - /// - /// Asynchronously reports a global event, awaiting an asynchronous report sink so the caller - /// is never blocked. - /// - /// The event. - /// The cancellation token. - ValueTask ReportEventAsync(IFilterTarget e, CancellationToken cancellationToken = default); - /// /// Asynchronously reports a global event, awaiting an asynchronous report sink so the caller /// is never blocked. diff --git a/src/Opc.Ua.Server/Server/ServerInternalData.cs b/src/Opc.Ua.Server/Server/ServerInternalData.cs index 511b785444..386daa068e 100644 --- a/src/Opc.Ua.Server/Server/ServerInternalData.cs +++ b/src/Opc.Ua.Server/Server/ServerInternalData.cs @@ -32,6 +32,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Opc.Ua.Identity; @@ -493,6 +494,49 @@ namespace Opc.Ua.Server /// The default system context. public ServerSystemContext DefaultSystemContext { get; } + /// + public ServerSystemContext CreateSystemContext(ISession session) + { + if (session == null) + { + throw new ArgumentNullException(nameof(session)); + } + + return DefaultSystemContext.Copy(session); + } + + /// + public T? FindPredefinedNode(NodeId nodeId) where T : NodeState + { + return DiagnosticsNodeManager?.FindPredefinedNode(nodeId); + } + + /// + public IEnumerable FindNodeManagers() where T : class + { + IMasterNodeManager? nodeManager = NodeManager; + if (nodeManager == null) + { + return []; + } + + // A node manager is registered as an asynchronous manager and surfaced again + // through a synchronous adapter. A manager that already implements INodeManager + // is its own adapter, so the two lists overlap and the capability may sit on + // either face; both are searched and the overlap is removed. + // + // The overlap is the *same instance* appearing twice, so identity is the only + // correct comparison. Distinct() would use EqualityComparer.Default and + // honour an Equals override on an implementation, silently collapsing two + // managers that merely compare equal. + List asyncManagers = [.. nodeManager.AsyncNodeManagers.OfType()]; + + return asyncManagers.Concat( + nodeManager.NodeManagers + .OfType() + .Where(manager => !asyncManagers.Exists(known => ReferenceEquals(known, manager)))); + } + /// /// The table of namespace uris known to the server. /// @@ -672,37 +716,11 @@ namespace Opc.Ua.Server } } - /// - /// Reads a value derived from the server diagnostics while holding the server's - /// diagnostics lock. - /// - /// - /// Do not let the diagnostics object itself escape the callback: once the lock is - /// released, any field read from it is unsynchronized. - /// - /// The type of the value produced. - /// The projection applied to the diagnostics. - /// Thrown if read is null. - public TResult ReadServerDiagnostics( - Func read) - { - if (read == null) - { - throw new ArgumentNullException(nameof(read)); - } - - lock (m_diagnosticsLock) - { - return read.Invoke(ServerDiagnostics); - } - } - /// /// Guards the server diagnostics and the server status value, which is constructed /// with this same lock so the two stay mutually exclusive. Never exposed: callers - /// reach the diagnostics through and - /// , and the status through - /// and . + /// reach the diagnostics through , and the + /// status through and . /// private readonly Lock m_diagnosticsLock = new(); @@ -771,22 +789,6 @@ namespace Opc.Ua.Server /// internal ServerStatusValue NonThreadSafeStatus { get; private set; } = null!; - /// - /// Closes the specified session. - /// - /// The context. - /// The session identifier. - /// if set to true subscriptions are to be deleted. - [Obsolete("Use CloseSessionAsync instead.")] - public void CloseSession( - OperationContext context, - NodeId sessionId, - bool deleteSubscriptions) - { - CloseSessionAsync(context, sessionId, deleteSubscriptions) - .AsTask().GetAwaiter().GetResult(); - } - /// /// Closes the specified session. /// diff --git a/src/Opc.Ua.Server/Server/StandardServer.cs b/src/Opc.Ua.Server/Server/StandardServer.cs index 8b26a8ba59..c3e258da89 100644 --- a/src/Opc.Ua.Server/Server/StandardServer.cs +++ b/src/Opc.Ua.Server/Server/StandardServer.cs @@ -2810,14 +2810,11 @@ namespace Opc.Ua.Server )] public ServerStatusDataType GetStatus() { - lock (Lock!) + if (m_serverInternal == null) { - if (m_serverInternal == null) - { - throw new ServiceResultException(StatusCodes.BadServerHalted); - } - return ServerInternal.Status.Value; + throw new ServiceResultException(StatusCodes.BadServerHalted); } + return m_serverInternal.NonThreadSafeStatus.Value; } /// @@ -3101,7 +3098,7 @@ namespace Opc.Ua.Server m_logger.ServerEnterStateState(state); - ServerInternal.CurrentState = state; + m_serverInternal.CurrentState = state; } finally { diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/AddressSpace/DistributedAddressSpaceStartupTaskTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/AddressSpace/DistributedAddressSpaceStartupTaskTests.cs index db036ad3c7..231d9a4dfc 100644 --- a/tests/Opc.Ua.Redundancy.Server.Tests/AddressSpace/DistributedAddressSpaceStartupTaskTests.cs +++ b/tests/Opc.Ua.Redundancy.Server.Tests/AddressSpace/DistributedAddressSpaceStartupTaskTests.cs @@ -97,6 +97,9 @@ namespace Opc.Ua.Server.Tests.Redundancy server.Setup(s => s.MessageContext).Returns(messageContext); server.Setup(s => s.NamespaceUris).Returns(messageContext.NamespaceUris); server.Setup(s => s.NodeManager).Returns(masterNodeManager.Object); + server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object)); + server.Setup(s => s.FindNodeManagers()) + .Returns([nodeManager.As().Object]); using var kv = new InMemorySharedKeyValueStore(); var election = new StaticLeaderElection(true); diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/Discovery/PeerDiscoveryBuilderExtensionsTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/Discovery/PeerDiscoveryBuilderExtensionsTests.cs index 51f54e315d..3860390597 100644 --- a/tests/Opc.Ua.Redundancy.Server.Tests/Discovery/PeerDiscoveryBuilderExtensionsTests.cs +++ b/tests/Opc.Ua.Redundancy.Server.Tests/Discovery/PeerDiscoveryBuilderExtensionsTests.cs @@ -135,6 +135,7 @@ namespace Opc.Ua.Server.Tests.Redundancy var server = new Mock(); server.Setup(s => s.Telemetry).Returns(NUnitTelemetryContext.Create()); + server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object)); await using (var task = new PeerDiscoveryStartupTask(discovery, provider, options, registry)) { @@ -158,6 +159,7 @@ namespace Opc.Ua.Server.Tests.Redundancy var server = new Mock(); server.Setup(s => s.Telemetry).Returns(NUnitTelemetryContext.Create()); + server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object)); await using (var task = new PeerDiscoveryStartupTask(discovery, provider, options)) { @@ -180,6 +182,7 @@ namespace Opc.Ua.Server.Tests.Redundancy var server = new Mock(); server.Setup(s => s.Telemetry).Returns(NUnitTelemetryContext.Create()); + server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object)); await using (var task = new PeerDiscoveryStartupTask(discovery, provider, options, gossipSink: null)) { diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/DiagnosticsServerHarness.cs b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/DiagnosticsServerHarness.cs index c930e7aa42..b670d5ee32 100644 --- a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/DiagnosticsServerHarness.cs +++ b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/DiagnosticsServerHarness.cs @@ -113,6 +113,12 @@ namespace Opc.Ua.Server.Tests.Redundancy ServerObjectState serverObject = manager.FindPredefinedNode(ObjectIds.Server); server.Setup(s => s.ServerObject).Returns(serverObject); server.Setup(s => s.DiagnosticsNodeManager).Returns(manager); + server.Setup(s => s.FindNodeManagers()).Returns([manager]); + server.Setup(s => s.FindNodeManagers()) + .Returns([configurationNodeManager.Object]); + server.Setup(s => s.FindPredefinedNode( + It.IsAny())) + .Returns((NodeId nodeId) => manager.FindPredefinedNode(nodeId)); return new DiagnosticsServerHarness(server, manager); } diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/LoadDirectionStartupTaskTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/LoadDirectionStartupTaskTests.cs index f559027265..0d377c2243 100644 --- a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/LoadDirectionStartupTaskTests.cs +++ b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/LoadDirectionStartupTaskTests.cs @@ -214,6 +214,7 @@ namespace Opc.Ua.Server.Tests.Redundancy server.Setup(s => s.Telemetry).Returns(telemetry); server.Setup(s => s.ServerUris).Returns(serverUris); server.Setup(s => s.MessageContext).Returns(context); + server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object)); return server; } diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/PeerDirectionPublishStartupTaskTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/PeerDirectionPublishStartupTaskTests.cs index 6459f705e1..7c0a187002 100644 --- a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/PeerDirectionPublishStartupTaskTests.cs +++ b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/PeerDirectionPublishStartupTaskTests.cs @@ -376,6 +376,7 @@ namespace Opc.Ua.Server.Tests.Redundancy server.Setup(s => s.Telemetry).Returns(telemetry); server.Setup(s => s.ServerUris).Returns(serverUris); server.Setup(s => s.MessageContext).Returns(context); + server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object)); return server; } diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/RequestServerStateChangeStartupTaskTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/RequestServerStateChangeStartupTaskTests.cs index b7ea290f67..ab28063eca 100644 --- a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/RequestServerStateChangeStartupTaskTests.cs +++ b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/RequestServerStateChangeStartupTaskTests.cs @@ -380,6 +380,11 @@ namespace Opc.Ua.Server.Tests.Redundancy ServerObjectState serverObject = manager.FindPredefinedNode(ObjectIds.Server); server.Setup(s => s.ServerObject).Returns(serverObject); server.Setup(s => s.DiagnosticsNodeManager).Returns(manager); + server.Setup(s => s.FindNodeManagers()) + .Returns([configurationNodeManager.Object]); + server.Setup(s => s.FindPredefinedNode( + It.IsAny())) + .Returns((NodeId nodeId) => manager.FindPredefinedNode(nodeId)); return new LoadedDiagnosticsServer(server, manager, configurationNodeManager); } diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/ServerRedundancyStartupTaskTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/ServerRedundancyStartupTaskTests.cs index 49342bbac6..d305ed6ece 100644 --- a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/ServerRedundancyStartupTaskTests.cs +++ b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/ServerRedundancyStartupTaskTests.cs @@ -287,6 +287,7 @@ namespace Opc.Ua.Server.Tests.Redundancy ServerObjectState serverObject = manager.FindPredefinedNode(ObjectIds.Server); server.Setup(s => s.ServerObject).Returns(serverObject); server.Setup(s => s.DiagnosticsNodeManager).Returns(manager); + server.Setup(s => s.FindNodeManagers()).Returns([manager]); return new LoadedDiagnosticsServer(server, manager); } diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/State/ReplicatedAddressSpaceStartupTaskTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/State/ReplicatedAddressSpaceStartupTaskTests.cs index 08e84748bc..c6380409ba 100644 --- a/tests/Opc.Ua.Redundancy.Server.Tests/State/ReplicatedAddressSpaceStartupTaskTests.cs +++ b/tests/Opc.Ua.Redundancy.Server.Tests/State/ReplicatedAddressSpaceStartupTaskTests.cs @@ -88,6 +88,9 @@ namespace Opc.Ua.Redundancy.Server.Tests server.Setup(s => s.Telemetry).Returns(telemetry); server.Setup(s => s.MessageContext).Returns(messageContext); server.Setup(s => s.NodeManager).Returns(masterNodeManager.Object); + server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object)); + server.Setup(s => s.FindNodeManagers()) + .Returns([optedIn.As().Object]); await using var task = new ReplicatedAddressSpaceStartupTask( EmptyServices(), new ReplicatedAddressSpaceOptions()); diff --git a/tests/Opc.Ua.Server.Tests/Hosting/ServerFluentApiHostingTests.cs b/tests/Opc.Ua.Server.Tests/Hosting/ServerFluentApiHostingTests.cs index c292efeec6..04215ac7cb 100644 --- a/tests/Opc.Ua.Server.Tests/Hosting/ServerFluentApiHostingTests.cs +++ b/tests/Opc.Ua.Server.Tests/Hosting/ServerFluentApiHostingTests.cs @@ -1031,9 +1031,9 @@ namespace Opc.Ua.Server.Tests.Hosting { public int InvocationCount => Volatile.Read(ref m_invocationCount); - public IServerInternal? ObservedServer { get; private set; } + public IServerContext? ObservedServer { get; private set; } - public ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default) + public ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default) { Interlocked.Increment(ref m_invocationCount); ObservedServer = server; diff --git a/tests/Opc.Ua.Server.Tests/ServerInternalDataTests.cs b/tests/Opc.Ua.Server.Tests/ServerInternalDataTests.cs index f76dfa18d1..2af6d04f9a 100644 --- a/tests/Opc.Ua.Server.Tests/ServerInternalDataTests.cs +++ b/tests/Opc.Ua.Server.Tests/ServerInternalDataTests.cs @@ -49,6 +49,8 @@ namespace Opc.Ua.Server.Tests private ServiceMessageContext m_messageContext; private ITelemetryContext m_telemetry; + private static readonly string[] s_locales = ["de-DE"]; + [SetUp] public void SetUp() { @@ -206,12 +208,65 @@ namespace Opc.Ua.Server.Tests Assert.That(data.NodeManager, Is.SameAs(mockNodeManager.Object)); } + [Test] + public void FindNodeManagersYieldsAManagerOnBothFacesOnce() + { + using ServerInternalData data = CreateServerInternalData(); + + // A manager that implements INodeManager is its own synchronous adapter, so + // the same instance appears on both faces. + var manager = new Mock(); + INodeManager syncFace = manager.As().Object; + + data.SetNodeManager(CreateMasterNodeManager([manager.Object], [syncFace])); + + Assert.That( + data.FindNodeManagers().ToList(), + Is.EqualTo(new[] { manager.Object }).AsCollection); + } + + [Test] + public void FindNodeManagersYieldsEveryDistinctManager() + { + using ServerInternalData data = CreateServerInternalData(); + var asyncOnly = new Mock(); + var syncManager = new Mock(); + IAsyncNodeManager syncAsAsync = syncManager.As().Object; + + data.SetNodeManager( + CreateMasterNodeManager([asyncOnly.Object], [syncManager.Object])); + + Assert.That( + data.FindNodeManagers().ToList(), + Is.EqualTo(new[] { asyncOnly.Object, syncAsAsync }).AsCollection); + } + + [Test] + public void FindNodeManagersReturnsEmptyWhenNoNodeManagerIsSet() + { + using ServerInternalData data = CreateServerInternalData(); + + Assert.That(data.FindNodeManagers(), Is.Empty); + } + + private static IMasterNodeManager CreateMasterNodeManager( + IAsyncNodeManager[] asyncNodeManagers, + INodeManager[] nodeManagers) + { + var mock = new Mock(); + mock.Setup(m => m.DiagnosticsNodeManager).Returns((IDiagnosticsNodeManager)null); + mock.Setup(m => m.ConfigurationNodeManager).Returns((IConfigurationNodeManager)null); + mock.Setup(m => m.CoreNodeManager).Returns((ICoreNodeManager)null); + mock.Setup(m => m.AsyncNodeManagers).Returns(asyncNodeManagers); + mock.Setup(m => m.NodeManagers).Returns(nodeManagers); + return mock.Object; + } + [Test] public void SetMainNodeManagerFactoryStoresFactory() { using ServerInternalData data = CreateServerInternalData(); var mockFactory = new Mock(); - data.SetMainNodeManagerFactory(mockFactory.Object); Assert.That(data.MainNodeManagerFactory, Is.SameAs(mockFactory.Object)); @@ -321,16 +376,6 @@ namespace Opc.Ua.Server.Tests Throws.TypeOf()); } - [Test] - public void ReadServerDiagnosticsThrowsOnNullRead() - { - using ServerInternalData data = CreateServerInternalData(); - - Assert.That( - () => data.ReadServerDiagnostics(null!), - Throws.TypeOf()); - } - [Test] public void DiagnosticsEnabledReturnsFalseWhenNoDiagnosticsNodeManager() { @@ -338,6 +383,56 @@ namespace Opc.Ua.Server.Tests Assert.That(data.DiagnosticsEnabled, Is.False); } + [Test] + public void ServerContextDefaultSystemContextIsTheServerSystemContext() + { + using ServerInternalData data = CreateServerInternalData(); + + Assert.That( + ((IServerContext)data).DefaultSystemContext, + Is.SameAs(data.DefaultSystemContext)); + } + + [Test] + public void CreateSystemContextCarriesTheSessionIdentityAndLocales() + { + using ServerInternalData data = CreateServerInternalData(); + + var sessionId = new NodeId(Guid.NewGuid()); + var identity = new UserIdentity(new AnonymousIdentityToken()); + + var session = new Mock(); + session.Setup(s => s.Id).Returns(sessionId); + session.Setup(s => s.Identity).Returns(identity); + session.Setup(s => s.PreferredLocales).Returns(s_locales); + + ServerSystemContext created = data.CreateSystemContext(session.Object); + + Assert.That(created, Is.Not.SameAs(data.DefaultSystemContext)); + Assert.That(created.PreferredLocales.ToArray(), Is.EqualTo(s_locales)); + Assert.That(created.NamespaceUris, Is.SameAs(data.NamespaceUris)); + } + + [Test] + public void CreateSystemContextThrowsOnNullSession() + { + using ServerInternalData data = CreateServerInternalData(); + + Assert.That( + () => data.CreateSystemContext(null!), + Throws.TypeOf()); + } + + [Test] + public void FindPredefinedNodeReturnsNullBeforeTheDiagnosticsNodeManagerExists() + { + using ServerInternalData data = CreateServerInternalData(); + + // The datastore is published before the node managers are bound, so callers + // must tolerate an address space that is not there yet. + Assert.That(data.FindPredefinedNode(ObjectIds.Server), Is.Null); + } + [Test] public void TelemetryReturnsMessageContextTelemetry() { diff --git a/tests/Opc.Ua.Server.Tests/SessionSecurityTests.cs b/tests/Opc.Ua.Server.Tests/SessionSecurityTests.cs index 815b886f08..878872e08f 100644 --- a/tests/Opc.Ua.Server.Tests/SessionSecurityTests.cs +++ b/tests/Opc.Ua.Server.Tests/SessionSecurityTests.cs @@ -73,7 +73,6 @@ namespace Opc.Ua.Server.Tests m_server.Setup(s => s.MessageContext).Returns( ServiceMessageContext.CreateEmpty(m_telemetry)); var serverDiagnostics = new ServerDiagnosticsSummaryDataType(); - m_server.Setup(s => s.ServerDiagnostics).Returns(serverDiagnostics); m_server .Setup(s => s.UpdateServerDiagnostics( It.IsAny>())) diff --git a/tests/Opc.Ua.Server.Tests/StandardServerCoverageTests.cs b/tests/Opc.Ua.Server.Tests/StandardServerCoverageTests.cs index 8134839e90..e27f0c3097 100644 --- a/tests/Opc.Ua.Server.Tests/StandardServerCoverageTests.cs +++ b/tests/Opc.Ua.Server.Tests/StandardServerCoverageTests.cs @@ -165,8 +165,10 @@ namespace Opc.Ua.Server.Tests using TestableStandardServer server = CreateServer(); #pragma warning disable CS0618 // GetStatus is obsolete but still exercised for coverage. - Assert.That(() => server.GetStatus(), Throws.Exception); + ServiceResultException ex = Assert.Throws( + () => server.GetStatus()); #pragma warning restore CS0618 + Assert.That(ex.StatusCode, Is.EqualTo((uint)StatusCodes.BadServerHalted)); } [Test] diff --git a/tests/Opc.Ua.Server.Tests/SubscriptionTests.cs b/tests/Opc.Ua.Server.Tests/SubscriptionTests.cs index 51ad54b75a..e7ef179936 100644 --- a/tests/Opc.Ua.Server.Tests/SubscriptionTests.cs +++ b/tests/Opc.Ua.Server.Tests/SubscriptionTests.cs @@ -68,7 +68,6 @@ namespace Opc.Ua.Server.Tests m_serverMock.Setup(s => s.NodeManager).Returns(m_nodeManagerMock.Object); m_serverMock.Setup(s => s.MonitoredItemQueueFactory).Returns(m_queueFactoryMock.Object); var serverDiagnostics = new ServerDiagnosticsSummaryDataType(); - m_serverMock.Setup(s => s.ServerDiagnostics).Returns(serverDiagnostics); m_serverMock .Setup(s => s.UpdateServerDiagnostics( It.IsAny>())) diff --git a/tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Unshipped.md b/tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Unshipped.md index 99862abafc..415301d53d 100644 --- a/tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Unshipped.md +++ b/tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Unshipped.md @@ -25,6 +25,6 @@ UA0020 | Migration | Warning | Replace `EncodeableFactory.GlobalFactory` / `En UA0021 | Migration | Info | Replace `CertificateValidator` / `CertificateValidationEventArgs` with the 1.6 `ICertificateManager` / `ICertificateValidatorEx` / `CertificateValidationResult` pipeline. See docs/migrate/2.0.x/certificates.md. UA0022 | Migration | Warning | Replace `ApplicationConfiguration.CertificateValidator` / `ServerBase.CertificateValidator` property access with `.CertificateManager`. UA0023 | Migration | Warning | Replace the legacy 1.04 PubSub top-level types (`UaPubSubApplication`, `IUaPubSubConnection`, `UaPubSubConnection`, `IUaPublisher`, `UaPublisher`, `IUaPubSubDataStore`, `UaPubSubDataStore`, `UaPubSubConfigurator`) with the new `IPubSubApplication` / `PubSubApplicationBuilder` surface (or `AddPubSub()` / `AddUdpTransport()` / `AddMqttTransport()` on `IOpcUaBuilder`). See docs/migrate/2.0.x/pubsub.md. -UA0024 | Migration | Warning | Replace `IServerInternal`/`ISession`/`ISubscription` `DiagnosticsLock`/`DiagnosticsWriteLock` access with `UpdateServerDiagnostics(...)` / `UpdateDiagnostics(...)` (and `ReadServerDiagnostics` / `ReadDiagnostics` for projections). +UA0024 | Migration | Warning | Replace `IServerInternal`/`ISession`/`ISubscription` `DiagnosticsLock`/`DiagnosticsWriteLock` access with `UpdateServerDiagnostics(...)` / `UpdateDiagnostics(...)` (and `ReadDiagnostics` for projections). UA0025 | Migration | Warning | Replace `ILocalNode.DataLock` / `Node.DataLock` access with a lock you own; the node guards its own state. UA0026 | Migration | Warning | Replace `BaseVariableValue.Lock` access with a lock you own and pass to the `BaseVariableValue` constructor. diff --git a/tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticDescriptors.cs b/tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticDescriptors.cs index 15b1badfaa..48fa5db0d5 100644 --- a/tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticDescriptors.cs +++ b/tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticDescriptors.cs @@ -202,7 +202,7 @@ namespace Opc.Ua.MigrationAnalyzer.Diagnostics "Diagnostics locks are no longer exposed", "'{0}.{1}' was removed in 2.0 — apply the change through '{0}.{2}' instead of taking the lock yourself", DiagnosticSeverity.Warning, - "IServerInternal, ISession and ISubscription no longer expose DiagnosticsLock / DiagnosticsWriteLock. A caller could not see what else took those locks, in what order, or for how long, and holding one across a call back into the stack could deadlock. Each owner now applies the mutation itself through UpdateDiagnostics / UpdateServerDiagnostics, with ReadDiagnostics / ReadServerDiagnostics for projections. Do not let the diagnostics object escape the callback: once it returns the lock is released. This rule reports rather than fixes, because turning a lock statement body into a lambda depends on what the body captures and returns."); + "IServerInternal, ISession and ISubscription no longer expose DiagnosticsLock / DiagnosticsWriteLock. A caller could not see what else took those locks, in what order, or for how long, and holding one across a call back into the stack could deadlock. Each owner now applies the mutation itself through UpdateDiagnostics / UpdateServerDiagnostics, and ISession and ISubscription add ReadDiagnostics for projections. Do not let the diagnostics object escape the callback: once it returns the lock is released. This rule reports rather than fixes, because turning a lock statement body into a lambda depends on what the body captures and returns."); public static readonly DiagnosticDescriptor UA0025_RemovedNodeDataLock = Create( DiagnosticIds.UA0025,