/* ======================================================================== * 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.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Opc.Ua.Server.NodeManager; namespace Opc.Ua.Server.Fluent { /// /// Opt-in base class for node managers that want to use the fluent /// Publish surface (external event sources delivered through /// ). The source-generator-emitted /// NodeManagerBase derives from this class when any wrapper /// in the design exposes a Publish binding; hand-written /// managers can also derive directly. /// /// /// /// This class owns an whose /// reconcile loop runs as long as the manager is alive. Override /// hooks the registry so it /// activates and deactivates sources in lock-step with /// . Dispose(bool) /// tears the registry down before the base implementation runs so /// no iterator outlives the manager. /// /// /// Subclasses should never call into /// outside the fluent builder pipeline; the surface is exposed for /// generated code only. /// /// public abstract class FluentNodeManagerBase : AsyncCustomNodeManager { /// /// Initializes the node manager. /// protected FluentNodeManagerBase( IServerInternal server, params string[] namespaceUris) : base(server, namespaceUris) { EventSources = new EventSourceRegistry(this, m_logger); Simulations = new SimulationRegistry(this, m_logger); } /// /// Initializes the node manager. /// protected FluentNodeManagerBase( IServerInternal server, ILogger logger, params string[] namespaceUris) : base(server, logger, namespaceUris) { EventSources = new EventSourceRegistry(this, m_logger); Simulations = new SimulationRegistry(this, m_logger); } /// /// Initializes the node manager. /// protected FluentNodeManagerBase( IServerInternal server, ApplicationConfiguration configuration, params string[] namespaceUris) : base(server, configuration, namespaceUris) { EventSources = new EventSourceRegistry(this, m_logger); Simulations = new SimulationRegistry(this, m_logger); } /// /// Initializes the node manager. /// protected FluentNodeManagerBase( IServerInternal server, ApplicationConfiguration configuration, ILogger logger, params string[] namespaceUris) : base(server, configuration, logger, namespaceUris) { EventSources = new EventSourceRegistry(this, m_logger); Simulations = new SimulationRegistry(this, m_logger); } /// /// Initializes the node manager. /// protected FluentNodeManagerBase( IServerInternal server, ApplicationConfiguration configuration, bool useSamplingGroups, params string[] namespaceUris) : base(server, configuration, useSamplingGroups, namespaceUris) { EventSources = new EventSourceRegistry(this, m_logger); Simulations = new SimulationRegistry(this, m_logger); } /// /// Initializes the node manager. /// protected FluentNodeManagerBase( IServerInternal server, ApplicationConfiguration configuration, bool useSamplingGroups, ILogger logger, params string[] namespaceUris) : base(server, configuration, useSamplingGroups, logger, namespaceUris) { EventSources = new EventSourceRegistry(this, m_logger); Simulations = new SimulationRegistry(this, m_logger); } /// /// Registry that the fluent Publish surface stores its /// registered event sources in. Accessed by /// during /// Configure and by generated wrappers; not intended for /// direct subclass use. /// internal EventSourceRegistry EventSources { get; } /// /// Registry that the fluent Simulation surface stores its /// registered periodic tick loops in. Started after /// Configure completes (via NodeManagerBuilder.Seal) /// and torn down on disposal. /// internal SimulationRegistry Simulations { get; } /// /// Constructs a for this /// manager and attaches the fluent event-source / simulation /// registries in a single call. Hand-written managers use this /// to collapse the imperative /// new NodeManagerBuilder(SystemContext, this, nsIndex, ...) /// + AttachToBuilder(builder) + Configure(builder) /// + builder.Seal() quadruple to a single fluent chain: /// /// this.CreateFluentBuilder(nsIndex) /// .Configure(Configure) /// .Seal(); /// /// The three root/nodeId/typeId lookups default to scanning the /// manager's /// dictionary, mirroring the resolver wiring that the /// source-generated NodeManagerBase.CreateAddressSpaceAsync /// emits. /// /// /// Namespace index used when a browse-path segment omits an /// explicit ns=N; prefix. Typically the manager's /// model-specific namespace index. /// /// /// A configured ready to /// receive Configure(builder) wiring; the fluent /// extensions /// and chain off it. /// public NodeManagerBuilder CreateFluentBuilder(ushort defaultNamespaceIndex) { var builder = new NodeManagerBuilder( SystemContext, this, defaultNamespaceIndex, browseName => PredefinedNodes.Values.FindByBrowseName(browseName)!, nodeId => PredefinedNodes.FindById(nodeId)!, PredefinedNodes.Values.FindByTypeDefinition); AttachToBuilder(builder); return builder; } /// /// Attaches this manager's event-source registry to the supplied /// fluent builder so that Publish extension methods can /// resolve it. The generator-emitted CreateAddressSpaceAsync /// invokes this immediately after constructing the builder; hand- /// written managers that build their own /// should call this once before /// passing the builder into Configure. /// /// /// The fluent builder that the manager's Configure /// partial(s) will receive. /// /// /// Raised when is null. /// public void AttachToBuilder(NodeManagerBuilder builder) { if (builder == null) { throw new System.ArgumentNullException(nameof(builder)); } builder.AttachEventSources(EventSources); builder.AttachSimulations(Simulations); } /// /// Signals the registry whenever a notifier's monitored-events /// ref-count flips so the reconcile loop can start or stop the /// matching iterator. Subclasses that further override /// /// must call base before doing their own work. /// protected override ValueTask OnSubscribeToEventsAsync( ServerSystemContext context, MonitoredNode2 monitoredNode, bool unsubscribe, CancellationToken cancellationToken = default) { EventSources.SignalReconcile(); return base.OnSubscribeToEventsAsync(context, monitoredNode, unsubscribe, cancellationToken); } /// /// Cancels every running iterator and waits (bounded by each /// source's /// ) before /// invoking the base disposer. Subclasses that further override /// Dispose must call base.Dispose(disposing). /// protected override void Dispose(bool disposing) { if (disposing) { Simulations.Dispose(); EventSources.Dispose(); } base.Dispose(disposing); } /// /// Internal trampoline used by /// to register a notifier as a root notifier from a background /// worker thread. Wraps /// /// so the registry does not have to know its protected /// signature. /// internal Task AddRootNotifierFromFluentAsync( NodeState notifier, CancellationToken cancellationToken) { return AddRootNotifierAsync(notifier, cancellationToken).AsTask(); } } }