fix(opcua): shelve declaration ids from a fixed set, not an address-space lookup; pin instance-id + state-machine-id shelve calls (#629)

New() ran FindNodeInAddressSpace under Lock, taking other node managers' locks (lock-order
risk); the three AlarmConditionType_ShelvingState_* ids are now a static set. Adds wire tests:
TimedShelve/Unshelve by the condition's own instance method ids (object = condition or its
ShelvingState) succeed and report, and a ShelvedStateMachineType_* id call is pinned as
BadMethodInvalid with a positive control.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 15:01:54 -04:00
parent 1de8357d13
commit 66c47c3ddd
3 changed files with 111 additions and 12 deletions
@@ -2499,6 +2499,20 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
_ => new AlarmConditionState(parent),
};
/// <summary>
/// Gitea #629 — the <c>AlarmConditionType_ShelvingState_*</c> method instance declarations a client calls
/// the shelve methods by. <see cref="New"/> records them as the methods' <c>MethodDeclarationId</c>
/// (the SDK's own is the <c>ShelvedStateMachineType</c>'s), so calls by these ids keep resolving after
/// the methods move to instance-scoped NodeIds. Calls by the <c>ShelvedStateMachineType_*</c> ids are
/// not supported (pinned by <c>ConditionChildNodeIdTests</c>).
/// </summary>
private static readonly HashSet<NodeId> ShelvingMethodDeclarationIds =
[
MethodIds.AlarmConditionType_ShelvingState_TimedShelve,
MethodIds.AlarmConditionType_ShelvingState_OneShotShelve,
MethodIds.AlarmConditionType_ShelvingState_Unshelve,
];
/// <summary>
/// Gitea #629 — the <see cref="INodeIdFactory"/> hook <c>NodeState.Create(…, assignNodeIds: true)</c> calls
/// for the node being created and then, top-down, for every child it built from the type's definition.
@@ -2543,15 +2557,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
// already, over an SDK-generated ns=0 NodeId. The ShelvingState methods do NOT: their declaration id
// is the ShelvedStateMachineType's (i=2948), and they were found only because their NodeId WAS the
// AlarmConditionType instance declaration every client uses. Adopt that id as the declaration before
// it is replaced — but only when it is a real node of the standard address space, never an
// SDK-generated number — or every shelve/unshelve call fails BadMethodInvalid.
if (node is MethodState method
&& method.NodeId is { NamespaceIndex: 0 } declarationId
&& declarationId != method.MethodDeclarationId
&& FindNodeInAddressSpace(declarationId) is MethodState)
{
method.MethodDeclarationId = declarationId;
}
// it is replaced, or every shelve/unshelve call fails BadMethodInvalid. A fixed set, deliberately: a
// lookup in the standard address space would take other node managers' locks while we hold Lock.
if (node is MethodState method && ShelvingMethodDeclarationIds.Contains(method.NodeId))
method.MethodDeclarationId = method.NodeId;
return new NodeId($"{parentId.Identifier}/{node.BrowseName.Name}", parentId.NamespaceIndex);
}