Files
lmxopcua/scripts/validation/opcua-lifecycle-memory/windows
Joseph Doherty aa60b3e34e
v2-ci / unit-tests (push) Has been cancelled
test(soak): #799 place the soak's UNS folder after #808; Windows x64 port of the lifecycle soak harness
Acceptance runs for #799 (owner scope 2026-11-12: Windows x64 + Linux) on current master.

Fixture: since #808 an UNS folder the authority snapshot does not place is denied under enforcement (fail
closed). The canonical soak creates its own "SoakEquipment" folder but never placed it, so on master the very
first cycle's UNS monitored item came back bad (retained failed Linux launch on b61f3e05, cycle 1). The rig
gains AdditionalFolderScopes (merged into FolderScopes by Enforce) and the soak places its folder there, as a
deployed generation does - the same fixture gap #888 fixed for the SDK matrix. No server authorization code,
cap, baseline or canonical operation changed; the soak test bodies are otherwise byte-identical to b18f9723.

Windows port (test/harness only, opt-in):
- DescriptorCountsAsync gains a Windows branch: total = every handle in the process handle table, PKI = disk
  file/directory handles whose GetFinalPathNameByHandle path lies under a canonicalised PKI root; dedicated
  thread under the same 5 s bound as the macOS lsof probe.
- Descriptor controls enabled on Windows (symlink alias; timeout control via cmd.exe + ping).
- LateSlopeRun.ValidateRequest accepts Linux ARM64 or Windows x64; duration/runtime/leak tracking unchanged.
- late-slope/analyze.py: expected architecture is a module constant (Linux default Arm64 unchanged).
- windows/: prepare.py (win-x64 self-contained 10.0.14 bundle + manifest), launch.ps1 (container.sh
  equivalent: bundle re-verification, 2/2 controls first, PID/apphost/coreclr verification, external
  sampler, 7440 s outer deadline), analyze.py (Linux row/window logic, X64), README with every adaptation.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
2026-11-12 12:42:16 -05:00
..

Windows x64 late-slope soak (lmxopcua#799 acceptance)

Owner scope 2026-11-12: #799 acceptance covers the deployment platforms, Windows x64 and Linux. This folder runs the same canonical test as the Linux late-slope run (Sdk2LifecycleSoakTests.Repeated_lifecycles_return_resources_to_a_steady_baseline, OTOPCUA_SDK2_SOAK_MODE=late-slope, 7,200 s) on Windows x64 with the protocol unchanged: cycle-3 cold baseline, +64 MiB retained managed / +128 MiB RSS / +32 descriptors, first-rejection latch with every canonical cycle and restart continuing, ownership/logical/operation/shutdown failures fatal, certificate leak tracking on, the same startup/cycle/restart/shutdown bounds, and the same fixed windows and least-squares report (analyze.py imports ../late-slope/analyze.py and only switches its expected architecture to X64).

Windows adaptations

Linux element Windows equivalent Why
RSS = Process.WorkingSet64 (= VmRSS) Working set, the same Process.WorkingSet64 call, unchanged test code Resident physical pages of the process, the direct analog of Linux RSS. Private bytes (commit charge) counts committed-but-not-resident memory and pagefile-backed pages, so it is not comparable to RSS; the launcher records it externally every 30 s as a descriptive series only.
Descriptors = entries of /proc/self/fd Every handle in the process handle table (NtQuerySystemInformation(SystemExtendedHandleInformation), own PID) Windows has no fd table; the handle table is the per-process kernel-resource table. It is broader than Linux fds (threads, events, keys, sections), so the unchanged +32 cap is if anything stricter.
PKI descriptors = fds whose link target lies under a PKI root Disk-file/directory handles (GetFileType = DISK) whose GetFinalPathNameByHandle path lies under a root; roots canonicalised the same way, compared case-insensitively Final paths resolve symlinks, junctions, 8.3 names and casing (the Windows analog of the macOS /var→/private/var lesson).
Probe bound: lsof child 5 s Enumeration on a dedicated thread, 5 s bound; a blocked name query is a TimeoutException, never a favourable zero Name queries can block on synchronous handles; only disk handles are name-queried.
Descriptor controls (symlink alias open/close; probe timeout reaps /bin/sh) Same two tests, Windows enabled: Directory.CreateSymbolicLink alias (Developer Mode / elevated); the timeout control runs cmd.exe holding both pipes open behind a 30 s ping Must execute 2/2 before the canonical test starts, as in container.sh.
Container: read-only root, --network none, private /task Fresh private task directory (inheritance removed; user + SYSTEM) holding TEMP/TMP, CLI home, results and fixture PKI; loopback-only fixture; machine otherwise idle No Windows container runtime on the deployment-class host.
/proc/<pid>/exe + mapped libcoreclr.so check Target PID's image must be the bundle …IntegrationTests.exe and coreclr.dll must be mapped from the bundle with the receipt hash Same proof that the self-contained 10.0.14 runtime is the one measured.
Container outer deadline 7,440 s Launcher kills only the task-owned process tree at 7,440 s Same bound.
— External sampler: working set, private bytes, handle count, threads every 30 s; MxAccessGw readiness every 5 min Descriptive diagnostics and proof the co-hosted service stayed healthy. Not gated.

LateSlopeRun.ValidateRequest accepts Linux ARM64 or Windows x64 (owner scope); the 7,200 s duration, runtime 10.0.14 and leak-tracking requirements are unchanged.

Run

python3 scripts/validation/opcua-lifecycle-memory/windows/prepare.py --source-revision "$rev" --output "$bundle"
# copy the bundle to the Windows host (COPYFILE_DISABLE=1 tar on macOS), then on the host:
powershell -NoProfile -ExecutionPolicy Bypass -File "$bundle\tools\launch.ps1" -Bundle "$bundle" -Output "$run" -SourceRevision "$rev"
# copy the run back, then:
python3 "$bundle/tools/analyze.py" --run "$run" --bundle-receipt "$bundle/receipt.json" --source-revision "$rev"

Launch the PowerShell launcher detached from any SSH session (e.g. Win32_Process.Create), because Win32-OpenSSH terminates a session's job object on disconnect.