aa60b3e34e
v2-ci / unit-tests (push) Has been cancelled
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 onb61f3e05, 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 tob18f9723. 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
107 lines
5.6 KiB
Python
107 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Publish and record a fresh immutable Windows x64 bundle of the canonical soak (lmxopcua#799) without running it.
|
|
|
|
The Windows counterpart of ../late-slope/prepare.py: same committed-source checks, same self-contained .NET 10.0.14
|
|
runtime, same exact SDK 2.0.1 / HistorianGateway 1.0.0 graph, same per-file SHA-256 manifest and managed MVIDs. Only
|
|
the RID (win-x64), the executable format check (PE32+ AMD64) and the absence of a Docker image differ.
|
|
"""
|
|
|
|
import argparse
|
|
from datetime import datetime, timezone
|
|
import json
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
sys.dont_write_bytecode = True
|
|
HERE = Path(__file__).resolve().parent
|
|
sys.path.insert(0, str(HERE.parent / "late-slope"))
|
|
|
|
from analyze import APP, RUNTIME, digest, read_object, require # noqa: E402
|
|
from bundle import CANONICAL_SOURCES, GATEWAY_PACKAGES, SDK_PACKAGES, check_modules, manifest # noqa: E402
|
|
import prepare as linux_prepare # noqa: E402
|
|
|
|
REPO = HERE.parents[3]
|
|
PROJECT = REPO / "tests/Server" / APP / (APP + ".csproj")
|
|
RID = "win-x64"
|
|
TOOLS = ("launch.ps1", "analyze.py", "README.md")
|
|
WINDOWS_SOURCES = (f"tests/Server/{APP}/Sdk2LifecycleWindowsDescriptors.cs", f"tests/Server/{APP}/ServerEventRig.cs")
|
|
|
|
|
|
def check_pe_amd64(path):
|
|
data = path.read_bytes()[:4096]
|
|
require(data[:2] == b"MZ", f"{path.name} is not a PE image")
|
|
offset = int.from_bytes(data[0x3C:0x40], "little")
|
|
require(data[offset:offset + 4] == b"PE\0\0" and data[offset + 4:offset + 6] == b"\x64\x86", f"{path.name} is not AMD64")
|
|
|
|
|
|
def check_application(app):
|
|
check_pe_amd64(app / f"{APP}.exe")
|
|
check_pe_amd64(app / "coreclr.dll")
|
|
options = read_object(app / f"{APP}.runtimeconfig.json")["runtimeOptions"]
|
|
frameworks = options["includedFrameworks"]
|
|
require(options["tfm"] == "net10.0" and "framework" not in options and "frameworks" not in options
|
|
and any(value["name"] == "Microsoft.NETCore.App" for value in frameworks)
|
|
and all(value["version"] == RUNTIME for value in frameworks), "Wrong self-contained application runtime")
|
|
dependencies = read_object(app / f"{APP}.deps.json")
|
|
require(dependencies["runtimeTarget"]["name"] == f".NETCoreApp,Version=v10.0/{RID}", "Wrong application RID")
|
|
libraries = dependencies["libraries"]
|
|
sdk = {key for key in libraries if key.startswith("OPCFoundation.")}
|
|
gateway = {key for key in libraries if key.startswith("ZB.MOM.WW.HistorianGateway.")}
|
|
require(sdk == SDK_PACKAGES and gateway == GATEWAY_PACKAGES, "Expected exact SDK 2.0.1 and HistorianGateway 1.0.0 graph")
|
|
return sorted(sdk), sorted(gateway)
|
|
|
|
|
|
def committed_sources(revision):
|
|
sources = linux_prepare.check_source(revision)
|
|
for name in (*WINDOWS_SOURCES, *(f"scripts/validation/opcua-lifecycle-memory/windows/{tool}" for tool in TOOLS),
|
|
"scripts/validation/opcua-lifecycle-memory/windows/prepare.py"):
|
|
sources[name] = digest(REPO / name)
|
|
return sources
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
parser.add_argument("--source-revision", required=True)
|
|
options = parser.parse_args()
|
|
sources = committed_sources(options.source_revision)
|
|
root = options.output.resolve()
|
|
root.mkdir(mode=0o700, parents=True, exist_ok=False)
|
|
with tempfile.TemporaryDirectory(prefix="opcua-windows-soak-publish-") as scratch:
|
|
build = Path(scratch)
|
|
command = ["dotnet", "publish", str(PROJECT), "-c", "Release", "-r", RID,
|
|
"--self-contained", "true", f"-p:RuntimeFrameworkVersion={RUNTIME}",
|
|
"-p:PublishSingleFile=false", "-p:PublishTrimmed=false", "-p:UseAppHost=true",
|
|
"--artifacts-path", str(build / "artifacts"), "-o", str(root / "app"), "-m:2", "--nologo"]
|
|
with (root / "publish.log").open("w", encoding="utf-8") as log:
|
|
subprocess.run(command, cwd=REPO, stdout=log, stderr=subprocess.STDOUT, check=True, timeout=900)
|
|
sdk, gateway = check_application(root / "app")
|
|
modules = linux_prepare.managed_modules(build, root)
|
|
tools = root / "tools"
|
|
tools.mkdir()
|
|
for name in TOOLS:
|
|
shutil.copy2(HERE / name, tools / name)
|
|
require(committed_sources(options.source_revision) == sources, "Committed inputs changed during publication")
|
|
files = manifest(root)
|
|
check_modules(modules, files)
|
|
receipt = {"kind": "windows-bundle", "protocol": "sdk2-late-slope-v1", "platform": "windows", "rid": RID,
|
|
"runtimeVersion": RUNTIME, "selfContained": True, "mode": "late-slope", "requestedSeconds": 7200,
|
|
"limits": {"fdGrowth": 32, "retainedManagedGrowthBytes": 64 << 20, "rssGrowthBytes": 128 << 20},
|
|
"certificateLeakTracking": True, "sourceRevision": options.source_revision, "sourceFiles": sources,
|
|
"preparedUtc": datetime.now(timezone.utc).isoformat(), "publishCommand": command,
|
|
"sdkLibraries": sdk, "gatewayLibraries": gateway, "managedModules": modules,
|
|
"coreClrSha256": files["app/coreclr.dll"], "files": files}
|
|
path = root / "receipt.json"
|
|
path.write_text(json.dumps(receipt, indent=2, allow_nan=False) + "\n", encoding="utf-8")
|
|
# A flat manifest the Windows launcher re-verifies on the target before launch.
|
|
(root / "manifest.sha256").write_text("".join(f"{sha} {name}\n" for name, sha in files.items()), encoding="utf-8")
|
|
print(json.dumps({"bundle": str(root), "sourceRevision": options.source_revision, "receiptSha256": digest(path),
|
|
"files": len(files), "workloadLaunched": False}, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|