/* ======================================================================== * 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.Globalization; using System.Threading; namespace Opc.Ua.Server.NodeManager { /// /// Helpers for the NodeVersion property defined in OPC UA /// Part 5 ยง9.32.2. The spec requires that only nodes carrying a /// NodeVersion property may trigger a ModelChangeEvent, /// that every emission bumps the property, and that any write to /// the property fires a model-change event. /// public static class NodeStateModelChangeExtensions { /// /// AsyncLocal guard set by so that /// the OnWriteValue handler installed by /// does not re-enter and /// double-fire a model-change event for framework-driven bumps. /// private static readonly AsyncLocal s_isFrameworkBump = new(); /// /// True when execution is inside a framework-driven NodeVersion /// bump (the framework already emitted the appropriate /// GeneralModelChangeEvent and the OnWriteValue hook must /// not echo it as a BaseModelChangeEvent). /// public static bool IsInsideFrameworkNodeVersionBump => s_isFrameworkBump.Value; /// /// Returns true when has a child /// property with BrowseName == BrowseNames.NodeVersion. /// public static bool HasNodeVersion(this NodeState node) { return GetNodeVersionProperty(node) != null; } /// /// Returns the NodeVersion property of /// if present, otherwise null. /// public static PropertyState? GetNodeVersionProperty(this NodeState node) { if (node == null) { return null; } BaseInstanceState? child = node.FindChild(null!, new QualifiedName(BrowseNames.NodeVersion)); return child as PropertyState; } /// /// Increments the NodeVersion of . /// Parses the current value as (treating /// missing / non-numeric as 0), increments, and writes back as /// the decimal string. No-op when the node has no /// NodeVersion property. /// /// /// The framework calls this from /// AsyncCustomNodeManager.EmitModelChange after recording /// a change for the node. The internal AsyncLocal flag /// is set while /// the underlying write executes so the OnWriteValue hook /// installed by does not /// echo the change as a BaseModelChangeEvent. /// public static void BumpNodeVersion(this NodeState node, ISystemContext context) { if (node == null) { throw new ArgumentNullException(nameof(node)); } PropertyState? nodeVersion = GetNodeVersionProperty(node); if (nodeVersion == null) { return; } ulong current = 0; if (!string.IsNullOrEmpty(nodeVersion.Value) && !ulong.TryParse(nodeVersion.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out current)) { current = 0; } unchecked { current++; if (current == 0) { current = 1; } } bool previous = s_isFrameworkBump.Value; s_isFrameworkBump.Value = true; try { nodeVersion.Value = current.ToString(CultureInfo.InvariantCulture); nodeVersion.Timestamp = DateTime.UtcNow; nodeVersion.StatusCode = StatusCodes.Good; nodeVersion.ClearChangeMasks(context, false); } finally { s_isFrameworkBump.Value = previous; } } /// /// Marks as eligible to trigger /// ModelChangeEvents by attaching a NodeVersion /// property in namespace if /// none is present, and wiring an OnWriteValue handler /// that posts a BaseModelChangeEvent through /// whenever the /// property is written by an external client. Framework-driven /// bumps (see ) are suppressed via /// an AsyncLocal guard so they do not echo. /// /// The node to enable tracking on. /// /// Namespace index for the new NodeVersion property when one is /// attached. /// /// /// Callback invoked when an external client writes the /// NodeVersion. Typically wired to /// AsyncCustomNodeManager.RaiseBaseModelChangeEvent. /// /// /// The existing or newly attached NodeVersion property. Always /// non-null. /// public static PropertyState EnableModelChangeTracking( this NodeState node, ushort namespaceIndex, Action? raiseBaseModelChangeEvent = null) { if (node == null) { throw new ArgumentNullException(nameof(node)); } PropertyState? nodeVersion = GetNodeVersionProperty(node); if (nodeVersion == null) { nodeVersion = new PropertyState.Implementation(node) { SymbolicName = BrowseNames.NodeVersion, BrowseName = new QualifiedName(BrowseNames.NodeVersion, 0), DisplayName = new LocalizedText(BrowseNames.NodeVersion), DataType = DataTypeIds.String, ValueRank = ValueRanks.Scalar, AccessLevel = AccessLevels.CurrentReadOrWrite, UserAccessLevel = AccessLevels.CurrentReadOrWrite, Value = "1", ReferenceTypeId = ReferenceTypeIds.HasProperty, TypeDefinitionId = VariableTypeIds.PropertyType, NodeId = new NodeId(Guid.NewGuid(), namespaceIndex) }; node.AddChild(nodeVersion); } if (raiseBaseModelChangeEvent != null) { NodeState owner = node; nodeVersion.OnWriteValue = ( ctx, target, indexRange, dataEncoding, ref value, ref statusCode, ref timestamp) => { if (target is PropertyState property && value.TryGetValue(out string? newValue)) { property.Value = newValue; } if (!IsInsideFrameworkNodeVersionBump) { raiseBaseModelChangeEvent(ctx, owner); } return ServiceResult.Good; }; } return nodeVersion; } } }