mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-17 05:11:13 +08:00
0374e19121
# Description _Describe the changes here to communicate to the maintainers why they should accept this pull request. By default - this will become the Commit message after merging and thus define history._ Add provider-backed MachineTool, bundled WMTP, GMS and CranesHoists companions using the existing Machinery/MachineVision fluent, DI and typed-client approach. **The PR is open/non-draft by user choice. Current head `ecc4a7d6ca3e8004cf28ab54da605da302d026bf` is mergeable and has 108 successful checks, six intentionally skipped checks, zero failures and zero pending checks. All five primary workflows completed successfully at that exact head.** | Family | Released specification/model | Packaging | | --- | --- | --- | | MachineTool | OPC 40501-1, 1.02.0 | Model, Server, Client and MCP; normal root versions | | WMTP | OPC 40505, 1.0.0 | Bundled in MachineTool; distinct CLR/wire namespace and independent WMTP-only hosting | | GMS | OPC 40210 reference 1.0.1, normative model 1.0.0 | Model, Server, Client and MCP; normal root versions | | CranesHoists | OPC 40020-1, 1.00 | Model, Server, Client and MCP; all four plus Debug variants forced preview | There are **12 new packable projects**. No separate WMTP package/profile/guide, root version bump, tag, release or package publication is included. Mandatory instance structure is the default; selected optional features require explicit providers. The implementation reuses Machinery results/jobs/files/ProcessValues and Robotics topology, including retained MachineTool Production, defined GMS jobs/parts, modern ISA95/Machinery jobs, shared-owner hosting and isolated instances alongside MachineVision. Runtime and integration fixes preserve raw quality/timestamps, coherent acquisition, namespace ownership, accepted-configuration state, borrowed-resource lifetime, session/node/file cleanup, terminal-value publication, event propagation and endpoint-aware capability publication. Firmware/licence transfer means provider acceptance, not installation/activation. Cancellation does not silently abort another caller's operation. The later upstream MCP expansion is integrated without losing either family set or its default-off privileged/live execution policy. ### Conformance and scope Canonical model inputs are pinned to UA-Nodeset revision `96a08945425d0295edb3607d2a38f6841ef44046`. **GMS upstream exception:** [Annex A.1](https://reference.opcfoundation.org/specs/OPC-40210/annex-a) makes the released NodeSet normative, but §12.3.2.7 requires undefined `GMS_ProductionStatisticsType`. Inherited statistics and defined jobs/parts are implemented without inventing a standard type/NodeId; the complete GMS Production facet is withheld. The requirement ledger records **102 positively evidenced CU rows, 96 directly negative-tested rows and six explicit negative-evidence limitations**. All 648 test-class/name/TRX links were independently resolved to passing cases. The six limitations concern Stacklight, retained legacy PowerOnDuration, Machinery PowerOnDuration, WorkingUnit, Obligation and MaintenanceMode. They are not claims that every invalid-input or hardware-control behavior was tested. The undefined GMS requirement is outside the 102 defined CUs. No external certification claim is made. Retained baseline complete-assembly line coverage, with generated code retained and no source exclusions: | Family | Model | Server | Client | | --- | ---: | ---: | ---: | | MachineTool/WMTP | 83.80% | 91.98% | 84.74% | | GMS | 95.51% | 90.63% | 88.38% | | CranesHoists | 92.76% | 94.45% | 92.24% | These nine measurements predate the later focused review fixes and merges; they are not relabelled as new `ecc4a7d6c` percentages. The current head's separate CI code-coverage check succeeded. No new exclusions or threshold relaxations were introduced. ## Related Issues - Fixes [#4764](https://github.com/OPCFoundation/UA-.NETStandard/issues/4764), the implementation/design and acceptance tracker. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [ ] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received.
640 lines
24 KiB
C#
640 lines
24 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 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;
|
|
using Opc.Ua.Server.Fluent;
|
|
|
|
namespace Opc.Ua.Machinery.Server.Builders
|
|
{
|
|
/// <summary>
|
|
/// Owns one machine under construction: the staged node graph, the
|
|
/// browse-name and NodeId reservations that keep two concurrent builds
|
|
/// apart, and the post-registration work that can only run once the
|
|
/// machine is part of the address space.
|
|
/// </summary>
|
|
internal sealed class MachineryBuildScope
|
|
{
|
|
public MachineryBuildScope(
|
|
IMachineryBuildContext buildContext,
|
|
NodeState parent,
|
|
QualifiedName browseName,
|
|
bool externallyRegistered = false)
|
|
{
|
|
BuildContext = buildContext ??
|
|
throw new ArgumentNullException(nameof(buildContext));
|
|
Parent = parent ?? throw new ArgumentNullException(nameof(parent));
|
|
m_buildCoordinator = GetBuildCoordinator(buildContext);
|
|
EnsureContextMutable(buildContext);
|
|
Context = buildContext.Context;
|
|
BrowseName = browseName;
|
|
IsExternallyRegistered = externallyRegistered;
|
|
m_rootBrowseNameReservation =
|
|
m_buildCoordinator.ReserveRootBrowseName(parent, browseName);
|
|
}
|
|
|
|
internal IMachineryBuildContext BuildContext { get; }
|
|
|
|
internal ISystemContext Context { get; }
|
|
|
|
internal NodeState Parent { get; }
|
|
|
|
internal QualifiedName BrowseName { get; }
|
|
|
|
internal NodeState Root { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Provider validation that must finish before any staged node is published.
|
|
/// </summary>
|
|
internal List<Func<CancellationToken, ValueTask>> PreRegistrationActions { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Asynchronous work that must run after the completed tree has been
|
|
/// registered with the node manager — binding a result store to the
|
|
/// transfer object, for example.
|
|
/// </summary>
|
|
internal List<Func<CancellationToken, ValueTask>> PostRegistrationActions { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Resources created during registration. They are released when the
|
|
/// build is rolled back, and handed to the registered root's lifetime.
|
|
/// Actual root removal or manager shutdown awaits their cleanup.
|
|
/// </summary>
|
|
internal List<IAsyncDisposable> RegisteredResources { get; } = [];
|
|
|
|
internal bool IsRegistered { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets whether another companion builder owns publication of this root.
|
|
/// </summary>
|
|
internal bool IsExternallyRegistered { get; }
|
|
|
|
internal void AttachRoot(NodeState root)
|
|
{
|
|
Root = root ?? throw new ArgumentNullException(nameof(root));
|
|
}
|
|
|
|
internal void Abort()
|
|
{
|
|
ReleaseReservations();
|
|
}
|
|
|
|
internal static void EnsureContextMutable(IMachineryBuildContext buildContext)
|
|
{
|
|
IMachineryBuildCoordinator coordinator = GetBuildCoordinator(buildContext);
|
|
if (coordinator.IsSealed)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadInvalidState,
|
|
"The Machinery build context is sealed.");
|
|
}
|
|
}
|
|
|
|
internal static IDisposable AcquireBuildLease(IMachineryBuildContext buildContext)
|
|
{
|
|
return GetBuildCoordinator(buildContext).AcquireBuildLease();
|
|
}
|
|
|
|
internal void EnsureMutable()
|
|
{
|
|
EnsureContextMutable(BuildContext);
|
|
if (m_registering || IsRegistered || m_aborted)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadInvalidState,
|
|
"Machine '{0}' has already started registration.",
|
|
BrowseName);
|
|
}
|
|
}
|
|
|
|
internal void EnsurePart(MachineryParts part, string what)
|
|
{
|
|
if (!BuildContext.Parts.HasFlag(part))
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadConfigurationError,
|
|
"{0} requires MachineryParts.{1}, which this server does not expose. " +
|
|
"Add the part to MachineryServerOptions.Parts.",
|
|
what,
|
|
part);
|
|
}
|
|
}
|
|
|
|
internal void RegisterBuilder(MachineryNodeBuilder builder)
|
|
{
|
|
m_builders.Add(builder);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Records a facet the build materialised. Before registration the
|
|
/// facets are held back and flushed only once the machine is actually
|
|
/// in the address space, so a rolled-back build advertises nothing.
|
|
/// Afterwards — a result-ready event raised at runtime, for example —
|
|
/// they go straight through.
|
|
/// </summary>
|
|
internal void RecordFacet(MachineryFacet facet)
|
|
{
|
|
if (IsRegistered)
|
|
{
|
|
m_buildCoordinator.RecordFacet(facet);
|
|
return;
|
|
}
|
|
m_facets.Add(facet);
|
|
}
|
|
|
|
internal async ValueTask RegisterAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (IsExternallyRegistered)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadInvalidState,
|
|
"An externally staged machine is completed by its registration owner, not BuildAsync.");
|
|
}
|
|
EnsureMutable();
|
|
if (Root == null)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadInvalidState,
|
|
"Machine '{0}' has no root node.",
|
|
BrowseName);
|
|
}
|
|
m_registering = true;
|
|
try
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
for (int ii = 0; ii < PreRegistrationActions.Count; ii++)
|
|
{
|
|
await PreRegistrationActions[ii](cancellationToken).ConfigureAwait(false);
|
|
}
|
|
VerifyNodeIds();
|
|
VerifyEnergyMainGrouping();
|
|
|
|
Parent.AddChild((BaseInstanceState)Root);
|
|
await BuildContext.Manager
|
|
.AddPredefinedNodeAsync(Root, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
CacheNodeBuilders();
|
|
|
|
for (int ii = 0; ii < PostRegistrationActions.Count; ii++)
|
|
{
|
|
await PostRegistrationActions[ii](cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
ArrayOf<IAsyncDisposable> resources = RegisteredResources.ToArrayOf();
|
|
RegisteredResources.Clear();
|
|
await m_buildCoordinator.AdoptResourcesAsync(Root, resources).ConfigureAwait(false);
|
|
IsRegistered = true;
|
|
for (int ii = 0; ii < m_facets.Count; ii++)
|
|
{
|
|
m_buildCoordinator.RecordFacet(m_facets[ii]);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
IsRegistered = false;
|
|
await RollbackRegistrationAsync().ConfigureAwait(false);
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
ReleaseReservations();
|
|
m_registering = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates an externally owned graph without publishing it.
|
|
/// </summary>
|
|
internal async ValueTask PrepareExternalAsync(CancellationToken cancellationToken)
|
|
{
|
|
EnsureMutable();
|
|
m_registering = true;
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
if (BuildContext.Manager.FindPredefinedNode(Root.NodeId) != null)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadInvalidState,
|
|
"A Machinery registration participant requires an unregistered root.");
|
|
}
|
|
MachineryBuilderUtilities.ClearModellingRules(Context, Root);
|
|
for (int ii = 0; ii < PreRegistrationActions.Count; ii++)
|
|
{
|
|
await PreRegistrationActions[ii](cancellationToken).ConfigureAwait(false);
|
|
}
|
|
VerifyNodeIds();
|
|
VerifyEnergyMainGrouping();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attaches bindings and an owned discovery reference after the owner's registration.
|
|
/// </summary>
|
|
internal async ValueTask BindExternalAsync(CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
if (!ReferenceEquals(BuildContext.Manager.FindPredefinedNode(Root.NodeId), Root))
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadInvalidState,
|
|
"The registration owner has not published this Machinery root.");
|
|
}
|
|
CacheNodeBuilders();
|
|
for (int ii = 0; ii < PostRegistrationActions.Count; ii++)
|
|
{
|
|
await PostRegistrationActions[ii](cancellationToken).ConfigureAwait(false);
|
|
}
|
|
AddDiscoveryReferences();
|
|
m_previousStateChanged = Root.OnStateChangedAsync;
|
|
m_deleteHandler = OnRootChangedAsync;
|
|
Root.OnStateChangedAsync = m_deleteHandler;
|
|
NodeManagerBuilder lifetimeBuilder = BuildContext.Manager.CreateFluentBuilder(
|
|
BuildContext.InstanceNamespaceIndex);
|
|
lifetimeBuilder.Node<BaseObjectState>(Root.NodeId).Attach(
|
|
(_, _, _) => new ValueTask<IAsyncDisposable?>(new ExternalBindingLifetime(this)));
|
|
await lifetimeBuilder.SealAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Records only successfully bound facets; resource lifetime stays with the actual root.
|
|
/// </summary>
|
|
internal void CommitExternal()
|
|
{
|
|
IsRegistered = true;
|
|
for (int ii = 0; ii < m_facets.Count; ii++)
|
|
{
|
|
m_buildCoordinator.RecordFacet(m_facets[ii]);
|
|
}
|
|
ReleaseReservations();
|
|
m_registering = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unwinds only this participant, without deleting or reparenting the owning tree.
|
|
/// </summary>
|
|
internal async ValueTask AbortExternalAsync()
|
|
{
|
|
try
|
|
{
|
|
if (!IsRegistered)
|
|
{
|
|
m_aborted = true;
|
|
await ReleaseExternalAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
ReleaseReservations();
|
|
m_registering = false;
|
|
}
|
|
}
|
|
|
|
private void VerifyNodeIds()
|
|
{
|
|
var nodes = new List<NodeState> { Root };
|
|
var nodeIds = new HashSet<NodeId>();
|
|
var children = new List<BaseInstanceState>();
|
|
for (int ii = 0; ii < nodes.Count; ii++)
|
|
{
|
|
NodeState node = nodes[ii];
|
|
if (node.NodeId.IsNull)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadConfigurationError,
|
|
"Machinery descendant '{0}' has a null NodeId after instance assignment.",
|
|
node.BrowseName);
|
|
}
|
|
if (node.NodeId.NamespaceIndex != BuildContext.InstanceNamespaceIndex)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadConfigurationError,
|
|
"Machinery descendant '{0}' has NodeId '{1}' outside the " +
|
|
"instance namespace.",
|
|
node.BrowseName,
|
|
node.NodeId);
|
|
}
|
|
if (!nodeIds.Add(node.NodeId))
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadConfigurationError,
|
|
"Machinery descendant '{0}' has duplicate NodeId '{1}'.",
|
|
node.BrowseName,
|
|
node.NodeId);
|
|
}
|
|
NodeState? indexedNode = BuildContext.Manager.FindPredefinedNode(node.NodeId);
|
|
if (indexedNode != null &&
|
|
(IsExternallyRegistered || !ReferenceEquals(indexedNode, node)))
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadConfigurationError,
|
|
"Machinery descendant '{0}' has NodeId '{1}', which is already " +
|
|
"indexed by node '{2}'.",
|
|
node.BrowseName,
|
|
node.NodeId,
|
|
indexedNode.BrowseName);
|
|
}
|
|
|
|
children.Clear();
|
|
node.GetChildren(Context, children);
|
|
for (int childIndex = 0; childIndex < children.Count; childIndex++)
|
|
{
|
|
nodes.Add(children[childIndex]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the OPC 40001-4 <c>Machinery Energy Main grouping</c> unit.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// §8.1 requires every resource folder below <c>Monitoring/Consumption</c>
|
|
/// to carry a <c>Main</c> metering point that groups the resource's
|
|
/// readings. <see cref="MachineryEnergyBuilder"/> always creates
|
|
/// <c>Main</c> itself, so a resource the builder made satisfies the unit
|
|
/// by construction — but an application is free to attach a resource
|
|
/// folder to <c>Consumption</c> directly, and such a folder would make
|
|
/// the server advertise a unit it does not meet.
|
|
/// </para>
|
|
/// <para>
|
|
/// Checked here rather than in the energy builder precisely because the
|
|
/// folders worth checking are the ones the energy builder never saw.
|
|
/// </para>
|
|
/// </remarks>
|
|
private void VerifyEnergyMainGrouping()
|
|
{
|
|
BaseInstanceState? consumption = FindConsumptionFolder();
|
|
if (consumption == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ushort energyNamespaceIndex = MachineryBuilderUtilities.NamespaceIndex(
|
|
Context,
|
|
Opc.Ua.Machinery.Energy.Namespaces.MachineryEnergy);
|
|
var mainName = new QualifiedName(
|
|
Opc.Ua.Machinery.Energy.BrowseNames.Main,
|
|
energyNamespaceIndex);
|
|
|
|
var resources = new List<BaseInstanceState>();
|
|
consumption.GetChildren(Context, resources);
|
|
for (int ii = 0; ii < resources.Count; ii++)
|
|
{
|
|
BaseInstanceState resource = resources[ii];
|
|
if (resource is not FolderState)
|
|
{
|
|
continue;
|
|
}
|
|
if (resource.FindChild(Context, mainName) is not Opc.Ua.ECM.EnergyMeasurementState)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadConfigurationError,
|
|
"OPC 40001-4 requires a Main metering point on every resource " +
|
|
"folder below Monitoring/Consumption, but '{0}' on machine '{1}' " +
|
|
"has none. Add it through the energy builder, or do not attach " +
|
|
"the folder to Consumption.",
|
|
resource.BrowseName,
|
|
BrowseName);
|
|
}
|
|
}
|
|
}
|
|
|
|
private BaseInstanceState? FindConsumptionFolder()
|
|
{
|
|
ushort machineryNamespaceIndex = MachineryBuilderUtilities.NamespaceIndex(
|
|
Context,
|
|
Opc.Ua.Machinery.Namespaces.Machinery);
|
|
BaseInstanceState? monitoring = Root?.FindChild(
|
|
Context,
|
|
new QualifiedName(
|
|
Opc.Ua.Machinery.BrowseNames.Monitoring,
|
|
machineryNamespaceIndex));
|
|
return monitoring?.FindChild(
|
|
Context,
|
|
new QualifiedName(
|
|
Opc.Ua.Machinery.BrowseNames.Consumption,
|
|
machineryNamespaceIndex));
|
|
}
|
|
|
|
private static IMachineryBuildCoordinator GetBuildCoordinator(
|
|
IMachineryBuildContext buildContext)
|
|
{
|
|
return buildContext as IMachineryBuildCoordinator ??
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadConfigurationError,
|
|
"The Machinery build context does not provide build coordination.");
|
|
}
|
|
|
|
private void CacheNodeBuilders()
|
|
{
|
|
for (int ii = 0; ii < m_builders.Count; ii++)
|
|
{
|
|
m_builders[ii].CacheNodeBuilder();
|
|
}
|
|
}
|
|
|
|
private void ReleaseNodeIdReservations()
|
|
{
|
|
if (!m_buildCoordinator.ReleasesNodeIdReservations || Root == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var nodes = new List<NodeState> { Root };
|
|
var children = new List<BaseInstanceState>();
|
|
for (int ii = 0; ii < nodes.Count; ii++)
|
|
{
|
|
NodeState node = nodes[ii];
|
|
m_buildCoordinator.ReleaseNodeIdReservation(node);
|
|
|
|
children.Clear();
|
|
node.GetChildren(Context, children);
|
|
for (int childIndex = 0; childIndex < children.Count; childIndex++)
|
|
{
|
|
nodes.Add(children[childIndex]);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ReleaseReservations()
|
|
{
|
|
ReleaseNodeIdReservations();
|
|
ReleaseRootBrowseNameReservation();
|
|
}
|
|
|
|
private void ReleaseRootBrowseNameReservation()
|
|
{
|
|
m_rootBrowseNameReservation?.Dispose();
|
|
m_rootBrowseNameReservation = null;
|
|
}
|
|
|
|
private async ValueTask RollbackRegistrationAsync()
|
|
{
|
|
await ReleaseResourcesAsync().ConfigureAwait(false);
|
|
|
|
if (Root == null)
|
|
{
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
if (!Root.NodeId.IsNull &&
|
|
ReferenceEquals(BuildContext.Manager.FindPredefinedNode(Root.NodeId), Root))
|
|
{
|
|
await BuildContext.Manager.DeleteNodeAsync(
|
|
BuildContext.Manager.SystemContext,
|
|
Root.NodeId,
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Parent.RemoveChild((BaseInstanceState)Root);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases resources in reverse order without masking the original build failure.
|
|
/// </summary>
|
|
private async ValueTask ReleaseResourcesAsync()
|
|
{
|
|
for (int ii = RegisteredResources.Count - 1; ii >= 0; ii--)
|
|
{
|
|
try
|
|
{
|
|
await RegisteredResources[ii].DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// A resource that fails to release must not mask the
|
|
// original build failure that triggered the rollback.
|
|
}
|
|
}
|
|
RegisteredResources.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds discovery links without changing the root's owning parent or reference type.
|
|
/// </summary>
|
|
private void AddDiscoveryReferences()
|
|
{
|
|
NodeId organizes = Opc.Ua.ReferenceTypeIds.Organizes;
|
|
m_forwardAdded = !Parent.ReferenceExists(organizes, false, Root.NodeId);
|
|
m_inverseAdded = !Root.ReferenceExists(organizes, true, Parent.NodeId);
|
|
if (m_forwardAdded)
|
|
{
|
|
Parent.AddReference(organizes, false, Root.NodeId);
|
|
}
|
|
if (m_inverseAdded)
|
|
{
|
|
Root.AddReference(organizes, true, Parent.NodeId);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases bindings once, either during rollback or through the manager's attachment ledger.
|
|
/// </summary>
|
|
private async ValueTask ReleaseExternalAsync()
|
|
{
|
|
if (Interlocked.Exchange(ref m_externalDisposed, 1) != 0)
|
|
{
|
|
return;
|
|
}
|
|
if (Root.OnStateChangedAsync == m_deleteHandler)
|
|
{
|
|
Root.OnStateChangedAsync = m_previousStateChanged;
|
|
}
|
|
if (m_forwardAdded)
|
|
{
|
|
Parent.RemoveReference(Opc.Ua.ReferenceTypeIds.Organizes, false, Root.NodeId);
|
|
}
|
|
if (m_inverseAdded)
|
|
{
|
|
Root.RemoveReference(Opc.Ua.ReferenceTypeIds.Organizes, true, Parent.NodeId);
|
|
}
|
|
await ReleaseResourcesAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases per-root resources when the existing node lifecycle reports deletion.
|
|
/// </summary>
|
|
private async ValueTask OnRootChangedAsync(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
NodeStateChangeMasks changes,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
NodeStateChangedAsyncHandler? previous = m_previousStateChanged;
|
|
if ((changes & NodeStateChangeMasks.Deleted) != 0)
|
|
{
|
|
await ReleaseExternalAsync().ConfigureAwait(false);
|
|
}
|
|
if (previous != null)
|
|
{
|
|
await previous(context, node, changes, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lets the manager's existing node-attachment ledger own externally bound resources.
|
|
/// </summary>
|
|
private sealed class ExternalBindingLifetime : IAsyncDisposable
|
|
{
|
|
/// <summary>
|
|
/// Creates the lifetime for one staged machine.
|
|
/// </summary>
|
|
public ExternalBindingLifetime(MachineryBuildScope scope)
|
|
{
|
|
m_scope = scope;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask DisposeAsync()
|
|
{
|
|
return m_scope.ReleaseExternalAsync();
|
|
}
|
|
|
|
private readonly MachineryBuildScope m_scope;
|
|
}
|
|
|
|
private readonly List<MachineryNodeBuilder> m_builders = [];
|
|
private readonly List<MachineryFacet> m_facets = [];
|
|
private readonly IMachineryBuildCoordinator m_buildCoordinator;
|
|
private IDisposable? m_rootBrowseNameReservation;
|
|
private bool m_forwardAdded;
|
|
private bool m_inverseAdded;
|
|
private int m_externalDisposed;
|
|
private NodeStateChangedAsyncHandler? m_previousStateChanged;
|
|
private NodeStateChangedAsyncHandler? m_deleteHandler;
|
|
private bool m_registering;
|
|
private bool m_aborted;
|
|
}
|
|
}
|