diff --git a/scripts/validation/opcua-lifecycle-memory/baseline-canonical/BaselineMetrics.cs b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/BaselineMetrics.cs new file mode 100644 index 00000000..a35513f2 --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/BaselineMetrics.cs @@ -0,0 +1,144 @@ +using System.Globalization; +using System.Collections; +using System.Diagnostics; +using System.Reflection; +using Opc.Ua; +using Opc.Ua.Server; +using Shouldly; +internal static class BaselineMetrics +{ + private static FieldInfo Field(object owner, string name) + { + for (var type = owner.GetType(); type is not null; type = type.BaseType) + if (type.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly) is { } member) return member; + throw new InvalidOperationException("Missing exact-.182 diagnostic field: " + name); + } + private static T Required(object owner, string name) => Field(owner, name).GetValue(owner) is T value ? value + : throw new InvalidOperationException("Unexpected exact-.182 diagnostic field type: " + name); + internal static (int Browse, int History) Points(ISession session) + { + using (Required(session, "m_lock").EnterScope()) + return (Count(session, "m_browseContinuationPoints"), Count(session, "m_historyContinuationPoints")); + } + private static int Count(object owner, string name) => Field(owner, name).GetValue(owner) switch + { null => 0, ICollection values => values.Count, _ => throw new InvalidOperationException("Unexpected continuation collection type") }; + internal static int CachedStoreReferences(CertificateValidator validator) + { + var gate = Required(validator, "m_semaphore"); + if (!gate.Wait(TimeSpan.FromSeconds(5))) throw new TimeoutException("Original validator diagnostic lock did not settle"); + try + { + return new[] { "m_trustedCertificateStore", "m_issuerCertificateStore", "m_rejectedCertificateStore" } + .Select(name => Field(validator, name).GetValue(validator)) + .Count(value => value switch + { + null => false, + CertificateStoreIdentifier store => Field(store, "m_store").GetValue(store) switch + { null => false, ICertificateStore => true, _ => throw new InvalidOperationException("Original cached store type changed") }, + _ => throw new InvalidOperationException("Original validator store identifier type changed") + }); + } + finally { gate.Release(); } + } + internal static int Counter(object manager, string name) + { + var property = manager.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("Missing original node-manager diagnostic property: " + name); + return property.PropertyType == typeof(int) && property.GetValue(manager) is int value ? value + : throw new InvalidOperationException("Unexpected original node-manager diagnostic property type: " + name); + } + internal static async Task<(int Total, int Pki)> DescriptorsAsync(CancellationToken ct, params string[] roots) + { + ct.ThrowIfCancellationRequested(); + var canonicalRoots = roots.Select(root => CanonicalPath(root)).Distinct(StringComparer.Ordinal).ToArray(); + if (OperatingSystem.IsLinux()) + { + var targets = Directory.EnumerateFileSystemEntries("/proc/self/fd") + .Select(path => new FileInfo(path).LinkTarget).ToArray(); + return (targets.Length, targets.Count(path => IsWithinRoots(path, canonicalRoots))); + } + if (OperatingSystem.IsMacOS()) + { + using var process = new Process { StartInfo = new ProcessStartInfo("/usr/sbin/lsof") + { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false } }; + foreach (var arg in new[] { "-a", "-p", Environment.ProcessId.ToString(CultureInfo.InvariantCulture), "-Fnf" }) + process.StartInfo.ArgumentList.Add(arg); + var text = await ReadDescriptorProbeAsync(process, TimeSpan.FromSeconds(5), ct); + process.ExitCode.ShouldBe(0); + var total = 0; + var pki = 0; + var descriptor = false; + foreach (var line in text.Split('\n')) + { + if (line.StartsWith('f')) + { + descriptor = line.Length > 1 && char.IsDigit(line[1]); + if (descriptor) total++; + } + else if (descriptor && line.StartsWith('n') && IsWithinRoots(line[1..], canonicalRoots)) pki++; + } + return (total, pki); + } + throw new PlatformNotSupportedException("This soak requires Linux procfs or macOS lsof for actual descriptor evidence."); + } + + private static async Task ReadDescriptorProbeAsync(Process process, TimeSpan timeout, CancellationToken ct) + { + using var deadline = CancellationTokenSource.CreateLinkedTokenSource(ct); + deadline.CancelAfter(timeout); + process.Start(); + var stdout = process.StandardOutput.ReadToEndAsync(deadline.Token); + var stderr = process.StandardError.ReadToEndAsync(deadline.Token); + var exited = process.WaitForExitAsync(deadline.Token); + try + { + // Waiting for exit alone does not bound reads: both redirected pipes must also finish. + await Task.WhenAll(stdout, stderr, exited).WaitAsync(deadline.Token).ConfigureAwait(false); + return await stdout.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + try { if (!process.HasExited) process.Kill(entireProcessTree: true); } + catch (InvalidOperationException) when (process.HasExited) { /* It exited between the check and kill. */ } + await process.WaitForExitAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false); + // Observe both cancelled drains; never leave a pipe read running after returning. + try { await Task.WhenAll(stdout, stderr).WaitAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false); } + catch (OperationCanceledException) { } + if (ct.IsCancellationRequested) throw; + throw new TimeoutException("Descriptor probe exceeded its process and redirected-stream deadline."); + } + } + + private static bool IsWithinRoots(string? path, IReadOnlyList roots) + { + if (path is null || !Path.IsPathFullyQualified(path)) return false; + // procfs marks an unlinked but still open file; its containing physical directory still counts. + const string deleted = " (deleted)"; + if (path.EndsWith(deleted, StringComparison.Ordinal)) path = path[..^deleted.Length]; + var canonical = CanonicalPath(path); + return roots.Any(root => canonical.Equals(root, StringComparison.Ordinal) + || canonical.StartsWith(Path.EndsInDirectorySeparator(root) ? root : root + Path.DirectorySeparatorChar, + StringComparison.Ordinal)); + } + + private static string CanonicalPath(string path, int depth = 0) + { + if (depth > 32) throw new IOException("Descriptor path link resolution exceeded its bound."); + var full = Path.GetFullPath(path); + var current = Path.GetPathRoot(full)!; + foreach (var component in full[current.Length..].Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)) + { + current = Path.Combine(current, component); + FileSystemInfo entry = Directory.Exists(current) ? new DirectoryInfo(current) : new FileInfo(current); + if (entry.LinkTarget is not null) + { + var target = entry.ResolveLinkTarget(returnFinalTarget: true) + ?? throw new IOException("Descriptor path link resolution returned no target."); + // Resolve parent links too: an alias target can itself begin with /var or /tmp. + current = CanonicalPath(target.FullName, depth + 1); + } + } + return Path.TrimEndingDirectorySeparator(current); + } + +} diff --git a/scripts/validation/opcua-lifecycle-memory/baseline-canonical/BaselineRig.cs b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/BaselineRig.cs new file mode 100644 index 00000000..3f2974fa --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/BaselineRig.cs @@ -0,0 +1,102 @@ +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using Microsoft.Extensions.Logging.Abstractions; +using Opc.Ua; +using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; +using ZB.MOM.WW.OtOpcUa.Configuration.Entities; +using ZB.MOM.WW.OtOpcUa.Configuration.Enums; +using ZB.MOM.WW.OtOpcUa.Core.Authorization; +using ZB.MOM.WW.OtOpcUa.OpcUaServer; +using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security; + +internal sealed class BaselineRig : IAsyncDisposable +{ + internal const string Cluster = "SITE-A", RawDeviceFolder = "Plant/Modbus/dev1", RawAlarmPath = "Plant/Modbus/dev1/temp_hi"; + internal const string EqA = "EqA", EqB = "EqB", KeyPrincipal = "event-probe", KeyId = "k-server-events"; + internal static string KeyPassword => Environment.GetEnvironmentVariable("OTOPCUA_BASELINE_CANONICAL_KEY") + ?? throw new InvalidOperationException("An ephemeral fixture credential is required"); + private readonly OpcUaApplicationHostOptions _options; + private OpcUaApplicationHost _host = null!; + internal OtOpcUaSdkServer Server { get; private set; } = null!; + internal OtOpcUaNodeManager Nm => Server.NodeManager!; + internal SdkAddressSpaceSink Sink { get; private set; } = null!; + internal string EndpointUrl => $"opc.tcp://127.0.0.1:{_options.OpcUaPort}/OtOpcUa"; + internal string PkiRoot { get; } + internal CertificateValidator Validator + { + get + { + var applicationField = typeof(OpcUaApplicationHost).GetField("_application", BindingFlags.NonPublic | BindingFlags.Instance) + ?? throw new InvalidOperationException("Original host application field is missing"); + return applicationField.GetValue(_host) is Opc.Ua.Configuration.ApplicationInstance application + && application.ApplicationConfiguration.CertificateValidator is CertificateValidator validator ? validator + : throw new InvalidOperationException("Original host application field type changed"); + } + } + internal BaselineRig(string directory) + { + PkiRoot = Path.Combine(directory, "server-pki"); + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + _options = new OpcUaApplicationHostOptions + { + ApplicationName = "OtOpcUa.SDK2LifecycleSoak", ApplicationUri = $"urn:OtOpcUa.SDK2LifecycleSoak.{Guid.NewGuid():N}", + OpcUaPort = ((IPEndPoint)listener.LocalEndpoint).Port, PublicHostname = "127.0.0.1", PkiStoreRoot = PkiRoot, + EnabledSecurityProfiles = [OpcUaSecurityProfile.None, OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt], + AutoAcceptUntrustedClientCertificates = true, + }; + } + internal async Task BootAsync(CancellationToken ct) + { + Server = new OtOpcUaSdkServer(); + _host = new OpcUaApplicationHost(_options, NullLogger.Instance, new Auth()); + await _host.StartAsync(Server, ct); + Canonical.Ledger.Record("host-start"); + Sink = new SdkAddressSpaceSink(Nm); + } + internal async Task RestartAsync(CancellationToken ct) + { + await _host.DisposeAsync(); + Canonical.Ledger.Record("host-stop"); + await BootAsync(ct); + } + internal DataPlaneAuthorityState Enforce(bool enabled, ApiKey[] keys, params NodeAcl[] grants) + { + var state = new DataPlaneAuthorityState(); + state.Apply(Cluster, "rev-1", keys, grants, new Dictionary> + { + [new AclScopeRef(NodeAclScopeKind.Equipment, "eq-" + EqA)] = [new NodeAclPath([NodeAclRealms.Uns, EqA])], + [new AclScopeRef(NodeAclScopeKind.Equipment, "eq-" + EqB)] = [new NodeAclPath([NodeAclRealms.Uns, EqB])] + }); + Nm.DataPlaneAuthority = state; + Nm.Authorizer = new DataPlaneAuthorizer(state, new TriePermissionEvaluator(state.Tries), enabled, NullLogger.Instance, TimeProvider.System); + Canonical.Ledger.Record("authority-restore"); + return state; + } + internal static ApiKey Key() => new() { ApiKeyId = KeyId, Name = KeyId, SecretHash = "hash", Principal = KeyPrincipal, + Enabled = true, CreatedUtc = DateTime.UtcNow.AddDays(-1) }; + internal static NodeAcl ClusterGrant(string principal, string id, NodePermissions permissions) => new() + { NodeAclId = id, ClusterId = Cluster, Principal = principal, ScopeKind = NodeAclScopeKind.Cluster, PermissionFlags = permissions }; + internal void MaterialiseNative() + { + Sink.EnsureFolder(RawDeviceFolder, null, "dev1", AddressSpaceRealm.Raw); + Sink.MaterialiseAlarmCondition(RawAlarmPath, RawDeviceFolder, "temp_hi", "OffNormalAlarm", 700, AddressSpaceRealm.Raw, isNative: true); + Sink.EnsureFolder(EqA, null, "A", AddressSpaceRealm.Uns); + Sink.EnsureFolder(EqB, null, "B", AddressSpaceRealm.Uns); + Sink.WireAlarmNotifiers(RawAlarmPath, AddressSpaceRealm.Raw, [EqA, EqB], AddressSpaceRealm.Uns); + } + public async ValueTask DisposeAsync() + { + await _host.DisposeAsync(); + Canonical.Ledger.Record("host-stop"); + if (Directory.Exists(PkiRoot)) Directory.Delete(PkiRoot, true); + } + private sealed class Auth : IOpcUaUserAuthenticator + { + public Task AuthenticateUserNameAsync(string username, string password, CancellationToken ct) => + Task.FromResult(username == "apikey" && password.StartsWith($"otua_{KeyId}_", StringComparison.Ordinal) + ? OpcUaUserAuthResult.AllowKey(KeyPrincipal, [OpcUaDataPlaneRoles.AlarmAck, OpcUaDataPlaneRoles.WriteOperate], KeyId) + : OpcUaUserAuthResult.Deny("Invalid fixture key")); + } +} diff --git a/scripts/validation/opcua-lifecycle-memory/baseline-canonical/CanonicalOperations.cs b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/CanonicalOperations.cs new file mode 100644 index 00000000..ec65770b --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/CanonicalOperations.cs @@ -0,0 +1,252 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Text; +using Opc.Ua; +using Opc.Ua.Client; +using Opc.Ua.Configuration; +using Shouldly; +using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; +using ZB.MOM.WW.OtOpcUa.Configuration.Enums; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using ZB.MOM.WW.OtOpcUa.Core.Authorization; +using ClientSession = Opc.Ua.Client.ISession; +using Subscription = Opc.Ua.Client.Subscription; +using MonitoredItem = Opc.Ua.Client.MonitoredItem; +internal static partial class Canonical +{ + private static void Configure(BaselineRig rig) + { + Ledger.Record("configure"); + rig.MaterialiseNative(); + rig.Nm.HistorianDataSource = new SqlLikeEventSource(Enumerable.Range(0, 40) + .Select(i => new HistoricalEvent($"soak-{i}", "source", Epoch.AddSeconds(i), Epoch, "soak", 500)).ToList()); + rig.Nm.NodeWriteGateway = new AcceptWrites(); + MaterialiseValues(rig); + rig.Enforce(true, [BaselineRig.Key()], Grant()); + } + + private static ZB.MOM.WW.OtOpcUa.Configuration.Entities.NodeAcl Grant() => BaselineRig.ClusterGrant( + BaselineRig.KeyPrincipal, "soak-grant", NodePermissions.Browse | NodePermissions.Read | NodePermissions.Subscribe + | NodePermissions.WriteOperate | NodePermissions.HistoryRead); + + private static void MaterialiseValues(BaselineRig rig) + { + rig.Sink.EnsureFolder(BaselineRig.RawDeviceFolder, null, "Device", AddressSpaceRealm.Raw); + rig.Sink.EnsureVariable(Scalar, BaselineRig.RawDeviceFolder, "Scalar", "Int32", true, AddressSpaceRealm.Raw); + rig.Sink.EnsureVariable(ArrayTag, BaselineRig.RawDeviceFolder, "Array", "Int16", true, AddressSpaceRealm.Raw, + isArray: true, arrayLength: 4); + rig.Sink.EnsureFolder("SoakEquipment", null, "Equipment", AddressSpaceRealm.Uns); + rig.Sink.EnsureVariable(UnsScalar, "SoakEquipment", "Scalar", "Int32", true, AddressSpaceRealm.Uns); + for (var i = 0; i < 16; i++) rig.Sink.EnsureVariable($"{BaselineRig.RawDeviceFolder}/Browse{i}", + BaselineRig.RawDeviceFolder, $"Browse{i}", "Int32", false, AddressSpaceRealm.Raw); + rig.Sink.WriteValue(Scalar, 0, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Raw); + rig.Sink.WriteValue(ArrayTag, new short[] { 0, 1, 2, 3 }, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Raw); + rig.Sink.WriteValue(UnsScalar, 0, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns); + } + + private static async Task ExerciseAsync(BaselineRig rig, ApplicationConfiguration config, int cycle, + StreamWriter evidence, CancellationToken ct) + { + await using var proxy = new PauseProxy(new Uri(rig.EndpointUrl).Port); + using var owner = await ConnectAsync(rig, config, proxy.Port, ct); + using var other = await ConnectAsync(rig, config, null, ct); + var ownedSessions = rig.Server.CurrentInstance.SessionManager.GetSessions().ToArray(); + ownedSessions.Length.ShouldBe(2); + var scalar = Raw(owner, Scalar); + var array = Raw(owner, ArrayTag); + var unsNs = owner.NamespaceUris.GetIndex(V3NodeIds.UnsNamespaceUri); + unsNs.ShouldBeGreaterThan(0); + var uns = new NodeId(UnsScalar, (ushort)unsNs); + owner.NamespaceUris.GetString(scalar.NamespaceIndex).ShouldBe(V3NodeIds.RawNamespaceUri); + owner.NamespaceUris.GetString(uns.NamespaceIndex).ShouldBe(V3NodeIds.UnsNamespaceUri); + using var subscription = new Subscription(owner.DefaultSubscription) { PublishingInterval = 100, KeepAliveCount = 10 }; + owner.AddSubscription(subscription); + try + { + await Ledger.Call("subscription-create", () => subscription.CreateAsync(ct)); + var seen = new ConcurrentDictionary(); + foreach (var id in new[] { scalar, array, uns }) + { + var item = new MonitoredItem(subscription.DefaultItem) { StartNodeId = id, AttributeId = Attributes.Value, + SamplingInterval = 50, QueueSize = 10 }; + item.Notification += (sender, _) => + { + foreach (var value in sender.DequeueValues()) if (StatusCode.IsGood(value.StatusCode)) seen[id] = value.WrappedValue; + }; + subscription.AddItem(item); + } + await Ledger.Call("subscription-apply", () => subscription.ApplyChangesAsync(ct)); + subscription.MonitoredItems.ShouldAllBe(item => ServiceResult.IsGood(item.Status.Error)); + PublicCounts(rig).ShouldBe((2, 1, 3)); + var values = new short[] { (short)(cycle % 30000), 2, 3, 4 }; + var writes = await Ledger.Call("write", () => owner.WriteAsync(null, new[] + { + new WriteValue { NodeId = scalar, AttributeId = Attributes.Value, Value = new DataValue(new Variant(cycle)) }, + new WriteValue { NodeId = array, AttributeId = Attributes.Value, Value = new DataValue(new Variant(values)) }, + new WriteValue { NodeId = uns, AttributeId = Attributes.Value, Value = new DataValue(new Variant(cycle)) }, + }, ct)); + writes.Results.ToList().ShouldAllBe(status => status == StatusCodes.Good); + (await Ledger.Call("read-value", () => owner.ReadValueAsync(scalar, ct))).Value.ShouldBeOfType().ShouldBe(cycle); + (await Ledger.Call("read-value", () => owner.ReadValueAsync(array, ct))).Value.ShouldBeOfType().ShouldBe(values); + (await Ledger.Call("read-value", () => owner.ReadValueAsync(uns, ct))).Value.ShouldBeOfType().ShouldBe(cycle); + await EventuallyAsync(() => seen.TryGetValue(scalar, out var value) && ScalarValue(value) == cycle + && seen.ContainsKey(array) && seen.TryGetValue(uns, out var mirror) && ScalarValue(mirror) == cycle, ct); + + // One explicitly released and one deliberately abandoned browse point. + var browse = await BrowseAsync(owner, ct); + RequirePoint(browse); + (await Ledger.Call("browse-next", () => owner.BrowseNextAsync(null, true, [browse], ct))).Results[0].StatusCode.ShouldBe(StatusCodes.Good); + RequirePoint(await BrowseAsync(owner, ct)); + // Production history points: cross-session denial, single-use resume, explicit release and close cleanup. + var first = (await PageAsync(owner, default, false, ct)).ContinuationPoint; + RequirePoint(first); + (await PageAsync(other, first, false, ct)).StatusCode.ShouldBe(StatusCodes.BadContinuationPointInvalid); + var next = (await PageAsync(owner, first, false, ct)).ContinuationPoint; + RequirePoint(next); + (await PageAsync(owner, next, true, ct)).StatusCode.ShouldBe(StatusCodes.Good); + for (var i = 0; i < 6; i++) RequirePoint((await PageAsync(owner, default, false, ct)).ContinuationPoint); + var held = ownedSessions.Select(ContinuationCounts).ToArray(); + held.Sum(count => count.Browse).ShouldBeGreaterThan(0); + held.Sum(count => count.History).ShouldBeGreaterThan(0); + var active = PublicCounts(rig); + await LogAsync(evidence, new { kind = "active", cycle, sessions = active.Sessions, + subscriptions = active.Subscriptions, items = active.Items, browsePoints = held.Sum(x => x.Browse), historyPoints = held.Sum(x => x.History) }); + + rig.Nm.UpdateTagAttributes(Scalar, false, null, "Int32", false, null, AddressSpaceRealm.Raw).ShouldBeTrue(); + Ledger.Record("writable-off"); + rig.Nm.UpdateTagAttributes(Scalar, true, null, "Int32", false, null, AddressSpaceRealm.Raw).ShouldBeTrue(); + Ledger.Record("writable-on"); + rig.Nm.RemoveVariableNode(ArrayTag, AddressSpaceRealm.Raw).ShouldBeTrue(); + Ledger.Record("variable-remove"); + rig.Sink.EnsureVariable(ArrayTag, BaselineRig.RawDeviceFolder, "Array", "Int16", true, AddressSpaceRealm.Raw, + isArray: true, arrayLength: 4); + rig.Sink.WriteValue(ArrayTag, values, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Raw); + var authority = rig.Nm.DataPlaneAuthority.ShouldBeOfType(); + Ledger.Record("key-revoke"); + authority.Apply(BaselineRig.Cluster, $"revoked-{cycle}", [], [Grant()], + new Dictionary>()); + var denied = await Ledger.Call("read-service", () => owner.ReadAsync(null, 0, TimestampsToReturn.Neither, + [new ReadValueId { NodeId = scalar, AttributeId = Attributes.Value }], ct)); + denied.Results[0].StatusCode.ShouldBe(StatusCodes.BadUserAccessDenied); + rig.Enforce(true, [BaselineRig.Key()], Grant()); + + proxy.Pause(); + Ledger.Record("network-pause"); + var stalled = Stopwatch.StartNew(); + string stalledStatus; + try + { + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); + timeout.CancelAfter(TimeSpan.FromSeconds(1)); + await Ledger.Call("read-value", () => owner.ReadValueAsync(scalar, timeout.Token)).WaitAsync(TimeSpan.FromSeconds(5), ct); + throw new InvalidOperationException("The deliberately paused network unexpectedly completed the read."); + } + catch (OperationCanceledException) when (!ct.IsCancellationRequested) { stalledStatus = "OperationCanceled"; } + // Exact-tag ChannelAsyncOperation maps a canceled completion waiter to BadRequestInterrupted. + catch (ServiceResultException error) when (error.StatusCode == StatusCodes.BadTimeout + || error.StatusCode == StatusCodes.BadRequestCancelledByClient || error.StatusCode == StatusCodes.BadRequestTimeout + || error.StatusCode == StatusCodes.BadRequestInterrupted) { stalledStatus = error.StatusCode.ToString(); } + finally { proxy.Resume(); } + (await Ledger.Call("read-value", () => owner.ReadValueAsync(scalar, ct))).StatusCode.ShouldBe(StatusCodes.Good); + await LogAsync(evidence, new { kind = "network-stall-recovered", cycle, status = stalledStatus, seconds = stalled.Elapsed.TotalSeconds }); + await Ledger.Call("subscription-delete", () => subscription.DeleteAsync(true, ct)); + await Ledger.Call("subscription-remove", () => owner.RemoveSubscriptionAsync(subscription, ct)); + rig.Sink.RebuildAddressSpace(); + Ledger.Record("address-space-rebuild"); + Configure(rig); + Ledger.Record("cycle-wire-body-completed"); + } + finally + { + proxy.Resume(); + await Ledger.Call("session-close", () => owner.CloseAsync(ct)).WaitAsync(TimeSpan.FromSeconds(10), ct); + await Ledger.Call("session-close", () => other.CloseAsync(ct)).WaitAsync(TimeSpan.FromSeconds(10), ct); + foreach (var closed in ownedSessions) + { + var before = ContinuationCounts(closed); + if (before != (0, 0)) + { + await LogAsync(evidence, new { kind = "closed-session-observation", cycle, + activeSessions = rig.Server.CurrentInstance.SessionManager.GetSessions().Count, + browsePoints = before.Browse, historyPoints = before.History, diagnosticReferenceHeld = true }); + await Task.Delay(250, ct); // Read-only failure investigation; never turns the failed guard into a pass. + var after = ContinuationCounts(closed); + await LogAsync(evidence, new { kind = "closed-session-after-fixed-observation", cycle, + observationMilliseconds = 250, browsePoints = after.Browse, historyPoints = after.History }); + } + before.ShouldBe((0, 0)); + } + } + } + + private static async Task RestartWithActiveResourcesAsync(BaselineRig rig, ApplicationConfiguration config, + CancellationToken ct) + { + using var client = await ConnectAsync(rig, config, null, ct); + using var subscription = new Subscription(client.DefaultSubscription) { PublishingInterval = 100 }; + client.AddSubscription(subscription); + await Ledger.Call("subscription-create", () => subscription.CreateAsync(ct)); + subscription.AddItem(new MonitoredItem(subscription.DefaultItem) + { + StartNodeId = Raw(client, Scalar), AttributeId = Attributes.Value, SamplingInterval = 50, + }); + await Ledger.Call("subscription-apply", () => subscription.ApplyChangesAsync(ct)); + PublicCounts(rig).ShouldBe((1, 1, 1)); + RequirePoint(await BrowseAsync(client, ct)); + RequirePoint((await PageAsync(client, default, false, ct)).ContinuationPoint); + var oldInternal = rig.Server.CurrentInstance; + var oldSessionManager = oldInternal.SessionManager; + var oldSubscriptionManager = oldInternal.SubscriptionManager; + var oldSessions = oldSessionManager.GetSessions().ToArray(); + var oldSubscriptions = oldSubscriptionManager.GetSubscriptions().ToArray(); + var oldValidator = rig.Validator; + var oldStores = BaselineMetrics.CachedStoreReferences(oldValidator); + await rig.RestartAsync(ct).WaitAsync(TimeSpan.FromSeconds(20), ct); + oldSessionManager.GetSessions().ShouldBeEmpty(); + oldSubscriptionManager.GetSubscriptions().ShouldBeEmpty(); + oldSubscriptions.ShouldAllBe(item => item.MonitoredItemCount == 0); + foreach (var session in oldSessions) ContinuationCounts(session).ShouldBe((0, 0)); + await LogAsync(CurrentEvidence, new { kind = "original-validator-after-stop", cachedStoreReferencesBefore = oldStores, + cachedStoreReferencesAfter = BaselineMetrics.CachedStoreReferences(oldValidator), + disposalFlagObservable = false, reason = "SDK1.5 has no CertificateManager disposal API or disposed flag" }); + PublicCounts(rig).ShouldBe((0, 0, 0)); + } + + private static async Task ConnectAsync(BaselineRig rig, ApplicationConfiguration config, + int? proxyPort, CancellationToken ct) + { + var telemetry = DefaultTelemetry.Create(_ => { }); + var endpoint = await CoreClientUtils.SelectEndpointAsync(config, rig.EndpointUrl, true, telemetry, ct) + ?? throw new InvalidOperationException("No encrypted endpoint."); + endpoint.SecurityMode.ShouldBe(MessageSecurityMode.SignAndEncrypt); + if (proxyPort is { } port) endpoint.EndpointUrl = $"opc.tcp://127.0.0.1:{port}/OtOpcUa"; + return await Ledger.Call("session-create", () => new DefaultSessionFactory(telemetry).CreateAsync(config, + new ConfiguredEndpoint(null, endpoint, EndpointConfiguration.Create(config)), false, false, + "SDK2LifecycleSoak", 15_000, new UserIdentity("apikey", Encoding.UTF8.GetBytes(BaselineRig.KeyPassword)), default, ct)); + } + + private static NodeId Raw(ClientSession session, string id) + { + var ns = session.NamespaceUris.GetIndex(V3NodeIds.RawNamespaceUri); + ns.ShouldBeGreaterThan(0); + return new NodeId(id, (ushort)ns); + } + + private static async Task BrowseAsync(ClientSession session, CancellationToken ct) => + (await Ledger.Call("browse", () => session.BrowseAsync(null, null, 1, [new BrowseDescription + { + NodeId = Raw(session, BaselineRig.RawDeviceFolder), BrowseDirection = BrowseDirection.Forward, + ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, IncludeSubtypes = true, + ResultMask = (uint)BrowseResultMask.All, + }], ct))).Results[0].ContinuationPoint; + + private static async Task PageAsync(ClientSession session, byte[]? point, + bool release, CancellationToken ct) => (await Ledger.Call("history-read", () => session.HistoryReadAsync(null, new ExtensionObject(new ReadEventDetails + { + StartTime = Epoch.AddDays(-1), EndTime = Epoch.AddDays(1), NumValuesPerNode = 4, + Filter = new EventFilter { SelectClauses = [new SimpleAttributeOperand(ObjectTypeIds.BaseEventType, + new QualifiedName(BrowseNames.EventId))] }, + }), TimestampsToReturn.Both, release, + [new HistoryReadValueId { NodeId = Raw(session, BaselineRig.RawDeviceFolder), ContinuationPoint = point }], ct))).Results[0]; + +} diff --git a/scripts/validation/opcua-lifecycle-memory/baseline-canonical/Inventory.cs b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/Inventory.cs new file mode 100644 index 00000000..921f7a8b --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/Inventory.cs @@ -0,0 +1,19 @@ +using System.Diagnostics; +using System.Reflection; +using System.Text.Json; +using Opc.Ua; +using Opc.Ua.Server; +using ZB.MOM.WW.OtOpcUa.OpcUaServer; +var types = new[] { typeof(Session), typeof(CertificateValidator), typeof(CertificateStoreIdentifier), + typeof(Opc.Ua.Configuration.ApplicationInstance), typeof(OtOpcUaSdkServer), typeof(OpcUaApplicationHost), typeof(OtOpcUaNodeManager) }; +Console.WriteLine(JsonSerializer.Serialize(new +{ + sdk = typeof(NodeId).Assembly.GetCustomAttribute()?.InformationalVersion, + sdkFile = FileVersionInfo.GetVersionInfo(typeof(NodeId).Assembly.Location).FileVersion, + modules = types.Select(t => new { type = t.FullName, assembly = t.Assembly.GetName().Name, mvid = t.Module.ModuleVersionId, + interfaces = t.GetInterfaces().Select(x => x.FullName), + fields = t.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .Select(f => new { name = f.Name, type = f.FieldType.FullName }), + lifecycle = t.GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(m => m.Name.Contains("Dispose") || m.Name.Contains("Close") || m.Name.Contains("Stop")) + .Select(m => m.ToString()) }) +}, new JsonSerializerOptions { WriteIndented = true })); diff --git a/scripts/validation/opcua-lifecycle-memory/baseline-canonical/Program.cs b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/Program.cs new file mode 100644 index 00000000..ee9f8af0 --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/Program.cs @@ -0,0 +1,210 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Text.Json; +using Opc.Ua; +using Opc.Ua.Configuration; +using Opc.Ua.Server; +using Shouldly; +using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using CoreHistoryReadResult = ZB.MOM.WW.OtOpcUa.Core.Abstractions.HistoryReadResult; +return await Canonical.RunAsync(args); +internal static partial class Canonical +{ + private const string Scalar = BaselineRig.RawDeviceFolder + "/SoakScalar"; + private const string ArrayTag = BaselineRig.RawDeviceFolder + "/SoakArray"; + private const string UnsScalar = "SoakEquipment/Scalar"; + private static readonly DateTime Epoch = new(2026,10,1,0,0,0,DateTimeKind.Utc); + internal static readonly OperationLedger Ledger = new(); + private static StreamWriter CurrentEvidence = null!; + internal static async Task RunAsync(string[] args) + { + var root=Path.GetFullPath(args[0]); var cycles=int.Parse(args[1]); + var isolatedRestart = args.Length == 3 && args[2] == "--active-restart-control"; + if (args.Length > 3 || (args.Length == 3 && !isolatedRestart)) throw new InvalidOperationException("Unknown short-control mode"); + if(cycles<1 || cycles>4) throw new InvalidOperationException("Only a 1..4-cycle short diagnostic smoke is authorized"); + if(Directory.Exists(root)) throw new InvalidOperationException("Use a fresh private artifact directory"); + Directory.CreateDirectory(root); + if(!OperatingSystem.IsWindows()) File.SetUnixFileMode(root,UnixFileMode.UserRead|UnixFileMode.UserWrite|UnixFileMode.UserExecute); + await using var output=new StreamWriter(new FileStream(Path.Combine(root,"resources.jsonl"),FileMode.CreateNew,FileAccess.Write,FileShare.Read)); + CurrentEvidence=output; + using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(120)); var ct=deadline.Token; + var sdk=typeof(NodeId).Assembly.GetCustomAttribute()?.InformationalVersion; + if(sdk is null || !sdk.StartsWith("1.5.378.182+",StringComparison.Ordinal)) throw new InvalidOperationException("The immutable .182 SDK is required"); + var clientRoot=Path.Combine(root,"private-fixture","client-pki"); + var telemetry=DefaultTelemetry.Create(_=>{}); + var config=new ApplicationConfiguration(telemetry) + { + ApplicationName="SDK2LifecycleSoak",ApplicationUri=$"urn:SDK2LifecycleSoak:{Guid.NewGuid():N}",ApplicationType=ApplicationType.Client, + SecurityConfiguration=new SecurityConfiguration + { + ApplicationCertificate=new CertificateIdentifier{StoreType=CertificateStoreType.Directory,StorePath=Path.Combine(clientRoot,"own"),SubjectName="CN=SDK2LifecycleSoak"}, + TrustedIssuerCertificates=new CertificateTrustList{StoreType=CertificateStoreType.Directory,StorePath=Path.Combine(clientRoot,"issuer")}, + TrustedPeerCertificates=new CertificateTrustList{StoreType=CertificateStoreType.Directory,StorePath=Path.Combine(clientRoot,"trusted")}, + RejectedCertificateStore=new CertificateTrustList{StoreType=CertificateStoreType.Directory,StorePath=Path.Combine(clientRoot,"rejected")},AutoAcceptUntrustedCertificates=true + },ClientConfiguration=new ClientConfiguration{DefaultSessionTimeout=15000},TransportQuotas=new TransportQuotas{OperationTimeout=5000} + }; + var application=new ApplicationInstance(telemetry){ApplicationConfiguration=config}; + await config.ValidateAsync(ApplicationType.Client,ct);await application.CheckApplicationInstanceCertificatesAsync(true,null,ct); + await using var rig=new BaselineRig(Path.Combine(root,"private-fixture")); + await rig.BootAsync(ct);Configure(rig); + var watch=Stopwatch.StartNew();var completed=0;var restarts=0; + await LogAsync(output,new {kind="start",sdk,productRebuilt=false,sourcePublication="83ed880e",requestedCycles=cycles, + processId=Environment.ProcessId,acceptance=false,scope="canonical operations short smoke; ownership differences remain explicit", + unsupportedObservables=new[]{"SDK2 CertificateManager disposed flag","SDK2 manager-owned cache-clear proof","ApplicationInstance.DisposeAsync"}}); + try + { + if (isolatedRestart) + { + if (cycles != 1) throw new InvalidOperationException("The isolated restart control performs exactly one restart"); + await RestartWithActiveResourcesAsync(rig,config,ct);Configure(rig);restarts++; + await LogAsync(output,new {kind="isolated-active-restart-completed",normalCycles=0,restarts, + acceptance=false,ownershipParity=false,ledger=Ledger.Snapshot()}); + return 2; + } + while(completedPublicCounts(rig)==(0,0,0),ct); + var counts=PublicCounts(rig);var fd=await BaselineMetrics.DescriptorsAsync(ct,clientRoot,rig.PkiRoot); + fd.Pki.ShouldBe(0); + foreach(var name in new[]{"LiveItemCount","LiveEventItemCount","MonitoredNodeCount"}) BaselineMetrics.Counter(rig.Nm,name).ShouldBe(0); + using var process=Process.GetCurrentProcess();process.Refresh(); + await LogAsync(output,new {kind="quiescent",cycle=completed,seconds=watch.Elapsed.TotalSeconds, + sessions=counts.Sessions,subscriptions=counts.Subscriptions,items=counts.Items, + pkiDescriptors=fd.Pki,descriptors=fd.Total,managedBytes=GC.GetTotalMemory(true),rssBytes=process.WorkingSet64, + serverCachedStoreReferences=BaselineMetrics.CachedStoreReferences(rig.Validator), + clientCachedStoreReferences=BaselineMetrics.CachedStoreReferences(config.CertificateValidator),jit=System.Runtime.JitInfo.GetCompiledMethodCount(),timers=Timer.ActiveCount}); + if(completed%4==0){await RestartWithActiveResourcesAsync(rig,config,ct);Configure(rig);restarts++;} + } + await LogAsync(output,new {kind="operations-completed",cycles=completed,restarts,seconds=watch.Elapsed.TotalSeconds, + acceptance=false,ownershipParity=false,reason="Original SDK has no equivalent manager-disposal observable; review required",ledger=Ledger.Snapshot()}); + return 2; + } + catch(Exception error) + { + await LogAsync(output,new {kind="failed",cycles=completed,restarts,exceptionType=error.GetType().FullName, + uaStatus=error is ServiceResultException ua ? ua.StatusCode.ToString() : null,ledger=Ledger.Snapshot(),acceptance=false}); + Console.Error.WriteLine(error);return 1; + } + } + private static int ScalarValue(Variant value)=>value.Value is int result?result:throw new InvalidOperationException("Expected scalar Int32 CLR shape"); + private static void RequirePoint(byte[]? point)=>(point is {Length:>0}).ShouldBeTrue(); + private static (int Browse,int History) ContinuationCounts(ISession session)=>BaselineMetrics.Points(session); + private static (int Sessions,int Subscriptions,int Items) PublicCounts(BaselineRig rig) + {var subscriptions=rig.Server.CurrentInstance.SubscriptionManager.GetSubscriptions();return(rig.Server.CurrentInstance.SessionManager.GetSessions().Count,subscriptions.Count,subscriptions.Sum(s=>s.MonitoredItemCount));} + private static async Task EventuallyAsync(Func condition,CancellationToken ct) + {var elapsed=Stopwatch.StartNew();while(!condition()&&elapsed.Elapsed WriteAsync(string id,object? value,AddressSpaceRealm realm,CancellationToken ct)=>Task.FromResult(new NodeWriteOutcome(true,null));} + internal sealed class SqlLikeEventSource(List events) : IHistorianDataSource + { + private readonly List _all = + events.OrderBy(e => e.EventTimeUtc).ThenBy(e => e.EventId, StringComparer.Ordinal).ToList(); + + public async Task ReadEventsAsync( + string? sourceName, DateTime startUtc, DateTime endUtc, int maxEvents, CancellationToken ct) + { + await Task.Delay(1, ct).ConfigureAwait(false); // complete on a pool thread, like a real backend + var window = _all.Where(e => e.EventTimeUtc >= startUtc && e.EventTimeUtc < endUtc).ToList(); + var cap = maxEvents <= 0 ? 1000 : maxEvents; + return new HistoricalEventsResult(window.Take(cap).ToList(), window.Count > cap ? new byte[] { 1 } : null); + } + + public bool SupportsReverseEvents => true; + + /// The #675 overload: reverse is (lo, hi] by (time, id) descending. + public async Task ReadEventsAsync( + string? sourceName, DateTime startUtc, DateTime endUtc, int maxEvents, bool reverse, CancellationToken ct) + { + if (!reverse) return await ReadEventsAsync(sourceName, startUtc, endUtc, maxEvents, ct).ConfigureAwait(false); + await Task.Delay(1, ct).ConfigureAwait(false); + var (lo, hi) = startUtc <= endUtc ? (startUtc, endUtc) : (endUtc, startUtc); + var window = _all.Where(e => e.EventTimeUtc > lo && e.EventTimeUtc <= hi).Reverse().ToList(); + var cap = maxEvents <= 0 ? 1000 : maxEvents; + return new HistoricalEventsResult(window.Take(cap).ToList(), window.Count > cap ? new byte[] { 1 } : null); + } + + public Task ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct) => + NullHistorianDataSource.Instance.ReadRawAsync(r, s, e, m, ct); + public Task ReadProcessedAsync( + string r, DateTime s, DateTime e, TimeSpan i, HistoryAggregateType a, CancellationToken ct) => + NullHistorianDataSource.Instance.ReadProcessedAsync(r, s, e, i, a, ct); + public Task ReadAtTimeAsync(string r, IReadOnlyList t, CancellationToken ct) => + NullHistorianDataSource.Instance.ReadAtTimeAsync(r, t, ct); + public HistorianHealthSnapshot GetHealthSnapshot() => NullHistorianDataSource.Instance.GetHealthSnapshot(); + public void Dispose() { } + } + /// Task-owned TCP relay. Pause blocks forwarding in both directions without closing either socket. + private sealed class PauseProxy : IAsyncDisposable + { + private readonly TcpListener _listener = new(IPAddress.Loopback, 0); + private readonly CancellationTokenSource _stop = new(); + private readonly ConcurrentBag _sockets = []; + private readonly ConcurrentBag _pumps = []; + private readonly Task _accept; + private TaskCompletionSource? _pause; + internal int Port => ((IPEndPoint)_listener.LocalEndpoint).Port; + internal PauseProxy(int targetPort) + { + _listener.Start(); + _accept = AcceptAsync(targetPort); + } + internal void Pause() => Volatile.Write(ref _pause, new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)); + internal void Resume() => Interlocked.Exchange(ref _pause, null)?.TrySetResult(); + private async Task AcceptAsync(int port) + { + try + { + while (!_stop.IsCancellationRequested) + { + var incoming = await _listener.AcceptTcpClientAsync(_stop.Token); + _sockets.Add(incoming); + var outgoing = new TcpClient(); _sockets.Add(outgoing); + await outgoing.ConnectAsync(IPAddress.Loopback, port, _stop.Token); + _pumps.Add(PumpAsync(incoming.GetStream(), outgoing.GetStream())); + _pumps.Add(PumpAsync(outgoing.GetStream(), incoming.GetStream())); + } + } + catch (OperationCanceledException) when (_stop.IsCancellationRequested) { } + } + private async Task PumpAsync(NetworkStream source, NetworkStream destination) + { + var buffer = new byte[16 * 1024]; + try + { + while (true) + { + var length = await source.ReadAsync(buffer, _stop.Token); + if (length == 0) return; + if (Volatile.Read(ref _pause) is { } paused) await paused.Task.WaitAsync(_stop.Token); + await destination.WriteAsync(buffer.AsMemory(0, length), _stop.Token); + } + } + catch (OperationCanceledException) when (_stop.IsCancellationRequested) { } + catch (IOException) when (_stop.IsCancellationRequested) { } + } + public async ValueTask DisposeAsync() + { + Resume(); _stop.Cancel(); _listener.Stop(); + foreach (var socket in _sockets) socket.Dispose(); + try { await _accept; await Task.WhenAll(_pumps); } + finally { _stop.Dispose(); } + } + } +} + +internal sealed class OperationLedger +{ + private readonly ConcurrentDictionary _counts=new(); + internal void Record(string name)=>_counts.AddOrUpdate(name,1,(_,count)=>count+1); + internal async Task Call(string name,Func> operation) + {Record(name+".attempted");try{var value=await operation();Record(name+".returned");return value;}catch{Record(name+".faulted");throw;}} + internal async Task Call(string name,Func operation) + {Record(name+".attempted");try{await operation();Record(name+".returned");}catch{Record(name+".faulted");throw;}} + internal IReadOnlyDictionary Snapshot()=>new SortedDictionary(_counts); +} diff --git a/scripts/validation/opcua-lifecycle-memory/baseline-canonical/README.md b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/README.md new file mode 100644 index 00000000..d7973b66 --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/README.md @@ -0,0 +1,124 @@ +# Original 1.5.378.182 canonical adapter — partial diagnostic proof + +This folder compiles a standalone diagnostic against the **unchanged original +83ed880e published host/client DLLs**. It does not rebuild or patch product code, +restore a substitute SDK, edit a baseline oracle, install a replacement session +factory, or approve a performance denominator. Only short 1–4-cycle smoke runs are +implemented. No original warmed/full/two-hour run has been performed. + +The immutable SDK reports `1.5.378.182+6cca32dd6d79c8b57d2e00e4d2071b991d2cf71b`. +The source workload was ported from `Sdk2LifecycleSoakTests` at `75057d8c`. +`evidence.json` records source/input hashes, exact SDK layout and short-run results. +Raw logs and task-owned PKI stay outside the repository; credentials are not logged. + +## Supported operation coverage + +`CanonicalOperations.cs` preserves the candidate's operation ordering, status +assertions, timeouts, model, grants, event data, page size and typed-array shape. +The representation changes are explicit: SDK2 Variant/ArrayOf helpers become the +original Variant constructor and CLR values; ByteString becomes byte[]; and +QualifiedName.From becomes its original constructor. There are no collection +shims or private-state mutations. + +| Canonical operation per normal cycle | Original short control observation | +|---|---| +| Two encrypted opc.tcp / SignAndEncrypt sessions, Raw/UNS URI binding | Completed | +| One subscription, three data items, notifications | Completed | +| One Write service with Int32/Int16[]/UNS Int32 values, three readbacks | Completed with original Good/type assertions | +| Two browse pages, one explicit release and one abandonment | Completed; active browse count 1 | +| History first page, foreign-session denial, resume, explicit release, six abandoned points | All 10 HistoryRead services returned; active history count 6 | +| Writable off/on, array removal/recreation/publication | Completed | +| Key revocation, denied read, authority restoration | Completed | +| Paused TCP read, one-second cancellation, resume and successful recovery read | Completed; original UA status 2156134400 on the canceled read | +| Subscription deletion/removal and address-space rebuild/reconfiguration | Completed | +| Both normal clients CloseAsync | Both returned; version-specific post-close list comparison differs | +| Every-fourth-cycle restart with an active session/subscription/item, browse/history points | Separate isolated one-restart control completed | + +The failed normal control attempted one whole wire body. Its ledger records +2 connects, 1 subscription create/apply/delete/remove, 1 Write service (3 values), +5 ReadValue attempts (4 returned, 1 expected canceled fault), 1 revoked Read +service, 2 Browse calls, 1 BrowseNext release, 10 HistoryRead calls, and 2 CloseAsync +returns. Surgery, revocation, stalled-network recovery and rebuild all occurred +before the post-close comparison. No normal cycle is reported as ownership-complete. + +The independent active-restart control does **not** stand in for a successful +four-cycle sequence: it starts one client/subscription/item, abandons browse and +history points, stops/reboots the same URI/port/PKI server, and checks the captured +old managers/sessions/subscription items and new manager counts. Those checks +completed. It returned **exit 2**, explicitly unsupported ownership parity, +not a green acceptance result. + +## Close and ownership differences — unresolved + +The first normal smoke returned **exit 1** at the copied zero-closed-list +comparison: a retained server Session still had **1 browse / 6 history** points. +The server SessionManager already had **zero active sessions**. A separate fixed +250 ms read-only observation saw the same lists; the original failed comparison +was still enforced. The first failure artifacts were left untouched. + +Exact immutable-binary inspection explains the internal difference: + +- SessionManager.CloseSession removes the session and calls Session.Close. +- Session.Close only deletes its diagnostics; it does not clear continuation lists. +- Session.Dispose clears both lists and disposes their retained values. +- SessionManager.Shutdown/Dispose disposes sessions still in its active dictionary. + +These are **version-specific diagnostics**, not proof that the original violates +the wire close contract, has an unbounded leak, or retains the same objects once +all strong diagnostic references are dropped. The adapter deliberately holds the +old Session references while checking these fields. The SDK2 zero-list invariant +cannot silently become a shared cross-version acceptance contract. + +The restart control follows the different active-session shutdown path: captured +old session/point lists and subscription items became empty. The original +validator's cached store **references** remained 1 before and after stop. A cached +store reference is not a count of open handles or proof of a leak. + +Original ApplicationInstance and CertificateValidator expose no Dispose API, and +there is no SDK2 CertificateManager/m_disposed/core-store-clear equivalent. The +adapter reports those observables as **unsupported**, never as zero. Actual PKI +file descriptors can be measured with the same bounded/canonical-path observer +as the candidate; those observations cannot invent the missing manager ownership +proof. No manual Session.Dispose, store replacement or cache clearing was added +to make an assertion pass. + +Before any common lifecycle/acceptance protocol can be proposed, a separately +reviewed short control must establish original SessionManager removal, rejection +of the old authentication token, rejection of old CP tokens from a fresh session, +then drop every strong session/point/test reference at an isolated helper boundary +and perform bounded GC/weak-reference observation. Any retained IDisposable point +resource needs direct source/ownership inspection. Certificate-store/native +ownership gaps must remain explicit. This proposal has **not** been executed. + +The standalone adapter also lacks the candidate's xUnit/VSTest host; equal process +memory overhead is not proven. Earlier original-subset managed/timer growth is an +observation, not an approved RSS denominator or a proven SDK1 defect. Resource and +operation parity require Astra/root review before any long run; no cap, baseline +or acceptance protocol changes are included here. + +## Build and short controls + +`build.py` selects original DLLs from the provided immutable directories (host +wins duplicate names), plus Shouldly 4.3.0/assertion dependencies only. It emits +SHA-256 provenance and a new external project. No production project/package file +is touched. The inventory lists exact types, fields, lifecycle APIs and MVIDs +without creating product instances. + +```bash +python3 scripts/validation/opcua-lifecycle-memory/baseline-canonical/build.py \ + --host "$originalHost" --client "$originalClient" --assertions "$assertionDlls" \ + --output /tmp/original-canonical-build +python3 scripts/validation/opcua-lifecycle-memory/baseline-canonical/run_smoke.py \ + --adapter /tmp/original-canonical-build/bin/Release/net10.0/adapter.dll \ + --output /tmp/original-canonical-close --mode normal +python3 scripts/validation/opcua-lifecycle-memory/baseline-canonical/run_smoke.py \ + --adapter /tmp/original-canonical-build/bin/Release/net10.0/adapter.dll \ + --output /tmp/original-canonical-active-restart --mode active-restart +``` + +Add `--inventory` to build the read-only metadata inventory instead. The runner +uses a fresh private directory and a temporary environment credential with the +same prefix/byte length as the canonical fixture; it preserves the original child +exit. Exit 1 is an observed control failure; exit 2 is operations completed with +unsupported ownership parity. Neither is a passing baseline. The smoke driver +has a hard 120-second bound and no long-run option. diff --git a/scripts/validation/opcua-lifecycle-memory/baseline-canonical/build.py b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/build.py new file mode 100644 index 00000000..49955d23 --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/build.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Compile diagnostic adapters against the immutable original publication; never build product code.""" +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import xml.etree.ElementTree as ET + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--host', type=Path, required=True) +parser.add_argument('--client', type=Path, required=True) +parser.add_argument('--assertions', type=Path, required=True, help='Directory containing Shouldly.dll; only assertion dependencies are selected') +parser.add_argument('--output', type=Path, required=True) +parser.add_argument('--inventory', action='store_true') +args = parser.parse_args() +root = args.output.resolve(); root.mkdir(parents=True, exist_ok=False) +source = Path(__file__).resolve().parent +references = {} +for directory in [args.host, args.client]: + for dll in sorted(directory.resolve(strict=True).glob('*.dll')): + references.setdefault(dll.name, dll) +for name in ['Shouldly.dll', 'DiffEngine.dll', 'EmptyFiles.Xml.dll']: + path = args.assertions.resolve(strict=True) / name + if path.exists(): references.setdefault(name, path) +project = ET.Element('Project', Sdk='Microsoft.NET.Sdk') +properties = ET.SubElement(project, 'PropertyGroup') +for key,value in {'OutputType':'Exe','TargetFramework':'net10.0','Nullable':'enable','ImplicitUsings':'enable', + 'TreatWarningsAsErrors':'true','EnableDefaultCompileItems':'false'}.items(): + ET.SubElement(properties,key).text=value +items = ET.SubElement(project,'ItemGroup') +ET.SubElement(items,'FrameworkReference',Include='Microsoft.AspNetCore.App') +for name,path in references.items(): + reference=ET.SubElement(items,'Reference',Include=path.stem); ET.SubElement(reference,'HintPath').text=str(path) +files = ['Inventory.cs'] if args.inventory else ['Program.cs','BaselineRig.cs','BaselineMetrics.cs','CanonicalOperations.cs'] +for name in files: ET.SubElement(items,'Compile',Include=str(source/name)) +path=root/'adapter.csproj'; ET.indent(project); ET.ElementTree(project).write(path,encoding='unicode') +manifest={'diagnosticOnly':True,'productRebuilt':False,'references':{name:hashlib.sha256(p.read_bytes()).hexdigest() for name,p in references.items()}, + 'sources':{name:hashlib.sha256((source/name).read_bytes()).hexdigest() for name in files}} +(root/'provenance.json').write_text(json.dumps(manifest,indent=2)+'\n') +with (root/'build.log').open('w') as log: + result=subprocess.run(['dotnet','build',str(path),'-c','Release','-m:2'],stdout=log,stderr=subprocess.STDOUT) +raise SystemExit(result.returncode) diff --git a/scripts/validation/opcua-lifecycle-memory/baseline-canonical/evidence.json b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/evidence.json new file mode 100644 index 00000000..b1066fea --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/evidence.json @@ -0,0 +1,1001 @@ +{ + "diagnosticOnly": true, + "acceptance": false, + "longRunPerformed": false, + "originalPublication": "83ed880e", + "sourceCanonicalSha256": "d3078c4b6849ac89c4326a9965a6e8680af1992ec526029a8eff76bcb76a6e65", + "runtimeScope": "Mac arm64 .NET10.0.14 standalone diagnostic, not xUnit/VSTest process parity", + "build": { + "warnings": 0, + "errors": 0, + "inputLogSha256": "dd8115bdbca3a92144759ed44951bc2ba785d71e1041e2a1b73a3e1b56d86237" + }, + "originalApiInventory": { + "sdk": "1.5.378.182+6cca32dd6d79c8b57d2e00e4d2071b991d2cf71b", + "sdkFile": "1.5.378.182", + "modules": [ + { + "type": "Opc.Ua.Server.Session", + "assembly": "Opc.Ua.Server", + "mvid": "5a15b9a7-481e-406a-ae50-1d2bbd38fd7a", + "interfaces": [ + "Opc.Ua.Server.ISession", + "System.IDisposable" + ], + "fields": [ + { + "name": "k__BackingField", + "type": "Opc.Ua.NodeId" + }, + { + "name": "k__BackingField", + "type": "Opc.Ua.IUserIdentity" + }, + { + "name": "k__BackingField", + "type": "Opc.Ua.IUserIdentity" + }, + { + "name": "k__BackingField", + "type": "Opc.Ua.UserIdentityToken" + }, + { + "name": "k__BackingField", + "type": "Opc.Ua.SessionDiagnosticsDataType" + }, + { + "name": "k__BackingField", + "type": "System.Byte[]" + }, + { + "name": "k__BackingField", + "type": "System.Security.Cryptography.X509Certificates.X509Certificate2" + }, + { + "name": "k__BackingField", + "type": "System.String[]" + }, + { + "name": "k__BackingField", + "type": "System.Boolean" + }, + { + "name": "k__BackingField", + "type": "Opc.Ua.EndpointDescription" + }, + { + "name": "k__BackingField", + "type": "System.String" + }, + { + "name": "k__BackingField", + "type": "System.Int32" + }, + { + "name": "m_lock", + "type": "System.Threading.Lock" + }, + { + "name": "m_logger", + "type": "Microsoft.Extensions.Logging.ILogger" + }, + { + "name": "m_server", + "type": "Opc.Ua.Server.IServerInternal" + }, + { + "name": "m_sessionName", + "type": "System.String" + }, + { + "name": "m_serverCertificate", + "type": "System.Security.Cryptography.X509Certificates.X509Certificate2" + }, + { + "name": "m_serverNonce", + "type": "Opc.Ua.Nonce" + }, + { + "name": "m_eccUserTokenSecurityPolicyUri", + "type": "System.String" + }, + { + "name": "m_eccUserTokenNonce", + "type": "Opc.Ua.Nonce" + }, + { + "name": "m_clientIssuerCertificates", + "type": "System.Security.Cryptography.X509Certificates.X509Certificate2Collection" + }, + { + "name": "m_maxHistoryContinuationPoints", + "type": "System.Int32" + }, + { + "name": "m_securityDiagnostics", + "type": "Opc.Ua.SessionSecurityDiagnosticsDataType" + }, + { + "name": "m_browseContinuationPoints", + "type": "System.Collections.Generic.List`1[[Opc.Ua.Server.ContinuationPoint, Opc.Ua.Server, Version=1.5.378.0, Culture=neutral, PublicKeyToken=bfa7a73c5cf4b6e8]]" + }, + { + "name": "m_historyContinuationPoints", + "type": "System.Collections.Generic.List`1[[Opc.Ua.Server.Session+HistoryContinuationPoint, Opc.Ua.Server, Version=1.5.378.0, Culture=neutral, PublicKeyToken=bfa7a73c5cf4b6e8]]" + } + ], + "lifecycle": [ + "Void Dispose()", + "Void Close()" + ] + }, + { + "type": "Opc.Ua.CertificateValidator", + "assembly": "Opc.Ua.Core", + "mvid": "018069a0-5465-459b-958f-82b5dcd68a1f", + "interfaces": [ + "Opc.Ua.ICertificateValidator" + ], + "fields": [ + { + "name": "m_semaphore", + "type": "System.Threading.SemaphoreSlim" + }, + { + "name": "m_logger", + "type": "Microsoft.Extensions.Logging.ILogger" + }, + { + "name": "m_telemetry", + "type": "Opc.Ua.ITelemetryContext" + }, + { + "name": "m_validatedCertificates", + "type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Security.Cryptography.X509Certificates.X509Certificate2, System.Security.Cryptography, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a]]" + }, + { + "name": "m_trustedCertificateStore", + "type": "Opc.Ua.CertificateStoreIdentifier" + }, + { + "name": "m_trustedCertificateList", + "type": "Opc.Ua.CertificateIdentifierCollection" + }, + { + "name": "m_issuerCertificateStore", + "type": "Opc.Ua.CertificateStoreIdentifier" + }, + { + "name": "m_issuerCertificateList", + "type": "Opc.Ua.CertificateIdentifierCollection" + }, + { + "name": "m_rejectedCertificateStore", + "type": "Opc.Ua.CertificateStoreIdentifier" + }, + { + "name": "m_CertificateValidation", + "type": "Opc.Ua.CertificateValidationEventHandler" + }, + { + "name": "m_CertificateUpdate", + "type": "Opc.Ua.CertificateUpdateEventHandler" + }, + { + "name": "m_applicationCertificates", + "type": "System.Collections.Generic.List`1[[System.Security.Cryptography.X509Certificates.X509Certificate2, System.Security.Cryptography, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a]]" + }, + { + "name": "m_protectFlags", + "type": "Opc.Ua.CertificateValidator+ProtectFlags" + }, + { + "name": "m_autoAcceptUntrustedCertificates", + "type": "System.Boolean" + }, + { + "name": "m_rejectSHA1SignedCertificates", + "type": "System.Boolean" + }, + { + "name": "m_rejectUnknownRevocationStatus", + "type": "System.Boolean" + }, + { + "name": "m_minimumCertificateKeySize", + "type": "System.UInt16" + }, + { + "name": "m_useValidatedCertificates", + "type": "System.Boolean" + }, + { + "name": "m_maxRejectedCertificates", + "type": "System.Int32" + } + ], + "lifecycle": [] + }, + { + "type": "Opc.Ua.CertificateStoreIdentifier", + "assembly": "Opc.Ua.Core", + "mvid": "018069a0-5465-459b-958f-82b5dcd68a1f", + "interfaces": [ + "Opc.Ua.IOpenStore", + "System.IFormattable", + "System.ICloneable" + ], + "fields": [ + { + "name": "k__BackingField", + "type": "System.String" + }, + { + "name": "m_storePath", + "type": "System.String" + }, + { + "name": "k__BackingField", + "type": "Opc.Ua.CertificateValidationOptions" + }, + { + "name": "m_store", + "type": "Opc.Ua.ICertificateStore" + }, + { + "name": "m_noPrivateKeys", + "type": "System.Boolean" + } + ], + "lifecycle": [] + }, + { + "type": "Opc.Ua.Configuration.ApplicationInstance", + "assembly": "Opc.Ua.Configuration", + "mvid": "e2d9dec0-e5a7-4185-8613-507dde86c748", + "interfaces": [ + "Opc.Ua.Configuration.IApplicationInstance" + ], + "fields": [ + { + "name": "k__BackingField", + "type": "System.String" + }, + { + "name": "k__BackingField", + "type": "Opc.Ua.ApplicationType" + }, + { + "name": "k__BackingField", + "type": "System.String" + }, + { + "name": "k__BackingField", + "type": "System.Type" + }, + { + "name": "k__BackingField", + "type": "Opc.Ua.ServerBase" + }, + { + "name": "k__BackingField", + "type": "Opc.Ua.ApplicationConfiguration" + }, + { + "name": "k__BackingField", + "type": "Opc.Ua.ICertificatePasswordProvider" + }, + { + "name": "k__BackingField", + "type": "System.Boolean" + }, + { + "name": "m_telemetry", + "type": "Opc.Ua.ITelemetryContext" + }, + { + "name": "m_logger", + "type": "Microsoft.Extensions.Logging.ILogger" + } + ], + "lifecycle": [ + "System.Threading.Tasks.ValueTask StopAsync()", + "Void Stop()" + ] + }, + { + "type": "ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaSdkServer", + "assembly": "ZB.MOM.WW.OtOpcUa.OpcUaServer", + "mvid": "66b52697-7eb9-448d-adb4-2ea79ec0651b", + "interfaces": [ + "Opc.Ua.IServerBase", + "Opc.Ua.IAuditEventCallback", + "System.IDisposable", + "Opc.Ua.ISessionServer", + "Opc.Ua.Server.IStandardServer" + ], + "fields": [ + { + "name": "_nodeManagerLogger", + "type": "Microsoft.Extensions.Logging.ILogger`1[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]" + }, + { + "name": "_otOpcUaNodeManager", + "type": "ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager" + } + ], + "lifecycle": [ + "Void ReportAuditCloseSecureChannelEvent(System.String, System.Exception)", + "System.Threading.Tasks.Task`1[Opc.Ua.CloseSessionResponse] CloseSessionAsync(Opc.Ua.SecureChannelContext, Opc.Ua.RequestHeader, Boolean, System.Threading.CancellationToken)", + "Opc.Ua.ResponseHeader CloseSession(Opc.Ua.SecureChannelContext, Opc.Ua.RequestHeader, Boolean)", + "Void Dispose()", + "System.Threading.Tasks.ValueTask StopAsync(System.Threading.CancellationToken)" + ] + }, + { + "type": "ZB.MOM.WW.OtOpcUa.OpcUaServer.OpcUaApplicationHost", + "assembly": "ZB.MOM.WW.OtOpcUa.OpcUaServer", + "mvid": "66b52697-7eb9-448d-adb4-2ea79ec0651b", + "interfaces": [ + "System.IAsyncDisposable" + ], + "fields": [ + { + "name": "_options", + "type": "ZB.MOM.WW.OtOpcUa.OpcUaServer.OpcUaApplicationHostOptions" + }, + { + "name": "_userAuthenticator", + "type": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Security.IOpcUaUserAuthenticator" + }, + { + "name": "_logger", + "type": "Microsoft.Extensions.Logging.ILogger`1[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OpcUaApplicationHost, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]" + }, + { + "name": "_application", + "type": "Opc.Ua.Configuration.ApplicationInstance" + }, + { + "name": "_server", + "type": "Opc.Ua.Server.StandardServer" + }, + { + "name": "_impersonateHandler", + "type": "Opc.Ua.Server.ImpersonateEventHandler" + } + ], + "lifecycle": [ + "System.Threading.Tasks.ValueTask DisposeAsync()" + ] + }, + { + "type": "ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager", + "assembly": "ZB.MOM.WW.OtOpcUa.OpcUaServer", + "mvid": "66b52697-7eb9-448d-adb4-2ea79ec0651b", + "interfaces": [ + "Opc.Ua.Server.INodeManager2", + "Opc.Ua.Server.INodeManager", + "Opc.Ua.INodeIdFactory", + "System.IDisposable" + ], + "fields": [ + { + "name": "_variables", + "type": "System.Collections.Concurrent.ConcurrentDictionary`2[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[Opc.Ua.BaseDataVariableState, Opc.Ua.Types, Version=1.5.378.0, Culture=neutral, PublicKeyToken=bfa7a73c5cf4b6e8]]" + }, + { + "name": "_folders", + "type": "System.Collections.Concurrent.ConcurrentDictionary`2[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[Opc.Ua.FolderState, Opc.Ua.Core, Version=1.5.378.0, Culture=neutral, PublicKeyToken=bfa7a73c5cf4b6e8]]" + }, + { + "name": "_alarmConditions", + "type": "System.Collections.Concurrent.ConcurrentDictionary`2[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[Opc.Ua.AlarmConditionState, Opc.Ua.Core, Version=1.5.378.0, Culture=neutral, PublicKeyToken=bfa7a73c5cf4b6e8]]" + }, + { + "name": "_nativeAlarmNodeIds", + "type": "System.Collections.Generic.HashSet`1[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]" + }, + { + "name": "_rebuildCarryOver", + "type": "System.Collections.Generic.Dictionary`2[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+NativeConditionCarryOver, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]" + }, + { + "name": "_restoredConditionReports", + "type": "System.Collections.Generic.List`1[[System.ValueTuple`2[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[Opc.Ua.AlarmConditionState, Opc.Ua.Core, Version=1.5.378.0, Culture=neutral, PublicKeyToken=bfa7a73c5cf4b6e8]], System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]" + }, + { + "name": "_historizedTagnames", + "type": "System.Collections.Concurrent.ConcurrentDictionary`2[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[System.String, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]" + }, + { + "name": "_arrayOnScalarRefused", + "type": "System.Collections.Concurrent.ConcurrentDictionary`2[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[System.Byte, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]" + }, + { + "name": "_notifierFolders", + "type": "System.Collections.Generic.Dictionary`2[[Opc.Ua.NodeId, Opc.Ua.Types, Version=1.5.378.0, Culture=neutral, PublicKeyToken=bfa7a73c5cf4b6e8],[Opc.Ua.FolderState, Opc.Ua.Core, Version=1.5.378.0, Culture=neutral, PublicKeyToken=bfa7a73c5cf4b6e8]]" + }, + { + "name": "_alarmNotifierWiring", + "type": "System.Collections.Generic.Dictionary`2[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+AlarmNotifierWiring, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]" + }, + { + "name": "_eventNotifierSources", + "type": "System.Collections.Concurrent.ConcurrentDictionary`2[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[System.String, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]" + }, + { + "name": "_childIndex", + "type": "System.Collections.Generic.Dictionary`2[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[System.Collections.Generic.HashSet`1[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]" + }, + { + "name": "_parentIndex", + "type": "System.Collections.Generic.Dictionary`2[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+RealmKey, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]" + }, + { + "name": "_root", + "type": "Opc.Ua.FolderState" + }, + { + "name": "_logger", + "type": "Microsoft.Extensions.Logging.ILogger`1[[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]" + }, + { + "name": "k__BackingField", + "type": "System.Action`1[[ZB.MOM.WW.OtOpcUa.Commons.OpcUa.AlarmCommand, ZB.MOM.WW.OtOpcUa.Commons, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]" + }, + { + "name": "k__BackingField", + "type": "System.Action`1[[ZB.MOM.WW.OtOpcUa.OpcUaServer.NativeAlarmAck, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]" + }, + { + "name": "k__BackingField", + "type": "System.Func`1[[System.Boolean, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]" + }, + { + "name": "k__BackingField", + "type": "System.Func`2[[ZB.MOM.WW.OtOpcUa.OpcUaServer.NativeAlarmAck, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[System.String, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]" + }, + { + "name": "k__BackingField", + "type": "System.Func`1[[System.Boolean, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]" + }, + { + "name": "k__BackingField", + "type": "System.TimeSpan" + }, + { + "name": "_clock", + "type": "System.TimeProvider" + }, + { + "name": "_nodeWriteGateway", + "type": "ZB.MOM.WW.OtOpcUa.Commons.OpcUa.IOpcUaNodeWriteGateway" + }, + { + "name": "_historianDataSource", + "type": "ZB.MOM.WW.OtOpcUa.Core.Abstractions.IHistorianDataSource" + }, + { + "name": "_historianDataUpdater", + "type": "ZB.MOM.WW.OtOpcUa.Core.Abstractions.IHistorianDataUpdater" + }, + { + "name": "_dataPlaneAuthority", + "type": "ZB.MOM.WW.OtOpcUa.Core.Authorization.IDataPlaneAuthoritySource" + }, + { + "name": "_authorizer", + "type": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Security.DataPlaneAuthorizer" + }, + { + "name": "k__BackingField", + "type": "System.Int32" + }, + { + "name": "k__BackingField", + "type": "System.Boolean" + }, + { + "name": "k__BackingField", + "type": "System.TimeSpan" + }, + { + "name": "k__BackingField", + "type": "System.Int32" + }, + { + "name": "k__BackingField", + "type": "System.Int32" + }, + { + "name": "k__BackingField", + "type": "System.TimeSpan" + }, + { + "name": "_historyReadBatchLimiter", + "type": "System.Threading.SemaphoreSlim" + }, + { + "name": "_historyReadBatchLimiterLock", + "type": "System.Object" + }, + { + "name": "_historyContinuationStore", + "type": "ZB.MOM.WW.OtOpcUa.OpcUaServer.IHistoryContinuationStore" + }, + { + "name": "_suppressedItems", + "type": "System.Int32" + }, + { + "name": "_cacheReferenceReleased", + "type": "System.Runtime.CompilerServices.ConditionalWeakTable`2[[Opc.Ua.Server.IMonitoredItem, Opc.Ua.Server, Version=1.5.378.0, Culture=neutral, PublicKeyToken=bfa7a73c5cf4b6e8],[System.Object, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]" + }, + { + "name": "_liveItems", + "type": "System.Collections.Concurrent.ConcurrentDictionary`2[[Opc.Ua.Server.ISampledDataChangeMonitoredItem, Opc.Ua.Server, Version=1.5.378.0, Culture=neutral, PublicKeyToken=bfa7a73c5cf4b6e8],[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+LiveItem, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]" + }, + { + "name": "_eventItems", + "type": "System.Collections.Concurrent.ConcurrentDictionary`2[[Opc.Ua.Server.IEventMonitoredItem, Opc.Ua.Server, Version=1.5.378.0, Culture=neutral, PublicKeyToken=bfa7a73c5cf4b6e8],[ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaNodeManager+LiveEventItem, ZB.MOM.WW.OtOpcUa.OpcUaServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]" + }, + { + "name": "_allEventsItems", + "type": "System.Collections.Concurrent.ConcurrentDictionary`2[[Opc.Ua.Server.IEventMonitoredItem, Opc.Ua.Server, Version=1.5.378.0, Culture=neutral, PublicKeyToken=bfa7a73c5cf4b6e8],[System.Byte, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]" + }, + { + "name": "_allEventsMonitoredConditions", + "type": "System.Collections.Generic.HashSet`1[[Opc.Ua.AlarmConditionState, Opc.Ua.Core, Version=1.5.378.0, Culture=neutral, PublicKeyToken=bfa7a73c5cf4b6e8]]" + }, + { + "name": "k__BackingField", + "type": "System.Int64" + } + ], + "lifecycle": [ + "Void Dispose()" + ] + } + ] + }, + "referenceSha256": { + "Akka.Cluster.Hosting.dll": "d9b44216b34fc9655c86c3e9d30422e47683969577e205a17096a9a706db768e", + "Akka.Cluster.Sharding.dll": "9f5b418e908a4d579858889af823f780a5e7ff6be2f27c24aa38309759d09423", + "Akka.Cluster.Tools.dll": "a819b0c3e73e08ea474936ad429763ec11681f43c090e2b92dbc1d37d7c94e35", + "Akka.Cluster.dll": "e64aee4dbd305f533c9eea995c9676c0ce7bc8e55a77437afd14b0cf8fe3c0a9", + "Akka.Coordination.dll": "2472a627de9cf9185b3a98860266b7751c173f859d3bd185b272d5f4078be26b", + "Akka.DependencyInjection.dll": "ebea19a2f75f2e275531fbd62be5499a5132563958ebccfc25a450979cfa0039", + "Akka.Discovery.dll": "8c1733537b6cedfb4e7234b86ae63a45cd204ad3c6bc8843f52a85643c507a89", + "Akka.DistributedData.LightningDB.dll": "56100ac684f6acef390b2e546d198899366f035aaa51bb64bc494ebaf2d85007", + "Akka.DistributedData.dll": "9ba2724f0cd49676338241b6249e3b9b95a5c50ddadea5e28adedf31b3ef9fc6", + "Akka.Hosting.dll": "937444c766e641d7c1f86e718e97a148587136a249f91c2986e1f77e2f8d9f92", + "Akka.Logger.Serilog.dll": "49068dc58456684f3c2804bb541223c3aa2dade07f4b1e9877b2e7e058d2056a", + "Akka.Persistence.Hosting.dll": "a9e840a8bc6d7c404e0c83ec5576fcaac3a848880c0ab2b0ed9f1bae33ec2076", + "Akka.Persistence.dll": "c11fc9b044bb927d764f85c0ca8a02c16e8084c05c5ecffd050869ed7810c390", + "Akka.Remote.Hosting.dll": "f0d2ca67e9affd0c68ef32dc859974fe415c00ec5a4710a8e06467542799ad6b", + "Akka.Remote.dll": "b1d27508442727938c4076561f6b4ee81cdd3caa0ef76a481cb45dbfc4c5a0fb", + "Akka.Streams.dll": "e3d9024ece608d6f8b2a15d15f211d50919ed983834fa85d9cc3c9be3c56eb73", + "Akka.dll": "b2d2b8422ba6759d315cbd07622e705fb69daf64e9425cda45f5605132303e34", + "Azure.Core.dll": "224d09cb39eae4d8c30272a0c49beb19d873c87347eae9487f0ddd859d752b56", + "Azure.Identity.dll": "af18f20be25b2895c912f3ba0a9f2901a82dda823edcad76e6a19eac9886a40f", + "BitFaster.Caching.dll": "7af73841cafd0c8b05ad166c45ad0886bcf80926d4c4a65e0fa7578ea1e04d03", + "DotNetty.Buffers.dll": "65f066d108192cefc6e44f18e2d92cf338be30056f4fad02d6b8ea692f81bd37", + "DotNetty.Codecs.dll": "809d9e582ce909356b34ef8d4f212adea5d5ca25e1492a31ccb81a6f9be23070", + "DotNetty.Common.dll": "7a375647446f11a5d5f5eddf72f0327b4b69956a91917100d0f4d8f120fa7962", + "DotNetty.Handlers.dll": "08ce96ee9108adda52adb87119c54fdb819806817867943987d77983c3f1a9c5", + "DotNetty.Transport.dll": "f393993cc027ec61f758b8832029b21f2efd8583807e0aa06af4df90d239eb09", + "FASTER.core.dll": "318092fdd4eccb8e30ab04493a5ce3e827714df2721770af62ef81aea8bd21d8", + "Google.Protobuf.dll": "c2769d0bc587db846b99fefcd778a8ffbb7fbcb4a03d2fcd8ce9573c7ef7042a", + "Grpc.AspNetCore.Server.ClientFactory.dll": "7340bd5dccbe5298825df443d4971a90afd87388e381c008a90556e42c2c04d1", + "Grpc.AspNetCore.Server.dll": "f200b4cae34c51f4f607c684dd72107ad2487808673ae20960edacdf437ec9dd", + "Grpc.Core.Api.dll": "3535e79c38523d239303a33d420c8a856babdcc4d08a167d532decdcc5453951", + "Grpc.Net.Client.dll": "ad2dae77484dabaa9dade62035d761a0d1ec26394e8b600e80dff7b792c088e7", + "Grpc.Net.ClientFactory.dll": "9bf1e593838013238a56d5b4e4e2dd2e8c93589063eaf2a3b2f71e3d3219fd3d", + "Grpc.Net.Common.dll": "d91c6969c65943f22f8a6f67f832e763def7fcb2af0ce16862b3748fa06e0521", + "LightningDB.dll": "b0ebedf36fd19f1dedd7547e49c5a7b6e4a6b53015dde70699e4e69ae8f50406", + "MQTTnet.dll": "1bb34492a9018fec3f627458364dc6e46fef076a647df181403246da65f5bce2", + "Microsoft.AspNetCore.Authentication.JwtBearer.dll": "2a2f99ce877c62a8c600c390ce1f15bb0cc2a92a973a9da60ddcf4bd60bdcb64", + "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.dll": "a768ee07664430adbbdf9a761ed0b691dbf52d67d2f1af833de606b851516394", + "Microsoft.AspNetCore.Http.Connections.Client.dll": "1ce2e273eab3c4ca003e65bfdf78318b3195f08385c814a02f10890bdd34d05f", + "Microsoft.AspNetCore.SignalR.Client.Core.dll": "5833a8070a9c50faa38331fead1c106c4c4bf4e6eca773b00a1d44e16c6911f8", + "Microsoft.AspNetCore.SignalR.Client.dll": "f5201eaf736cab8651013a49b6bd8bbd2d80fb2f46ffdef0e40ce21bfcb161f3", + "Microsoft.Bcl.AsyncInterfaces.dll": "136d5965cf4768e8420b547a8bddea882921f426c371833d558a858a2f0c235a", + "Microsoft.Bcl.Cryptography.dll": "009a27b8e3093fdd2960bba63e4acd48eab05c4f8f8a6d9fe8c24b4657f44c4f", + "Microsoft.CodeAnalysis.CSharp.Scripting.dll": "d0f4798de9cb78c13463a0f3df747814c177ec15e03e1c03aee17136798a2cfe", + "Microsoft.CodeAnalysis.CSharp.dll": "36f5ac8da30a6107f53a2ca6509b61e284c08eb59aa1b22956d4e200e4514aa5", + "Microsoft.CodeAnalysis.Scripting.dll": "7c3973a0997a1901c4f5978ef901e3ae96ddb780dead70a42f65f2e3f6f23e8f", + "Microsoft.CodeAnalysis.dll": "10f489db67b8ac7489e58d392166c928302ba5698506dd652311da5d89f0a0f8", + "Microsoft.Data.SqlClient.dll": "5a34e2fa3479b1508dbc20fbeb2c9172f18c3868bdb147a185213a446645b400", + "Microsoft.Data.Sqlite.dll": "9f1fe804321767b6cc8db751ecedd56ab308e5f15492a7f40d7093bb61ab7638", + "Microsoft.EntityFrameworkCore.Abstractions.dll": "34aaa94035fdb18400c9c40df82a731d935153e71c99932a92830e5807cf41d6", + "Microsoft.EntityFrameworkCore.Relational.dll": "ae537828754942c45e884795bf666205f5cff414c9238641bbe8687ea455cb79", + "Microsoft.EntityFrameworkCore.SqlServer.dll": "9261df9184d4aa3914e9f20cbe9a8abc478702de3ebdddcb622143f798698464", + "Microsoft.EntityFrameworkCore.dll": "1783882ff577f09cd2b220f322e0b3eea0f77bb86f80611089274482ae1dbf75", + "Microsoft.Extensions.DependencyModel.dll": "3a39ec75b47ea48175fe0586dc7513bb4f56423d2946662b8dac24afde925d8f", + "Microsoft.Extensions.Hosting.WindowsServices.dll": "39de9399e095736aeae2a29568207ea349d889bc3b72affa68f92d6581bf6f1a", + "Microsoft.Identity.Client.Extensions.Msal.dll": "9c1f736b16cacd318afe07d3b9d707a4b4b56def5e403b0aff471c1af413aaf0", + "Microsoft.Identity.Client.dll": "10d59df4170053ea9597e3c20adc9607ad17318ad5d8747eda3d930a5a4b463a", + "Microsoft.IdentityModel.Abstractions.dll": "b438fd27bdd5ba7dc4a482c1f135d6305301b1685f624bb36ac0ed24f499fc2d", + "Microsoft.IdentityModel.JsonWebTokens.dll": "cf889db7a04e956d03726954f9389a23a952666a66137021c712ee977630bd37", + "Microsoft.IdentityModel.Logging.dll": "da14a0467587838b7fedc6670315452ff9c7a9fe54fb7f0742fe7a34274753b7", + "Microsoft.IdentityModel.Protocols.OpenIdConnect.dll": "3fdcb7b20fe91de47857aa3fee0d13affaecfd0544cae5a3ad3f7f105a3d8cd6", + "Microsoft.IdentityModel.Protocols.dll": "1126552d469948ed6d7887e8c24cbd65fddad103193eeea869930e27da2bc010", + "Microsoft.IdentityModel.Tokens.dll": "b3d4bcb1d9f9c469825e7aa041bc35c33d85651a88ee24b5ef9ea29d7502d214", + "Microsoft.SqlServer.Server.dll": "a9dca1b42bc875f966b78b2c0880e62b838f0ed6567166d7f1f07ae4b76ec4bf", + "MySqlConnector.dll": "e7734ef5db26f5e5626f8ef354831c60adff9a8e88447cc5e865cfe1f9e466bc", + "Newtonsoft.Json.dll": "a28c251dfe36d881e9e2462e171441b8b0ec156fe3f452602c9149b1b9efe05b", + "Novell.Directory.Ldap.NETStandard.dll": "85918b6f9e91acffac9bd081b70404b0456c7a6f0c9c5666ccda8951bd8120d4", + "Npgsql.dll": "e2bcafe1660fe4543fe9dfcabed972ec8d32259e5ea8009462ce1de23c7034c2", + "Opc.Ua.Client.dll": "fd000b186f4bc5dbd9947e876f96c178e8c4259ee3e8717d672a4ac2f4c3a058", + "Opc.Ua.Configuration.dll": "30fb5142f13a3b3083c0484bcdedaec2efe2f50e57f10ec6a61257ca5359aa7e", + "Opc.Ua.Core.dll": "602e85316943ad7ebae33a23d60a69a6ea50d91250ad0556906b816435daa37c", + "Opc.Ua.Security.Certificates.dll": "a2a05c64348f6f8aa60de0fe27d8e5e22d12e09b1ba6172ca34b3773620e394c", + "Opc.Ua.Server.dll": "d8d9706dea577858325730cd89896826ed82f7bfcf9503ccd0260311663a69f4", + "Opc.Ua.Types.dll": "c5c5e36cf6381444b408fe1b51726b17deca74173764cd62ccee954b3c8c2718", + "OpenTelemetry.Api.ProviderBuilderExtensions.dll": "5c5727de149a0232825774a8ee75b60581ce19ce8b944d422bd545e2e3d7f915", + "OpenTelemetry.Api.dll": "ca5ddb7a51bcbb1ab8bcc6484603c70a4077142a8745d521c4bd2363f9e7c217", + "OpenTelemetry.Exporter.OpenTelemetryProtocol.dll": "20bf20b94e2e92a73d20d97b81d9cd3b26ef0967ff29f80d84b421c4317ac77e", + "OpenTelemetry.Exporter.Prometheus.AspNetCore.dll": "60dcd62f532a61821328e52e511bd47db620c301c7105c4aaf70619c0f217d65", + "OpenTelemetry.Extensions.Hosting.dll": "a627ebf2a7d14e0f0b3c2568e6ee37773397de25c90ee89d4560422c42d8b945", + "OpenTelemetry.Instrumentation.AspNetCore.dll": "247771e79186a704722510e03aefb2f09c10286ff3fc658dcd4bb81ba347f6c3", + "OpenTelemetry.Instrumentation.GrpcNetClient.dll": "8edab07d2cc02b81cc53bd9ccc0f6160e483a4b42ced3cf339de530fecfe88ee", + "OpenTelemetry.Instrumentation.Http.dll": "154c41bc484085a74a0b1886478ffb4aeaf850686d173d0c93a11ad0283aea2e", + "OpenTelemetry.Instrumentation.Process.dll": "a789454f78efbf4d2f04605b98d3f143f6396bec208f4b5765e8e8d2d0f35143", + "OpenTelemetry.Instrumentation.Runtime.dll": "08b62837d801db4afe0ac77d27fe66fff99493cbb6b9c49a5876627c2aabd7a8", + "OpenTelemetry.dll": "edc9799efd614cb083193d90eafcd99abf70496738c3065933093cf99d162cae", + "Oracle.ManagedDataAccess.dll": "a76f5f278e9db505d76ce21bc57163c0079ec03ef1dda7d9a524d9bec3a3bbbf", + "OtOpcUa.Host.dll": "e88fc71f59e69e15c3191a782c29ce44127e320c7560ddd26ce1759787806f98", + "Polly.Core.dll": "802ffe5c74c26cf6bdd0f0ecacc36965807a267765f61b63eea8605da15fe0e3", + "Reactive.Streams.dll": "dfb748806727fe261f48e15ed6c1e3ab952f7021208fec4df0184615ba5ec6f0", + "S7.Net.dll": "e293fe25f6e774069a5c38f0778e47f2c8a0d7327741a6df1c62fb0f98d497cf", + "SQLitePCLRaw.batteries_v2.dll": "bc69777001eb4db81c580f62835396f67e74aaa381eef446193a904499d5310b", + "SQLitePCLRaw.core.dll": "50c999a1ab2afeacc93bce5ac2948d3bb630d99d23efc3cfcbd8bc11d3426eb7", + "SQLitePCLRaw.provider.e_sqlite3.dll": "02ad639f45a26de1919ffbb6696bdb399ad01afc149970a163c3867162f310b3", + "Serilog.AspNetCore.dll": "a550c57df7a04edbed00da036fc14be12acd0d425b7401855975fb628e3dbada", + "Serilog.Extensions.Hosting.dll": "3e6d0284179ee0e1c197568fd79b58f869d15298fff8ecb8447322fe87d44f47", + "Serilog.Extensions.Logging.dll": "80d3edbe3e3ac0b4c5bc764474220e7da11df8d285e0098f3dc1b65a734c8507", + "Serilog.Formatting.Compact.dll": "319fbcefc2161dd50ccdd8d60b30edee48a09f794aedb28421a23f35ddd670d9", + "Serilog.Settings.Configuration.dll": "ec8cfd1f8970a44e2aa3f7bcc71116ca2691b196564bff83e3e3ead0c1143b67", + "Serilog.Sinks.Console.dll": "dc88002ddd9f5fd0ffec9a6568ef89954671f60f2c9161593ee78932538c1e3e", + "Serilog.Sinks.Debug.dll": "df543d3df8bd00d72e5eb2b9b2a15e67a6e4d10c13834de4cedbf500b8793e01", + "Serilog.Sinks.File.dll": "4f4282671c2ac68284b4dc99f7a76eac9cbd4e659978cef642cff9e6c01b676d", + "Serilog.Sinks.OpenTelemetry.dll": "e17a2dd847560cd22bdb6df8302f8fd202c3f073888cdd9ad8b1df9e80da4351", + "Serilog.dll": "e6b551a75ff7fff20dd0024670d5c42dfb74afa16d54aa0201106dea400a9505", + "System.ClientModel.dll": "ce44a4fae4c4787c12475700e19a365fc2f388ae5d0f6182557ce5ea175be7f6", + "System.ComponentModel.Composition.dll": "434522d676f626589e21aedea883f6acfbe276f9c3a935dfb4d8af7a19feb75a", + "System.Configuration.ConfigurationManager.dll": "08b618461d62fe65451bdac4dbc25f8ac8ab16a914a687f2dbe3923bf0e8b3e7", + "System.Data.Odbc.dll": "703fa599513e6ef21193933f061f2b6cdd57f268d593344faa5980b823e8226c", + "System.Diagnostics.PerformanceCounter.dll": "c596c35317c7bd5ae3938854996d62c24c7f67f61d3710a379bf1f3ab1a9e3b4", + "System.DirectoryServices.Protocols.dll": "096102bad94746479987ffc4fc6c9faafb9e1ae6ed1c74c9a7dccae0e4f3ea9f", + "System.IdentityModel.Tokens.Jwt.dll": "56cf18b0b16ea80738e0768cdcbc9368f13bdbedaca86025756009e30f969fe6", + "System.Interactive.Async.dll": "75a516771f2e10fdb76c1ea603f8d11e702af7ec97b383f1036cee114ef3f491", + "System.Linq.Async.dll": "149757a4113ae6102c425f1469a7cc14deb28635e94cbc9e0404f5e4dc518001", + "System.Memory.Data.dll": "bae79a4ff6e8fcfb65a3d88eb53d281898e699e5471ebd780843e5616753f0c3", + "System.Reactive.dll": "c1cae1a8b97d5e0ed64f7a9947f6ae44498398b6e9ee71473c2bef24646e91f8", + "System.Security.Cryptography.ProtectedData.dll": "02321322ec85e0a68f28ad33e549213b1dbf3de0e66c3f7803f9ae32c0d00ffd", + "System.ServiceProcess.ServiceController.dll": "62ca0ff8bccaf622bd75f03b6a5ab46b8ad7b28c5eb6ba6a0a9e4ecc49cca640", + "TwinCAT.Ads.Abstractions.dll": "f9892e164816e0b1cc9aa47c2efe3f48814ea8fbe4f04a125460555eaac9d9ab", + "TwinCAT.Ads.ConfigurationProviders.dll": "016eb9f6687832b0a7a819609ffb3227204f75732919ef19fb3366c038a11c5e", + "TwinCAT.Ads.Server.dll": "7da06f37c0572d5a8af8e0a5d6a81bea18c3ce1c08ee7501668570fc70a7d033", + "TwinCAT.Ads.dll": "2a9c2ca91889e24f088123ce313fc96060897b9e34232022b0566dfd2ea32a0d", + "ZB.MOM.WW.Audit.dll": "8dfda63f20ac0a34c43716f593bcad60755daf71ae4aefbf7ab1f34a0951de11", + "ZB.MOM.WW.Auth.Abstractions.dll": "24099d4d38811a364569cba4924af2430fe1e8ad8d3e735bd840dbfaa548cef8", + "ZB.MOM.WW.Auth.AspNetCore.dll": "ac062c1f634055ad9b80bd13344481318d6f45ac45f40dbaeed861ee20a75984", + "ZB.MOM.WW.Auth.Ldap.dll": "8625dea672beff26bd8f4f2aa54307cf652e50ad66a2c6625bb27d282199c32d", + "ZB.MOM.WW.Configuration.dll": "ad89f1d2abcad33be99b03bca37b26a9b92f90c354e98a99928d6176b12d58a7", + "ZB.MOM.WW.Health.Akka.dll": "a1345d712159544954955b41e7d04bc35180e1b9b36fdddcaedff5821a09d9c1", + "ZB.MOM.WW.Health.EntityFrameworkCore.dll": "d4fa98ca71512d1927037db22477facf49472249854898fca55c49c726307693", + "ZB.MOM.WW.Health.dll": "3c95dff17440b9df9195092812949b6e16e0c02456953650a839963af66df633", + "ZB.MOM.WW.HistorianGateway.Client.dll": "a4567376dba7d63f566aab1eea7509832c82ec8bb66c89575d85041ef370d46f", + "ZB.MOM.WW.HistorianGateway.Contracts.dll": "d5cd87ee5279b1bc532c6508dcd227af4bf883819b2f3a54dfec7d799c944547", + "ZB.MOM.WW.LocalDb.Contracts.dll": "d4da2a5bdfd3ebcb56f7ec948cda12bf378dc34079cd20e864509902a93bed38", + "ZB.MOM.WW.LocalDb.Replication.dll": "234e31795fd0641f8f873f9ce9641d7d4b028be724463abdafb0f119dc18d800", + "ZB.MOM.WW.LocalDb.dll": "ac243f94af069faa0b171556a6e24bd5140237f275f3548c37789993c221ee6c", + "ZB.MOM.WW.MxGateway.Client.dll": "0c058c2827829d54a8556308e4ed552f1cb0011b863c26dafd40cef7bff5c42d", + "ZB.MOM.WW.MxGateway.Contracts.dll": "af97aa90aa944afd6e7613d5bbdbe85cd43c6f33f2bcdcf74b1ef5d854369775", + "ZB.MOM.WW.OtOpcUa.AdminUI.dll": "1ae11959c22bc53a648b91c6c1ae41e4a5d717e396bd645eae4fb08dc1633597", + "ZB.MOM.WW.OtOpcUa.Cluster.dll": "d6bc21641172d832004b62b8bb76aeb40622a943be2172f394e5c6333c04ef57", + "ZB.MOM.WW.OtOpcUa.Commons.dll": "be97a0b975afde7ec4ebf0b10de53c26f212ceca1f6a3ae090e40ebfde304413", + "ZB.MOM.WW.OtOpcUa.Configuration.dll": "195dab9f7a9f80c45902e653ff361116a307d8110a3f87baad814bea57689b84", + "ZB.MOM.WW.OtOpcUa.ControlPlane.dll": "36954f51b09930df414675e5a0c038933ad2b27cd6560bf797d99801c02b30a1", + "ZB.MOM.WW.OtOpcUa.Core.Abstractions.dll": "b0f4e332ced57eae3ab1dcc282c073f51ea0c545a381aef50479ee58f0c23efc", + "ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.dll": "a61a6ea68236dd4ed86ab4e74e4d632c5e260103aa5ebd92f4f995eb9f170b22", + "ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.dll": "f1e429c0b8fcab8216b9107440008b92eea1236ff1225e79580ad70f51d56431", + "ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions.dll": "ea24898698c8351bc9cf20fcce725dccc93ccf05ef3af7783f73854cc2033c31", + "ZB.MOM.WW.OtOpcUa.Core.Scripting.OutOfProcess.dll": "cf44009a720a00bf9192374d371c7c76e75afbe62cc216b5c1be913b92679127", + "ZB.MOM.WW.OtOpcUa.Core.Scripting.Protocol.dll": "07800332d0842b93c3987fca1dc10a1390bcb16f44567610d867513ad9fe17e0", + "ZB.MOM.WW.OtOpcUa.Core.Scripting.dll": "9637b7ffb4984f54bc54363ff2d81b6a3401f0cb7eeed8694db626ef41d54d5b", + "ZB.MOM.WW.OtOpcUa.Core.VirtualTags.dll": "107aaf2ef2d7704e320dabc1072bc50217faabb2d1488e66c91b1f756b440c45", + "ZB.MOM.WW.OtOpcUa.Core.dll": "34a4d7c0dddf4efa5fae91a3798c2f1c08b992faf4a827a9e05ed1338c6e4720", + "ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.dll": "315dcc540d304c9f1d5b48caa25e5f081569f1a41aa8784cf371c1d6cef7322d", + "ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts.dll": "d7b34798587d0939cc78eb69d9df1d617c1c75a61b8dd11a5308958a43a8c4f1", + "ZB.MOM.WW.OtOpcUa.Driver.AbCip.dll": "73073627fcdc8079df645478d6717576975117c3b21cfa7e9422f1e3bc5f4e44", + "ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts.dll": "5c66d76dd455c42f0c5b0aed11240fa31e1bdbd2f0a8368d8f41976977e93502", + "ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.dll": "cbf92a935d86596a9d4dbeeaa625e304c2045851a0367c3a6b0de03bf78bd6fa", + "ZB.MOM.WW.OtOpcUa.Driver.Calculation.Contracts.dll": "29c079414497b30debe8d3ecd7e77bdc4eb16c4fc076e675680a2e880e54b6c2", + "ZB.MOM.WW.OtOpcUa.Driver.Calculation.dll": "132e36042a4691cc8e252ab9e9d8c90592fcd897959f533aa37e33f193cf00c8", + "ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts.dll": "2bdbf743194193d5a7bc323912e2c7f740cc21633154a4a3cc8e034fd15bb7e8", + "ZB.MOM.WW.OtOpcUa.Driver.FOCAS.dll": "17c28f27fdecd09a46b971b19655bdb933f84990e91bb80c7092c344a0ca974f", + "ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser.dll": "f2796d323c222708c28cd0a4289c46db31ec3b6537da7f5cd260fc76f8abdc76", + "ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts.dll": "780a11f69194713e2f87e74277e99882ee3518b2824551b45cd04e3b9bdf6648", + "ZB.MOM.WW.OtOpcUa.Driver.Galaxy.dll": "a8e19baa5a736544d914f942d526a5cc038f6b982451bfa82a77365a159434e4", + "ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.dll": "400b1896e55d08781231efeba302596299df2b00b2f48afd43d46c22d59b515c", + "ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.dll": "8e91a8cce804023639a4bdb2dc60f9fc950992882e50786d45ceeb0f38ed1f68", + "ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.dll": "2746b8e9c03b5b8c330152803eee91cf2069f0e6c074d70d03017b1f1f1a2398", + "ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.dll": "94b9bcd4506427a44158e0b299a5ae7bcee14fd40b85cc9f63420e4631aa1a6f", + "ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts.dll": "6582151085ddea7d319b990c2c58057b6ba5a49860593229b3642dff527ae204", + "ZB.MOM.WW.OtOpcUa.Driver.MTConnect.dll": "c9670c5d169056563294673839ca63321a72a438c519263be708b55f3fc818f9", + "ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.dll": "ed07d04709372ef804f465995431b15acdef791f66c8130ab7282372728f7cda", + "ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts.dll": "724873ac2f6c08f307a150c5575cc3c022d7e367124ed924c0c7899b0e9575a5", + "ZB.MOM.WW.OtOpcUa.Driver.Modbus.dll": "db1e3e7c79dc1a46e1d4ffaa5ea1aac9ed73814d7f06cbfa855ebb3b27a8810d", + "ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser.dll": "ace18db746c30307e67efa3acd68e915cc2b62b0a85bdba833df4ea7c85b5f6c", + "ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts.dll": "24e795bd8027d7b3ca3d95630b73be6d7cef774243357b01850f145b6066dcb9", + "ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Transport.dll": "bec0cecc0bbb06b4715d7b909ccf4d357420e120b2bff3c10ee9f59f71044927", + "ZB.MOM.WW.OtOpcUa.Driver.Mqtt.dll": "da64eacb80ddca9cd0a40295bba2c8ff81ee9c68ccaf32cbd329ae854375deb7", + "ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.dll": "7e2ac501136d24a3d0ad6bdae2e4a47460439ad8ab78af538bbccca2e38e5695", + "ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.dll": "c15e6db37681cbbef8684d5301e1bba54186ae503aaf55d1c2d06a0567ab28fe", + "ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session.dll": "910604abd4daa6196c8fdcd2db085873ee5a4c27b5a67c359590071117c6e4dd", + "ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.dll": "cd689b0ec777820d91f914abfd9e4a921143a46df84ee43d0214c66f74225ac0", + "ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts.dll": "6fe895674318dc42c0c3cc74266831862d3fe2f13098c26e7d826baf79d7fc47", + "ZB.MOM.WW.OtOpcUa.Driver.S7.dll": "d23a7ac62d781f0c14dd7c9d4f51b712c13599ee78b9b6d302d30dfd5e77da37", + "ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.dll": "c341d49bf100e0617d9c4e7d24f63d8affd1bc0fe2edce15e51cc14366fa2207", + "ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts.dll": "f0a0cc852116f739247422a0427550e52c938b27d1a8b4e685f45a19a23635ab", + "ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect.dll": "7d78655da50c07ac049cd687c59f2a7b6eb2c491543bd4089ffbe60fb805205f", + "ZB.MOM.WW.OtOpcUa.Driver.Sql.dll": "6ec1a9bc7443c1d7b8084ee2fc5e67cbfd2a8d61add303a980cef78a46cb5c67", + "ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts.dll": "96db9e5d162b19914cf0ba55c1408c1c9271c80c1a1a12604e684f5785de4b96", + "ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.dll": "c24cfc897bde6c0eb65b9958bad952814ed02a27fe25a55c416c1ba61be2c261", + "ZB.MOM.WW.OtOpcUa.OpcUaServer.dll": "ee1f704fa1574b77bf56cef415c47c33a4428c6852db52bb1443d6765d092cb1", + "ZB.MOM.WW.OtOpcUa.Runtime.dll": "50e752383ae81ef6bb23e360fafe9162be0d1dcb7dd2d8731f16bce762834dd4", + "ZB.MOM.WW.OtOpcUa.Security.dll": "a88b1577d1e4dd580e012e7673e31d5363c3d9a79fb415bef4c3d63849678a4d", + "ZB.MOM.WW.Secrets.Abstractions.dll": "3a39c92e9a96a305bd229e99d2f59f2c273f8d08c03e431d47e24bd8b8b0ef49", + "ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.dll": "d6a3780f3487f5c8432fe12c057defb709d1dc7678c59307acd19b4cb0d6bdb7", + "ZB.MOM.WW.Secrets.Ui.dll": "782f903b42bce46fb3de827e24d1edcbe41187976b6918c740383de158e35064", + "ZB.MOM.WW.Secrets.dll": "0565fd86abead89be7b4520dddf92cabd5d0994f3620069aa496b8dfa41c9f39", + "ZB.MOM.WW.Telemetry.Serilog.dll": "bd33ce7657de5d2966a7768a014f8282ca434a208b05defa2d27bf24c6f124c5", + "ZB.MOM.WW.Telemetry.dll": "834772ecc662ea7c49a5b512a26f2447db55a072ef1fad16ca768fbee0cfe853", + "ZB.MOM.WW.Theme.dll": "12bf7d7ec7a618acdf9f5bff9a7c67091ce715698d3befb9aa610cc5c8b1b221", + "libplctag.NativeImport.dll": "dec38f56006ae9e2119e9f61f9ff0e1aa9c08503ba4f42943c2d7632f60cbad5", + "libplctag.dll": "1545b5813148d7ade7662282683e15d2761e0e34cb853bafa3b11a4d47795aa4", + "CliFx.dll": "af06268aba8215c17b300fe8ec81bbef8d9982b5958a3291563f504537ce844d", + "Microsoft.Extensions.DependencyInjection.Abstractions.dll": "ca708422782fb25819941a16a057ea96d6d18b037e7e5cbfdf340b7a04362b57", + "Microsoft.Extensions.DependencyInjection.dll": "47f8175d34e9087d3858644803c558cb9cf52b29bffda796f65c1c527d1b6a22", + "Microsoft.Extensions.Logging.Abstractions.dll": "ec244a2db19dffe90cd299fa4b0691456ced342a078b4a93cf3726a9b431e9ac", + "Microsoft.Extensions.Logging.dll": "74d8fd2d7997fc638474e2bd79916458a30481e6a6925b50e7e96b4b852031c3", + "Microsoft.Extensions.Options.dll": "644350f8c2404d4479b97a250edb29585e5fafec1b6ce8486ae600c5015fd7fb", + "Microsoft.Extensions.Primitives.dll": "a0794c3bab2d7e0c848f0dffb3b9aa4b43ce07dc0abd122ebb7bf6dc79452ec4", + "ZB.MOM.WW.OtOpcUa.Client.CLI.dll": "38ff27df4c1389d2cf97a7ec1fe206733f02c3664b29c4fe18586ede225a53ca", + "ZB.MOM.WW.OtOpcUa.Client.Shared.dll": "326e14df7bf2a7602637fbff36988c3dd4be38de936da24a1b4287c0ab685be6", + "Shouldly.dll": "498dad657f324ab068e800d5711ae36feb4af4ba9a572eb941197e2fb677549d", + "DiffEngine.dll": "0c6b99a47c609a4263b80feaed95a357e30be3ad5a1cda79355663e407682aa7" + }, + "runs": { + "initial-comparison": { + "inputSha256": "4f6be7a2c8588537340cd278abd463c5fe0990a19f6c91832836d0d965749fb7", + "rows": [ + { + "kind": "start", + "sdk": "1.5.378.182+6cca32dd6d79c8b57d2e00e4d2071b991d2cf71b", + "productRebuilt": false, + "sourcePublication": "83ed880e", + "requestedCycles": 4, + "acceptance": false, + "scope": "canonical operations short smoke; ownership differences remain explicit", + "unsupportedObservables": [ + "SDK2 CertificateManager disposed flag", + "SDK2 manager-owned cache-clear proof", + "ApplicationInstance.DisposeAsync" + ] + }, + { + "kind": "active", + "cycle": 1, + "sessions": 2, + "subscriptions": 1, + "items": 3, + "browsePoints": 1, + "historyPoints": 6 + }, + { + "kind": "network-stall-recovered", + "cycle": 1, + "status": "2156134400", + "seconds": 1.0064679 + }, + { + "kind": "failed", + "cycles": 0, + "restarts": 0, + "exceptionType": "Shouldly.ShouldAssertException", + "uaStatus": null, + "ledger": { + "address-space-rebuild": 1, + "authority-restore": 3, + "browse-next.attempted": 1, + "browse-next.returned": 1, + "browse.attempted": 2, + "browse.returned": 2, + "configure": 2, + "history-read.attempted": 10, + "history-read.returned": 10, + "host-start": 1, + "key-revoke": 1, + "network-pause": 1, + "read-service.attempted": 1, + "read-service.returned": 1, + "read-value.attempted": 5, + "read-value.faulted": 1, + "read-value.returned": 4, + "session-close.attempted": 2, + "session-close.returned": 2, + "session-create.attempted": 2, + "session-create.returned": 2, + "subscription-apply.attempted": 1, + "subscription-apply.returned": 1, + "subscription-create.attempted": 1, + "subscription-create.returned": 1, + "subscription-delete.attempted": 1, + "subscription-delete.returned": 1, + "subscription-remove.attempted": 1, + "subscription-remove.returned": 1, + "variable-remove": 1, + "writable-off": 1, + "writable-on": 1, + "write.attempted": 1, + "write.returned": 1 + }, + "acceptance": false + } + ] + }, + "fixed-after-close-observation": { + "inputSha256": "7c6a7a45360b294c80c968464d935e7eda2e8a8c496c2541d85dda45f9abcd5f", + "rows": [ + { + "kind": "start", + "sdk": "1.5.378.182+6cca32dd6d79c8b57d2e00e4d2071b991d2cf71b", + "productRebuilt": false, + "sourcePublication": "83ed880e", + "requestedCycles": 1, + "acceptance": false, + "scope": "canonical operations short smoke; ownership differences remain explicit", + "unsupportedObservables": [ + "SDK2 CertificateManager disposed flag", + "SDK2 manager-owned cache-clear proof", + "ApplicationInstance.DisposeAsync" + ] + }, + { + "kind": "active", + "cycle": 1, + "sessions": 2, + "subscriptions": 1, + "items": 3, + "browsePoints": 1, + "historyPoints": 6 + }, + { + "kind": "network-stall-recovered", + "cycle": 1, + "status": "2156134400", + "seconds": 1.00373 + }, + { + "kind": "closed-session-observation", + "cycle": 1, + "activeSessions": 0, + "browsePoints": 1, + "historyPoints": 6, + "diagnosticReferenceHeld": true + }, + { + "kind": "closed-session-after-fixed-observation", + "cycle": 1, + "observationMilliseconds": 250, + "browsePoints": 1, + "historyPoints": 6 + }, + { + "kind": "failed", + "cycles": 0, + "restarts": 0, + "exceptionType": "Shouldly.ShouldAssertException", + "uaStatus": null, + "ledger": { + "address-space-rebuild": 1, + "authority-restore": 3, + "browse-next.attempted": 1, + "browse-next.returned": 1, + "browse.attempted": 2, + "browse.returned": 2, + "configure": 2, + "cycle-wire-body-completed": 1, + "history-read.attempted": 10, + "history-read.returned": 10, + "host-start": 1, + "key-revoke": 1, + "network-pause": 1, + "read-service.attempted": 1, + "read-service.returned": 1, + "read-value.attempted": 5, + "read-value.faulted": 1, + "read-value.returned": 4, + "session-close.attempted": 2, + "session-close.returned": 2, + "session-create.attempted": 2, + "session-create.returned": 2, + "subscription-apply.attempted": 1, + "subscription-apply.returned": 1, + "subscription-create.attempted": 1, + "subscription-create.returned": 1, + "subscription-delete.attempted": 1, + "subscription-delete.returned": 1, + "subscription-remove.attempted": 1, + "subscription-remove.returned": 1, + "variable-remove": 1, + "writable-off": 1, + "writable-on": 1, + "write.attempted": 1, + "write.returned": 1 + }, + "acceptance": false + } + ] + }, + "isolated-active-restart": { + "inputSha256": "503243eb7b4c23c3a287754b0c841d3c7a97331433211317d318c67d337359cc", + "rows": [ + { + "kind": "start", + "sdk": "1.5.378.182+6cca32dd6d79c8b57d2e00e4d2071b991d2cf71b", + "productRebuilt": false, + "sourcePublication": "83ed880e", + "requestedCycles": 1, + "acceptance": false, + "scope": "canonical operations short smoke; ownership differences remain explicit", + "unsupportedObservables": [ + "SDK2 CertificateManager disposed flag", + "SDK2 manager-owned cache-clear proof", + "ApplicationInstance.DisposeAsync" + ] + }, + { + "kind": "original-validator-after-stop", + "cachedStoreReferencesBefore": 1, + "cachedStoreReferencesAfter": 1, + "disposalFlagObservable": false, + "reason": "SDK1.5 has no CertificateManager disposal API or disposed flag" + }, + { + "kind": "isolated-active-restart-completed", + "normalCycles": 0, + "restarts": 1, + "acceptance": false, + "ownershipParity": false, + "ledger": { + "authority-restore": 2, + "browse.attempted": 1, + "browse.returned": 1, + "configure": 2, + "history-read.attempted": 1, + "history-read.returned": 1, + "host-start": 2, + "host-stop": 1, + "session-create.attempted": 1, + "session-create.returned": 1, + "subscription-apply.attempted": 1, + "subscription-apply.returned": 1, + "subscription-create.attempted": 1, + "subscription-create.returned": 1 + } + } + ] + } + }, + "unsupportedObservables": [ + "SDK2 manager disposed flag", + "SDK2 manager-owned cache-clear proof", + "ApplicationInstance.DisposeAsync" + ], + "blocker": "No approved cross-version closed-session/point/certificate ownership observable; zero old closed-list counts are not the old SDK close contract. No denominator approval." +} diff --git a/scripts/validation/opcua-lifecycle-memory/baseline-canonical/run_smoke.py b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/run_smoke.py new file mode 100644 index 00000000..c86c9f73 --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/baseline-canonical/run_smoke.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Run one bounded original-only diagnostic control; preserve failures and unsupported exit 2.""" +import argparse +import json +import os +from pathlib import Path +import secrets +import subprocess + +parser=argparse.ArgumentParser(description=__doc__) +parser.add_argument('--adapter',type=Path,required=True) +parser.add_argument('--output',type=Path,required=True) +parser.add_argument('--mode',choices=['normal','active-restart'],required=True) +args=parser.parse_args() +output=args.output.resolve() +if output.exists():raise RuntimeError('Use a fresh private output directory') +log=output.with_suffix('.log') +environment=os.environ.copy() +environment['OTOPCUA_BASELINE_CANONICAL_KEY']='otua_k-server-events_'+secrets.token_hex(3) +command=['dotnet',str(args.adapter.resolve(strict=True)),str(output),'1'] +if args.mode=='active-restart':command+=['--active-restart-control'] +with os.fdopen(os.open(log,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600),'w') as stream: + try:result=subprocess.run(command,env=environment,stdout=stream,stderr=subprocess.STDOUT,timeout=120);exit_code=result.returncode + except subprocess.TimeoutExpired:exit_code=124 +if output.exists(): + (output/'runner-receipt.json').write_text(json.dumps({'mode':args.mode,'exitCode':exit_code,'acceptance':False, + 'productRebuilt':False,'timeoutSeconds':120,'credentialBytes':len(environment['OTOPCUA_BASELINE_CANONICAL_KEY'].encode())},indent=2)+'\n') +print(json.dumps({'mode':args.mode,'exitCode':exit_code,'acceptance':False})) +raise SystemExit(exit_code)