Mirror VirtualTagHostActor's _nodeIdByVtag pattern for driver values: a shared
EquipmentNodeIds helper (kills the duplicated formula), DriverInstanceId on
AttributeValuePublished, and a (DriverInstanceId,FullName)->NodeId[] map built +
resolved in DriverHostActor.ForwardToMux. No OpcUaPublishActor change.
Driver-value delivery only; native alarms + historian remain separate.
Replace "SystemPlatform mirror tag", "Galaxy alias", and "SystemPlatform-kind" in doc-comments and
test names with neutral accurate wording ("FolderPath-scoped tag", "EquipmentId == null", etc.).
No code, logic, or test bodies changed — comments and one test method name only.
Resolves the code-review notes on 95be607a + the AdminUI bundle: the
EnsureVariable docs (IOpcUaAddressSpaceSink, OtOpcUaNodeManager) and the Tag
entity doc no longer say 'Galaxy / SystemPlatform / alias'; the DriverHostActor
ForwardToMux comment now states the real equipment-tag value-routing gap (the
FullName→NodeId 'live values' milestone) instead of claiming Galaxy values map
straight through.
18-task plan to make GalaxyMxGateway an Equipment-kind driver: retire the
SystemPlatform NamespaceKind split + mirror + alias/relay machinery, author
Galaxy points as ordinary equipment tags via the standard TagModal. Mostly
deletion + a single EF migration dropping the per-kind unique constraint.
Phases B (native alarms) + C (server historian) remain out of scope.
Co-located .tasks.json for resume.
Brainstorming-approved design to normalize GalaxyMxGateway into the standard
Equipment-driver model: retire the SystemPlatform/Equipment namespace split +
the SystemPlatform mirror + the alias-tag/relay machinery, author Galaxy points
as ordinary equipment tags, port native IAlarmSource alarms onto the
equipment-tag materialization path, and add a driver-agnostic server-side
HistoryRead backend (over the existing Wonderware Historian reader). Three
phases (A de-split + UI, B native alarms, C historian); clean break, no
migration converter; one EF migration to drop NamespaceKind.
The net48 sidecar's TcpFrameServer.RunOneConnectionAsync registered the
cancellation token to Stop() only the listener (to unblock a parked
AcceptTcpClientAsync), but never closed the active client. On net48
NetworkStream.ReadAsync ignores the CancellationToken, so while the frame
loop is parked reading an idle connected client, cancelling the token cannot
unblock it — only closing the socket can. RunAsync therefore never returned
on Ctrl-C/service-stop while a connection was open (Program.Main's
RunAsync().GetAwaiter().GetResult() would hang until NSSM force-killed).
Register the cancel to Close() the active client, and convert the resulting
cancel-time read/handshake exception to OperationCanceledException so RunAsync
unwinds cleanly without logging it as a connection failure or counting it
toward MaxConsecutiveFailures.
Caught by the first-ever net48 execution of TcpRoundTripTests on the Windows
VM (these only compile on macOS): SingleActive_SecondClientHelloCompletesOnly
AfterFirstCloses deadlocked in teardown. Full net48 historian suite now green
(122 passed, 0 failed, 2 skipped); all 6 TcpRoundTrip tests pass.
Live verification on a Windows VM surfaced a crash loop: TcpFrameServer.EnsureListening
assigned _listener = new TcpListener(...) BEFORE calling Start(). When Start() throws —
e.g. the port is in a Windows excluded/reserved range (WSAEACCES) or already in use — the
field was left non-null-but-unstarted, so the `if (_listener is not null) return` guard
permanently skipped re-Start() and every subsequent AcceptTcpClientAsync() threw the
misleading InvalidOperationException "Not listening" → 20 failures → exit 2 → NSSM restart
→ loop. Now _listener is assigned only after Start() succeeds, so a transient bind failure
is retried and a permanent one surfaces the real bind error each iteration. Adds a
regression test that forces a bind conflict and asserts the SocketException persists.
Replace OTOPCUA_HISTORIAN_PIPE/OTOPCUA_ALLOWED_SID with TCP transport
env (OTOPCUA_HISTORIAN_TCP_PORT, OTOPCUA_HISTORIAN_BIND,
OTOPCUA_HISTORIAN_TLS_ENABLED, OTOPCUA_HISTORIAN_TLS_CERT/PASSWORD)
in Install-Services.ps1; add idempotent Windows Firewall inbound rule
for the TCP port. Add new params for all TCP/TLS options with cert
provisioning guidance. Update Refresh-Services.ps1 Step 4b comment
(PipeServer → TcpFrameServer) and add a Step 5 note clarifying that
TCP/TLS env is set at install time, not on refresh.
Add a post-publish Step 4b that fails the refresh if the sidecar deploy
folder is missing any load-bearing file (System.Memory.dll, MessagePack.dll,
the .exe, or the net48 binding-redirect .exe.config). A partial/stale deploy
that bypasses `dotnet publish` drops System.Memory.dll, so the sidecar
JIT-load-faults in PipeServer.RunOneConnectionAsync (FileNotFoundException
'System.Memory, Version=4.0.1.2') and NSSM crash-loops it (exit 2 every
~2 min). Catching this at publish time beats discovering it as a production
crash-loop. Guarded by -WhatIf; throws before any service is started.
All five suppressed advisories are now resolved at baseline/resolved versions,
so every NuGetAuditSuppress is removed repo-wide:
- System.Security.Cryptography.Xml (GHSA-37gx-xxp4-5rgx / GHSA-w3x6-4m5h-cxqf)
-> fixed by the .NET 10 baseline (10.0.6)
- OPCFoundation Opc.Ua.Core (GHSA-h958-fxgg-g7w3) -> fixed at resolved 1.5.378.106
Two were still live and are now patched via direct security pins:
- OpenTelemetry.Api 1.9.0 -> 1.15.3 (GHSA-g94r-2vxg-569j) pinned in Cluster;
Runtime/ControlPlane/AdminUI + tests inherit via project reference
- Tmds.DBus.Protocol 0.20.0 -> 0.21.3 (GHSA-xrw6-gwf8-vvr9) pinned in Client.UI
Also correct the Historian sidecar runtime comments (x86 -> x64, matching the
csproj PlatformTarget). Solution audit: 0 vulnerable packages; full build clean.
LZ4-decompression out-of-bounds read in MessagePack < 2.5.301 (transitive
via the Historian.Wonderware driver). 2.5.301 is the patched v2-series
release; stays on v2 (no v3 major bump). Restore no longer fails NU1903.