Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 47b66caa8d | |||
| c6ce30121c | |||
| 62c3ba6c37 | |||
| b583840fbc |
@@ -126,6 +126,7 @@
|
||||
provider that breaks the "managed only" rule, which is why it is opt-in per instance. -->
|
||||
<PackageVersion Include="System.Data.Odbc" Version="10.0.14" />
|
||||
<PackageVersion Include="Novell.Directory.Ldap.NETStandard" Version="3.6.0" />
|
||||
<PackageVersion Include="OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer" Version="2.0.1" />
|
||||
<PackageVersion Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="2.0.1" />
|
||||
<!-- Core carries Opc.Ua.StatusCodes, the oracle StatusCodeParityTests checks the drivers'
|
||||
hard-coded uint constants against. Referenced by that TEST project only — the drivers
|
||||
@@ -242,7 +243,7 @@
|
||||
<PackageVersion Include="ZB.MOM.WW.Secrets.Replicator.AkkaDotNet" Version="0.6.2" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Audit" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Theme" Version="0.5.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.HistorianGateway.Client" Version="1.0.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.HistorianGateway.Contracts" Version="1.0.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.HistorianGateway.Client" Version="0.3.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.HistorianGateway.Contracts" Version="0.3.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+4
-51
@@ -22,37 +22,6 @@ Design references: [docs/plans/2026-06-14-galaxy-phase-c-historian-design.md](pl
|
||||
|
||||
---
|
||||
|
||||
## SDK 2.0.1 compatibility (#782)
|
||||
|
||||
The server targets exact OPC Foundation SDK **2.0.1**, replacing **1.5.378.182**.
|
||||
The upgrade preserves existing application results, including the SQL backends'
|
||||
documented divergences from the old SDK below. Live `AggregateFilter` calculations
|
||||
use an application-private source port of the **37 legacy aggregate factories**,
|
||||
registered through the public SDK aggregate manager; they do not adopt SDK 2's new
|
||||
algorithms. Runtime and advertised aggregate defaults retain
|
||||
`TreatUncertainAsBad=false`. History backend capability sets remain separate from
|
||||
the live aggregate set; no backend gains aggregates or history operations through
|
||||
this upgrade.
|
||||
|
||||
The isolated [original-SDK oracle](../scripts/validation/opcua-aggregate-baseline/README.md)
|
||||
generates **6,144 vectors** using the published 1.5.378.182 package, covering value
|
||||
kinds, statuses, timestamps/picoseconds, reverse reads and edge configurations.
|
||||
These are expected-output fixtures, not a claim that release validation passed.
|
||||
The candidate never loads the old SDK binary. Older references below to "the SDK"
|
||||
as an aggregate oracle mean that preserved baseline, rather than SDK 2's algorithms.
|
||||
|
||||
SDK FILETIME-zero timestamps are translated back to the application's existing CLR
|
||||
unset-time sentinel at history boundaries. Opaque history continuation points remain
|
||||
session-owned, node/topology-bound and subject to current authorization; reconnect or
|
||||
rollback requires a fresh read. Request cancellation flows to the backend. No
|
||||
ConfigDb migration is planned; actual schema and serialization effects must be
|
||||
recorded at the [rollout gate](plans/2026-11-10-opcua-sdk-2-rollout.md).
|
||||
|
||||
`HistorianGateway.Client` and `HistorianGateway.Contracts` are already **0.4.0**
|
||||
(on gateway master `16111a5e`, historiangw#119 closed). This package alignment is
|
||||
separate from the real gateway validation tracked by historiangw#78; it is not
|
||||
live AVEVA or backend-conformance evidence for SDK 2.
|
||||
|
||||
## Historized TagConfig schema
|
||||
|
||||
A tag is historized by setting the **Historize this tag** checkbox and optional **Historian
|
||||
@@ -337,7 +306,7 @@ and is registered the same way for every backend.
|
||||
|
||||
> **Cross-repo follow-up — a multi-tag batch `WriteLiveValues` on the gateway contract.** The drain's
|
||||
> unit of work is one gateway round-trip **per historian tag**, because
|
||||
> `ZB.MOM.WW.HistorianGateway.Client` **1.0.0** exposes no RPC that writes values for more than one tag
|
||||
> `ZB.MOM.WW.HistorianGateway.Client` **0.3.0** exposes no RPC that writes values for more than one tag
|
||||
> in a call. Bounded parallelism (`DrainConcurrency`) is therefore the ceiling: it overlaps the
|
||||
> round-trips but cannot remove them, so a wide tag set still pays `⌈tags / concurrency⌉` network
|
||||
> hops per drain pass. Collapsing that to a single hop needs a **batch/multi-tag `WriteLiveValues`** on
|
||||
@@ -345,22 +314,6 @@ and is registered the same way for every backend.
|
||||
> lands, raising `DrainBatchSize` widens each hop's payload but not the hop count — which is why the
|
||||
> concurrency window, not the batch width, is what governs drain throughput today.
|
||||
|
||||
The HistorianGateway **Client and Contracts packages are pinned together at 1.0.0**
|
||||
([historiangw#131](https://gitea.dohertylan.com/dohertj2/historiangw/issues/131)). The adapter maps
|
||||
`AllowUntrustedServerCertificate` directly and only for TLS: the default verifies the server, while an
|
||||
explicit opt-out applies only when `UseTls=true`. Stale certificate options are ignored on plaintext h2c.
|
||||
This follows the earlier [0.4.0 update](https://gitea.dohertylan.com/dohertj2/historiangw/issues/119). The removed
|
||||
`ReadBlocks` RPC and `ReadBlocksAsync` wrapper were unused here. A partially imported CA PEM bundle now
|
||||
fails client creation instead of accepting a narrower certificate pin; repair the configured bundle if
|
||||
startup reports `ArgumentException` for `CaCertificatePath`.
|
||||
|
||||
This package update does not enable new gateway capabilities. OtOpcUa supplies its own non-empty event
|
||||
GUIDs, sends OPC DA quality bytes (at most 255), retains its UInt16-to-UInt4 provisioning policy, and does
|
||||
not consume the new store-forward buffer measurement fields. String backfill remains disabled: an older
|
||||
gateway ignores the new `StringValue` field and stores 0. Upgrade the gateway under
|
||||
[historiangw#78](https://gitea.dohertylan.com/dohertj2/historiangw/issues/78) before relying on string
|
||||
backfill, gateway-generated event IDs, UInt1-to-UInt2 creation, or other new server behavior.
|
||||
|
||||
### Typed values, real status and outage markers
|
||||
|
||||
The recorder no longer assumes "a numeric Good sample". Each mux delta is coerced to a typed `HistorianValue`
|
||||
@@ -1851,9 +1804,9 @@ value at **10:00**, not 09:00, and no point ever falls before the window. The in
|
||||
`(start − (k+1)·interval, start − k·interval]`, the earliest one truncated, each stamped with its later edge.
|
||||
|
||||
**SDK parity (#677).** `SdkAggregateParityTests` (unit, `Sql.Common.Tests`) and
|
||||
`ProcessedConformance.Processed_reads_match_the_SDK_aggregate_calculators` (both SQL backends, through the real SQL) use the preserved
|
||||
1.5.378.182 calculator source through `CompatibilityAggregateFactory`, not the new SDK 2 algorithms, over the same samples,
|
||||
forward and reverse, and compare every
|
||||
`ProcessedConformance.Processed_reads_match_the_SDK_aggregate_calculators` (both SQL backends, through the real SQL) run the pinned SDK's
|
||||
own calculators (`AverageAggregateCalculator`, `MinMaxAggregateCalculator`, `CountAggregateCalculator`, the base `AggregateCalculator`
|
||||
for Interpolative, `StatusAggregateCalculator` for PercentGood/Bad) over the same samples, forward and reverse, and compare every
|
||||
interval's timestamp, value, status code and Partial bit. The per-sample comparison covers Average, Minimum, Maximum, Count, Range,
|
||||
Start, End, Delta and Minimum/MaximumActualTime; the time-weighted one TimeAverage, TimeAverage2, Total, Total2, Interpolative and
|
||||
PercentGood/Bad (#680), including datasets whose data ends inside the window and with an interval wholly inside an outage; reverse
|
||||
|
||||
@@ -1,88 +1,185 @@
|
||||
# OPC UA Client test fixture
|
||||
|
||||
This is the fixture and command map for the driver/browser upgrade from SDK
|
||||
**1.5.378.182** to exact **2.0.1** (#782). It describes available tests; execution,
|
||||
counts, skips and release decisions belong in [#782](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/782)
|
||||
and the linked [execution evidence](../plans/2026-11-10-opcua-sdk-2-evidence.md).
|
||||
A listed test or historical run is not candidate release evidence.
|
||||
Coverage map + gap inventory for the OPC UA Client (gateway / aggregation)
|
||||
driver.
|
||||
|
||||
## Fixture layers
|
||||
**TL;DR:** Wire-level coverage now exists via
|
||||
[opc-plc](https://github.com/Azure-Samples/iot-edge-opc-plc) — Microsoft
|
||||
Industrial IoT's OPC UA PLC simulator running in Docker (task #215). Real
|
||||
Secure Channel, real Session, real MonitoredItem exchange against an
|
||||
independent server implementation. Unit tests still carry the exhaustive
|
||||
capability matrix (cert auth / security policies / reconnect / failover /
|
||||
attribute mapping). Gaps remaining: upstream-server-specific quirks
|
||||
(historian aggregates, typed ConditionType events, SDK-publish-queue edge
|
||||
behavior under load) — opc-plc uses the same OPCFoundation stack internally
|
||||
so fully-independent-stack coverage needs `open62541/open62541` as a second
|
||||
image (follow-up).
|
||||
|
||||
| Layer | Repository location | Scope and limitation |
|
||||
|---|---|---|
|
||||
| Driver unit | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` | Faked session contracts: lifecycle/generations, value conversion, per-item errors, policy selection, credentials, namespace remapping, subscription cleanup, discovery, alarms and host-owned reinitialization. No real secure channel. |
|
||||
| Browser unit | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/` | Browse attributes/pagination and certificate-identity ownership, including failure cleanup. No external server. |
|
||||
| In-process wire | `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/` | Real driver against OtOpcUa: `OpcUaClientDriverWireTests`, `OpcUaClientDriverStallWireTests`, `OpcUaClientDriverReconnectWireTests`. Real channels but both sides use this repository's SDK. |
|
||||
| External simulator | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/` | `OpcUaClientSmokeTests`: real read, varied-type batch read and subscription against opc-plc. Does not prove all upstream products, secured authentication, write or alarm compatibility. |
|
||||
| Browser integration | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/` | Separate fixture-backed browser tests; inspect fixture availability and individual results. |
|
||||
## What the fixture is
|
||||
|
||||
The SDK 2 unit boundary cases include `OpcUaClientSdkValueBoundaryTests` and
|
||||
`OpcUaClientContinuationPointReleaseTests`. They preserve CLR value types and
|
||||
null/empty buffers, reject unsupported matrix items without shifting batch results,
|
||||
and release abandoned browse points. Certificate tests cover lifetime through
|
||||
session close; unit mocks alone do not establish trust-chain interoperability.
|
||||
**Integration layer** (task #215):
|
||||
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/` stands up
|
||||
`mcr.microsoft.com/iotedge/opc-plc:2.14.10` via `Docker/docker-compose.yml`
|
||||
on `opc.tcp://10.100.0.35:50000` (the shared Docker host; override via
|
||||
`OPCUA_SIM_ENDPOINT`). `OpcPlcFixture` probes the port at
|
||||
collection init + skips tests with a clear message when the container's
|
||||
not running (matches the Modbus/pymodbus + S7/python-snap7 skip pattern).
|
||||
Docker is the launcher — no PowerShell wrapper needed because opc-plc
|
||||
ships pre-containerized. Compose-file flags: `--ut` (unsecured transport
|
||||
advertised), `--aa` (auto-accept client certs — opc-plc's cert trust store
|
||||
resets on each spin-up), `--alm` (alarm simulation for IAlarmSource
|
||||
follow-up coverage), `--pn=50000` (port).
|
||||
|
||||
The in-process stall proxy holds bytes without closing the session, then resumes;
|
||||
its hard-close case refuses connections. These exercise stale notification rejection,
|
||||
quality transitions and explicit host reinitialization. The driver does not use
|
||||
`SessionReconnectHandler` or `TransferSubscriptions`; historical transfer-based
|
||||
coverage descriptions do not apply to the current lifecycle (#670/#676).
|
||||
**Unit layer**:
|
||||
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` is still the primary
|
||||
coverage. Tests inject fakes through the driver's construction path; the
|
||||
OPCFoundation.NetStandard `Session` surface is wrapped behind an interface
|
||||
the tests mock.
|
||||
|
||||
The driver has **no upstream HistoryRead capability** (#655). Its tags can be
|
||||
recorded into the local server-wide historian; backend conformance and aggregate
|
||||
oracles are documented in [Historian.md](../Historian.md).
|
||||
## What it actually covers
|
||||
|
||||
## Running locally
|
||||
### Integration (opc-plc Docker, task #215)
|
||||
|
||||
Run from the repository root. Build first and use a fresh result directory for
|
||||
**each** project. The [runbook](../plans/2026-11-10-opcua-sdk-2-rollout.md#verification-commands)
|
||||
provides the result helper that preserves test exit status and rejects all-skipped
|
||||
runs. For example, after defining that helper:
|
||||
- `OpcUaClientSmokeTests.Client_connects_and_reads_StepUp_node_through_real_OPC_UA_stack` —
|
||||
full Secure Channel + Session + `ns=3;s=StepUp` Read round-trip
|
||||
- `OpcUaClientSmokeTests.Client_reads_batch_of_varied_types_from_live_simulator` —
|
||||
batch Read of UInt32 / Int32 / Boolean; asserts `bool`-specific Variant
|
||||
decoding to catch a common attribute-mapping regression
|
||||
- `OpcUaClientSmokeTests.Client_subscribe_receives_StepUp_data_changes_from_live_server` —
|
||||
real `MonitoredItem` subscription against `ns=3;s=FastUInt1` (ticks every
|
||||
100 ms); asserts `OnDataChange` fires within 3 s of subscribe
|
||||
|
||||
```bash
|
||||
opc_test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests.csproj
|
||||
opc_test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests.csproj
|
||||
opc_test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.csproj
|
||||
```
|
||||
Wire-level surfaces verified: `IDriver` + `IReadable` + `ISubscribable` +
|
||||
`IHostConnectivityProbe` (via the Secure Channel exchange).
|
||||
|
||||
The simulator compose file is
|
||||
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/docker-compose.yml`.
|
||||
It launches `mcr.microsoft.com/iotedge/opc-plc:2.14.10` with `--ut --aa --alm --pn=50000`.
|
||||
Use the shared-host procedure in [infra/README.md](../../infra/README.md); coordinate
|
||||
shared fixture use before restarting containers. These dev flags do not establish
|
||||
production trust or secure-policy coverage.
|
||||
### In-process wire tests (no opc-plc)
|
||||
|
||||
`OpcPlcFixture` probes `OPCUA_SIM_ENDPOINT` (default `opc.tcp://10.100.0.35:50000`)
|
||||
and skips when unavailable. With an available, reserved fixture:
|
||||
`tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/OpcUaClientDriverWireTests` drives the real driver
|
||||
against the in-process OtOpcUa server (`ServerEventRig`, authenticating with the rig's API-key credential):
|
||||
read (+ missing node), write through the node-write gateway (+ read-only refusal, + failed-write revert),
|
||||
subscribe/unsubscribe, host-status Running -> not-Running when the server stops, and discovery. These need
|
||||
no Docker. There is no HistoryRead leg: the driver has no history capability (#655).
|
||||
|
||||
```bash
|
||||
export OPCUA_SIM_ENDPOINT=opc.tcp://10.100.0.35:50000
|
||||
opc_test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests.csproj
|
||||
```
|
||||
### Unit
|
||||
|
||||
A nonzero suite count does not prove every required scenario ran: inspect individual
|
||||
TRX results and record required skips separately. No tests were run by preparing
|
||||
this documentation.
|
||||
The surface is broad because `OpcUaClientDriver` is the richest-capability
|
||||
driver in the fleet (it's a gateway for another OPC UA server, so it
|
||||
mirrors the full capability matrix):
|
||||
|
||||
## Required compatibility evidence
|
||||
- `OpcUaClientDriverScaffoldTests` — `IDriver` lifecycle
|
||||
- `OpcUaClientReadWriteTests` — read + write lifecycle
|
||||
- `OpcUaClientSubscribeAndProbeTests` — monitored-item subscription + probe
|
||||
state transitions
|
||||
- `OpcUaClientDiscoveryTests` — `GetEndpoints` + endpoint selection
|
||||
- `OpcUaClientAttributeMappingTests` — OPC UA node attribute → driver value
|
||||
mapping
|
||||
- `OpcUaClientSecurityPolicyTests` — `SignAndEncrypt` / `Sign` / `None`
|
||||
policy negotiation contract
|
||||
- `OpcUaClientCertAuthTests` — cert store paths, revocation-list config
|
||||
- `OpcUaClientReconnectTests` — SDK reconnect hook + `TransferSubscriptions`
|
||||
across the disconnect boundary
|
||||
- `OpcUaClientFailoverTests` — primary → secondary session fallback per
|
||||
driver config
|
||||
- `OpcUaClientAlarmTests` — A&E severity bucket (1–1000 → Low / Medium /
|
||||
High / Critical), subscribe / unsubscribe / ack contract
|
||||
|
||||
Release validation must identify both application builds and SDK versions, endpoint
|
||||
policy, token kind, certificate chain, configuration and executed capabilities.
|
||||
Use the baseline client against the candidate server and candidate client against
|
||||
the baseline server, plus the actual ScadaBridge adapter and external upstreams.
|
||||
For secured paths include trusted SignAndEncrypt, API-key allow/deny/revocation,
|
||||
certificate rejection and restart/rebind. Apply only operations the peer implements;
|
||||
a simulator need not implement OtOpcUa's API-key or historian contract.
|
||||
Capability surfaces whose contract is verified: `IDriver`, `ITagDiscovery`,
|
||||
`IReadable`, `IWritable`, `ISubscribable`, `IHostConnectivityProbe`,
|
||||
`IAlarmSource`.
|
||||
|
||||
For alarms verify typed EventFilter select clauses, result-field indexes, actual
|
||||
ConditionType/SourceNode/EventId identity, refresh, acknowledgements and no replay
|
||||
after an ambiguous transport failure. For all supported paths preserve existing
|
||||
operation statuses, locale behavior and null/empty filter meaning. Constructing an
|
||||
empty SDK 2 `ContentFilter` is an API adaptation, not evidence of a changed filter
|
||||
contract. Record any stricter protocol response and consumer impact before acceptance.
|
||||
## What it does NOT cover
|
||||
|
||||
opc-plc uses the OPC Foundation stack too. Independent upstream behavior, secured
|
||||
authentication variants, large publish queues, real alarm products, packet-loss
|
||||
windows and performance require separate evidence. The rollout runbook links the
|
||||
pending gates; no simulator run substitutes for ScadaBridge, live gateway, Windows,
|
||||
Linux or mixed-version cluster validation.
|
||||
### 1. Full real-stack exchange (unit tests only)
|
||||
|
||||
The **unit** suite mocks `Session.ReadAsync`, `Session.CreateSubscription`,
|
||||
`Session.AddItem`, etc. — no UA Secure Channel is opened. The **integration**
|
||||
suite (`OpcUaClientSmokeTests`, task #215) does open a real Secure Channel
|
||||
against opc-plc and exercises Read + Subscribe end-to-end. What remains
|
||||
untested even in the integration suite: certificate validation under
|
||||
non-anonymous security policies, signing/encryption, nonce handling, chunk
|
||||
assembly, keep-alive cadence — all SDK-internal.
|
||||
|
||||
### 2. Subscription transfer across reconnect
|
||||
|
||||
Contract test: "after a simulated reconnect, `TransferSubscriptions` is
|
||||
called with the right handles." Real behavior: SDK re-publishes against the
|
||||
new channel and some events can be lost depending on publish-queue state.
|
||||
The lossy window is not characterized.
|
||||
|
||||
### 3. Large-scale subscription stress
|
||||
|
||||
100+ monitored items with heterogeneous publish intervals under a single
|
||||
session — the shape that breaks publish-queue-size tuning in the wild — is
|
||||
not exercised.
|
||||
|
||||
### 4. Real A&E events
|
||||
|
||||
Alarm subscription is mocked via filtered monitored items; the actual
|
||||
`EventFilter` select-clause behavior against a server that exposes typed
|
||||
ConditionType events (non-base `BaseEventType`) is not verified.
|
||||
|
||||
### 6. Authentication variants
|
||||
|
||||
- Anonymous, UserName/Password, X509 cert tokens — each is contract-tested
|
||||
but not exchanged against a server that actually enforces each.
|
||||
- LDAP-backed `UserName` (matching this repo's server-side
|
||||
`LdapOpcUaUserAuthenticator`) requires a live LDAP round-trip; not tested.
|
||||
|
||||
## When to trust OpcUaClient tests, when to reach for a server
|
||||
|
||||
| Question | Unit tests | Integration (opc-plc) | Real upstream server |
|
||||
| --- | --- | --- | --- |
|
||||
| "Does severity 750 bucket as High?" | yes | - | yes |
|
||||
| "Does the driver call `TransferSubscriptions` after reconnect?" | yes | - | yes |
|
||||
| "Does a real OPC UA read round-trip work?" | no | yes | yes |
|
||||
| "Does a real OPC UA subscribe deliver changes?" | no | yes | yes |
|
||||
| "Does write round-trip work against a live server?" | no | no (not yet exercised) | yes (required) |
|
||||
| "Does event-filter-based alarm subscription return ConditionType events?" | no | no | yes (required) |
|
||||
| "Does history read from AVEVA Historian return correct aggregates?" | no | no | yes (required) |
|
||||
| "Does the SDK's publish queue lose notifications under load?" | no | no | yes (stress) |
|
||||
|
||||
## Follow-up candidates
|
||||
|
||||
The easiest win here is to **wire the client driver tests against this
|
||||
repo's own server**. The v2 integration test project
|
||||
`tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs`
|
||||
(the v2 replacement for the retired v1 `OpcUaServerIntegrationTests`) already
|
||||
stands up a real OPC UA server on a non-default port with a seeded
|
||||
FakeDriver. An `OpcUaClientLiveLoopbackTests` that connects the client
|
||||
driver to that server would give:
|
||||
|
||||
- Real Secure Channel negotiation
|
||||
- Real Session / Subscription / MonitoredItem exchange
|
||||
- Real read/write round-trip
|
||||
- Real certificate validation (the integration test already sets up PKI)
|
||||
|
||||
It wouldn't cover upstream-server-specific quirks (AVEVA Historian, Kepware,
|
||||
Prosys), but it would cover 80% of the SDK surface the driver sits on top
|
||||
of.
|
||||
|
||||
Beyond that:
|
||||
|
||||
1. **Prosys OPC UA Simulation Server** — free, Windows-available, scriptable.
|
||||
2. **UaExpert Server-Side Simulator** — Unified Automation's sample server;
|
||||
good coverage of typed ConditionType events.
|
||||
3. **Dedicated historian integration lab** — only path for
|
||||
historian-specific coverage.
|
||||
|
||||
## Key fixture / config files
|
||||
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` — unit tests with
|
||||
mocked `Session`
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcPlcFixture.cs`
|
||||
— collection fixture; parses `OPCUA_SIM_ENDPOINT` (default
|
||||
`opc.tcp://10.100.0.35:50000`), TCP-probes at collection init, records
|
||||
`SkipReason` when unreachable
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientSmokeTests.cs`
|
||||
— wire-level test suite (3 `[Fact]` methods: read, batch read, subscribe)
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/docker-compose.yml`
|
||||
— `mcr.microsoft.com/iotedge/opc-plc:2.14.10` with `--ut --aa --alm --pn=50000`
|
||||
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs` — ctor +
|
||||
session-factory seam tests mock through; implements `IAlarmSource`;
|
||||
does NOT implement `IRediscoverable`
|
||||
- `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs` —
|
||||
the v2 dual-endpoint integration harness a future loopback client test could
|
||||
piggyback on (v1 `OpcUaServerIntegrationTests.cs` retired with the v1 server project)
|
||||
|
||||
@@ -9,9 +9,8 @@ and for the test-harness map read [OpcUaClient-Test-Fixture.md](OpcUaClient-Test
|
||||
A **remote OPC UA server**. This driver runs the *opposite* direction from the
|
||||
usual "server exposes PLC data" flow: it acts as an OPC UA **client**, opens a
|
||||
`Session` against an upstream server, and re-exposes that server's address space
|
||||
through the local OtOpcUa server. Browse, read, write, subscribe and alarm
|
||||
operations use the upstream endpoint. HistoryRead uses the local server-wide
|
||||
historian instead; see [History](#history-not-supported-gitea-655).
|
||||
through the local OtOpcUa server. Browse, read, write, subscribe, alarm, and
|
||||
history calls are passed through to the upstream endpoint.
|
||||
|
||||
It is built on the OPC Foundation UA .NET Standard reference SDK and runs
|
||||
in-process in the OtOpcUa server's .NET 10 AnyCPU host — pure managed, no
|
||||
@@ -21,31 +20,6 @@ out-of-process isolation.
|
||||
> a remote OPC UA endpoint by hand, point the general-purpose
|
||||
> [Client CLI](../Client.CLI.md) at it directly.
|
||||
|
||||
## SDK 2.0.1 compatibility (#782)
|
||||
|
||||
The driver, shared session layer and AdminUI browser target exact SDK **2.0.1**
|
||||
(previous **1.5.378.182**). Their existing abstractions and host-owned reconnect
|
||||
lifecycle remain in place. SDK immutable values are detached at the boundary into
|
||||
existing CLR scalars and one-dimensional arrays, preserving null/empty ByteStrings,
|
||||
UTC timestamps, Guid values, numeric widths, item order and per-item status. Matrix
|
||||
payloads remain unsupported and fail per item; this upgrade adds no matrix or
|
||||
upstream-history capability.
|
||||
|
||||
Certificate-based identities retain their loaded certificate until their owning
|
||||
session closes, including failure cleanup. The browser releases abandoned browse
|
||||
continuation points. Endpoint policy selection, namespace-URI remapping, old-session
|
||||
notification rejection and the existing refusal to replay non-idempotent writes or
|
||||
alarm acknowledgements remain compatibility requirements.
|
||||
|
||||
No application status-code, locale-selection or event-filter semantic change is
|
||||
approved by this upgrade. SDK value wrappers and empty-filter construction are API
|
||||
adaptations; callers must continue to inspect operation and select-clause results
|
||||
by index. Confirm actual client/server combinations with the
|
||||
[fixture map](OpcUaClient-Test-Fixture.md) and
|
||||
[rollout gates](../plans/2026-11-10-opcua-sdk-2-rollout.md) before deployment.
|
||||
Historical SDK 1.5 observations elsewhere on this page describe their original
|
||||
measurements; they are not fresh SDK 2.0.1 interoperability evidence.
|
||||
|
||||
## Project split
|
||||
|
||||
| Project | Target | Role |
|
||||
@@ -391,7 +365,7 @@ They are not part of `DriverHealth` or the
|
||||
## Testing
|
||||
|
||||
- **Unit tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/`
|
||||
cover the session lifecycle, namespace remapping, alarm dispatch,
|
||||
cover the session lifecycle, namespace remapping, alarm/history passthrough,
|
||||
and config binding against a faked SDK session.
|
||||
- **Integration fixture** — exercises the driver against a reference OPC UA
|
||||
server (opc-plc) on the shared docker host; see
|
||||
|
||||
@@ -4,7 +4,7 @@ Execution started 2026-11-10 for [#782](https://gitea.dohertylan.com/dohertj2/lm
|
||||
|
||||
## Baseline and authority
|
||||
|
||||
- Baseline `83ed880e4471344deaf008da9bd028c7e170bdc7` equaled `origin/master` when execution started; original checkout was clean. Master subsequently advanced to the independent HistorianGateway package update `16111a5e`, merged into the candidate without content conflicts. Dedicated managed worktree, branch `codex/opcua-sdk-2`.
|
||||
- Baseline `83ed880e4471344deaf008da9bd028c7e170bdc7` equals fetched `origin/master`; original checkout clean. Dedicated managed worktree, branch `codex/opcua-sdk-2`.
|
||||
- Target remains exact stable SDK `2.0.1`, upstream `07e62f72500cc0923a2bab940653c8c1116fa286`. Tagged source and restored assemblies control API decisions.
|
||||
- Baseline packages are already `1.5.378.182`, not the plan preparation checkout's `.106`. #751 reflection helper exists. AggregateManager application configuration override already exists; preserve it.
|
||||
- #782 previously parked the major upgrade; the user's current explicit execution instruction supersedes that scheduling decision. No deployment authorization overrides the Wonder-box remote-execution hold.
|
||||
@@ -65,11 +65,11 @@ No optional ManagedSession/DI/AOT/cluster redesign, trust relaxation, silent war
|
||||
## Gates
|
||||
|
||||
- G0 baseline/design: accepted for migration, with corrections and external/trust-matrix gaps recorded below.
|
||||
- G1 restored 2.0 graph / break inventory: passed; compilation convergence subsequently passed G2.
|
||||
- G2 compilation convergence: passed; whole-solution Release builds have zero warnings/errors and temporary migration assets are removed.
|
||||
- G1 restored 2.0 graph / break inventory: passed for lane execution; compilation convergence pending.
|
||||
- G2 compilation convergence: pending.
|
||||
- G3 behavioral/security/interoperability: pending.
|
||||
- G4 lifecycle/performance/publish: pending.
|
||||
- Astra security/ownership source findings: closed for the reviewed identity, history, audit and browse changes; final release review remains pending.
|
||||
- Astra security/ownership and release reviews: pending.
|
||||
|
||||
No upgraded artifact is approved for release.
|
||||
|
||||
@@ -129,266 +129,4 @@ Restore succeeds without downgrade suppression or source changes. The first comp
|
||||
|
||||
This is the exposed first wave, not the solution-wide final break count: server overrides, removed collections, immutable/null/Variant APIs, certificate manager, and async identity construction block downstream compilation. Log and exact diagnostics: `artifacts/opcua2/d9984028/sdk2-spike/{build.log,diagnostics.txt}`. Later diagnostics are inventoried by lane until G2 convergence; no compile failure is claimed as behavioral success.
|
||||
|
||||
Restored project graphs contain no mixed Foundation SDK versions: every Foundation package (including analyzer-only development transitive GDS assets) is 2.0.1. Host graph resolves Bcl.TimeProvider 10.0.12 and Http.Resilience 10.10.0; client assets resolve RateLimiting 10.0.12, while the Host shared framework prunes that separate package. G1 is satisfied for lane execution; full build/tests remain required at G2/G3. Frozen boundary contract is above; production and unit paths are split by plan ownership, integration/historian oracle edits coordinator-only.
|
||||
|
||||
|
||||
## Integrated implementation and source reviews
|
||||
|
||||
The implementation is on `codex/opcua-sdk-2`, not deployed or merged to master.
|
||||
Master's independently delivered HistorianGateway client/contracts 0.4.0 change
|
||||
`16111a5e` is included, and master ancestry was merged at `88ee30e0` without a
|
||||
content change. historiangw#119 is closed; historiangw#78 deployment is separate.
|
||||
|
||||
The server, client and driver lanes were integrated and validated in isolated
|
||||
worktrees. Astra reviewed the design, aggregate compatibility architecture,
|
||||
identity/peer provenance, certificate ownership, production history continuation
|
||||
store, write conversions/revert ownership, alarm compatibility, and filtered event
|
||||
and audit delivery. No source review is represented as release approval.
|
||||
|
||||
- Identity registry admission is installed before listeners and remains until
|
||||
shutdown. Rejected/disabled credentials cannot fall through to a default identity.
|
||||
SDK role augmentation preserves the application's principal and key ID.
|
||||
- Activation uses public transport peer evidence, scoped across asynchronous work
|
||||
and restored in `finally`. Unknown peers retain the existing process-wide budget;
|
||||
this application has no CIDR admission or reverse-connect policy to invent.
|
||||
- Immutable SDK values are normalized only at existing application boundaries.
|
||||
Scalar numeric leniency, checked one-dimensional conversions, pass-through arrays,
|
||||
byte/ByteString rank, null/empty and detached buffers remain pinned. Matrices remain
|
||||
unsupported. Public SDK-facing client DTOs remain SDK-facing.
|
||||
- Session history tokens are consumed/released through the real SDK store, with
|
||||
exact node/realm, read kind, topology epoch and current authority checks. Existing
|
||||
canonical-query semantics are retained; no undocumented filter fingerprint was
|
||||
introduced.
|
||||
- SDK2 alarm early AlreadyAcked/AlreadyConfirmed checks are adapted only for the
|
||||
current nonempty trunk EventId, preserving application authorization and routing.
|
||||
Write reverts bind the actual node instance and committed attempt, preventing a
|
||||
late failure from undoing a newer write/publish or replacement node.
|
||||
- Audit delivery uses a private, revocable operation context for one exact source
|
||||
and type. Both existing permission callbacks can veto; no global role/node
|
||||
permission array is widened. Ordinary SDK automatic audits remain denied. The
|
||||
encrypted wire test proves exactly one application audit to the Read audience
|
||||
and none to the denied audience.
|
||||
|
||||
A cross-version wire test found an additional SDK2 creation change: `Create`
|
||||
reset an alarm's preassigned reference to the null NodeId, hiding it from its raw
|
||||
parent's hierarchical browse. Restoring HasComponent **after** Create fixed all
|
||||
three supported alarm types in both realms (`fa823378`, integrated `41e65f6a`).
|
||||
All 12 actual-node/wire regressions failed first, then passed. Full server units
|
||||
at that fix: **1115 passed, zero failures, one existing skip**. Astra approved
|
||||
its source scope and the matching separate-process results.
|
||||
|
||||
## Preserved aggregate algorithms and independent oracles
|
||||
|
||||
The owner explicitly chose current application results over changed SDK2
|
||||
aggregate semantics. A private source adapter contains the eight original MIT
|
||||
algorithm/factory files from 1.5.378.182 with original source hashes. It is not an
|
||||
old SDK runtime. A fresh SDK AggregateManager registers the same 37 factories
|
||||
exactly once; calling the base registration first would duplicate diagnostic nodes.
|
||||
|
||||
The separately pinned original-SDK generator produced **6,144 vectors**: the
|
||||
original 4,144 plus 2,000 numeric-width, nonfinite/conversion, tie/picosecond and
|
||||
configuration edge cases. Fixture SHA-256:
|
||||
`2ec2a1ac97f05d199c18da55157f8bbe0276853c3ba73ebf1b509c535c5a7dd8`.
|
||||
Original SDK assembly SHA-256:
|
||||
`d8d9706dea577858325730cd89896826ed82f7bfcf9503ccd0260311663a69f4`.
|
||||
|
||||
All cases pass both directly and through the booted production aggregate manager.
|
||||
Values, CLR types, all status bits, source/server time and picoseconds, exceptions,
|
||||
queue acceptance and incremental/partial results are compared. SDK2 FILETIME zero
|
||||
is explicitly observed as the original CLR unset sentinel; no calculation or
|
||||
expected output was regenerated from the candidate.
|
||||
|
||||
The SQL application's PercentGood policy already differs from SDK1.5.378.182.
|
||||
The same ten of 275 parity tests fail on the original and candidate with identical
|
||||
names/messages. Independent application results and regression expectations stay
|
||||
unchanged; [#796](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/796)
|
||||
tracks that baseline debt. It is not a green test run.
|
||||
|
||||
## Local compatibility and security evidence
|
||||
|
||||
- Full wire integration reached **187 passed, zero failures/skips** in the second
|
||||
CI-equivalent run, excluding the separately gated E2E soak category. It includes
|
||||
all original contracts plus production continuation, trusted-channel, audit,
|
||||
hierarchy and current authority regressions.
|
||||
- Focused final security probes: **17/17**, zero skips. Seven trusted cases cover
|
||||
explicit DER trust, Sign and SignAndEncrypt, invalid credentials, an untrusted
|
||||
client, same-session reactivation, and concurrent IPv4/IPv6 attempts correlated
|
||||
with their own peer before/after an await barrier. Eighteen earlier continuation
|
||||
cases cover raw/modified/events with alias, replacement, rebuild, grant/key
|
||||
revocation and expiry, proving no backend dispatch or saved-data disclosure.
|
||||
- Native current-trunk retry tests use a narrow raw-device grant with zero global
|
||||
roles, then revoke/expire authority and require no route/state/EventId/event
|
||||
change. Scripted retry tests explicitly use the existing cluster grant.
|
||||
A separate real-session original/candidate probe preserves the opaque scripted
|
||||
ID's equipment-scope denial and broad-grant positive. This **preexisting**
|
||||
production feature gap remains [#797](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/797);
|
||||
a slash-qualified synthetic ID is not a valid UI-authored workaround.
|
||||
- Target certificate manager/endpoint identity tests: **7/7** (valid, expired,
|
||||
revoked, missing issuer, issuer usage, hostname and URI). These are manager/API
|
||||
checks, not seven network handshakes.
|
||||
- Corrected a preexisting client creation-policy bug: 2048 meant months, not RSA
|
||||
bits. New certificates use the SDK's 12-month default; existing longer-valid
|
||||
certificates and identical private-key bytes are reused. A real-factory red
|
||||
lifetime case became green; all **324 Shared tests passed**. RSA minimum remains
|
||||
the SDK's separate 2048-bit default.
|
||||
- Separate-process original→candidate, candidate→original, candidate→candidate,
|
||||
and original→original Shared legs plus candidate driver/actual CLI legs:
|
||||
**62/62 passed**. Strict alarm identities remain asserted. Shared/server/driver
|
||||
use explicit mutual DER trust and encrypted API-key sessions; CLI's existing
|
||||
autoaccept is disclosed and is not rejection-policy evidence. The same harness
|
||||
first exposed the hierarchy defect at **60/62**. Commands, artifact hashes,
|
||||
policies and outcomes are checked in under
|
||||
`scripts/validation/opcua-sdk-compatibility/evidence/2026-11-10/`.
|
||||
- The actual unchanged ScadaBridge adapter, SDK1.5.378.106, passed **9 None/API-key
|
||||
cases against each server** (18 total). Strict encrypted connections fail on
|
||||
both original/candidate because its application certificate is unconfigured:
|
||||
[ScadaBridge#121](https://gitea.dohertylan.com/dohertj2/ScadaBridge/issues/121).
|
||||
That is a retained failure, not a secure interoperability pass. Reproducible
|
||||
isolated consumer receipts are in `scripts/validation/scadabridge-sdk-compatibility/`.
|
||||
- Real local SQL Server: **195/195** on each version. QuestDB10.0.1: **233 passed,
|
||||
zero failed, one existing opt-in race-stress skip** on each version. Case maps
|
||||
match exactly. Only task databases/prefixes and the task QuestDB container/volume
|
||||
were removed. See [backend receipts](evidence/2026-11-10-opcua2/backend-matrix.md).
|
||||
This is not real AVEVA evidence.
|
||||
|
||||
## Solution, performance and package evidence
|
||||
|
||||
Whole-solution Release builds pass with **zero warnings/errors**, including all
|
||||
inventory consumers. All restored production Foundation packages are 2.0.1; no
|
||||
MigrationAnalyzer reference/runtime asset remains. The only old SDK is isolated
|
||||
in the explicitly original-SDK validation process outside the solution.
|
||||
|
||||
The first full CI-equivalent run executed **18,459** cases with execution guard
|
||||
passed (floor12,000), but failed203: 192 from an incorrect test-oracle reflection
|
||||
port (repaired and **554/554 Core.Abstractions tests passed**), the ten known
|
||||
PercentGood baseline failures, and a ScriptWorker250ms timing assertion.
|
||||
The latter also fails independently on untouched baseline83ed880e (880.13ms),
|
||||
so no threshold/filter/skip was changed; it is
|
||||
[#798](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/798).
|
||||
The fresh second full solution run produced **63 TRX files, 19,121 total,
|
||||
18,486 executed, 18,475 passed, 11 failed and 635 existing skipped cases**.
|
||||
Its test exit is **1**, execution guard **0** (floor12,000). The exact eleven
|
||||
failures are the same ten PercentGood cases (#796) and one timing case (#798);
|
||||
there are no additional failure classes. This is baseline-matching evidence,
|
||||
**not a green solution run**. The command/logs are retained under ignored
|
||||
`artifacts/opcua2/88ee30e0/solution-tests2/`;
|
||||
[versioned counters and exact failures](evidence/2026-11-10-opcua2/solution-tests2.json)
|
||||
retain the nonzero status. Fixture-backed gates remain separate.
|
||||
|
||||
Three alternating warmed lock measurements per version show medians17→15ms for
|
||||
10k-tag rebuild and81→80ms for50k-node removal, with equal visit counts.
|
||||
See [bounded subset and host-load caveats](evidence/2026-11-10-opcua2/lock-benchmarks.md).
|
||||
The separate 72,000-call warmed native-process experiment records median p95
|
||||
changes +14.95% scalar read, +1.58% scalar write, -16.28% array read, -17.24%
|
||||
array write, plus lower allocated bytes/workload operation. It retains the
|
||||
scalar-read outlier, small-workload scope, host-load and JIT-mode caveats. Neither
|
||||
these samples nor the short cold-tiered measurements certify full G4.
|
||||
|
||||
Four self-contained publishes (Linux ARM64 and Windows x64 Host/CLI) pass with
|
||||
runtime10.0.14, unique SDK DLL basenames, native RID validation and file hashes.
|
||||
Actual Linux Host passes two start/stop cycles with unchanged certificate identity,
|
||||
healthy web checks, real HMAC-key positive/negative cases and strict peer negatives.
|
||||
Console/rolling logs contain zero task-secret occurrences. See
|
||||
[publish/runtime receipts](evidence/2026-11-10-opcua2/publish-smoke.md).
|
||||
Windows service/runtime installation remains
|
||||
[#800](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/800), not inferred from
|
||||
cross-publishing. Approved live AVEVA/consumer/pair validation remains
|
||||
[#801](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/801).
|
||||
|
||||
## Resource finding and long-run scope
|
||||
|
||||
The Mac SDK-server restart soak failed twice around17 minutes at the unchanged
|
||||
initial-RSS+128MiB guard. One run completed250 cycles/62 restarts; managed memory,
|
||||
FDs, cached stores and logical collections remained bounded, but process RSS did
|
||||
not meet its limit. **No two-hour pass is claimed.**
|
||||
[#799](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/799) retains the failed
|
||||
TRX/JSONL and active investigation. Explicit SDK wrapper tracking found created=
|
||||
disposed and no outstanding/unreachable-undisposed wrapper after cleanup;
|
||||
controlled private PFX imports/SDK copies accumulate native Apple Security
|
||||
objects, unlike shared-handle/public-only controls. These controls do not attribute the entire RSS increase or isolate the server
|
||||
from the co-located SDK client. Astra found no safe narrow public copy-policy
|
||||
setting in exact2.0.1. Switching channel/session factories would change lifecycle
|
||||
semantics and requires separate design. No unproven production patch or resource-
|
||||
budget waiver was applied.
|
||||
|
||||
The original Mac PKI-specific descriptor zero was invalid because `/var` and
|
||||
`/private/var` were compared literally. Corrected canonical, directory-bounded
|
||||
instrumentation proves0→1→0 for an open test file and reaps a timed-out probe.
|
||||
The original logical/global-FD evidence remains usable; its PKI-zero claim does
|
||||
not. A corrected90-second smoke is explicitly not two-hour evidence.
|
||||
|
||||
The same-cap self-contained **Linux ARM64 server/node-manager workload also
|
||||
failed**, at **248 cycles / 61 restarts / 903.508 seconds**: RSS353,931,264
|
||||
exceeded351,850,496 bytes (cycle3 baseline+128MiB). Managed memory~20.5MB,
|
||||
descriptors189, PKI descriptors0, cached stores2 and logical collections remained
|
||||
bounded. Runtime10.0.14, SDK2.0.1; test/container/launcher fail. This broadens
|
||||
#799 beyond macOS; no complete/two-hour pass exists.
|
||||
[Selected Linux failed-run receipts](evidence/2026-11-10-opcua2/server-linux-soak-failed.json)
|
||||
retain hashes, unchanged limits, positive descriptor controls and failure.
|
||||
|
||||
The independent root-owned actual production Shared/driver/live-reinitialize/
|
||||
browser lifecycle run on Linux is still in progress. Its final elapsed duration,
|
||||
counters, resource limits and completion/exit guards must be recorded separately.
|
||||
A short diagnostic, a running process, or a different workload is never substituted
|
||||
for a required passing gate. G4 and release approval remain open.
|
||||
|
||||
|
||||
## Final integrated source review and diagnostic instrumentation
|
||||
|
||||
Astra (`gpt-6-astra`, xhigh) reviewed integrated source `aa9af0dc` read-only and
|
||||
reported **no new must-fix migration source/security finding**. It rechecked
|
||||
identity registry/rejection, scoped public peer evidence, restricted identity
|
||||
behavior, actual continuation ownership/node/topology binding, temporary audit
|
||||
capability, exact alarm retry guards, write/node attempt ownership and client/
|
||||
driver generation/disposal. Its independently inspected security receipt is
|
||||
17/17 with zero skips. This is source/security review, **not release approval**:
|
||||
Astra retains #799 and explicitly withholds G4/release approval on both platforms.
|
||||
The reviewed production tree remains unchanged by later diagnostic commits.
|
||||
|
||||
Test-only instrumentation now writes the complete captured resource tuple before
|
||||
resource assertions and includes GC committed/heap/fragmentation, pinned/finalizer,
|
||||
JIT method/IL, thread and timer counters. Explicit bounded profile mode preserves
|
||||
the first original budget rejection, marks subsequent samples rejected, and
|
||||
rethrows it before completion; full/diagnostic modes still fail immediately.
|
||||
The full7200-second minimum and every original cap are unchanged. Both descriptor
|
||||
positive/timeout controls passed again, **2/2**, at integrated `b57595cb`.
|
||||
Historical failed-run hashes and source revisions remain exact; they are not
|
||||
relabeled as results from this newer instrumentation.
|
||||
|
||||
The pushed implementation is reviewable as
|
||||
[draft PR#802](https://gitea.dohertylan.com/dohertj2/lmxopcua/pulls/802).
|
||||
It is explicitly held from merge/release, and #782 remains open.
|
||||
|
||||
|
||||
## Completed cross-platform RSS investigation
|
||||
|
||||
The [reproducers, report](../../scripts/validation/opcua-lifecycle-memory/README.md)
|
||||
and [hashed evidence](../../scripts/validation/opcua-lifecycle-memory/evidence.json)
|
||||
preserve four failed canonical workload/profile attempts and the unchanged caps.
|
||||
A 30-minute Linux **diagnostic subset** completed on both versions: candidate
|
||||
768 cycles/192 restarts, original736/184. Candidate retained managed memory was
|
||||
19.25 MB, final RSS334.18 MB, with19,272 certificate wrappers created and disposed
|
||||
and none outstanding. It has **no full-gate pass** and omits canonical history,
|
||||
revocation, topology surgery and network-stall work. The original subset's growing
|
||||
managed/timer footprint cannot provide a favorable steady-state performance
|
||||
comparison denominator.
|
||||
|
||||
EventPipe attributed a large late step to6,690 distinct generated standard-node
|
||||
factory methods receiving Tier1 compilation (49,141,484 cumulative native method
|
||||
bytes). A later180-second interval had zero SDK/factory JIT starts and zero lost
|
||||
events. These code-event sums are not RSS/PSS allocation measurements. This
|
||||
supports a finite compilation burst in the subset; full-workload saturation has
|
||||
not been established. macOS native Apple Security object retention continues
|
||||
independently. No narrow application workaround, factory swap, runtime flag,
|
||||
resource threshold change or acceptance-baseline reset was adopted.
|
||||
|
||||
Astra reviewed the packaged sources/hashes/raw-receipt consistency/sanitization
|
||||
at integrated `6f793d25` and found no must-fix report/source issue. It approved
|
||||
only a **prospective diagnostic saturation design**, recorded in the report:
|
||||
384 fixed canonical cycles/96 restarts plus one settling cycle within1,800 seconds;
|
||||
fixed traced windows321–352 and353–384; zero generated-factory compilations,
|
||||
new SDK assembly loads and lost events; ownership checks active throughout.
|
||||
That experiment has not run and cannot pass G4. Matched full acceptance requires
|
||||
an original-SDK adapter for the identical canonical workload and at least7,200
|
||||
seconds **after** a prospectively reviewed fixed warmup. Historical failures and
|
||||
macOS native retention remain open in #799. No release approval was given.
|
||||
Restored project graphs contain no mixed Foundation SDK versions: every Foundation package (including analyzer-only development transitive GDS assets) is 2.0.1. Host graph resolves Bcl.TimeProvider 10.0.12, RateLimiting 10.0.12, Http.Resilience 10.10.0. G1 is satisfied for lane execution; full build/tests remain required at G2/G3. Frozen boundary contract is above; production and unit paths are split by plan ownership, integration/historian oracle edits coordinator-only.
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
# OPC UA SDK 2.0.1 rollout and rollback
|
||||
|
||||
This operator runbook accompanies [#782](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/782),
|
||||
the [implementation plan](2026-11-10-opcua-sdk-2-upgrade.md) and
|
||||
[execution evidence](2026-11-10-opcua-sdk-2-evidence.md). Gitea is the single
|
||||
status system. This runbook was reconciled with checkout `aa9af0dc` and the
|
||||
commit-specific local receipts linked below. It records local validation, not
|
||||
operator release approval or a fleet deployment. Receipt revisions vary; use each
|
||||
report's full source/artifact identity rather than treating every result as a test
|
||||
of this documentation commit. Open failures and operator gates still prevent a
|
||||
release go decision.
|
||||
|
||||
The target is exact OPC Foundation SDK **2.0.1**; the known-good SDK baseline is
|
||||
**1.5.378.182** (application baseline `83ed880e4471344deaf008da9bd028c7e170bdc7`).
|
||||
Restore the actual archived known-good application as a unit during rollback.
|
||||
Package downgrades alone cannot reverse the application/API migration.
|
||||
|
||||
## Operating constraints
|
||||
|
||||
**The remote-execution hold on wonder boxes vd03/vq03/vq04 remains binding.**
|
||||
No WinRM, psbridge or replacement remote-exec channel may be used until the user
|
||||
clears a channel. UNC/SMB file copying and permitted SQL access do not authorize
|
||||
service control, PowerShell execution or deployment on those hosts. Steps requiring
|
||||
a command on a held host remain pending. No fleet deployment was performed.
|
||||
|
||||
Run the service commands below only from an approved local operator session on an
|
||||
authorized Windows node. Record the node, account, channel and maintenance window
|
||||
in #782 first. Linux runtime/service handling requires its own approved environment
|
||||
and smoke record; this runbook supplies no invented Linux service-unit name.
|
||||
Do not use `scripts/install/Refresh-Services.ps1` as a blanket upgrade action: it
|
||||
also touches gateway/retired sidecar paths outside this SDK rollout.
|
||||
|
||||
## Preserved contract
|
||||
|
||||
- Existing raw/UNS namespace URIs, identifier forms, endpoint profiles, API-key/anonymous
|
||||
admission, ACL flag behavior, role and key revocation gates remain the contract.
|
||||
Source uses public transport peer evidence for miss budgets, not an IP admission rule.
|
||||
- Scalar leniency, checked numeric-array conversion and pass-through driver behavior
|
||||
stay as before. SDK immutable values are detached at boundaries; no new matrix support.
|
||||
- No application status-code, locale or event-filter semantic change is approved.
|
||||
SDK status wrappers and explicit empty `ContentFilter` construction are API changes,
|
||||
not evidence of changed protocol values. Record any stricter response, field-level
|
||||
filter error or localization difference with a consumer-impact decision before go.
|
||||
- Auditing stays opt-in. Ordinary SDK automatic audit delivery retains its type
|
||||
restrictions. Only the application's rejected-write audit gets a private, temporary
|
||||
receive-permission context after current source `Read` ACL and audit/encryption checks;
|
||||
no shared role metadata or general audit access is widened. See [security](../security.md).
|
||||
- The private source port retains the **37 legacy aggregate factories** and existing
|
||||
application results rather than adopting SDK 2 algorithms. Original-SDK fixtures
|
||||
contain **6,144 vectors**. `TreatUncertainAsBad=false` stays explicit; historian
|
||||
backend aggregate sets and existing documented differences remain unchanged.
|
||||
SDK FILETIME-zero maps to the existing CLR unset-time sentinel at history boundaries.
|
||||
- No new historian backend, companion specification, PubSub, ManagedSession, AOT or
|
||||
cluster rolling-upgrade capability is claimed. HistorianGateway Client/Contracts
|
||||
**0.4.0** is already on gateway master `16111a5e` (historiangw#119 closed);
|
||||
the real gateway gate historiangw#78 remains separate from package alignment.
|
||||
- Certificate creation correction `bd3143b7` uses the SDK's **12-month** default
|
||||
instead of treating 2048 as months. The separate RSA minimum stays 2048 bits;
|
||||
existing certificates/private-key bytes and PKI identity are retained.
|
||||
|
||||
## Release record and preflight
|
||||
|
||||
The following local results have recorded evidence. They do not substitute for
|
||||
unexecuted operator or duration gates. Keep secrets out of logs and release records.
|
||||
|
||||
| Local validation | Recorded result and receipt |
|
||||
|---|---|
|
||||
| G2 compilation and graph | Whole-solution Release builds pass with **0 warnings/errors**; production Foundation packages are exact 2.0.1, with no runtime migration analyzer. [Build/package evidence](2026-11-10-opcua-sdk-2-evidence.md#solution-performance-and-package-evidence). |
|
||||
| Security and application contracts | **17/17** final security probes; **187** wire cases; **324** Shared tests; **1115** server units with one existing skip. Seven certificate-manager/API checks are separate from network handshakes. [Case scope and provenance](2026-11-10-opcua-sdk-2-evidence.md#local-compatibility-and-security-evidence). |
|
||||
| Original aggregate results | **6,144 vectors pass directly and through the production manager**. Fixture SHA-256 `2ec2a1ac97f05d199c18da55157f8bbe0276853c3ba73ebf1b509c535c5a7dd8`; [original-SDK provenance and comparison scope](2026-11-10-opcua-sdk-2-evidence.md#preserved-aggregate-algorithms-and-independent-oracles). |
|
||||
| Separate-process compatibility | **62/62** baseline/candidate Shared, driver and actual CLI cases pass. Strict mutual DER trust/encrypted API-key coverage is separate from CLI autoaccept. [Matrix receipts](../../scripts/validation/opcua-sdk-compatibility/evidence/2026-11-10/). |
|
||||
| Actual ScadaBridge adapter | **18 None/API-key cases pass**, nine against each server. Encrypted attempts fail against both due to the preexisting own-certificate configuration gap, [ScadaBridge#121](https://gitea.dohertylan.com/dohertj2/ScadaBridge/issues/121); secure interoperability is blocked. [Consumer receipts](../../scripts/validation/scadabridge-sdk-compatibility/). |
|
||||
| Local historian backends | SQL Server **195/195** on each version; QuestDB **233 passed, 0 failed, 1 existing opt-in race-stress skip** on each. Case outcomes match. [Backend matrix](evidence/2026-11-10-opcua2/backend-matrix.md). This is not live AVEVA evidence. |
|
||||
| Bounded performance subset | Three warmed measurements per version: median rebuild **17→15 ms**, removal **81→80 ms**, equal visit counts. [Benchmark scope/load caveats](evidence/2026-11-10-opcua2/lock-benchmarks.md); these are not full wire-throughput, p95 or steady-RSS approval. |
|
||||
| Publish/package and Linux smoke | **Four** self-contained Linux ARM64/Windows x64 Host/CLI publishes pass. Actual Linux full Host passes **two start/stop cycles**, strict security/health checks and unchanged PKI. [Per-artifact revisions, SHA-256 manifests and runtime receipts](evidence/2026-11-10-opcua2/publish-smoke.md). Windows execution is still pending. |
|
||||
| Source review | Independent reviews covered identity, ownership, aggregate preservation, hierarchy and filtered audit paths. [Review scope](2026-11-10-opcua-sdk-2-evidence.md#integrated-implementation-and-source-reviews). Source review is not release approval. |
|
||||
|
||||
The second full CI-equivalent run completed with **63 TRX, 19,121 discovered,
|
||||
18,486 executed, 18,475 passed, 11 failed and 635 existing skips**. Test exit was
|
||||
**1**; execution guard exit was **0** at floor 12,000. It is not green: the exact
|
||||
failures are ten unchanged PercentGood cases and one ShadowMode 250 ms timing
|
||||
assertion (585.7377 ms), independently reproduced on baseline, with no new failure
|
||||
classes. Summary: `/tmp/opcua2-ci2-summary.json`; final provenance is in the
|
||||
[main evidence](2026-11-10-opcua-sdk-2-evidence.md#solution-performance-and-package-evidence).
|
||||
The ten PercentGood failures are tracked in
|
||||
[#796](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/796), and the separately
|
||||
reproduced baseline ScriptWorker timing failure is
|
||||
[#798](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/798).
|
||||
The opaque scripted-alarm narrow-scope denial is a separate preexisting functionality
|
||||
gap, [#797](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/797), not a new
|
||||
SDK incompatibility or an approved synthetic-ID workaround.
|
||||
|
||||
**The server resource gate failed.** The Mac restart soak failed twice around
|
||||
17 minutes at the unchanged RSS cap. The same-cap self-contained Linux server
|
||||
workload also failed at **248 cycles / 903.5 seconds**: RSS **353,931,264 bytes**
|
||||
exceeded **351,850,496 bytes**. Its retained result is
|
||||
`/tmp/opcua2-server-linux-full-20261110/task/results/soak.trx`.
|
||||
[#799](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/799) owns investigation;
|
||||
no cap waiver or two-hour server pass is recorded. The distinct actual-production
|
||||
Shared/driver/reinitialize/browser two-hour Linux lifecycle run is still active;
|
||||
its partial duration is not a pass. See [long-run scope](2026-11-10-opcua-sdk-2-evidence.md#resource-finding-and-long-run-scope)
|
||||
for instrumentation limitations and final-result updates.
|
||||
|
||||
| Operator release field | Still required before its release decision |
|
||||
|---|---|
|
||||
| Recovery set and candidate selection | Actual deployed known-good application/config archive hashes, protected PKI/permission backup, selected complete candidate artifact and matching source/manifest. Local artifact manifests above are available; a fleet recovery set is not inferred. |
|
||||
| Windows runtime/service | Approved-node installation, service account/ACL checks, preserved-PKI two-start/stop and protocol checks: [#800](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/800). Cross-publishing does not satisfy this. |
|
||||
| Live AVEVA/consumer/pair | Licensed gateway/backend, remaining secure consumer work and mixed-version Akka/config/failover validation: [#801](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/801), with historiangw#78 and ScadaBridge#121 linked as applicable. No rolling-safe claim. |
|
||||
| Resource/performance acceptance | Resolve #799, record completed required-duration runs and remaining representative load/budget evidence; no shortened run or changed cap substitutes. |
|
||||
| Rollout/rollback authorization and results | Named operator, cleared channel, environment/window, actual schema/serialization assessment, canary/rollback checks and final review/go decision in #782. The wonder-box hold remains binding. |
|
||||
|
||||
Before staging:
|
||||
|
||||
1. Inventory actual service executable/arguments, roles, endpoints, advertised host,
|
||||
application URI, raw/UNS namespace identities, certificate thumbprints, active/standby
|
||||
state and connected client builds. Record configured queues, request/transport quotas,
|
||||
history caps and expected load. Establish performance budgets and stop thresholds.
|
||||
2. Archive complete known-good binaries and node configuration, preserving access controls.
|
||||
Securely back up PKI own/private keys, peer/issuer chains, CRLs and their permissions.
|
||||
Verify recovery access under the service identity, private-key presence, expiry,
|
||||
application URI/hostname match and CA/key-usage suitability. Retain existing certificates.
|
||||
3. Inventory ConfigDb, LocalDb, historian/outbox and secret-provider state/locations. No
|
||||
ConfigDb migration is planned. Record the actual schema/serialization diff, including
|
||||
Akka/deployment contracts; if persistence changes appear, reopen the design gate and
|
||||
define data rollback before deployment. Never overwrite live historian/outbox data
|
||||
with an old binary backup or rewrite persisted sessions/subscriptions.
|
||||
4. Publish into fresh directories, hash artifacts, stage separately from the live install
|
||||
and validate graph contents. Keep baseline and candidate ports, PKI, config and stores
|
||||
separate in the isolated rig; never run both against the same mutable fixture.
|
||||
5. Confirm required positive/negative checks, reviewers and recovery artifact access.
|
||||
Missing evidence, inaccessible environments or an uncleared command channel means hold.
|
||||
|
||||
## Verification commands
|
||||
|
||||
These commands are recipes, not execution results. Run from the repository root in
|
||||
Bash with .NET 10. Record the final SHA and logs in a fresh evidence directory; inspect
|
||||
exit status before continuing. Do not pipe a failing build/test into a successful logger.
|
||||
|
||||
```bash
|
||||
git rev-parse HEAD
|
||||
git status --short
|
||||
dotnet --info
|
||||
dotnet restore ZB.MOM.WW.OtOpcUa.slnx
|
||||
dotnet build ZB.MOM.WW.OtOpcUa.slnx -c Release --no-restore
|
||||
```
|
||||
|
||||
Define the helper once after a successful build. Each invocation creates its own
|
||||
fresh TRX directory. It preserves both test failure and zero-executed-test failure.
|
||||
A successful guard still requires inspection of required cases and skips.
|
||||
|
||||
```bash
|
||||
opc_test() {
|
||||
local opc_project="$1"
|
||||
local opc_results
|
||||
opc_results=$(mktemp -d "${TMPDIR:-/tmp}/opcua2-test.XXXXXXXX") || return
|
||||
local opc_test_status=0
|
||||
dotnet test "$opc_project" -c Release --no-build --logger trx \
|
||||
--results-directory "$opc_results" || opc_test_status=$?
|
||||
local opc_count_status=0
|
||||
MIN_EXECUTED=1 bash scripts/ci/assert-not-all-skipped.sh "$opc_results"/*.trx \
|
||||
|| opc_count_status=$?
|
||||
printf 'TRX directory: %s\n' "$opc_results"
|
||||
if [ "$opc_test_status" -ne 0 ]; then return "$opc_test_status"; fi
|
||||
return "$opc_count_status"
|
||||
}
|
||||
opc_test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.csproj
|
||||
opc_test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.csproj
|
||||
opc_test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests.csproj
|
||||
opc_test tests/Client/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.csproj
|
||||
opc_test tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.csproj
|
||||
```
|
||||
|
||||
Driver/browser and simulator commands are in the
|
||||
[fixture map](../drivers/OpcUaClient-Test-Fixture.md#running-locally). Backend and
|
||||
live historian prerequisites are in [Historian.md](../Historian.md) and
|
||||
[infra/README.md](../../infra/README.md). Live tests can write sandbox data: use
|
||||
only designated sandbox tags and approved credentials through the existing secret
|
||||
provider/environment. A skipped live/DB fixture never satisfies its gate.
|
||||
Generate original aggregate evidence only with the isolated
|
||||
[oracle procedure](../../scripts/validation/opcua-aggregate-baseline/README.md),
|
||||
a new output path and original 1.5.378.182 binary; never derive expectations from
|
||||
the candidate's calculators.
|
||||
|
||||
Also run the full CI-equivalent test leg after integration; it does not replace
|
||||
fixture-specific or live gates:
|
||||
|
||||
```bash
|
||||
opc_solution_test() {
|
||||
local opc_results
|
||||
opc_results=$(mktemp -d "${TMPDIR:-/tmp}/opcua2-solution.XXXXXXXX") || return
|
||||
local opc_test_status=0
|
||||
dotnet test ZB.MOM.WW.OtOpcUa.slnx -c Release --no-build \
|
||||
-m:1 -p:TestTfmsInParallel=false --settings scripts/ci/ci.runsettings \
|
||||
--filter 'Category!=E2E&Category!=LiveIntegration' \
|
||||
--logger trx --results-directory "$opc_results" || opc_test_status=$?
|
||||
local opc_count_status=0
|
||||
MIN_EXECUTED=12000 bash scripts/ci/assert-not-all-skipped.sh "$opc_results"/*.trx \
|
||||
|| opc_count_status=$?
|
||||
printf 'TRX directory: %s\n' "$opc_results"
|
||||
if [ "$opc_test_status" -ne 0 ]; then return "$opc_test_status"; fi
|
||||
return "$opc_count_status"
|
||||
}
|
||||
opc_solution_test
|
||||
```
|
||||
|
||||
Publish the Host project to a fresh operator-selected `$OPC_PUBLISH_DIR` using the
|
||||
approved `$OPC_RID` and deployment's self-contained choice. The Windows install
|
||||
expects `OtOpcUa.Host.exe`; [ServiceHosting.md](../ServiceHosting.md) also describes
|
||||
script-worker packaging. Example self-contained Windows artifact command:
|
||||
|
||||
```bash
|
||||
dotnet publish src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj \
|
||||
-c Release -r win-x64 --self-contained true -o "$OPC_PUBLISH_DIR"
|
||||
```
|
||||
|
||||
Repeat with the approved Linux RID/framework settings in its validation environment;
|
||||
cross-publishing is not runtime/install evidence. Hash the complete staged artifact
|
||||
and retain a manifest outside it (PowerShell 7, where `$Candidate` and `$Manifest`
|
||||
are approved absolute local paths and the manifest is outside the candidate):
|
||||
|
||||
```powershell
|
||||
Get-ChildItem -LiteralPath $Candidate -Recurse -File |
|
||||
Get-FileHash -Algorithm SHA256 |
|
||||
Export-Csv -LiteralPath $Manifest -NoTypeInformation
|
||||
```
|
||||
|
||||
## Sequence and stop/go decision
|
||||
|
||||
Progress only after the preceding stage's recorded checks and explicit gate decision:
|
||||
**isolated rig → ScadaBridge/upstream interoperability → DEV canary → QA pair →
|
||||
production canary**. Attach source/artifact identity, start/end times, operator,
|
||||
checks, observations and go/hold/rollback decision for each stage to #782.
|
||||
|
||||
A standby-first pair restart is allowed only after the application's mixed-version
|
||||
Akka serialization, deployment/config and failover gate succeeds. Verify standby
|
||||
health, security, data, history and events; exercise failover before upgrading its
|
||||
peer. OPC UA wire interoperability alone does not prove cluster rolling safety.
|
||||
Without that gate, arrange a maintenance window and coordinated pair restart;
|
||||
record expected service interruption and recovery checks.
|
||||
|
||||
The following Windows commands are **local-session steps only**. Bind `$Install`
|
||||
to the inspected current binary directory, `$Candidate` to the reviewed staged
|
||||
artifact and `$Backup` to a new protected directory. Keep PKI/data stores outside
|
||||
binary replacement; if they are inside the install tree, define exclusions and a
|
||||
separate protected backup before using these commands. Save configuration outside
|
||||
`$Candidate`, then apply the reviewed node configuration to the staging directory;
|
||||
never overwrite it with publisher defaults or expose secrets in command output.
|
||||
|
||||
```powershell
|
||||
Get-CimInstance Win32_Service -Filter "Name='OtOpcUaHost'" |
|
||||
Select-Object Name, State, PathName, StartName
|
||||
Stop-Service -Name OtOpcUaHost -ErrorAction Stop
|
||||
(Get-Service -Name OtOpcUaHost).WaitForStatus('Stopped', [TimeSpan]::FromSeconds(60))
|
||||
robocopy $Install $Backup /E /COPYALL /DCOPY:DAT /R:1 /W:1
|
||||
if ($LASTEXITCODE -ge 8) { throw 'Known-good backup failed' }
|
||||
# After checking the backup and candidate manifest, swap complete binary directories.
|
||||
# Use sibling paths on the same volume; the service PathName must still resolve to $Install.
|
||||
Move-Item -LiteralPath $Install -Destination $PreviousInstall -ErrorAction Stop
|
||||
Move-Item -LiteralPath $Candidate -Destination $Install -ErrorAction Stop
|
||||
Start-Service -Name OtOpcUaHost -ErrorAction Stop
|
||||
(Get-Service -Name OtOpcUaHost).WaitForStatus('Running', [TimeSpan]::FromSeconds(60))
|
||||
```
|
||||
|
||||
`$PreviousInstall` must be a fresh reviewed sibling path. Handle any failure between
|
||||
moves by restoring that complete directory before starting. Service `Running` is
|
||||
only a process check. Run the substantive checks below after every start and failover:
|
||||
|
||||
- Check `/healthz`, `/health/ready` and role-appropriate `/health/active` at the
|
||||
recorded HTTP base URL; compare ServiceLevel and active/standby state to baseline.
|
||||
- Confirm certificate thumbprints, trust/issuer chains, endpoint policies/application
|
||||
URI and namespace-URI/NodeId identity. Prove trusted SignAndEncrypt works and
|
||||
untrusted/revoked/inappropriate certificates and wrong keys fail without downgrade.
|
||||
- Exercise raw and UNS browse/read, scalar/array writes on sandbox nodes, subscriptions,
|
||||
quality and source/server timestamps. Verify current legitimate ACL allows as well
|
||||
as denies, key expiry/revocation and audience-specific event/audit filtering.
|
||||
- Verify alarms/refresh/ack and no missing or duplicate events; verify configured raw,
|
||||
modified, processed and event history, paging/release/reauthentication and expected
|
||||
unsupported operations. Verify recorder/outbox progress without duplicate replay.
|
||||
- Observe reconnects, stale values, legitimate-call denials, continuation-point errors,
|
||||
session/item/certificate-handle growth, shutdown, queue depths and latency.
|
||||
|
||||
Use [Client.CLI.md](../Client.CLI.md) for exact connect/browse/read/subscribe/history
|
||||
and alarm syntax. Supply credentials through the approved operator mechanism; do
|
||||
not record command lines containing API-key passwords in release evidence.
|
||||
|
||||
Stop promotion and restore the known-good application for any authorization regression,
|
||||
missing/duplicate alarm or history data, changed identifiers, insecure fallback,
|
||||
restart failure or sustained performance-budget miss. Compare like hardware/config/load:
|
||||
at least three warmed measurements, provisional limits ≤10% median throughput loss
|
||||
and ≤15% p95 latency/allocation/RSS growth, plus at least two hours of lifecycle soak.
|
||||
Any deviation needs investigation and a recorded acceptance decision. Do not lower
|
||||
thresholds or count absent environments as passed.
|
||||
|
||||
## Rollback
|
||||
|
||||
1. Freeze promotion; record the trigger and affected nodes/clients in #782. Preserve
|
||||
candidate logs and manifests with secrets redacted. Stop the candidate through the
|
||||
approved local channel; the remote-exec hold still applies during rollback.
|
||||
2. With the service stopped, move the candidate install to a fresh quarantine path,
|
||||
restore the complete `$PreviousInstall` (or verified `$Backup`) to the inspected
|
||||
service path, and restore the matching node configuration and preserved PKI with
|
||||
original permissions. Check hashes/thumbprints before starting. Do not delete or
|
||||
roll back live historian, outbox or ConfigDb data without a reviewed data strategy.
|
||||
3. Start `OtOpcUaHost` locally with the same `Start-Service`/wait commands above.
|
||||
On Linux, use only the environment's approved recorded supervisor procedure.
|
||||
Verify health, endpoint/namespace/certificate identity and active/standby state.
|
||||
4. Reconnect clients as needed. In-memory sessions, subscriptions and continuation
|
||||
points do not survive process rollback; discard old points and begin new reads.
|
||||
Rebind namespaces by URI. Never replay ambiguous non-idempotent writes or alarm
|
||||
acknowledgements. Confirm API-key writes/ack, subscriptions, alarm counts and history
|
||||
continuity with the same positive and negative checks used at rollout.
|
||||
5. Record recovery time, checks, data reconciliation and final hold/go decision.
|
||||
If rolling source back, revert package and application commits together; never
|
||||
mix old package binaries with SDK 2 application assemblies. Keep candidate evidence
|
||||
for diagnosis and link follow-up issues from #782.
|
||||
|
||||
## Results handoff
|
||||
|
||||
Local build, security/wire, aggregate, backend, compatibility, publish and Linux
|
||||
smoke evidence is linked above with its actual scope and revisions. The server
|
||||
RSS gate is failed (#799), full CI retains the 11 baseline failures (#796/#798),
|
||||
the client lifecycle run is still active, secure ScadaBridge interoperability is
|
||||
blocked (ScadaBridge#121), and
|
||||
Windows/live AVEVA/pair/operator gates remain pending (#800/#801). No fleet rollout
|
||||
or rollback has been executed. The coordinator records the final client-lifecycle result
|
||||
and remaining decisions in #782 and the main evidence; none is inferred from a
|
||||
passing subset or cross-published artifact.
|
||||
@@ -1,113 +0,0 @@
|
||||
{
|
||||
"issue": "https://gitea.dohertylan.com/dohertj2/historiangw/issues/131",
|
||||
"consumerBase": "16111a5e",
|
||||
"opcUaSdk": "1.5.378.182 (independent of SDK2 migration)",
|
||||
"build": {
|
||||
"command": "dotnet build ZB.MOM.WW.OtOpcUa.slnx -c Release -m:1",
|
||||
"exit": 0,
|
||||
"warnings": 0,
|
||||
"errors": 0,
|
||||
"seconds": 37.33,
|
||||
"logSha256": "cb4f1ee96bf05254e5c707179c1e81d99d8267e28c5117a84afa0b0c853fa329"
|
||||
},
|
||||
"packages": [
|
||||
{
|
||||
"package": "ZB.MOM.WW.HistorianGateway.Client/1.0.0",
|
||||
"type": "package",
|
||||
"source": "https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json",
|
||||
"repositoryCommit": "ea9f21c850fb618767e12690924f9966f1e433ac",
|
||||
"sha256": {
|
||||
"lib/net10.0/ZB.MOM.WW.HistorianGateway.Client.dll": "b89a02e0d228ec549c4dfb755694f7f67b573a6077f06ff2781165e884fc5e12",
|
||||
"zb.mom.ww.historiangateway.client.1.0.0.nupkg": "0a3f2836727671c07c0c393e705cc9737d2ed44258213a51fb4acf0d0ab7129e"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "ZB.MOM.WW.HistorianGateway.Contracts/1.0.0",
|
||||
"type": "package",
|
||||
"source": "https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json",
|
||||
"repositoryCommit": "ea9f21c850fb618767e12690924f9966f1e433ac",
|
||||
"sha256": {
|
||||
"lib/net10.0/ZB.MOM.WW.HistorianGateway.Contracts.dll": "e60d3d4650be5d251982f7847404daf1b666f61d56e2b0ccb844af8677e97743",
|
||||
"zb.mom.ww.historiangateway.contracts.1.0.0.nupkg": "83ffd12bcbbf919c973ba522560e10aa93a4cb8c686a3232030a2cfa5f00995e"
|
||||
}
|
||||
}
|
||||
],
|
||||
"sourceOverrides": false,
|
||||
"tests": [
|
||||
{
|
||||
"project": "gateway",
|
||||
"testExit": 0,
|
||||
"executionGuardExit": 0,
|
||||
"counters": {
|
||||
"total": "529",
|
||||
"executed": "523",
|
||||
"passed": "523",
|
||||
"failed": "0",
|
||||
"error": "0",
|
||||
"timeout": "0",
|
||||
"aborted": "0",
|
||||
"inconclusive": "0",
|
||||
"passedButRunAborted": "0",
|
||||
"notRunnable": "0",
|
||||
"notExecuted": "0",
|
||||
"disconnected": "0",
|
||||
"warning": "0",
|
||||
"completed": "0",
|
||||
"inProgress": "0",
|
||||
"pending": "0"
|
||||
},
|
||||
"outcomes": {
|
||||
"Passed": 523,
|
||||
"Failed": 0,
|
||||
"NotExecuted": 6
|
||||
},
|
||||
"requiredTlsWireTests": [
|
||||
{
|
||||
"name": "ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.HistorianGatewayTlsPolicyWireTests.Explicit_tls_opt_out_reaches_the_same_self_signed_server_shape",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"name": "ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.HistorianGatewayTlsPolicyWireTests.Default_tls_rejects_an_untrusted_server_before_the_rpc",
|
||||
"outcome": "Passed"
|
||||
},
|
||||
{
|
||||
"name": "ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.HistorianGatewayTlsPolicyWireTests.Plaintext_ignores_stale_tls_opt_out_and_ca_settings_and_reaches_the_rpc",
|
||||
"outcome": "Passed"
|
||||
}
|
||||
],
|
||||
"trxSha256": "68086e66da0942214c0be805366b47c443f633d90f217c91e07ec685e8aea9d9"
|
||||
},
|
||||
{
|
||||
"project": "runtime",
|
||||
"testExit": 0,
|
||||
"executionGuardExit": 0,
|
||||
"counters": {
|
||||
"total": "1005",
|
||||
"executed": "1002",
|
||||
"passed": "1002",
|
||||
"failed": "0",
|
||||
"error": "0",
|
||||
"timeout": "0",
|
||||
"aborted": "0",
|
||||
"inconclusive": "0",
|
||||
"passedButRunAborted": "0",
|
||||
"notRunnable": "0",
|
||||
"notExecuted": "0",
|
||||
"disconnected": "0",
|
||||
"warning": "0",
|
||||
"completed": "0",
|
||||
"inProgress": "0",
|
||||
"pending": "0"
|
||||
},
|
||||
"outcomes": {
|
||||
"Passed": 1002,
|
||||
"Failed": 0,
|
||||
"NotExecuted": 3
|
||||
},
|
||||
"requiredTlsWireTests": [],
|
||||
"trxSha256": "c12308fd650be0646beba044a4b3301dc2aefc43097b2da667d0eb5da408fc65"
|
||||
}
|
||||
],
|
||||
"review": "Astra approved source/security mapping and corrected real-wire tests; actual published package verification complete.",
|
||||
"deploymentPerformed": false
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
# Local historian backend matrix — 2026-11-10
|
||||
|
||||
Candidate a8e1082d69a112ea0e16788f023067979698527a (SDK 2.0.1) compared with baseline 83ed880e4471344deaf008da9bd028c7e170bdc7 (SDK 1.5.378.182). This is the existing direct backend integration test matrix over real local SQL Server and QuestDB. It is not an OPC UA wire test, AVEVA validation, or stress/soak approval. No source edits, expected-value changes, new skips, suppressions, issue updates, pushes, or commits were made for this task.
|
||||
|
||||
| Version | Backend | Passed | Failed | Skipped | Build / test / guard exit |
|
||||
|---|---|---:|---:|---:|---|
|
||||
| candidate | SqlServer | 195 | 0 | 0 | 0 / 0 / 0 |
|
||||
| candidate | QuestDb | 233 | 0 | 1 | 0 / 0 / 0 |
|
||||
| baseline | SqlServer | 195 | 0 | 0 | 0 / 0 / 0 |
|
||||
| baseline | QuestDb | 233 | 0 | 1 | 0 / 0 / 0 |
|
||||
|
||||
Every invocation was unfiltered, used a fresh SHA/project run directory and TRX, preserved the dotnet test exit status, and ran the repository MIN_EXECUTED=1 guard. Project builds were serial within each worktree; test invocations were serial against the same isolated fixture endpoints. Exact commands, UTC start/end, SDK dependency versions and test/SDK binary SHA-256 hashes are in each result.json.
|
||||
|
||||
Candidate positive coverage includes, on each backend, 13 raw-read, 14 AtTime, 11 modified-value, 7 event-read, 12 update, 64 processed-read, 6 bounding-value and 4 quality-contract conformance cases, plus backend-specific schema, write, provisioning, retention (SQL), tombstone/index/TTL (QuestDB), and alarm-writer tests.
|
||||
|
||||
SqlServer: baseline/candidate display names and outcomes identical = True across 195 discovered cases. Baseline failures: 0; candidate failures: 0.
|
||||
QuestDb: baseline/candidate display names and outcomes identical = True across 234 discovered cases. Baseline failures: 0; candidate failures: 0.
|
||||
|
||||
No PercentGood failure appeared in these selected integration suites. Their empty failure sets match where both versions have completed; there is no failing message pair to compare. This does not resolve or replace the separate standalone SDK-oracle PercentGood issue #796.
|
||||
|
||||
The existing QuestDB LiveWriteReadRaceStressTests.Per_sample_reads_of_a_tag_being_written_never_fault_and_never_return_a_valueless_non_bad_result case skips because HISTORIAN_QUESTDB_STRESS_SECONDS is unset. It was not enabled, shortened, reclassified, or replaced by a normal passing case. No five-million-row workload or two-hour stress/soak was run. Those gates remain pending.
|
||||
|
||||
## Fixture ownership and exact images
|
||||
|
||||
SQL Server: existing otopcua-dev-sql-1, endpoint 127.0.0.1:14330. Its documented local dev credential was read from docs/Historian.md and supplied only via environment; the password and full secret-bearing connection string were not printed or included in the report. SqlHistorianFixture was verified before execution: it forces master only for CREATE/DROP, generates OtOpcUaHistorian_T_<GUID>, directs all test schemas into that new database, clears its pools, and drops only its generated name. The fixture source is unchanged between baseline and candidate. Five preexisting OtOpcUaHistorian_T_GUID databases were recorded and were never dropped by this task.
|
||||
|
||||
SQL container ID: `74ea75e5904c647f18e89b8b14c9ac385d7149c0484fbf13f6c16ec00cf3e24f`; image ID: `sha256:a2fbff321505724b8a271a214d4dec020a8e7a745c83eede0b9d762b51a9ab34`.
|
||||
|
||||
QuestDB: new task-owned otopcua-sdk2-questdb, loopback-only PG port 32770 and HTTP port 32771, new storage volume `otopcua-sdk2-questdb-data-20261110t170141z`. No existing QuestDB service or volume was used. Connection settings use ServerCompatibilityMode=NoTypeLoading and Maximum Pool Size=16; the fixture applies its existing one-second idle pruning. QuestDbFixture assigns unique t<12 hex>_ prefixes, validates them, locates owned tables with starts_with (not wildcard LIKE), and drops only those prefixes. The same unchanged fixture source and image were used by both versions.
|
||||
|
||||
QuestDB image: `questdb/questdb:10.0.1`; immutable image ID `sha256:41ceca901724ad10a0035da0cb0c1f99f88bf3692abdc9090baabdfff1d215f9`; digest `questdb/questdb@sha256:67eaed863ebb2383227919ea9a5499a4a39ceca3862c20b974c84a10e11cdf89`.
|
||||
|
||||
## Run evidence
|
||||
|
||||
| Version / backend | UTC start | UTC end | Directory |
|
||||
|---|---|---|---|
|
||||
| candidate SqlServer | 2026-11-10T17:03:02.101289+00:00 | 2026-11-10T17:03:52.950418+00:00 | `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/a8e1082d/backend-matrix-20261110T170141Z/SqlServer` |
|
||||
| candidate QuestDb | 2026-11-10T17:03:58.392642+00:00 | 2026-11-10T17:11:13.275488+00:00 | `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/a8e1082d/backend-matrix-20261110T170141Z/QuestDb` |
|
||||
| baseline SqlServer | 2026-11-10T17:11:19.005717+00:00 | 2026-11-10T17:12:11.817250+00:00 | `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/83ed880e/backend-matrix-20261110T170141Z/SqlServer` |
|
||||
| baseline QuestDb | 2026-11-10T17:12:16.867323+00:00 | 2026-11-10T17:19:37.746023+00:00 | `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/83ed880e/backend-matrix-20261110T170141Z/QuestDb` |
|
||||
|
||||
Session index / fixture metadata / aggregate results / exact failure-set comparison:
|
||||
|
||||
/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/a8e1082d/backend-matrix-20261110T170141Z
|
||||
|
||||
Each project directory contains build.log, test.log, a fresh TRX, guard.log and result.json. Candidate directories also contain class-counters.json. The session has fixture-metadata.json and readiness/WAL snapshots; cleanup.json and questdb-container.log are written after final cleanup.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Completed at 2026-11-10T17:19:44.751608+00:00. SQL GUID-database inventory unchanged: True; SQL container ID unchanged: True. QuestDB tables remaining after all tests: 0. Task-owned container absent: True; task-owned volume absent: True. Only the captured task container ID and its verified task-labelled volume were removed. No other container, volume or database was removed.
|
||||
|
||||
## Reproduction and remaining gates
|
||||
|
||||
Completed scripts (kept in /tmp; they create new timestamped evidence directories and protect ownership):
|
||||
|
||||
```bash
|
||||
python3 /tmp/opcua2-backend-setup.py
|
||||
python3 /tmp/opcua2-backend-matrix.py
|
||||
python3 /tmp/opcua2-backend-cleanup.py
|
||||
python3 /tmp/opcua2-backend-report.py
|
||||
```
|
||||
|
||||
For each version/backend the actual commands were dotnet build <owning-project.csproj> -c Release, then dotnet test <owning-project.csproj> -c Release --no-build --no-restore --logger trx --results-directory <fresh-directory>, with only the appropriate real connection environment variable set. Guard: MIN_EXECUTED=1 bash scripts/ci/assert-not-all-skipped.sh <fresh-directory>/*.trx. The orchestration preserves nonzero test and guard statuses instead of overwriting them. SQL connection acquisition is from the documented local dev recipe without echo; QuestDB endpoints are obtained from the newly created task container.
|
||||
|
||||
AVEVA/HistorianGateway live read/write/alarm/provisioning and licensed backend validation remain external and pending. The separate SDK-oracle issue #796, wire/security interoperability, five-million-row workload, two-hour stress/soak and release/performance gates are not completed by these local backend runs.
|
||||
@@ -1,60 +0,0 @@
|
||||
# OPC UA SDK lock benchmark diagnostic — 2026-11-10
|
||||
|
||||
Compared baseline `83ed880e4471344deaf008da9bd028c7e170bdc7` (SDK 1.5.378.182) with candidate `a8e1082d69a112ea0e16788f023067979698527a` (SDK 2.0.1). Both Release project builds passed with 0 warnings/errors. No source edits or new commits were made for this evidence task.
|
||||
|
||||
Same Mac16,7, 14 logical/physical CPU cores, 48 GiB RAM, .NET SDK 10.0.403. OS details and exact assembly SHA-256 hashes are in metadata.json. All runs used OTOPCUA_BENCH=1 and the existing NodeManagerLockTimeBenchmarkTests class. Scenario sizes and assertions are identical; the only benchmark source differences are SDK-required OperationContext.RequestLifetime.None and IAsyncNodeManager interface changes. Each test boots a private server with its own temporary PKI and probed port. No containers or global fixture were used.
|
||||
|
||||
One warmup invocation per version was followed by three recorded invocations per version, alternating baseline/candidate with 25 seconds between processes. Each invocation is a fresh process; warmup primes host/file caches, not persistent in-process JIT state. Default xUnit test order varied. Warmups are excluded from the medians.
|
||||
|
||||
| Recorded metric | Baseline runs | SDK 2 runs | Baseline median | SDK 2 median | Change |
|
||||
|---|---|---|---:|---:|---:|
|
||||
| Rebuild 10k-tag folder | 18, 15, 17 | 15, 15, 14 | 17 ms | 15 ms | -11.76% |
|
||||
| Remove 200 equipment / 50k nodes | 81, 50, 86 | 80, 68, 80 | 81 ms | 80 ms | -1.23% |
|
||||
| Visits for 10 removals / 50 conditions | 10, 10, 10 | 10, 10, 10 | 10 visits | 10 visits | +0.00% |
|
||||
| OS command maximum resident set size | 385.17, 136.20, 136.30 | 136.39, 136.81, 136.80 | 136.30 MiB | 136.80 MiB | +0.37% |
|
||||
|
||||
The median teardown timings do not exceed the provisional +15% latency-growth reference. Their inverse-time local teardown-rate proxies change by +13.33% (rebuild) and +1.25% (removal), so neither shows a >10% drop in this subset. These derived rates are not wire throughput, and three samples do not establish p95 latency. Command maximum-RSS median rose 0.37%, below the +15% reference, but it does not measure steady server RSS. No change to budgets or acceptance decision is implied.
|
||||
|
||||
All eight invocations passed all three cases: 24 passed, 0 failed, 0 skipped (18 recorded case executions plus 6 warmup). Every test exit and MIN_EXECUTED=3 guard exit was 0; aggregate orchestrator exit was 0. The original 5000 ms opt-in teardown assertions remained enforced; the largest recorded operation was 86 ms. The strict, hardware-independent visit counter was exactly 10 in all eight invocations.
|
||||
|
||||
Host-load caveat: other solution testing/build activity shared this host. Initial preparation saw load averages 19.78/30.02/23.48 on 14 cores; per-run before/after uptime snapshots are retained in result.json. Recorded one-minute load ranged 5.58–10.90; five-minute load ranged 15.26–20.32 and fifteen-minute load 18.45–20.56. BENCH uses integer-millisecond Stopwatch output. Timing spread, quantization and changing xUnit ordering preclude a stable performance claim. OS /usr/bin/time -l maximum RSS is command-invocation rusage, not isolated teardown allocation or steady server RSS; baseline run1 is an outlier and is included unchanged. GC/allocation counters are not exposed by the existing harness and were not measured. This subset does not complete wire throughput, p50/p95/p99 latency, allocation, steady RSS, history-page throughput, load, soak, publish, or resource-lifecycle gates.
|
||||
|
||||
## Recorded invocations
|
||||
|
||||
| Version / phase | UTC start | UTC end | Test / guard exit | Evidence directory |
|
||||
|---|---|---|---|---|
|
||||
| baseline warmup | 2026-11-10T16:54:37.980941+00:00 | 2026-11-10T16:54:42.012296+00:00 | 0 / 0 | `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/83ed880e/lock-bench-20261110T165437Z/warmup` |
|
||||
| candidate warmup | 2026-11-10T16:55:07.020267+00:00 | 2026-11-10T16:55:11.610491+00:00 | 0 / 0 | `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/a8e1082d/lock-bench-20261110T165437Z/warmup` |
|
||||
| baseline run1 | 2026-11-10T16:55:36.615335+00:00 | 2026-11-10T16:55:40.023724+00:00 | 0 / 0 | `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/83ed880e/lock-bench-20261110T165437Z/run1` |
|
||||
| candidate run1 | 2026-11-10T16:56:05.032437+00:00 | 2026-11-10T16:56:09.352092+00:00 | 0 / 0 | `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/a8e1082d/lock-bench-20261110T165437Z/run1` |
|
||||
| baseline run2 | 2026-11-10T16:56:34.361326+00:00 | 2026-11-10T16:56:37.712286+00:00 | 0 / 0 | `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/83ed880e/lock-bench-20261110T165437Z/run2` |
|
||||
| candidate run2 | 2026-11-10T16:57:02.720388+00:00 | 2026-11-10T16:57:06.894343+00:00 | 0 / 0 | `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/a8e1082d/lock-bench-20261110T165437Z/run2` |
|
||||
| baseline run3 | 2026-11-10T16:57:31.903230+00:00 | 2026-11-10T16:57:35.306953+00:00 | 0 / 0 | `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/83ed880e/lock-bench-20261110T165437Z/run3` |
|
||||
| candidate run3 | 2026-11-10T16:58:00.315244+00:00 | 2026-11-10T16:58:04.720985+00:00 | 0 / 0 | `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/a8e1082d/lock-bench-20261110T165437Z/run3` |
|
||||
|
||||
Each directory contains a fresh TRX (BENCH stdout attached to individual test results), test.log, resource.log, guard.log, and result.json. Session metadata/results/summary:
|
||||
|
||||
/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/a8e1082d/lock-bench-20261110T165437Z
|
||||
|
||||
Reproduction (in each version worktree; choose a new, nonexistent absolute result directory each invocation):
|
||||
|
||||
```bash
|
||||
OPC_RUN_DIR=/absolute/fresh/result/directory
|
||||
test ! -e "$OPC_RUN_DIR" || exit 2
|
||||
mkdir -p "$OPC_RUN_DIR" || exit
|
||||
opc_test_status=0
|
||||
OTOPCUA_BENCH=1 /usr/bin/time -l dotnet test \
|
||||
tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.csproj \
|
||||
-c Release --no-build --no-restore \
|
||||
--filter FullyQualifiedName~NodeManagerLockTimeBenchmarkTests \
|
||||
--logger trx --logger "console;verbosity=detailed" \
|
||||
--results-directory "$OPC_RUN_DIR" \
|
||||
> "$OPC_RUN_DIR/test.log" 2> "$OPC_RUN_DIR/resource.log" || opc_test_status=$?
|
||||
opc_guard_status=0
|
||||
MIN_EXECUTED=3 bash scripts/ci/assert-not-all-skipped.sh "$OPC_RUN_DIR"/*.trx \
|
||||
> "$OPC_RUN_DIR/guard.log" 2>&1 || opc_guard_status=$?
|
||||
if [ "$opc_test_status" -ne 0 ]; then exit "$opc_test_status"; fi
|
||||
exit "$opc_guard_status"
|
||||
```
|
||||
|
||||
Exact completed orchestration script: /tmp/opcua2-lock-bench.py. It records environment, SDK dependencies, assembly hashes, start/end UTC, host load, metrics, counts and statuses, and alternates the eight invocations with 25-second gaps.
|
||||
@@ -1,70 +0,0 @@
|
||||
# Actual Host / CLI publish and runtime smoke — 2026-11-10
|
||||
|
||||
Linux ARM64 actual full Host start/stop smoke PASSED. Windows x64 cross-publish and static inspection PASSED. Windows runtime/service installation remains a formal pending operator gate; this report does not complete Gate G4.
|
||||
|
||||
Reproducible probe commit: `3a11087b37728b4a97c2884d55f2d7d9949923b9`. Only four new files under scripts/validation/opcua-publish-smoke were added. No production, existing documentation, package policy, fleet, sister-repository or issue changes were made by this lane. Root commits 2d75cecb and bd3143b7 were merged into the isolated checkout before the applicable publishes.
|
||||
|
||||
## Published artifacts
|
||||
|
||||
The repository install recipe documents dotnet publish -c Release -r win-x64 --self-contained. The four commands used that setting, with PublishSingleFile=false and PublishTrimmed=false explicitly retained, one MSBuild node and new output folders. The Linux RID matches the cached ARM64 container. No Native AOT was attempted. All four actual publish exits were zero.
|
||||
|
||||
| RID / app | Source revision at publish | Files / bytes | SDK assemblies | Native inventory entries |
|
||||
|---|---|---:|---:|---:|
|
||||
| linux-arm64/host | `d31560cd360144132f1433a04fcb5acb25dfa3fd` | 1388 / 356618849 | 8 | 32 |
|
||||
| linux-arm64/cli | `8a1028c4e2f308a29bcc4a504596df6d667554f8` | 236 / 111341495 | 7 | 15 |
|
||||
| win-x64/host | `8a1028c4e2f308a29bcc4a504596df6d667554f8` | 1390 / 342188283 | 8 | 37 |
|
||||
| win-x64/cli | `8a1028c4e2f308a29bcc4a504596df6d667554f8` | 236 / 103181689 | 7 | 16 |
|
||||
|
||||
Linux Host was published before the root client-factory-only correction; it has no Client.Shared dependency. Both CLI publishes and Windows Host include the merged bd3143b7 correction. Host production source is unchanged across these two publish revisions. The exact per-artifact source, command and SHA-256 manifest are retained in validation-receipt.json.
|
||||
|
||||
Every OPCFoundation dependency is 2.0.1. Each SDK assembly basename appears once per application publish tree; no old SDK copy or migration analyzer runtime artifact was found. All inspected Linux native entries are AArch64 ELF and Windows entries are x86-64 PE; apphosts and bundled runtimes match their RID. Both Host publishes contain the separately self-contained script-worker. Runtimeconfig includedFrameworks records Microsoft.NETCore.App 10.0.14, plus Microsoft.AspNetCore.App 10.0.14 on Host. This validates packaging and the in-process default only; the out-of-process script execution path was not exercised here.
|
||||
|
||||
| Artifact | Apphost SHA-256 | Full-file-manifest SHA-256 |
|
||||
|---|---|---|
|
||||
| linux-arm64/host | `d97f318f268246852603166a19d66c2da788a75a7f9124b2242e6c794985dcbb` | `32ec603b22ba7f7df62904bc738e9193e0c1275fcc7a9fb5c65b9aa5d65c0f5e` |
|
||||
| linux-arm64/cli | `b4840db34c07ec7f0fa8cbc220c5552fac87527d3b222c4e47260c68c7eedb19` | `992bc65bcf74c0d57aa6d789085abdfcd2290b0496931a445a11e801de2312a6` |
|
||||
| win-x64/host | `7896ec7a0db88dd00d893ea9a55bc22e40849e7c789af7eea79a4514ed587972` | `b42baf99acbbe0646c487fbaf193f9f4fea66adfd393a41b841b55ba95d53aa0` |
|
||||
| win-x64/cli | `d880d4c83448f9ee5a2ea79c3573c1b2301b5fc17c277cf072153a589543bc5d` | `28a9872354539d600659c4af463e24c7ba40136c4ab67a5cb40c36b5d9575060` |
|
||||
|
||||
Complete paths, sizes and SHA-256 values are in the four *-sha256.json manifests. Managed SDK/package inventory is in publish-inventory.json; native paths, file-format/architecture and hashes are in native-inventory.json. Runtime-generated logs and private PKI/state are excluded from shipped-artifact manifests.
|
||||
|
||||
## Actual Linux runtime
|
||||
|
||||
Image: mcr.microsoft.com/dotnet/sdk:10.0.103-noble; cached immutable ID `sha256:b04611ee9e1b7d36797012ed921962c57189cb4dbb3dff12afb8e158110f293b`, architecture `arm64`, digest `mcr.microsoft.com/dotnet/sdk@sha256:e362a8dbcd691522456da26a5198b8f3ca1d7641c95624fadc5e3e82678bd08a`. The published self-contained OtOpcUa.Host executable was the container command; the SDK image supplies only the portable probe framework. No ProcessHost server substitute was used.
|
||||
|
||||
The real Host ran Cluster:Roles=driver, ConfigSource:Mode=FetchAndCache, a self-only otopcua Akka seed, and an unreachable loopback CentralFetchEndpoint. No ConfigDb was registered. A task-only LocalDb deployment cache was seeded through public production AddOtOpcUaLocalDb / IDeploymentArtifactCache APIs, using the public DeploymentId formatter. The artifact contains one raw folder and a real peppered HMAC API key with read/browse/subscribe grants. Production API-key authentication was used. PKI, LocalDb and encrypted secrets SQLite lived in one fresh GUID state directory. Random secrets were environment-only; none are included in this report or recorded arguments.
|
||||
|
||||
All preparation and runtime containers used --network none and task labels. HTTP, UA and Akka ports were randomly chosen within that private namespace; none were published to the host. The same task-owned container, application executable, LocalDb and GUID PKI were started and stopped twice.
|
||||
|
||||
| Executed check | Actual result |
|
||||
|---|---|
|
||||
| /healthz, /health/ready and /health/active on each boot | 6 requests passed; ready/active Healthy and Akka Up |
|
||||
| Strict trusted Basic256Sha256 SignAndEncrypt + real API-key session / ServerStatus.State read | 2 passed, Good quality, peer autoaccept false |
|
||||
| Wrong secret for the same real key | 2 denied with BadIdentityTokenRejected |
|
||||
| Untrusted server certificate | Denied with BadCertificateUntrusted |
|
||||
| Untrusted client certificate | Denied with BadSecurityChecksFailed |
|
||||
| Actual published CLI --help | Exit 0 |
|
||||
| Actual published CLI encrypted API-key ServerStatus.State reads | 2 passed, Good quality, exit 0 |
|
||||
| Actual Host stop / restart / stop | Both stops exit 0; Running=false, OOMKilled=false |
|
||||
| Cache restore | Production restored-served-state log appears on both boots |
|
||||
| Required execution guard | Exactly 2 boots and 6 strict SDK probe cases executed; overall exit 0 |
|
||||
|
||||
The CLI retains its existing AutoAcceptCertificates=true behavior and has no strict-validation switch. Its successful reads are separate from the strict SDK peer-validation checks. Its matching application-name/URI certificate was prepared through the public SDK API, then trusted by the server before first boot. A closed-port CLI setup preflight intentionally returned nonzero; it is not counted as a passing connection case.
|
||||
|
||||
Application certificate SHA-256 on BOTH boots: `19000d33d71bb11c92f282724a3515b9119783e070df06667b05f49e41413640`. Preserved thumbprint on BOTH boots: `sha1 Fingerprint=DE:CB:33:B0:DE:32:9B:FB:40:FF:A8:5C:66:48:CE:44:01:2A:48:F8`. The public certificate bytes and thumbprint were unchanged; no rotation was performed.
|
||||
|
||||
Raw process stdout/stderr and this run’s appended rolling script/application/selflog bytes were scanned before sanitized copies were saved. Exact task API-key/secret, pepper, fetch key and master-key matches: zero. Successful-run appended Serilog selflog: 0 bytes. Negative authentication and certificate results remained diagnosable. The private environment file was mode 0600 and removed at the end.
|
||||
|
||||
Final successful runtime directory: `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/publish-smoke/20261110T172336Z/runtime-8e794a04a90d4b769fd94cd330aba544`. summary.json, commands.json, health JSON, individual protocol/CLI logs, full Host logs and appended rolling-file slices contain the actual results, UTC times, statuses and exits. The task-labelled container `f725df5c31d907db6626a70446294006e9e18d6dd7efb3bc9f1f385267faf7f2` was removed after its second clean stop. No task containers remain; no existing fleet container, volume, database or PKI was modified.
|
||||
|
||||
Earlier fresh setup attempts exited nonzero and remain in sibling runtime-* directories. They exposed fixture mistakes: closed-port CLI certificate preparation, an unnecessary split cluster role, the liveness route spelling (/healthz is correct), and a hyphenated deployment identifier. Those were corrected only in the new smoke harness. The application’s configuration validator failed closed, and valid configuration subsequently passed. No SDK regression was attributed to those setup errors.
|
||||
|
||||
## Reproduction and pending operator gate
|
||||
|
||||
The committed scripts/validation/opcua-publish-smoke/README.md contains exact four-publish, probe-build and runtime commands. The runtime script requires a fresh private state directory per invocation, preserves each command exit, enforces the two-boot/six-case count and raw-log checks, and removes only its captured task-labelled container. Build/publish commands and immutable hashes for this completed run are in validation-receipt.json.
|
||||
|
||||
Evidence root: `/Users/dohertj2/.codex/worktrees/opcua2-test-convergence/OtOpcUa/artifacts/opcua2/publish-smoke/20261110T172336Z`.
|
||||
|
||||
**PENDING — Windows runtime / installation:** cross-publishing PE artifacts on macOS does not execute Windows service registration, startup, endpoint negotiation or shutdown. An operator must perform scripts/install/Install-Services.ps1 and the two-start/stop, preserved-PKI health/UA checks on an approved Windows environment. The Wonder-box remote-execution hold remains binding: no WinRM, psbridge or replacement remote-execution channel was used or is authorized by this smoke. No deployment was performed.
|
||||
|
||||
Performance/load, full driver/native transport operation, Windows service ACL/account behavior, backend/live AVEVA, failover and the two-hour lifecycle soak remain separate gates.
|
||||
@@ -1,361 +0,0 @@
|
||||
{
|
||||
"issue": "#799",
|
||||
"status": "failed",
|
||||
"scope": "In-process SDK client and production server/node-manager lifecycle; not standalone server attribution",
|
||||
"source": {
|
||||
"launcherSourceRevision": "38098ce142ab8c382825aa754e0758071dd3a46b",
|
||||
"publishedSourceRevision": "74c938f37cbb9cf08b6ca72e4effc511120dc274",
|
||||
"productionEquivalentTo": "7b353d7bae5311a2f0a33c0a82c88d2c5b49d76f",
|
||||
"productionDiffEmpty": true,
|
||||
"canonicalDescriptorProbeCommit": "642aefe3",
|
||||
"testSourceSha256": "e695783e05787d94bf5d167b25b1c48b446b800c28259f023c28ed6012d737d1",
|
||||
"publishCommand": "reused self-contained artifact: /private/tmp/opcua2-server-linux-diagnostic-20261110a/app",
|
||||
"appPath": "/private/tmp/opcua2-server-linux-diagnostic-20261110a/app",
|
||||
"apphost": {
|
||||
"format": "ELF64 AArch64",
|
||||
"sha256": "97531b8005df9dc8b129d2b22ba2049cf572d5d970046f7e8644ec632e860032"
|
||||
},
|
||||
"runtimeConfig": {
|
||||
"runtimeOptions": {
|
||||
"tfm": "net10.0",
|
||||
"includedFrameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "10.0.14"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App",
|
||||
"version": "10.0.14"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||
"System.Reflection.NullabilityInfoContext.IsSupported": true,
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"sdkDependencies": [
|
||||
"OPCFoundation.NetStandard.Opc.Ua.Client/2.0.1",
|
||||
"OPCFoundation.NetStandard.Opc.Ua.Configuration/2.0.1",
|
||||
"OPCFoundation.NetStandard.Opc.Ua.Core.Schema/2.0.1",
|
||||
"OPCFoundation.NetStandard.Opc.Ua.Core.Types/2.0.1",
|
||||
"OPCFoundation.NetStandard.Opc.Ua.Core/2.0.1",
|
||||
"OPCFoundation.NetStandard.Opc.Ua.Security.Certificates/2.0.1",
|
||||
"OPCFoundation.NetStandard.Opc.Ua.Server/2.0.1",
|
||||
"OPCFoundation.NetStandard.Opc.Ua.Types/2.0.1"
|
||||
],
|
||||
"image": {
|
||||
"Id": "sha256:b04611ee9e1b7d36797012ed921962c57189cb4dbb3dff12afb8e158110f293b",
|
||||
"RepoDigests": [
|
||||
"mcr.microsoft.com/dotnet/sdk@sha256:e362a8dbcd691522456da26a5198b8f3ca1d7641c95624fadc5e3e82678bd08a"
|
||||
],
|
||||
"Architecture": "arm64",
|
||||
"Os": "linux"
|
||||
},
|
||||
"hostDotnet": ".NET SDK:\n Version: 10.0.403\n Commit: 5a2162bea6\n Workload version: 10.0.400-manifests.a00bebb6\n MSBuild version: 18.9.15+5a2162bea\n\nRuntime Environment:\n OS Name: Mac OS X\n OS Version: 26.5\n OS Platform: Darwin\n RID: osx-arm64\n Base Path: /opt/homebrew/Cellar/dotnet/10.0.403/libexec/sdk/10.0.403/\n\n.NET workloads installed:\nThere are no installed workloads to display.\nConfigured to use workload sets when installing new manifests.\nNo workload sets are installed. Run \"dotnet workload restore\" to install a workload set.\n\nHost:\n Version: 10.0.14\n Architecture: arm64\n Commit: 0427b9abbe\n\n.NET SDKs installed:\n 10.0.403 [/opt/homebrew/Cellar/dotnet/10.0.403/libexec/sdk]\n\n.NET runtimes installed:\n Microsoft.AspNetCore.App 10.0.14 [/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.AspNetCore.App]\n Microsoft.NETCore.App 10.0.14 [/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.NETCore.App]\n\nOther architectures found:\n None\n\nEnvironment variables:\n DOTNET_ROOT [/opt/homebrew/Cellar/dotnet/10.0.403/libexec]\n\nglobal.json file:\n /Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/global.json\n\nLearn more:\n https://aka.ms/dotnet/info\n\nDownload .NET:\n https://aka.ms/dotnet/download\n",
|
||||
"network": "none",
|
||||
"mode": "full",
|
||||
"durationSeconds": 7200,
|
||||
"startedUtc": "2026-11-10T18:10:21Z"
|
||||
},
|
||||
"launcherResult": {
|
||||
"status": "failed",
|
||||
"containerExitCode": 1,
|
||||
"containerElapsedSeconds": 907.5196822080761,
|
||||
"validationError": "TRX expected exact passed/executed count 1: /private/tmp/opcua2-server-linux-full-20261110/task/results/soak.trx"
|
||||
},
|
||||
"start": {
|
||||
"kind": "start",
|
||||
"utc": "2026-11-10T18:10:24.882222+00:00",
|
||||
"processId": 192,
|
||||
"mode": "full",
|
||||
"requestedSeconds": 7200,
|
||||
"minimumFullSeconds": 7200,
|
||||
"build": "38098ce142ab8c382825aa754e0758071dd3a46b",
|
||||
"sdk": "2.0.1+07e62f72500cc0923a2bab940653c8c1116fa286",
|
||||
"diagnosticInstrumentation": "SDK2.0.1 CP lists and certificate-validation cached stores; read-only",
|
||||
"limits": {
|
||||
"retainedManagedGrowthBytes": 67108864,
|
||||
"rssGrowthBytes": 134217728,
|
||||
"fdGrowth": 32
|
||||
},
|
||||
"coverage": "encrypted sessions; scalar/Int16[] Raw+UNS; subscription removal; browse abandonment; history release/cross-session/close; surgical changes/rebuild; revocation; paused TCP read/recovery; same-PKI restart"
|
||||
},
|
||||
"failed": {
|
||||
"kind": "failed",
|
||||
"cycles": 248,
|
||||
"restarts": 61,
|
||||
"seconds": 903.5081762,
|
||||
"exceptionType": "Shouldly.ShouldAssertException"
|
||||
},
|
||||
"test": {
|
||||
"executionId": "b3f13597-f945-44bf-9134-cb0282f6455c",
|
||||
"testId": "3a3639bf-5159-da35-0b1a-0a6c89f03e2c",
|
||||
"testName": "ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.Sdk2LifecycleSoakTests.Repeated_lifecycles_return_resources_to_a_steady_baseline",
|
||||
"computerName": "b0dfc9462250",
|
||||
"duration": "00:15:04.9982391",
|
||||
"startTime": "2026-11-10T18:10:23.4089949+00:00",
|
||||
"endTime": "2026-11-10T18:25:28.4141750+00:00",
|
||||
"testType": "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b",
|
||||
"outcome": "Failed",
|
||||
"testListId": "8c84fa94-04c1-424b-9868-57a2d4851a1d",
|
||||
"relativeResultsDirectory": "b3f13597-f945-44bf-9134-cb0282f6455c"
|
||||
},
|
||||
"error": "Shouldly.ShouldAssertException : 353931264L\r\n should be less than or equal to\r\n351850496L\r\n but was not",
|
||||
"counters": {
|
||||
"total": "1",
|
||||
"executed": "1",
|
||||
"passed": "0",
|
||||
"failed": "1",
|
||||
"error": "0",
|
||||
"timeout": "0",
|
||||
"aborted": "0",
|
||||
"inconclusive": "0",
|
||||
"passedButRunAborted": "0",
|
||||
"notRunnable": "0",
|
||||
"notExecuted": "0",
|
||||
"disconnected": "0",
|
||||
"warning": "0",
|
||||
"completed": "0",
|
||||
"inProgress": "0",
|
||||
"pending": "0"
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"Cycle": 1,
|
||||
"Seconds": 2.6707062,
|
||||
"Sessions": 0,
|
||||
"Subscriptions": 0,
|
||||
"Items": 0,
|
||||
"BrowsePoints": 0,
|
||||
"HistoryPoints": 0,
|
||||
"LiveDataItems": 0,
|
||||
"LiveEventItems": 0,
|
||||
"MonitoredNodes": 0,
|
||||
"Variables": 19,
|
||||
"Folders": 4,
|
||||
"Conditions": 1,
|
||||
"CachedCertificateStores": 2,
|
||||
"Descriptors": 187,
|
||||
"PkiDescriptors": 0,
|
||||
"ManagedBytes": 19829880,
|
||||
"RssBytes": 212717568,
|
||||
"AllocatedBytes": 45694176,
|
||||
"Gen0": 3,
|
||||
"Gen1": 2,
|
||||
"Gen2": 2
|
||||
},
|
||||
{
|
||||
"Cycle": 3,
|
||||
"Seconds": 7.5077403,
|
||||
"Sessions": 0,
|
||||
"Subscriptions": 0,
|
||||
"Items": 0,
|
||||
"BrowsePoints": 0,
|
||||
"HistoryPoints": 0,
|
||||
"LiveDataItems": 0,
|
||||
"LiveEventItems": 0,
|
||||
"MonitoredNodes": 0,
|
||||
"Variables": 19,
|
||||
"Folders": 4,
|
||||
"Conditions": 1,
|
||||
"CachedCertificateStores": 2,
|
||||
"Descriptors": 187,
|
||||
"PkiDescriptors": 0,
|
||||
"ManagedBytes": 20362800,
|
||||
"RssBytes": 217632768,
|
||||
"AllocatedBytes": 52216848,
|
||||
"Gen0": 7,
|
||||
"Gen1": 6,
|
||||
"Gen2": 6
|
||||
},
|
||||
{
|
||||
"Cycle": 25,
|
||||
"Seconds": 90.9064734,
|
||||
"Sessions": 0,
|
||||
"Subscriptions": 0,
|
||||
"Items": 0,
|
||||
"BrowsePoints": 0,
|
||||
"HistoryPoints": 0,
|
||||
"LiveDataItems": 0,
|
||||
"LiveEventItems": 0,
|
||||
"MonitoredNodes": 0,
|
||||
"Variables": 19,
|
||||
"Folders": 4,
|
||||
"Conditions": 1,
|
||||
"CachedCertificateStores": 2,
|
||||
"Descriptors": 189,
|
||||
"PkiDescriptors": 0,
|
||||
"ManagedBytes": 20360784,
|
||||
"RssBytes": 269185024,
|
||||
"AllocatedBytes": 349530024,
|
||||
"Gen0": 57,
|
||||
"Gen1": 50,
|
||||
"Gen2": 50
|
||||
},
|
||||
{
|
||||
"Cycle": 50,
|
||||
"Seconds": 181.3146113,
|
||||
"Sessions": 0,
|
||||
"Subscriptions": 0,
|
||||
"Items": 0,
|
||||
"BrowsePoints": 0,
|
||||
"HistoryPoints": 0,
|
||||
"LiveDataItems": 0,
|
||||
"LiveEventItems": 0,
|
||||
"MonitoredNodes": 0,
|
||||
"Variables": 19,
|
||||
"Folders": 4,
|
||||
"Conditions": 1,
|
||||
"CachedCertificateStores": 2,
|
||||
"Descriptors": 189,
|
||||
"PkiDescriptors": 0,
|
||||
"ManagedBytes": 20444424,
|
||||
"RssBytes": 230686720,
|
||||
"AllocatedBytes": 652625024,
|
||||
"Gen0": 113,
|
||||
"Gen1": 100,
|
||||
"Gen2": 100
|
||||
},
|
||||
{
|
||||
"Cycle": 100,
|
||||
"Seconds": 361.5237015,
|
||||
"Sessions": 0,
|
||||
"Subscriptions": 0,
|
||||
"Items": 0,
|
||||
"BrowsePoints": 0,
|
||||
"HistoryPoints": 0,
|
||||
"LiveDataItems": 0,
|
||||
"LiveEventItems": 0,
|
||||
"MonitoredNodes": 0,
|
||||
"Variables": 19,
|
||||
"Folders": 4,
|
||||
"Conditions": 1,
|
||||
"CachedCertificateStores": 2,
|
||||
"Descriptors": 190,
|
||||
"PkiDescriptors": 0,
|
||||
"ManagedBytes": 20637848,
|
||||
"RssBytes": 229531648,
|
||||
"AllocatedBytes": 1261146560,
|
||||
"Gen0": 225,
|
||||
"Gen1": 200,
|
||||
"Gen2": 200
|
||||
},
|
||||
{
|
||||
"Cycle": 150,
|
||||
"Seconds": 546.7687542,
|
||||
"Sessions": 0,
|
||||
"Subscriptions": 0,
|
||||
"Items": 0,
|
||||
"BrowsePoints": 0,
|
||||
"HistoryPoints": 0,
|
||||
"LiveDataItems": 0,
|
||||
"LiveEventItems": 0,
|
||||
"MonitoredNodes": 0,
|
||||
"Variables": 19,
|
||||
"Folders": 4,
|
||||
"Conditions": 1,
|
||||
"CachedCertificateStores": 2,
|
||||
"Descriptors": 189,
|
||||
"PkiDescriptors": 0,
|
||||
"ManagedBytes": 20388720,
|
||||
"RssBytes": 281825280,
|
||||
"AllocatedBytes": 1907065880,
|
||||
"Gen0": 338,
|
||||
"Gen1": 300,
|
||||
"Gen2": 300
|
||||
},
|
||||
{
|
||||
"Cycle": 200,
|
||||
"Seconds": 727.6899225,
|
||||
"Sessions": 0,
|
||||
"Subscriptions": 0,
|
||||
"Items": 0,
|
||||
"BrowsePoints": 0,
|
||||
"HistoryPoints": 0,
|
||||
"LiveDataItems": 0,
|
||||
"LiveEventItems": 0,
|
||||
"MonitoredNodes": 0,
|
||||
"Variables": 19,
|
||||
"Folders": 4,
|
||||
"Conditions": 1,
|
||||
"CachedCertificateStores": 2,
|
||||
"Descriptors": 189,
|
||||
"PkiDescriptors": 0,
|
||||
"ManagedBytes": 20678608,
|
||||
"RssBytes": 269180928,
|
||||
"AllocatedBytes": 2516228704,
|
||||
"Gen0": 450,
|
||||
"Gen1": 400,
|
||||
"Gen2": 400
|
||||
},
|
||||
{
|
||||
"Cycle": 245,
|
||||
"Seconds": 896.2277082,
|
||||
"Sessions": 0,
|
||||
"Subscriptions": 0,
|
||||
"Items": 0,
|
||||
"BrowsePoints": 0,
|
||||
"HistoryPoints": 0,
|
||||
"LiveDataItems": 0,
|
||||
"LiveEventItems": 0,
|
||||
"MonitoredNodes": 0,
|
||||
"Variables": 19,
|
||||
"Folders": 4,
|
||||
"Conditions": 1,
|
||||
"CachedCertificateStores": 2,
|
||||
"Descriptors": 189,
|
||||
"PkiDescriptors": 0,
|
||||
"ManagedBytes": 20292328,
|
||||
"RssBytes": 341528576,
|
||||
"AllocatedBytes": 3109239976,
|
||||
"Gen0": 552,
|
||||
"Gen1": 490,
|
||||
"Gen2": 490
|
||||
},
|
||||
{
|
||||
"Cycle": 247,
|
||||
"Seconds": 901.025049,
|
||||
"Sessions": 0,
|
||||
"Subscriptions": 0,
|
||||
"Items": 0,
|
||||
"BrowsePoints": 0,
|
||||
"HistoryPoints": 0,
|
||||
"LiveDataItems": 0,
|
||||
"LiveEventItems": 0,
|
||||
"MonitoredNodes": 0,
|
||||
"Variables": 19,
|
||||
"Folders": 4,
|
||||
"Conditions": 1,
|
||||
"CachedCertificateStores": 2,
|
||||
"Descriptors": 189,
|
||||
"PkiDescriptors": 0,
|
||||
"ManagedBytes": 20547624,
|
||||
"RssBytes": 349171712,
|
||||
"AllocatedBytes": 3115447904,
|
||||
"Gen0": 556,
|
||||
"Gen1": 494,
|
||||
"Gen2": 494
|
||||
}
|
||||
],
|
||||
"maximumRecordedQuiescent": {
|
||||
"ManagedBytes": 20890032,
|
||||
"RssBytes": 349171712,
|
||||
"Descriptors": 191,
|
||||
"PkiDescriptors": 0
|
||||
},
|
||||
"evidenceSha256": {
|
||||
"result.json": "cb3d4dc9c3dcd80392e66f21d6b6e36907d8438c550a1723dced57a0968e7e89",
|
||||
"source.json": "5bd909d1f289d38039b996ed110b450cd3914a6a69d96c7f3aa83b016629497d",
|
||||
"task/soak.jsonl": "1d24d32382b5be6ebca2615389929818dd6c9874b7787cb91482ceaa5d3b9cd3",
|
||||
"task/results/soak.trx": "798c97c3158ec35d9dbae754e98f57a7bf403ae10b81473f98a52a89c2401265",
|
||||
"task/results/descriptor-controls.trx": "99ca030ff7542e2dc43959d8cf9a2f129352db9c6a44d5d011c6e10fa9327f01"
|
||||
},
|
||||
"limitations": [
|
||||
"No complete row; no two-hour pass.",
|
||||
"Failed sample248 RSS is reported in TRX before JSONL quiescent emission.",
|
||||
"No resource budget or production workaround changed.",
|
||||
"Private PKI and full task output are deliberately excluded."
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+6
-38
@@ -36,37 +36,6 @@ Transport security and OPC UA authentication are per-node concerns configured in
|
||||
|
||||
## Transport Security
|
||||
|
||||
### SDK 2.0.1 compatibility (#782)
|
||||
|
||||
The application targets exact OPC Foundation SDK **2.0.1**, replacing **1.5.378.182**.
|
||||
The upgrade retains the existing host, security profiles, anonymous-admission setting,
|
||||
API-key authentication and ACL flag semantics. It adds no companion-specification or
|
||||
PubSub capability. Release evidence and operator gates are linked from the
|
||||
[rollout runbook](plans/2026-11-10-opcua-sdk-2-rollout.md); source changes alone do not establish release approval.
|
||||
|
||||
SDK role augmentation preserves the application's principal, API-key id and application
|
||||
roles. A rejected API key must remain rejected rather than falling through to another
|
||||
identity provider. Per-IP authentication miss accounting uses the public transport-observed
|
||||
`SecureChannelContext.PeerAddress` during activation; an unknown peer retains the existing
|
||||
process-wide budget fallback. This is accounting evidence, not a client-supplied address or
|
||||
a new IP admission policy.
|
||||
|
||||
SDK event receive checks compose with application ACL filtering. For the application's
|
||||
rejected-write `AuditWriteUpdateEvent` only, a temporary validation context supplies
|
||||
`ReceiveEvents` to the recipient's SDK roles after the current source variable, effective
|
||||
identity, source `Read` grant and audit/encryption guard are checked. It expires before
|
||||
queueing; shared role-permission metadata is not widened. Prior permission callback
|
||||
errors veto delivery. SDK-generated audits retain SDK defaults. This does not enable
|
||||
auditing or grant a general audit feed.
|
||||
|
||||
Certificate configuration uses the SDK certificate manager, and certificate ownership
|
||||
lasts through the owning host/session's lifetime. Preserve the existing application
|
||||
certificate, private key, issuer/peer stores and filesystem permissions during rollout;
|
||||
verify key access and CA usage under the service identity. Do not rotate certificates,
|
||||
enable auto-accept, add `None`, or relax revocation checks to get the candidate running.
|
||||
Continuation points remain session-owned and bound to node/topology and current authority;
|
||||
restart requires fresh sessions and fresh points.
|
||||
|
||||
### Overview
|
||||
|
||||
The OtOpcUa Server supports configurable OPC UA transport security profiles that control how data is protected on the wire between OPC UA clients and the server.
|
||||
@@ -301,13 +270,12 @@ guards in front of the store, bound from the same `Security:ApiKeys` section
|
||||
**Per-client accounting and its limits (#751).** `IOpcUaUserAuthenticator` has an endpoint-carrying
|
||||
`AuthenticateUserNameAsync(username, password, remoteEndpoint, ct)` overload (default: ignore the endpoint
|
||||
and delegate), which the server calls and `ApiKeyOpcUaUserAuthenticator` forwards to
|
||||
`IApiKeyStore.FindAsync(..., remoteAddress, ...)`. SDK 2.0.1 supplies the public transport-observed
|
||||
`SecureChannelContext.PeerAddress` on session activation. `OtOpcUaSdkServer` scopes it to the current
|
||||
asynchronous activation and restores the prior context in `finally`; `OpcUaApplicationHost` adapts it to
|
||||
the existing IP-budget seam with port zero (no peer port is exposed). The former private-channel-map
|
||||
reflection helper is removed. An unknown address is charged to the process-wide bucket only, which
|
||||
therefore stays as the backstop. `ApiKeyMissBudgetPerEndpointTests` (OpcUaServer.IntegrationTests)
|
||||
pins per-client accounting over the wire; concurrent IPv4/IPv6 activation probes check peer isolation. At most `BoundedApiKeyStore.MaxTrackedEndpoints`
|
||||
`IApiKeyStore.FindAsync(..., remoteAddress, ...)`. The OPC UA SDK (1.5.378) exposes no client address on any
|
||||
public surface, so `SecureChannelRemoteEndpoint` reads it from the TCP listener's private channel map by
|
||||
reflection, keyed by the session's secure channel id. It is fail-soft: an unresolvable address (an SDK upgrade
|
||||
that renamed the members, or a client re-activating over a new channel) is charged to the process-wide bucket
|
||||
only, which therefore stays as the backstop. `ApiKeyMissBudgetPerEndpointTests` (OpcUaServer.IntegrationTests)
|
||||
pins the resolution over the wire so such an upgrade fails a test. At most `BoundedApiKeyStore.MaxTrackedEndpoints`
|
||||
(4096) addresses are tracked; when full, idle (refilled) buckets are pruned and a new address beyond that is
|
||||
charged process-wide only — never handed a fresh budget. Clients behind one NAT address share a bucket. Neither
|
||||
guard applies to a driver-only node: its `ArtifactApiKeyStore` is two in-memory dictionary probes and is never
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#Requires -Version 7.0
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Runs the production application SDK compatibility matrix in isolated processes.
|
||||
.DESCRIPTION
|
||||
Uses exact baseline host/client publish artifacts and builds the candidate from a
|
||||
separate checkout. Both SDK versions remain in separate processes. Evidence,
|
||||
artifact SHA-256 hashes, negotiated policies and individual results are retained.
|
||||
The output directory must not exist. No fleet endpoint or store is used.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$BaselineRoot,
|
||||
[Parameter(Mandatory)][string]$BaselineArtifacts,
|
||||
[Parameter(Mandatory)][string]$CandidateRoot,
|
||||
[Parameter(Mandatory)][string]$OutputDirectory,
|
||||
[string]$ContainerImage = 'mcr.microsoft.com/dotnet/sdk:10.0.103-noble'
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$runner = Join-Path $PSScriptRoot '../validation/opcua-sdk-compatibility/run.py'
|
||||
& python3 $runner --baseline-root $BaselineRoot --baseline-artifacts $BaselineArtifacts --candidate-root $CandidateRoot --output $OutputDirectory --container-image $ContainerImage
|
||||
exit $LASTEXITCODE
|
||||
@@ -1,56 +0,0 @@
|
||||
# HistorianGateway consumer TLS migration — issue 131
|
||||
|
||||
`consumer-1.0.0.patch` is a future consumer change, kept separate from the buildable
|
||||
application while the paired Client/Contracts 1.0.0 packages are being released.
|
||||
The application in this checkout remains pinned to published 0.4.0.
|
||||
|
||||
The patch changes both central package pins to 1.0.0 and maps the new option as:
|
||||
|
||||
```csharp
|
||||
AllowUntrustedServerCertificate = options.UseTls && options.AllowUntrustedServerCertificate,
|
||||
```
|
||||
|
||||
The old inverse-mapping and obsolete h2c-history comments are removed. The existing
|
||||
h2c construction test now includes a stale opt-out as well as a stale CA path.
|
||||
Three new bounded local wire tests verify behavior through the real adapter and
|
||||
published client API, without reflection or replacing the transport handler:
|
||||
|
||||
- TLS with normal defaults rejects a self-signed server before the metadata RPC.
|
||||
- Explicit opt-out permits that self-signed server shape and executes one RPC.
|
||||
- Plaintext with stale opt-out and CA settings executes one RPC, proving TLS-only
|
||||
settings were not forwarded to the client's validating options.
|
||||
|
||||
The local server uses an ephemeral loopback port and a fresh short-lived
|
||||
self-signed certificate. RPC completion is bounded to 15 seconds; the consumer
|
||||
call timeout is two seconds. No gateway, deployment, or global trust store is used.
|
||||
|
||||
Before applying the patch, query the live Gitea NuGet version indexes for BOTH
|
||||
packages and require actual stable 1.0.0. Source `main` having the new API is not a
|
||||
package release. At initial inspection, both feed indexes listed exactly
|
||||
0.4.0, 0.3.0, 0.2.0, 0.1.0, and the new API was only in Unreleased at HG commit
|
||||
`e4321915ebc5e0704a68ecad21b85c1cc1c80d6b`.
|
||||
|
||||
```bash
|
||||
git apply --check scripts/validation/historiangw-131/consumer-1.0.0.patch
|
||||
git apply scripts/validation/historiangw-131/consumer-1.0.0.patch
|
||||
# From the consumer checkout, using a fresh run/results directory:
|
||||
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests \
|
||||
-c Release --logger trx --results-directory /absolute/fresh/results
|
||||
# Preserve dotnet test's exit; run the repository execution-count guard separately.
|
||||
MIN_EXECUTED=1 bash scripts/ci/assert-not-all-skipped.sh /absolute/fresh/results/*.trx
|
||||
```
|
||||
|
||||
Published-package verification must use the real 1.0.0 packages, preserve all test
|
||||
and guard exits, run the full owning project, and complete the consumer solution
|
||||
build/review before issue 131 is closed. The patch is transport-only and can be
|
||||
applied independently of the OPC UA SDK 2 rollout.
|
||||
|
||||
If release availability lags, validate only in an archived staging copy: materialize
|
||||
exact HG commit e432191 with `git archive`, pack both packages with the local-only
|
||||
version `1.0.0-validation.131`, set an isolated `NUGET_PACKAGES`, and map only those
|
||||
two package IDs to a task-local feed. Apply the future patch to a separate consumer
|
||||
archive, then replace ONLY its two package pins with the validation prerelease.
|
||||
Never pack/build the user's HG checkout, publish these validation packages, place
|
||||
them in the normal NuGet cache, or pass prerelease validation off as stable-release
|
||||
verification. Local validation and release availability results are recorded in
|
||||
`RESULTS.md` when the run completes.
|
||||
@@ -1,13 +0,0 @@
|
||||
# Validation status
|
||||
|
||||
Preparation only: future patch passes `git apply --check` in the SDK 2 candidate
|
||||
checkout. Published Client/Contracts 1.0.0 package verification is pending.
|
||||
|
||||
Initial primary feed query returned versions 0.4.0, 0.3.0, 0.2.0, 0.1.0 for both
|
||||
packages. HG e4321915ebc5e0704a68ecad21b85c1cc1c80d6b contains the new option under
|
||||
Unreleased; both source package version declarations still read 0.4.0. The cached
|
||||
published Client 0.4.0 exposes RequireCertificateValidation and does not expose the
|
||||
new property. Current consumer files remain on 0.4.0 and unchanged.
|
||||
|
||||
No issue comment/close, package publication, or HG user-checkout modification has
|
||||
been performed. Test results will be added after actual execution.
|
||||
@@ -1,226 +0,0 @@
|
||||
diff --git a/Directory.Packages.props b/Directory.Packages.props
|
||||
--- a/Directory.Packages.props
|
||||
+++ b/Directory.Packages.props
|
||||
@@ -242,7 +242,7 @@
|
||||
<PackageVersion Include="ZB.MOM.WW.Secrets.Replicator.AkkaDotNet" Version="0.6.2" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Audit" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Theme" Version="0.5.0" />
|
||||
- <PackageVersion Include="ZB.MOM.WW.HistorianGateway.Client" Version="0.4.0" />
|
||||
- <PackageVersion Include="ZB.MOM.WW.HistorianGateway.Contracts" Version="0.4.0" />
|
||||
+ <PackageVersion Include="ZB.MOM.WW.HistorianGateway.Client" Version="1.0.0" />
|
||||
+ <PackageVersion Include="ZB.MOM.WW.HistorianGateway.Contracts" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
\ No newline at end of file
|
||||
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs
|
||||
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs
|
||||
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs
|
||||
@@ -57,23 +57,15 @@
|
||||
$"ServerHistorian:Endpoint must be an absolute http(s) URI (e.g. https://host:5222); got '{options.Endpoint}'.");
|
||||
}
|
||||
|
||||
- // TLS-only options must stay at their defaults on a plaintext h2c connection: the client rejects
|
||||
- // each of them outright when UseTls=false ("<name> is a TLS-only option and requires
|
||||
- // UseTls=true"). Forwarding them unconditionally made h2c UNREACHABLE — AllowUntrustedServer-
|
||||
- // Certificate defaults to false, so RequireCertificateValidation was always sent as true and
|
||||
- // every http:// deployment crashed at startup, even though the scheme is documented as the
|
||||
- // supported way to select h2c. There is no certificate to have a posture about here.
|
||||
+ // Certificate settings apply only to TLS. The client rejects an explicit opt-out or a
|
||||
+ // CA path on h2c, where there is no certificate to validate.
|
||||
var clientOptions = new HistorianGatewayClientOptions
|
||||
{
|
||||
Endpoint = endpointUri,
|
||||
ApiKey = options.ApiKey,
|
||||
UseTls = options.UseTls,
|
||||
CaCertificatePath = options.UseTls ? options.CaCertificatePath : null,
|
||||
- // INVERTED mapping (TLS only): ServerHistorianOptions.AllowUntrustedServerCertificate
|
||||
- // (opt-in to accept a self-signed cert) is the negation of the client's
|
||||
- // RequireCertificateValidation. Allowing an untrusted cert == not requiring validation; a
|
||||
- // pinned CaCertificatePath always verifies.
|
||||
- RequireCertificateValidation = options.UseTls && !options.AllowUntrustedServerCertificate,
|
||||
+ AllowUntrustedServerCertificate = options.UseTls && options.AllowUntrustedServerCertificate,
|
||||
DefaultCallTimeout = options.CallTimeout,
|
||||
LoggerFactory = loggerFactory,
|
||||
};
|
||||
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/HistorianGatewayClientAdapterTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/HistorianGatewayClientAdapterTests.cs
|
||||
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/HistorianGatewayClientAdapterTests.cs
|
||||
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/HistorianGatewayClientAdapterTests.cs
|
||||
@@ -27,23 +27,6 @@
|
||||
/// A plaintext <c>h2c</c> gateway (an <c>http://</c> endpoint with <c>UseTls=false</c>) must
|
||||
/// construct, using nothing but documented defaults.
|
||||
/// </summary>
|
||||
- /// <remarks>
|
||||
- /// <para>
|
||||
- /// Found by the LocalDb Phase 2 live gate, on the third consecutive crash-loop of the rig.
|
||||
- /// <c>docs/Historian.md</c> and CLAUDE.md both document <c>http://</c> as a supported
|
||||
- /// transport ("scheme selects transport: https = TLS, http = h2c"), but it was in fact
|
||||
- /// <b>unreachable</b>: this factory forwarded the TLS-only options unconditionally, and
|
||||
- /// <see cref="ServerHistorianOptions.AllowUntrustedServerCertificate"/> defaults to
|
||||
- /// <see langword="false"/>, so it always sent <c>RequireCertificateValidation=true</c> —
|
||||
- /// which the client rejects outright when <c>UseTls=false</c>
|
||||
- /// ("RequireCertificateValidation is a TLS-only option and requires UseTls=true").
|
||||
- /// </para>
|
||||
- /// <para>
|
||||
- /// So <b>every</b> h2c deployment crashed at startup, and the only way to avoid it was to
|
||||
- /// set <c>AllowUntrustedServerCertificate=true</c> — asserting a certificate posture for a
|
||||
- /// connection that has no certificate at all.
|
||||
- /// </para>
|
||||
- /// </remarks>
|
||||
[Fact]
|
||||
public void Adapter_constructs_for_plaintext_h2c_endpoint()
|
||||
{
|
||||
@@ -71,6 +54,7 @@
|
||||
ApiKey = "histgw_x_y",
|
||||
UseTls = false,
|
||||
CaCertificatePath = "/etc/ssl/certs/leftover-from-a-tls-config.pem",
|
||||
+ AllowUntrustedServerCertificate = true,
|
||||
};
|
||||
|
||||
using var adapter = HistorianGatewayClientAdapter.Create(opts, NullLoggerFactory.Instance);
|
||||
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/HistorianGatewayTlsPolicyWireTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/HistorianGatewayTlsPolicyWireTests.cs
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/HistorianGatewayTlsPolicyWireTests.cs
|
||||
@@ -0,0 +1,142 @@
|
||||
+using System.Net;
|
||||
+using System.Security.Authentication;
|
||||
+using System.Security.Cryptography;
|
||||
+using System.Security.Cryptography.X509Certificates;
|
||||
+using Grpc.Core;
|
||||
+using Microsoft.AspNetCore.Builder;
|
||||
+using Microsoft.AspNetCore.Hosting;
|
||||
+using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
+using Microsoft.Extensions.DependencyInjection;
|
||||
+using Microsoft.Extensions.Logging;
|
||||
+using Microsoft.Extensions.Logging.Abstractions;
|
||||
+using Shouldly;
|
||||
+using Xunit;
|
||||
+using ZB.MOM.WW.HistorianGateway.Client;
|
||||
+using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
|
||||
+using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
|
||||
+
|
||||
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
|
||||
+
|
||||
+/// <summary>historiangw#131: verify the mapped TLS posture through an actual local gRPC connection.</summary>
|
||||
+public sealed class HistorianGatewayTlsPolicyWireTests(ITestOutputHelper output)
|
||||
+{
|
||||
+ private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
||||
+
|
||||
+ [Fact]
|
||||
+ public async Task Default_tls_rejects_an_untrusted_server_before_the_rpc()
|
||||
+ {
|
||||
+ await using var server = await Server.StartAsync(tls: true);
|
||||
+ using var adapter = HistorianGatewayClientAdapter.Create(server.Options(), NullLoggerFactory.Instance);
|
||||
+
|
||||
+ var error = await Should.ThrowAsync<HistorianGatewayUnavailableException>(
|
||||
+ () => adapter.TagExistsAsync("T", Ct).WaitAsync(TimeSpan.FromSeconds(15), Ct));
|
||||
+ output.WriteLine($"TLS rejection: {error}");
|
||||
+ ContainsTlsAuthenticationFailure(error).ShouldBeTrue("the failure must be TLS authentication, not an arbitrary connection failure");
|
||||
+ server.MetadataCalls.ShouldBe(0);
|
||||
+ }
|
||||
+
|
||||
+ [Fact]
|
||||
+ public async Task Explicit_tls_opt_out_reaches_the_same_self_signed_server_shape()
|
||||
+ {
|
||||
+ await using var server = await Server.StartAsync(tls: true);
|
||||
+ var options = server.Options(allowUntrusted: true);
|
||||
+ using var adapter = HistorianGatewayClientAdapter.Create(options, NullLoggerFactory.Instance);
|
||||
+
|
||||
+ (await adapter.TagExistsAsync("T", Ct).WaitAsync(TimeSpan.FromSeconds(15), Ct)).ShouldBeTrue();
|
||||
+ server.MetadataCalls.ShouldBe(1);
|
||||
+ }
|
||||
+
|
||||
+ [Fact]
|
||||
+ public async Task Plaintext_ignores_stale_tls_opt_out_and_ca_settings_and_reaches_the_rpc()
|
||||
+ {
|
||||
+ await using var server = await Server.StartAsync(tls: false);
|
||||
+ var options = server.Options(allowUntrusted: true, caPath: "/nonexistent/historiangw-131-stale-ca.pem");
|
||||
+ using var adapter = HistorianGatewayClientAdapter.Create(options, NullLoggerFactory.Instance);
|
||||
+
|
||||
+ (await adapter.TagExistsAsync("T", Ct).WaitAsync(TimeSpan.FromSeconds(15), Ct)).ShouldBeTrue();
|
||||
+ server.MetadataCalls.ShouldBe(1);
|
||||
+ }
|
||||
+
|
||||
+ private static bool ContainsTlsAuthenticationFailure(Exception error)
|
||||
+ {
|
||||
+ var pending = new Stack<Exception>();
|
||||
+ var seen = new HashSet<Exception>(ReferenceEqualityComparer.Instance);
|
||||
+ pending.Push(error);
|
||||
+ while (pending.TryPop(out var current))
|
||||
+ {
|
||||
+ if (!seen.Add(current)) continue;
|
||||
+ if (current is AuthenticationException) return true;
|
||||
+ if (current.InnerException is { } inner) pending.Push(inner);
|
||||
+ if (current is RpcException { Status.DebugException: { } debug }) pending.Push(debug);
|
||||
+ }
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ private sealed class TagsService : HistorianTags.HistorianTagsBase
|
||||
+ {
|
||||
+ public int Calls;
|
||||
+ public override Task<GetTagMetadataReply> GetTagMetadata(GetTagMetadataRequest request, ServerCallContext context)
|
||||
+ {
|
||||
+ Interlocked.Increment(ref Calls);
|
||||
+ return Task.FromResult(new GetTagMetadataReply
|
||||
+ { Metadata = new HistorianTagMetadata { Name = request.Tag } });
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ private sealed class Server(WebApplication app, TagsService service, string endpoint,
|
||||
+ RSA? key, X509Certificate2? certificate) : IAsyncDisposable
|
||||
+ {
|
||||
+ public int MetadataCalls => Volatile.Read(ref service.Calls);
|
||||
+ public ServerHistorianOptions Options(bool allowUntrusted = false, string? caPath = null) => new()
|
||||
+ {
|
||||
+ Endpoint = endpoint, ApiKey = "histgw_131_test", UseTls = certificate is not null,
|
||||
+ CallTimeout = TimeSpan.FromSeconds(2),
|
||||
+ AllowUntrustedServerCertificate = allowUntrusted, CaCertificatePath = caPath,
|
||||
+ };
|
||||
+
|
||||
+ public static async Task<Server> StartAsync(bool tls)
|
||||
+ {
|
||||
+ RSA? key = null;
|
||||
+ X509Certificate2? certificate = null;
|
||||
+ if (tls)
|
||||
+ {
|
||||
+ key = RSA.Create(2048);
|
||||
+ var request = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
+ request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
|
||||
+ request.CertificateExtensions.Add(new X509KeyUsageExtension(
|
||||
+ X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, true));
|
||||
+ var names = new SubjectAlternativeNameBuilder();
|
||||
+ names.AddDnsName("localhost"); names.AddIpAddress(IPAddress.Loopback);
|
||||
+ request.CertificateExtensions.Add(names.Build());
|
||||
+ certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1));
|
||||
+ }
|
||||
+ var service = new TagsService();
|
||||
+ var builder = WebApplication.CreateBuilder();
|
||||
+ builder.Logging.ClearProviders();
|
||||
+ builder.WebHost.ConfigureKestrel(options => options.Listen(IPAddress.Loopback, 0, listen =>
|
||||
+ {
|
||||
+ listen.Protocols = HttpProtocols.Http2;
|
||||
+ if (certificate is not null) listen.UseHttps(certificate);
|
||||
+ }));
|
||||
+ builder.Services.AddGrpc(); builder.Services.AddSingleton(service);
|
||||
+ var app = builder.Build();
|
||||
+ app.MapGrpcService<TagsService>();
|
||||
+ try
|
||||
+ {
|
||||
+ await app.StartAsync(Ct);
|
||||
+ return new Server(app, service, app.Urls.Single(), key, certificate);
|
||||
+ }
|
||||
+ catch
|
||||
+ {
|
||||
+ await app.DisposeAsync(); certificate?.Dispose(); key?.Dispose();
|
||||
+ throw;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ public async ValueTask DisposeAsync()
|
||||
+ {
|
||||
+ try { await app.StopAsync(Ct); }
|
||||
+ finally { await app.DisposeAsync(); certificate?.Dispose(); key?.Dispose(); }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
@@ -1,4 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup><OutputType>Exe</OutputType></PropertyGroup>
|
||||
<ItemGroup><PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Server" Version="1.5.378.182" /></ItemGroup>
|
||||
</Project>
|
||||
@@ -1,4 +0,0 @@
|
||||
<Project>
|
||||
<!-- Deliberately isolated baseline process: never inherit or load the candidate SDK. -->
|
||||
<PropertyGroup><TargetFramework>net10.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings><TreatWarningsAsErrors>true</TreatWarningsAsErrors><ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally></PropertyGroup>
|
||||
</Project>
|
||||
@@ -1 +0,0 @@
|
||||
<Project><PropertyGroup><ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally></PropertyGroup></Project>
|
||||
@@ -1,242 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Server;
|
||||
|
||||
if (args.Length != 1) throw new ArgumentException("Pass a fresh output JSON path.");
|
||||
if (File.Exists(args[0])) throw new IOException("Refusing to overwrite existing baseline evidence.");
|
||||
var epoch = new DateTime(2026, 10, 1, 8, 0, 0, DateTimeKind.Utc).AddTicks(1234567);
|
||||
var telemetry = DefaultTelemetry.Create(_ => { });
|
||||
var offsets = new double[] { -10, 0, 5, 15, 25, 35, 50, 80 };
|
||||
var data = new Dictionary<string, Sample[]>
|
||||
{
|
||||
["double"] = offsets.Select((s, i) => new Sample(s, "Double", (10 + i * 7 % 13).ToString(CultureInfo.InvariantCulture), StatusCodes.Good)).ToArray(),
|
||||
["uncertain"] = offsets.Select((s, i) => new Sample(s, i == 4 ? "Null" : "Double", i == 4 ? null : i == 2 ? "100" : (10 + i * 7 % 13).ToString(CultureInfo.InvariantCulture), i == 2 ? StatusCodes.UncertainDataSubNormal : i == 4 ? StatusCodes.BadNoCommunication : StatusCodes.Good)).ToArray(),
|
||||
["integer"] = offsets.Select((s, i) => new Sample(s, "Int64", (i * 7 - 10).ToString(CultureInfo.InvariantCulture), i == 3 ? StatusCodes.UncertainDataSubNormal : StatusCodes.Good)).ToArray(),
|
||||
["boolean"] = offsets.Select((s, i) => new Sample(s, "Boolean", (i % 2 == 0).ToString(), i == 2 ? StatusCodes.UncertainDataSubNormal : StatusCodes.Good)).ToArray(),
|
||||
["sparse"] = [new(-10, "Double", "10", StatusCodes.Good), new(7, "Double", "20", StatusCodes.Good), new(18, "Null", null, StatusCodes.BadNoCommunication), new(45, "Double", "30", StatusCodes.Good)],
|
||||
["bad"] = offsets.Select(s => new Sample(s, "Null", null, StatusCodes.BadNoCommunication)).ToArray(),
|
||||
["empty"] = [],
|
||||
};
|
||||
var names = new string[]
|
||||
{
|
||||
"AggregateFunction_Interpolative",
|
||||
"AggregateFunction_Average",
|
||||
"AggregateFunction_TimeAverage",
|
||||
"AggregateFunction_TimeAverage2",
|
||||
"AggregateFunction_Total",
|
||||
"AggregateFunction_Total2",
|
||||
"AggregateFunction_Minimum",
|
||||
"AggregateFunction_Maximum",
|
||||
"AggregateFunction_MinimumActualTime",
|
||||
"AggregateFunction_MaximumActualTime",
|
||||
"AggregateFunction_Range",
|
||||
"AggregateFunction_Minimum2",
|
||||
"AggregateFunction_Maximum2",
|
||||
"AggregateFunction_MinimumActualTime2",
|
||||
"AggregateFunction_MaximumActualTime2",
|
||||
"AggregateFunction_Range2",
|
||||
"AggregateFunction_Count",
|
||||
"AggregateFunction_AnnotationCount",
|
||||
"AggregateFunction_DurationInStateZero",
|
||||
"AggregateFunction_DurationInStateNonZero",
|
||||
"AggregateFunction_NumberOfTransitions",
|
||||
"AggregateFunction_Start",
|
||||
"AggregateFunction_End",
|
||||
"AggregateFunction_Delta",
|
||||
"AggregateFunction_StartBound",
|
||||
"AggregateFunction_EndBound",
|
||||
"AggregateFunction_DeltaBounds",
|
||||
"AggregateFunction_DurationGood",
|
||||
"AggregateFunction_DurationBad",
|
||||
"AggregateFunction_PercentGood",
|
||||
"AggregateFunction_PercentBad",
|
||||
"AggregateFunction_WorstQuality",
|
||||
"AggregateFunction_WorstQuality2",
|
||||
"AggregateFunction_StandardDeviationPopulation",
|
||||
"AggregateFunction_VariancePopulation",
|
||||
"AggregateFunction_StandardDeviationSample",
|
||||
"AggregateFunction_VarianceSample",
|
||||
};
|
||||
var vectors = new List<Vector>();
|
||||
// Preserve the original seven-dataset grid, input ordering and expected values exactly.
|
||||
foreach (var name in names)
|
||||
foreach (var dataset in data)
|
||||
foreach (var reverse in new[] { false, true })
|
||||
foreach (var stepped in new[] { false, true })
|
||||
foreach (var uncertainAsBad in new[] { false, true })
|
||||
foreach (var incremental in new[] { false, true })
|
||||
Run(name, dataset.Key, reverse, stepped, uncertainAsBad, incremental);
|
||||
|
||||
var edges = new Dictionary<string, Sample[]>
|
||||
{
|
||||
["edge-sbyte"] = Typed("SByte", ["-128", "-127", "-1", "0", "1", "126", "127", "2"]),
|
||||
["edge-byte"] = Typed("Byte", ["0", "1", "127", "128", "254", "255", "3", "2"]),
|
||||
["edge-int16"] = Typed("Int16", ["-32768", "-32767", "-1", "0", "1", "32766", "32767", "2"]),
|
||||
["edge-uint16"] = Typed("UInt16", ["0", "1", "32767", "32768", "65534", "65535", "3", "2"]),
|
||||
["edge-int32"] = Typed("Int32", ["-2147483648", "-2147483647", "-1", "0", "1", "2147483646", "2147483647", "2"]),
|
||||
["edge-uint32"] = Typed("UInt32", ["0", "1", "2147483647", "2147483648", "4294967294", "4294967295", "3", "2"]),
|
||||
["edge-int64"] = Typed("Int64", ["-9223372036854775808", "-9007199254740993", "-1", "0", "1", "9007199254740993", "9223372036854775806", "9223372036854775807"]),
|
||||
["edge-uint64"] = Typed("UInt64", ["0", "1", "9007199254740991", "9007199254740993", "9223372036854775807", "9223372036854775808", "18446744073709551614", "18446744073709551615"]),
|
||||
["edge-float"] = Typed("Single", ["-3.4028235E+38", "1E-45", "-0", "1.5", "3.4028235E+38", "NaN", "Infinity", "-Infinity"]),
|
||||
["edge-double"] = Typed("Double", ["-1.5", "-0", "0.5", "1.5", "NaN", "Infinity", "-Infinity", "1.7976931348623157E+308"]),
|
||||
// Interpolative/Delta casts back to the earlier sample's type: fractional, negative,
|
||||
// overflowing and nonfinite Double values must exercise the original SDK conversion.
|
||||
["edge-mixed-conversion"] =
|
||||
[
|
||||
new(-10, "Byte", "0", StatusCodes.Good), new(0, "Byte", "1", StatusCodes.Good),
|
||||
new(5, "Double", "2.5", StatusCodes.Good), new(15, "SByte", "-128", StatusCodes.Good),
|
||||
new(25, "Double", "255.5", StatusCodes.Good), new(35, "Single", "3.4028235E+38", StatusCodes.Good),
|
||||
new(50, "Double", "Infinity", StatusCodes.Good), new(80, "UInt64", "18446744073709551615", StatusCodes.Good),
|
||||
],
|
||||
};
|
||||
foreach (var dataset in edges) data.Add(dataset.Key, dataset.Value);
|
||||
foreach (var name in names)
|
||||
foreach (var dataset in edges)
|
||||
foreach (var reverse in new[] { false, true })
|
||||
foreach (var incremental in new[] { false, true })
|
||||
Run(name, dataset.Key, reverse, false, false, incremental, Settings(false));
|
||||
|
||||
// Ties include an identical source timestamp+picoseconds pair; input order is stable.
|
||||
// Distinct server ticks and picoseconds must survive raw-result selection independently.
|
||||
data.Add("edge-duplicate-picoseconds",
|
||||
[
|
||||
Pico(-10, "1", 3, 19, 7), Pico(0, "2", 1, 23, 11), Pico(0, "3", 9999, 29, 13),
|
||||
Pico(5, "4", 7, 31, 17), Pico(5, "5", 7, 37, 19), Pico(20, "6", 2, 41, 23),
|
||||
Pico(20, "7", 9000, 43, 29), Pico(40, "8", 5, 47, 31), Pico(80, "9", 11, 53, 37),
|
||||
]);
|
||||
foreach (var name in names)
|
||||
foreach (var reverse in new[] { false, true })
|
||||
foreach (var incremental in new[] { false, true })
|
||||
Run(name, "edge-duplicate-picoseconds", reverse, false, false, incremental, Settings(false));
|
||||
|
||||
var configurations = new[]
|
||||
{
|
||||
new AggregateSettings(false, 0, 0, false, false),
|
||||
new AggregateSettings(false, 1, 99, true, false),
|
||||
new AggregateSettings(true, 50, 50, false, false),
|
||||
new AggregateSettings(true, 100, 0, true, false),
|
||||
};
|
||||
var configuredNames = new[]
|
||||
{
|
||||
"AggregateFunction_Interpolative", "AggregateFunction_TimeAverage", "AggregateFunction_TimeAverage2",
|
||||
"AggregateFunction_Total", "AggregateFunction_Total2", "AggregateFunction_PercentGood", "AggregateFunction_PercentBad",
|
||||
};
|
||||
foreach (var name in configuredNames)
|
||||
foreach (var dataset in new[] { "uncertain", "sparse" })
|
||||
foreach (var config in configurations)
|
||||
foreach (var reverse in new[] { false, true })
|
||||
foreach (var incremental in new[] { false, true })
|
||||
Run(name, dataset, reverse, false, config.TreatUncertainAsBad, incremental, config);
|
||||
|
||||
var assembly = typeof(Aggregators).Assembly.Location;
|
||||
var assemblySha256 = Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(assembly)));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(args[0]))!);
|
||||
// One vector per line keeps golden changes reviewable without pretty-printing every value.
|
||||
using (var writer = new StreamWriter(new FileStream(args[0], FileMode.CreateNew, FileAccess.Write)))
|
||||
{
|
||||
writer.WriteLine("{");
|
||||
writer.WriteLine($" \"sdk\":\"1.5.378.182\",\"assemblySha256\":\"{assemblySha256}\",");
|
||||
writer.WriteLine($" \"epochTicks\":{epoch.Ticks},\"intervalMilliseconds\":20000,");
|
||||
writer.WriteLine($" \"data\":{JsonSerializer.Serialize(data)},");
|
||||
writer.WriteLine(" \"vectors\":[");
|
||||
for (var i = 0; i < vectors.Count; i++)
|
||||
writer.WriteLine(" " + JsonSerializer.Serialize(vectors[i]) + (i + 1 < vectors.Count ? "," : ""));
|
||||
writer.WriteLine(" ]");
|
||||
writer.WriteLine("}");
|
||||
}
|
||||
Console.WriteLine($"Generated {vectors.Count} vectors ({vectors.Count - 4144} focused additions) for {names.Length} aggregate IDs; baseline assembly SHA256 {assemblySha256}.");
|
||||
|
||||
Sample[] Typed(string type, string[] values) => offsets.Zip(values, (s, value) => new Sample(s, type, value, StatusCodes.Good)).ToArray();
|
||||
static Sample Pico(double seconds, string value, ushort source, ushort server, long serverOffset) =>
|
||||
new(seconds, "Int32", value, StatusCodes.Good)
|
||||
{ SourcePicoseconds = source, ServerPicoseconds = server, ServerOffsetTicks = serverOffset };
|
||||
static AggregateSettings Settings(bool uncertainAsBad) => new(uncertainAsBad, 100, 100, false, false);
|
||||
|
||||
void Run(string name, string dataset, bool reverse, bool stepped, bool uncertainAsBad, bool incremental,
|
||||
AggregateSettings? settings = null)
|
||||
{
|
||||
var id = (NodeId)typeof(ObjectIds).GetField(name)!.GetValue(null)!;
|
||||
var start = epoch.AddSeconds(reverse ? 70 : 0);
|
||||
var end = epoch.AddSeconds(reverse ? 0 : 70);
|
||||
var cfg = settings ?? Settings(uncertainAsBad);
|
||||
var config = new AggregateConfiguration
|
||||
{
|
||||
TreatUncertainAsBad = cfg.TreatUncertainAsBad, PercentDataBad = cfg.PercentDataBad,
|
||||
PercentDataGood = cfg.PercentDataGood, UseSlopedExtrapolation = cfg.UseSlopedExtrapolation,
|
||||
UseServerCapabilitiesDefaults = cfg.UseServerCapabilitiesDefaults,
|
||||
};
|
||||
var calculator = Aggregators.CreateStandardCalculator(id, start, end, 20_000, stepped, config, telemetry);
|
||||
var output = new List<Value>();
|
||||
var accepted = new List<bool>();
|
||||
string? error = null;
|
||||
try
|
||||
{
|
||||
var samples = data[dataset].OrderBy(x => x.Seconds);
|
||||
foreach (var sample in reverse ? samples.Reverse() : samples)
|
||||
{
|
||||
var time = epoch.AddSeconds(sample.Seconds);
|
||||
var value = new DataValue(new Variant(Payload(sample)!), sample.Status, time,
|
||||
time.AddTicks(sample.ServerOffsetTicks ?? 0))
|
||||
{
|
||||
SourcePicoseconds = sample.SourcePicoseconds ?? 0,
|
||||
ServerPicoseconds = sample.ServerPicoseconds ?? 0,
|
||||
};
|
||||
accepted.Add(calculator.QueueRawValue(value));
|
||||
if (incremental) Drain(false);
|
||||
}
|
||||
Drain(true);
|
||||
}
|
||||
catch (Exception ex) { error = ex.GetType().FullName; }
|
||||
vectors.Add(new(name, dataset, reverse, stepped, uncertainAsBad, incremental, output.ToArray(), error)
|
||||
{ Configuration = settings, QueueAccepted = settings is null ? null : accepted.ToArray() });
|
||||
void Drain(bool partial)
|
||||
{
|
||||
for (var guard = 0; guard < 100; guard++)
|
||||
{
|
||||
var value = calculator.GetProcessedValue(partial);
|
||||
if (value is null) return;
|
||||
output.Add(new(partial, value.Value?.GetType().Name, Format(value.Value), value.StatusCode.Code, value.SourceTimestamp.Ticks, value.ServerTimestamp.Ticks, value.SourcePicoseconds, value.ServerPicoseconds));
|
||||
}
|
||||
throw new InvalidOperationException("Baseline calculator did not terminate.");
|
||||
}
|
||||
}
|
||||
|
||||
static object? Payload(Sample sample) => sample.Type switch
|
||||
{
|
||||
"SByte" => sbyte.Parse(sample.Payload!, CultureInfo.InvariantCulture),
|
||||
"Byte" => byte.Parse(sample.Payload!, CultureInfo.InvariantCulture),
|
||||
"Int16" => short.Parse(sample.Payload!, CultureInfo.InvariantCulture),
|
||||
"UInt16" => ushort.Parse(sample.Payload!, CultureInfo.InvariantCulture),
|
||||
"Int32" => int.Parse(sample.Payload!, CultureInfo.InvariantCulture),
|
||||
"UInt32" => uint.Parse(sample.Payload!, CultureInfo.InvariantCulture),
|
||||
"Int64" => long.Parse(sample.Payload!, CultureInfo.InvariantCulture),
|
||||
"UInt64" => ulong.Parse(sample.Payload!, CultureInfo.InvariantCulture),
|
||||
"Single" => float.Parse(sample.Payload!, CultureInfo.InvariantCulture),
|
||||
"Double" => double.Parse(sample.Payload!, CultureInfo.InvariantCulture),
|
||||
"Boolean" => bool.Parse(sample.Payload!),
|
||||
_ => null,
|
||||
};
|
||||
static string? Format(object? value) => value switch
|
||||
{
|
||||
null => null,
|
||||
double number => number.ToString("R", CultureInfo.InvariantCulture),
|
||||
float number => number.ToString("R", CultureInfo.InvariantCulture),
|
||||
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
|
||||
_ => value.ToString(),
|
||||
};
|
||||
record Sample(double Seconds, string Type, string? Payload, uint Status)
|
||||
{
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ushort? SourcePicoseconds { get; init; }
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ushort? ServerPicoseconds { get; init; }
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public long? ServerOffsetTicks { get; init; }
|
||||
}
|
||||
record Value(bool PartialRequested, string? Type, string? Payload, uint Status, long SourceTicks, long ServerTicks, ushort SourcePicoseconds, ushort ServerPicoseconds);
|
||||
record AggregateSettings(bool TreatUncertainAsBad, byte PercentDataBad, byte PercentDataGood, bool UseSlopedExtrapolation, bool UseServerCapabilitiesDefaults);
|
||||
record Vector(string Aggregate, string DataSet, bool Reverse, bool Stepped, bool UncertainAsBad, bool Incremental, Value[] Output, string? Exception)
|
||||
{
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AggregateSettings? Configuration { get; init; }
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool[]? QueueAccepted { get; init; }
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
# Original SDK aggregate oracle
|
||||
|
||||
This standalone .NET 10 process generates aggregate expectations from the published
|
||||
`OPCFoundation.NetStandard.Opc.Ua.Server` **1.5.378.182** package. Its local build and
|
||||
package properties isolate it from the application's SDK 2 graph. It is outside the
|
||||
solution; never add its baseline SDK assemblies to a candidate test or production process.
|
||||
Expectations must come from this original SDK, never from the compatibility calculators.
|
||||
|
||||
Run from the repository root with a fresh output path:
|
||||
|
||||
```bash
|
||||
dotnet run --project scripts/validation/opcua-aggregate-baseline/Baseline.csproj \
|
||||
-c Release -- /tmp/opcua-original-aggregates-new.json
|
||||
```
|
||||
|
||||
An existing output path is refused. The JSON records the original aggregate assembly's
|
||||
SHA-256, epoch and processing interval. The expected assembly hash is
|
||||
`d8d9706dea577858325730cd89896826ed82f7bfcf9503ccd0260311663a69f4`.
|
||||
Output uses compact metadata and data, followed by one vector per line for reviewable diffs.
|
||||
|
||||
The first **4,144 vectors** retain the original seven datasets and complete grid over 37
|
||||
aggregate IDs, forward/reverse reads, stepped/sloped interpolation, uncertain-as-bad and
|
||||
incremental/final processing. Their parsed objects and the seven original input datasets
|
||||
are unchanged. The request window is epoch +0 through +70 seconds, reversed for reverse
|
||||
vectors, with a 20,000 ms processing interval.
|
||||
|
||||
The next **2,000 focused vectors** avoid expanding every dataset into the complete grid:
|
||||
|
||||
| Cases | Coverage |
|
||||
|---:|---|
|
||||
| 1,628 | Eleven numeric datasets across all 37 IDs, both directions and both processing modes: all eight integer widths, Single, Double and mixed-type conversion. Includes narrow limits, Int64/UInt64 values around 2^53 and 2^63 and their full ranges, fractional interpolation, signed zero, nonfinite values and overflow. |
|
||||
| 148 | Duplicate timestamp ties across all 37 IDs, both directions and processing modes; nonzero source/server picoseconds, independent server timestamp offsets and an exact source timestamp/picosecond tie. |
|
||||
| 224 | Interpolative, TimeAverage/2, Total/2 and PercentGood/Bad over uncertain and sparse data, both directions and processing modes. Four explicit profiles cover PercentDataBad/Good 0/0, 1/99, 50/50 and 100/0, both uncertainty policies and sloped extrapolation. |
|
||||
|
||||
New vectors use sloped interpolation (`Stepped=false`); their `Configuration` object records
|
||||
all five effective aggregate settings. `UseServerCapabilitiesDefaults=false` records the
|
||||
resolved configuration passed directly to the calculator. Existing vectors retain their
|
||||
original fields: omitted configuration means the vector's uncertainty policy, 100/100
|
||||
thresholds, and both extrapolation and server-capability defaults disabled.
|
||||
|
||||
New samples may carry `SourcePicoseconds`, `ServerPicoseconds` and `ServerOffsetTicks`;
|
||||
absent fields mean zero. The server timestamp is epoch + `Seconds` + `ServerOffsetTicks`.
|
||||
Samples are sorted stably by `Seconds`, then the whole sequence is reversed for a reverse
|
||||
read, preserving a fully specified tie order. `QueueAccepted` records each attempted
|
||||
queue result through the point of any exception.
|
||||
|
||||
Each output retains its CLR type, round-trip value spelling, complete numeric status,
|
||||
source/server timestamp ticks, source/server picoseconds, and whether it came from an
|
||||
incremental (`PartialRequested=false`) or final (`true`) drain. `Exception` retains the
|
||||
exact original exception type and any outputs already emitted. NaN, infinities and signed
|
||||
zero are stored as strings so JSON does not change their numeric representation.
|
||||
|
||||
When importing fresh evidence into the candidate fixture, first compare the original
|
||||
4,144 parsed vectors, seven datasets and metadata with the previous oracle. Investigate
|
||||
candidate mismatches; do not regenerate expectations from candidate output or adjust
|
||||
original exception types to make the comparison pass.
|
||||
@@ -1 +0,0 @@
|
||||
__pycache__/
|
||||
@@ -1,249 +0,0 @@
|
||||
#pragma warning disable OTOPCUA0001
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.Shared;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
using WriteRequest = ZB.MOM.WW.OtOpcUa.Core.Abstractions.WriteRequest;
|
||||
using SharedSecurityMode = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.SecurityMode;
|
||||
|
||||
internal static partial class ProcessHost
|
||||
{
|
||||
static object ServerCounts(OtOpcUaSdkServer server)
|
||||
{
|
||||
var sessions = server.CurrentInstance.SessionManager.GetSessions();
|
||||
var subs = server.CurrentInstance.SubscriptionManager.GetSubscriptions();
|
||||
return new { utc = DateTime.UtcNow, sessions = sessions.Count, subscriptions = subs.Count,
|
||||
items = subs.Sum(s => s.MonitoredItemCount), serverProcess = Environment.ProcessId };
|
||||
}
|
||||
}
|
||||
|
||||
internal static class Lifecycle
|
||||
{
|
||||
static string Root = "";
|
||||
static string Password => Environment.GetEnvironmentVariable("OPCUA_MATRIX_PASSWORD") ?? throw new InvalidOperationException("Missing fixture credential");
|
||||
static readonly JsonSerializerOptions Json = new() { WriteIndented = true };
|
||||
static int managerChecks, sharedPasses, driverPasses, browserPasses;
|
||||
static readonly List<WeakReference> Weak = [];
|
||||
static void Require(bool pass, string message) { if (!pass) throw new InvalidOperationException(message); }
|
||||
static object? FieldOrNull(object owner, string name)
|
||||
{
|
||||
var field = owner.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)
|
||||
?? throw new InvalidOperationException("Diagnostic layout changed: " + owner.GetType().FullName + "." + name);
|
||||
return field.GetValue(owner);
|
||||
}
|
||||
static object Field(object owner, string name) => FieldOrNull(owner, name) ?? throw new InvalidOperationException("Diagnostic owner null: " + name);
|
||||
static void Empty(object owner, string name) => Require(Field(owner, name) is ICollection items && items.Count == 0, "Retained collection: " + name);
|
||||
static bool Disposed(object manager)
|
||||
{
|
||||
Require(manager.GetType().FullName == "Opc.Ua.CertificateManager", "Unexpected SDK certificate manager type");
|
||||
Require(manager.GetType().Assembly.GetName().Version?.ToString() == "2.0.1.0", "Diagnostic requires SDK 2.0.1.0");
|
||||
var field = manager.GetType().GetField("m_disposed", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
|
||||
Require(field?.FieldType == typeof(bool), "SDK 2.0.1 CertificateManager.m_disposed bool layout changed");
|
||||
return (bool)field!.GetValue(manager)!;
|
||||
}
|
||||
static object SharedManager(OpcUaClientService shared) => Field(Field(shared, "_session"), "_certificateManager");
|
||||
static object AppManager(object owner) => ((ApplicationInstance)Field(owner, "_ownedApplication")).ApplicationConfiguration!.CertificateManager;
|
||||
static void CheckManager(object manager, bool disposed)
|
||||
{
|
||||
Require(Disposed(manager) == disposed, "Certificate manager disposal observation mismatch");
|
||||
if (disposed) managerChecks++;
|
||||
}
|
||||
static ConnectionSettings Settings(string endpoint, string? password = null) => new()
|
||||
{
|
||||
EndpointUrl = endpoint, Username = "apikey", Password = password ?? Password,
|
||||
SecurityMode = SharedSecurityMode.SignAndEncrypt, AutoAcceptCertificates = false,
|
||||
CertificateStorePath = Path.Combine(Root, "data", "shared-pki"), SessionTimeoutSeconds = 30
|
||||
};
|
||||
static OpcUaClientDriverOptions Options(string endpoint) => new()
|
||||
{
|
||||
EndpointUrl = endpoint, SecurityPolicy = OpcUaSecurityPolicy.Basic256Sha256,
|
||||
SecurityMode = OpcUaSecurityMode.SignAndEncrypt, AuthType = OpcUaAuthType.Username,
|
||||
Username = "apikey", Password = Password, AutoAcceptCertificates = false,
|
||||
SessionName = "ClientLifecycle", ApplicationUri = "urn:localhost:ClientLifecycle",
|
||||
RawTags = [new("scalar", "{\"nodeId\":\"nsu=https://zb.com/otopcua/raw;s=Matrix/dev/Scalar\"}", true),
|
||||
new("vector", "{\"nodeId\":\"nsu=https://zb.com/otopcua/raw;s=Matrix/dev/Vector\"}", true)]
|
||||
};
|
||||
static async Task Wait(Func<bool> condition, CancellationToken ct)
|
||||
{
|
||||
while (!condition()) await Task.Delay(25, ct);
|
||||
}
|
||||
static object? Clr(DataValue value) => ZB.MOM.WW.OtOpcUa.Client.Shared.Helpers.ValueConverter.ToClrObject(value.WrappedValue);
|
||||
static async Task Prepare()
|
||||
{
|
||||
// Public connection failures create and release the same production applications as a live attempt.
|
||||
// No production private method is invoked and no SDK state is written by diagnostics.
|
||||
using var ct = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
||||
using (var shared = new OpcUaClientService())
|
||||
{ try { await shared.ConnectAsync(Settings("opc.tcp://127.0.0.1:1"), ct.Token); } catch { } }
|
||||
await using (var driver = new OpcUaClientDriver(Options("opc.tcp://127.0.0.1:1"), "lifecycle"))
|
||||
{ try { await driver.InitializeAsync("{}", ct.Token); } catch { } }
|
||||
var browser = new OpcUaClientDriverBrowser();
|
||||
try { await using var session = await browser.OpenAsync(JsonSerializer.Serialize(Options("opc.tcp://127.0.0.1:1")), ct.Token); } catch { }
|
||||
foreach (var folder in new[] { "data/shared-pki", "data/OtOpcUa/pki", "data/OtOpcUa/adminui-browse-pki" })
|
||||
Require(Directory.GetFiles(Path.Combine(Root, folder, "own", "certs")).Length == 1, "Expected one reusable application DER certificate: " + folder);
|
||||
}
|
||||
static async Task Cycle(string endpoint, int cycle)
|
||||
{
|
||||
using var budget = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
var ct = budget.Token;
|
||||
using var shared = new OpcUaClientService(); Weak.Add(new(shared));
|
||||
await using var driver = new OpcUaClientDriver(Options(endpoint), "lifecycle"); Weak.Add(new(driver));
|
||||
var browser = new OpcUaClientDriverBrowser(); Weak.Add(new(browser));
|
||||
// Two connections on the same Shared service and two initializations on the same driver.
|
||||
for (var pass = 0; pass < 2; pass++)
|
||||
{
|
||||
var seen = new ConcurrentQueue<int>();
|
||||
EventHandler<DataChangedEventArgs> handler = (_, e) => { if (Clr(e.Value) is int n) seen.Enqueue(n); };
|
||||
shared.DataChanged += handler;
|
||||
var info = await shared.ConnectAsync(Settings(endpoint), ct);
|
||||
Require(info.SecurityMode == "SignAndEncrypt" && info.SecurityPolicyUri == SecurityPolicies.Basic256Sha256, "Shared policy downgrade");
|
||||
object? manager = SharedManager(shared); CheckManager(manager, false);
|
||||
var nss = (string[])Clr(await shared.ReadValueAsync(VariableIds.Server_NamespaceArray, ct))!;
|
||||
var ns = (ushort)Array.IndexOf(nss, "https://zb.com/otopcua/raw"); Require(ns > 0, "Raw namespace missing");
|
||||
var scalar = new NodeId("Matrix/dev/Scalar", ns); var vector = new NodeId("Matrix/dev/Vector", ns);
|
||||
await shared.SubscribeAsync(scalar, 100, ct); await shared.SubscribeAsync(vector, 100, ct);
|
||||
var target = cycle * 10 + pass;
|
||||
Require(StatusCode.IsGood(await shared.WriteValueAsync(scalar, target, ct)), "Shared scalar write failed");
|
||||
Require(StatusCode.IsGood(await shared.WriteValueAsync(vector, new[] { target, 2, 3 }, ct)), "Shared vector write failed");
|
||||
await Wait(() => seen.Contains(target), ct);
|
||||
Require(Equals(Clr(await shared.ReadValueAsync(scalar, ct)), target), "Shared scalar roundtrip mismatch");
|
||||
Require(((int[])Clr(await shared.ReadValueAsync(vector, ct))!).SequenceEqual(new[] { target, 2, 3 }), "Shared vector mismatch");
|
||||
Require((await shared.BrowseAsync(new NodeId("Matrix/dev", ns), ct)).Count > 0, "Shared browse empty");
|
||||
await shared.UnsubscribeAsync(scalar, ct); await shared.UnsubscribeAsync(vector, ct);
|
||||
await shared.DisconnectAsync(ct);
|
||||
Require(!shared.IsConnected && shared.CurrentConnectionInfo is null, "Shared public connection state retained");
|
||||
Require(FieldOrNull(shared, "_session") is null, "Shared session retained"); Empty(shared, "_activeDataSubscriptions");
|
||||
CheckManager(manager, true); manager = null; shared.DataChanged -= handler; sharedPasses++;
|
||||
|
||||
if (pass == 0) await driver.InitializeAsync("{}", ct);
|
||||
else await driver.ReinitializeAsync("{}", ct);
|
||||
Require(driver.GetHealth().State == DriverState.Healthy, "Driver initialize health");
|
||||
manager = AppManager(driver); CheckManager(manager, false);
|
||||
var values = new ConcurrentQueue<int>();
|
||||
EventHandler<DataChangeEventArgs> driverHandler = (_, e) => { if (e.Snapshot.Value is int n) values.Enqueue(n); };
|
||||
driver.OnDataChange += driverHandler;
|
||||
var sub = await driver.SubscribeAsync(["scalar", "vector"], TimeSpan.FromMilliseconds(100), ct);
|
||||
target++;
|
||||
var writes = await driver.WriteAsync([new WriteRequest("scalar", target), new WriteRequest("vector", new[] { target, 4, 5 })], ct);
|
||||
Require(writes.Count == 2 && writes.All(w => w.StatusCode == 0), "Driver write failed");
|
||||
await Wait(() => values.Contains(target), ct);
|
||||
var reads = await driver.ReadAsync(["scalar", "vector"], ct);
|
||||
Require(reads.Count == 2 && reads.All(r => r.StatusCode == 0) && Equals(reads[0].Value, target)
|
||||
&& ((int[])reads[1].Value!).SequenceEqual(new[] { target, 4, 5 }), "Driver roundtrip mismatch");
|
||||
await driver.UnsubscribeAsync(sub, ct); sub = null!;
|
||||
// Reinitialize a live connection with an active replacement subscription, as the watchdog does.
|
||||
sub = await driver.SubscribeAsync(["scalar", "vector"], TimeSpan.FromMilliseconds(100), ct);
|
||||
await driver.ReinitializeAsync("{}", ct); sub = null!;
|
||||
CheckManager(manager, true); manager = null;
|
||||
manager = AppManager(driver); CheckManager(manager, false);
|
||||
Require(driver.GetHealth().State == DriverState.Healthy, "Live reinitialize failed");
|
||||
var reinitialized = await driver.ReadAsync(["scalar"], ct);
|
||||
Require(reinitialized.Count == 1 && reinitialized[0].StatusCode == 0 && Equals(reinitialized[0].Value, target), "Live reinitialize read failed");
|
||||
await driver.ShutdownAsync(ct); driver.OnDataChange -= driverHandler;
|
||||
Require(driver.GetHealth().State == DriverState.Unknown, "Driver shutdown public state retained");
|
||||
Require(FieldOrNull(driver, "<Session>k__BackingField") is null, "Driver session retained");
|
||||
Empty(driver, "_subscriptions"); Empty(driver, "_alarmSubscriptions");
|
||||
CheckManager(manager, true); manager = null; driverPasses++;
|
||||
|
||||
var browse = await browser.OpenAsync(JsonSerializer.Serialize(Options(endpoint)), ct); Weak.Add(new(browse));
|
||||
manager = AppManager(browse); CheckManager(manager, false);
|
||||
Require((await browse.RootAsync(ct)).Count > 0, "Browser root empty");
|
||||
await browse.DisposeAsync().AsTask().WaitAsync(ct); CheckManager(manager, true); manager = null; browserPasses++;
|
||||
}
|
||||
}
|
||||
static object Resources(int cycle, double elapsed)
|
||||
{
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
var survivors = Weak.Count(w => w.IsAlive); Weak.RemoveAll(w => !w.IsAlive);
|
||||
Require(survivors == 0, "Disposed production client/driver/browser object retained after full GC");
|
||||
var fd = Directory.GetFiles("/proc/self/fd");
|
||||
var owned = 0; var pki = 0;
|
||||
foreach (var path in fd)
|
||||
{
|
||||
var target = new FileInfo(path).LinkTarget;
|
||||
if (target is null || !Path.IsPathFullyQualified(target)) continue;
|
||||
target = Path.GetFullPath(target);
|
||||
if (target.StartsWith(Path.GetFullPath(Root) + Path.DirectorySeparatorChar, StringComparison.Ordinal)) owned++;
|
||||
if (target.StartsWith(Path.Combine(Path.GetFullPath(Root), "data") + Path.DirectorySeparatorChar, StringComparison.Ordinal)) pki++;
|
||||
}
|
||||
using var p = Process.GetCurrentProcess();
|
||||
return new { cycle, elapsedSeconds = elapsed, utc = DateTime.UtcNow, p.WorkingSet64, p.PrivateMemorySize64,
|
||||
managedBytes = GC.GetTotalMemory(false), allocatedBytes = GC.GetTotalAllocatedBytes(),
|
||||
gc = new[] { GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2) },
|
||||
fdCount = fd.Length, ownedRootFdCount = owned, ownedPkiFdCount = pki, survivingProductionObjects = survivors,
|
||||
managerDisposalsVerified = managerChecks, sharedPasses, driverPasses, browserPasses };
|
||||
}
|
||||
static async Task Run(string endpoint, int seconds, bool full)
|
||||
{
|
||||
Require(!full || seconds >= 7200, "Full soak requires >=7200 requested seconds");
|
||||
// Deliberate owned-manager positive control proves the exact-tag read-only diagnostic sees both states.
|
||||
var control = CertificateManagerFactory.Create(new SecurityConfiguration(), DefaultTelemetry.Create(_ => { }));
|
||||
Require(!Disposed(control), "Disposal diagnostic control false-state failure");
|
||||
await control.DisposeAsync(); Require(Disposed(control), "Disposal diagnostic control true-state failure");
|
||||
control = null!;
|
||||
var originalCerts = CertificateHashes();
|
||||
var elapsed = Stopwatch.StartNew(); var cycle = 0;
|
||||
File.AppendAllText(Path.Combine(Root, "resources.jsonl"), JsonSerializer.Serialize(Resources(0, 0)) + "\n");
|
||||
while (elapsed.Elapsed.TotalSeconds < seconds)
|
||||
{
|
||||
await Cycle(endpoint, ++cycle).WaitAsync(TimeSpan.FromSeconds(65));
|
||||
// The async cycle has returned, dropping its strong manager/session/item/event references.
|
||||
await Task.Delay(300);
|
||||
var serverPath = Path.Combine(Root, "server", "live-resources.json");
|
||||
var settledOk = false;
|
||||
for (var attempt = 0; attempt < 30; attempt++)
|
||||
{
|
||||
using var settled = JsonDocument.Parse(await File.ReadAllTextAsync(serverPath));
|
||||
settledOk = new[] { "sessions", "subscriptions", "items" }.All(name => settled.RootElement.GetProperty(name).GetInt32() == 0);
|
||||
if (settledOk) break;
|
||||
await Task.Delay(100);
|
||||
}
|
||||
Require(settledOk, "Server sessions/subscriptions/items retained after bounded settling");
|
||||
if (cycle % 10 == 0 || elapsed.Elapsed.TotalSeconds >= seconds)
|
||||
File.AppendAllText(Path.Combine(Root, "resources.jsonl"), JsonSerializer.Serialize(Resources(cycle, elapsed.Elapsed.TotalSeconds)) + "\n");
|
||||
}
|
||||
Require(elapsed.Elapsed.TotalSeconds >= seconds && (!full || elapsed.Elapsed.TotalSeconds >= 7200), "Real elapsed duration gate failed");
|
||||
Require(managerChecks == cycle * 8 && sharedPasses == cycle * 2 && driverPasses == cycle * 2 && browserPasses == cycle * 2, "Incomplete lifecycle accounting");
|
||||
Require(CertificateHashes().SequenceEqual(originalCerts), "Application certificate unexpectedly rotated or PKI changed");
|
||||
var summary = new { status = "passed", diagnosticPositiveControl = "exact SDK2 manager false-before/true-after DisposeAsync", applicationCertificateHashes = originalCerts, requestedSeconds = seconds, elapsedSeconds = elapsed.Elapsed.TotalSeconds,
|
||||
fullTwoHourLifecycle = full && elapsed.Elapsed.TotalSeconds >= 7200, cycles = cycle, sharedPasses, driverPasses, browserPasses,
|
||||
managerDisposalsVerified = managerChecks, finalResources = Resources(cycle, elapsed.Elapsed.TotalSeconds),
|
||||
scope = "Production Shared connect/disconnect/reconnect; production driver Initialize/Reinitialize/Shutdown/dispose; production browser open/browse/dispose; scalar and Int32[3] read/write/subscribe/unsubscribe; strict mutual trusted DER/Basic256Sha256 SignAndEncrypt/API-key fixture. No network stalls, revocation, surgical edits/rebuild, CP abandonment or Windows/AVEVA/performance G4 claim." };
|
||||
File.WriteAllText(Path.Combine(Root, "summary.json"), JsonSerializer.Serialize(summary, Json));
|
||||
}
|
||||
static string[] CertificateHashes() => Directory.GetFiles(Path.Combine(Root, "data"), "*.der", SearchOption.AllDirectories)
|
||||
.Where(p => p.Contains(Path.DirectorySeparatorChar + "own" + Path.DirectorySeparatorChar + "certs" + Path.DirectorySeparatorChar, StringComparison.Ordinal))
|
||||
.Order().Select(p => Path.GetRelativePath(Root, p) + ":" + Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(p))).ToLowerInvariant()).ToArray();
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
if (args[0] == "server") return await ProcessHost.HelperMain(args);
|
||||
Root = Path.GetFullPath(args[1]); Directory.CreateDirectory(Root);
|
||||
try
|
||||
{
|
||||
var local = Path.GetFullPath(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
|
||||
Require(local == Path.Combine(Root, "data"), "XDG task PKI isolation failed");
|
||||
if (args[0] == "prepare") { await Prepare(); return 0; }
|
||||
Require(args[0] == "run", "Unknown mode");
|
||||
await Run(args[2], int.Parse(args[3]), args[4] == "full");
|
||||
var sdk = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name!.StartsWith("Opc.Ua", StringComparison.Ordinal))
|
||||
.Select(a => new { name = a.GetName().Name, version = a.GetName().Version?.ToString(), sha256 = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(a.Location))).ToLowerInvariant() });
|
||||
File.WriteAllText(Path.Combine(Root, "sdk-assemblies.json"), JsonSerializer.Serialize(sdk, Json));
|
||||
return 0;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
File.WriteAllText(Path.Combine(Root, "failure.txt"), error.ToString().Replace(Password, "<fixture-secret>"));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup><OutputType>Exe</OutputType><StartupObject>Lifecycle</StartupObject><DefineConstants>SDK2</DefineConstants></PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<ProjectReference Include="../../../src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj" />
|
||||
<ProjectReference Include="../../../src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/ZB.MOM.WW.OtOpcUa.Client.Shared.csproj" />
|
||||
<ProjectReference Include="../../../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj" />
|
||||
<ProjectReference Include="../../../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.csproj" />
|
||||
</ItemGroup></Project>
|
||||
@@ -1,360 +0,0 @@
|
||||
// Copy of compatibility ProcessHost at 7b353d7b; lifecycle-owned changes only.
|
||||
#pragma warning disable OTOPCUA0001 // This wire harness deliberately exercises public driver capabilities directly.
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.Shared;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
|
||||
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.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
|
||||
using AlarmEventArgs = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.AlarmEventArgs;
|
||||
using WriteRequest = ZB.MOM.WW.OtOpcUa.Core.Abstractions.WriteRequest;
|
||||
using SharedSecurityMode = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.SecurityMode;
|
||||
using CoreHistory = ZB.MOM.WW.OtOpcUa.Core.Abstractions.HistoryReadResult;
|
||||
|
||||
internal static partial class ProcessHost
|
||||
{
|
||||
static readonly DateTime Epoch = new(2026, 10, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
const string RawUri = "https://zb.com/otopcua/raw", UnsUri = "https://zb.com/otopcua/uns";
|
||||
const string Folder = "Matrix/dev", Scalar = Folder + "/Scalar", Vector = Folder + "/Vector", Alarm = Folder + "/Alarm";
|
||||
static readonly CancellationToken Ct = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Environment.GetEnvironmentVariable("OPCUA_LIFECYCLE_SERVER_SECONDS") ?? "240"))).Token;
|
||||
static string Password => Environment.GetEnvironmentVariable("OPCUA_MATRIX_PASSWORD") ?? throw new InvalidOperationException("Missing ephemeral credential");
|
||||
static readonly List<object> Cases = [];
|
||||
static readonly List<object> Observations = [];
|
||||
static int Failures;
|
||||
static string Root = "";
|
||||
static object? Clr(DataValue v)
|
||||
{
|
||||
#if SDK2
|
||||
return ZB.MOM.WW.OtOpcUa.Client.Shared.Helpers.ValueConverter.ToClrObject(v.WrappedValue);
|
||||
#else
|
||||
return v.Value;
|
||||
#endif
|
||||
}
|
||||
static DateTime SourceTime(DataValue value)
|
||||
{
|
||||
#if SDK2
|
||||
return value.SourceTimestamp.ToDateTime();
|
||||
#else
|
||||
return value.SourceTimestamp;
|
||||
#endif
|
||||
}
|
||||
static DateTime ServerTime(DataValue value)
|
||||
{
|
||||
#if SDK2
|
||||
return value.ServerTimestamp.ToDateTime();
|
||||
#else
|
||||
return value.ServerTimestamp;
|
||||
#endif
|
||||
}
|
||||
static void Require(bool yes, string message) { if (!yes) throw new InvalidOperationException(message); }
|
||||
static async Task Case(string name, Func<Task> action)
|
||||
{
|
||||
try { await action(); Cases.Add(new { name, status = "passed" }); }
|
||||
catch (Exception e) { Failures++; Cases.Add(new { name, status = "failed", error = e.ToString() }); }
|
||||
}
|
||||
static async Task Wait(Func<bool> predicate)
|
||||
{
|
||||
for (int i = 0; i < 200; i++) { if (predicate()) return; await Task.Delay(50, Ct); }
|
||||
Require(false, "Notification deadline exceeded");
|
||||
}
|
||||
static ConnectionSettings Settings(string endpoint) => new()
|
||||
{
|
||||
EndpointUrl = endpoint,
|
||||
Username = "apikey",
|
||||
Password = Password,
|
||||
SecurityMode = SharedSecurityMode.SignAndEncrypt,
|
||||
AutoAcceptCertificates = false,
|
||||
CertificateStorePath = Environment.GetEnvironmentVariable("OPCUA_MATRIX_CLIENT_PKI") ?? Path.Combine(Root, "client-pki")
|
||||
};
|
||||
public static async Task<int> HelperMain(string[] args)
|
||||
{
|
||||
Root = Path.GetFullPath(args[1]); Directory.CreateDirectory(Root);
|
||||
try
|
||||
{
|
||||
switch (args[0])
|
||||
{
|
||||
case "local-data-path": Console.WriteLine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)); return 0;
|
||||
case "driver": await Driver(args[2]); break;
|
||||
case "server": await Serve(); return 0;
|
||||
case "client": await Client(args[2]); break;
|
||||
case "performance": await Performance(args[2]); break;
|
||||
default: throw new ArgumentException("Expected prepare/server/client");
|
||||
}
|
||||
}
|
||||
catch (Exception e) { Failures++; Cases.Add(new { name = "process", status = "failed", error = e.ToString() }); }
|
||||
File.WriteAllText(Path.Combine(Root, "result.json"), JsonSerializer.Serialize(new { sdk = typeof(NodeId).Assembly.GetName().Version?.ToString(), process = Environment.ProcessId, cases = Cases, observations = Observations, failures = Failures, loadedSdkAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name!.StartsWith("Opc.Ua", StringComparison.Ordinal)).Select(a => new { name = a.GetName().Name, version = a.GetName().Version?.ToString(), path = a.Location }) }, new JsonSerializerOptions { WriteIndented = true }));
|
||||
return Failures == 0 ? 0 : 1;
|
||||
}
|
||||
static async Task Serve()
|
||||
{
|
||||
var options = new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = "OtOpcUa.Matrix",
|
||||
ApplicationUri = "urn:OtOpcUa.Matrix.ClientLifecycle",
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = Path.Combine(Root, "server-pki"),
|
||||
AllowAnonymous = false,
|
||||
AutoAcceptUntrustedClientCertificates = false,
|
||||
EnabledSecurityProfiles = Environment.GetEnvironmentVariable("OPCUA_MATRIX_SERVER_SECURITY") == "none"
|
||||
? [OpcUaSecurityProfile.None] : [OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt]
|
||||
};
|
||||
using (var listener = new TcpListener(IPAddress.Loopback, 0)) { listener.Start(); options.OpcUaPort = ((IPEndPoint)listener.LocalEndpoint).Port; }
|
||||
var server = new OtOpcUaSdkServer();
|
||||
await using var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance, new Auth());
|
||||
await host.StartAsync(server, Ct);
|
||||
var nm = server.NodeManager!; var sink = new SdkAddressSpaceSink(nm);
|
||||
var authority = new DataPlaneAuthorityState();
|
||||
authority.Apply("matrix", "rev1", [new ApiKey { ApiKeyId = "matrix", Name = "matrix", SecretHash = "fixture", Principal = "matrix", Enabled = true, CreatedUtc = DateTime.UtcNow.AddDays(-1) }],
|
||||
[new NodeAcl { NodeAclId = "all", ClusterId = "matrix", Principal = "matrix", ScopeKind = NodeAclScopeKind.Cluster, PermissionFlags = (NodePermissions)int.MaxValue }], new Dictionary<AclScopeRef, IReadOnlyList<NodeAclPath>>());
|
||||
nm.DataPlaneAuthority = authority;
|
||||
nm.Authorizer = new DataPlaneAuthorizer(authority, new TriePermissionEvaluator(authority.Tries), true, NullLogger.Instance, TimeProvider.System);
|
||||
var history = new Source(Root); nm.HistorianDataSource = history;
|
||||
sink.EnsureFolder(Folder, null, "dev", AddressSpaceRealm.Raw);
|
||||
sink.EnsureFolder("Eq", null, "Eq", AddressSpaceRealm.Uns);
|
||||
foreach (var realm in new[] { AddressSpaceRealm.Raw, AddressSpaceRealm.Uns })
|
||||
{
|
||||
var scalar = realm == AddressSpaceRealm.Raw ? Scalar : "Eq/Scalar";
|
||||
var vector = realm == AddressSpaceRealm.Raw ? Vector : "Eq/Vector";
|
||||
var parent = realm == AddressSpaceRealm.Raw ? Folder : "Eq";
|
||||
sink.EnsureVariable(scalar, parent, "Scalar", "Int32", true, realm, "matrix.scalar");
|
||||
sink.EnsureVariable(vector, parent, "Vector", "Int32", true, realm, isArray: true, arrayLength: 3);
|
||||
sink.WriteValue(scalar, 17, 0, Epoch, realm); sink.WriteValue(vector, new[] { 1, 2, 3 }, 0, Epoch, realm);
|
||||
}
|
||||
sink.AddReference("Eq/Scalar", AddressSpaceRealm.Uns, Scalar, AddressSpaceRealm.Raw);
|
||||
sink.MaterialiseAlarmCondition(Alarm, Folder, "Alarm", "OffNormalAlarm", 800, AddressSpaceRealm.Raw, isNative: true);
|
||||
sink.WireAlarmNotifiers(Alarm, AddressSpaceRealm.Raw, ["Eq"], AddressSpaceRealm.Uns);
|
||||
sink.EnsureVariable(Folder + "/UnsetTimestamp", Folder, "UnsetTimestamp", "Int32", false, AddressSpaceRealm.Raw);
|
||||
sink.WriteValue(Folder + "/UnsetTimestamp", 3, 0, DateTime.MinValue, AddressSpaceRealm.Raw);
|
||||
nm.NodeWriteGateway = new Gateway();
|
||||
var state = new AlarmConditionSnapshot(true, false, true, true, AlarmShelvingKind.Unshelved, 800, "matrix-alarm");
|
||||
nm.NativeAlarmAckRouter = _ => Task.Run(() => { state = state with { Acknowledged = true }; sink.WriteAlarmCondition(Alarm, state, DateTime.UtcNow, AddressSpaceRealm.Raw); });
|
||||
File.WriteAllText(Path.Combine(Root, "endpoint.txt"), $"opc.tcp://127.0.0.1:{options.OpcUaPort}/OtOpcUa");
|
||||
File.WriteAllText(Path.Combine(Root, "ready.json"), JsonSerializer.Serialize(new { endpoint = $"opc.tcp://127.0.0.1:{options.OpcUaPort}/OtOpcUa", process = Environment.ProcessId, sdk = typeof(NodeId).Assembly.GetName().Version?.ToString(), rawUri = RawUri, unsUri = UnsUri }));
|
||||
int tick = 0;
|
||||
while (!File.Exists(Path.Combine(Root, "stop")))
|
||||
{
|
||||
sink.WriteAlarmCondition(Alarm, state with { Message = ++tick % 2 == 0 ? "matrix-alarm" : "matrix-tick" }, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
var counts = ServerCounts(server);
|
||||
File.WriteAllText(Path.Combine(Root, "live-resources.tmp"), JsonSerializer.Serialize(counts));
|
||||
File.Move(Path.Combine(Root, "live-resources.tmp"), Path.Combine(Root, "live-resources.json"), true);
|
||||
await Task.Delay(250, Ct);
|
||||
}
|
||||
using var process = System.Diagnostics.Process.GetCurrentProcess();
|
||||
File.WriteAllText(Path.Combine(Root, "server-resources.json"), JsonSerializer.Serialize(new { process.WorkingSet64, process.PrivateMemorySize64, process.HandleCount, threads=process.Threads.Count, cpuMilliseconds=process.TotalProcessorTime.TotalMilliseconds, allocatedBytes=GC.GetTotalAllocatedBytes(), collections=new[]{GC.CollectionCount(0),GC.CollectionCount(1),GC.CollectionCount(2)} }, new JsonSerializerOptions {WriteIndented=true}));
|
||||
}
|
||||
static async Task Client(string endpoint)
|
||||
{
|
||||
using var client = new OpcUaClientService();
|
||||
await Case("trusted-SignAndEncrypt-API-key", async () => { var info = await client.ConnectAsync(Settings(endpoint), Ct); Require(info.SecurityMode == "SignAndEncrypt" && info.SecurityPolicyUri == SecurityPolicies.Basic256Sha256, "Unexpected negotiated policy"); Observations.Add(new { info.SecurityMode, info.SecurityPolicyUri }); });
|
||||
if (!client.IsConnected) return;
|
||||
var namespaces = (string[])Clr(await client.ReadValueAsync(VariableIds.Server_NamespaceArray, Ct))!;
|
||||
var raw = (ushort)Array.IndexOf(namespaces, RawUri); var uns = (ushort)Array.IndexOf(namespaces, UnsUri);
|
||||
NodeId ScalarId(ushort ns) => new(ns == raw ? Scalar : "Eq/Scalar", ns);
|
||||
NodeId VectorId(ushort ns) => new(ns == raw ? Vector : "Eq/Vector", ns);
|
||||
await Case("raw-UNS-URI-browse", async () =>
|
||||
{
|
||||
Require(raw > 0 && uns > 0, "Missing namespace URI");
|
||||
var rawChildren = await client.BrowseAsync(new NodeId(Folder, raw), Ct);
|
||||
var unsChildren = await client.BrowseAsync(new NodeId("Eq", uns), Ct);
|
||||
Observations.Add(new { rawChildren, unsChildren });
|
||||
// Supplement the production Shared results with unfiltered wire-reference metadata.
|
||||
var adapter = typeof(OpcUaClientService).GetField("_session", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(client)!;
|
||||
var sdkSession = (Opc.Ua.Client.Session)adapter.GetType().GetField("_session", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(adapter)!;
|
||||
var (_, _, rawReferences) = await sdkSession.BrowseAsync(null, null, new NodeId(Folder, raw), 0u,
|
||||
BrowseDirection.Forward, ReferenceTypeIds.References, true, 0u);
|
||||
Observations.Add(new { rawReferences = (rawReferences.ToArray() ?? []).Select(reference => new { nodeId = reference.NodeId.ToString(), browseName = reference.BrowseName.ToString(), nodeClass = reference.NodeClass.ToString(), typeDefinition = reference.TypeDefinition.ToString(), referenceType = reference.ReferenceTypeId.ToString(), reference.IsForward }).ToArray() });
|
||||
var expectedRaw = new[] { Scalar, Vector, Alarm, Folder + "/UnsetTimestamp" }.Select(id => new NodeId(id, raw).ToString());
|
||||
Require(expectedRaw.All(id => rawChildren.Any(child => child.NodeId == id)), "Missing raw children: " + string.Join(",", expectedRaw.Where(id => !rawChildren.Any(child => child.NodeId == id))));
|
||||
Require(new[] { "Eq/Scalar", "Eq/Vector" }.Select(id => new NodeId(id, uns).ToString()).All(id => unsChildren.Any(child => child.NodeId == id)), "Missing UNS scalar/array children");
|
||||
});
|
||||
await Case("initial-quality-explicit-source-and-server-timestamps", async () => { var initial = await client.ReadValueAsync(ScalarId(raw), Ct); Require(StatusCode.IsGood(initial.StatusCode), "Bad initial quality"); Require(SourceTime(initial) == Epoch && ServerTime(initial) > Epoch, "Lost explicit initial timestamps"); Observations.Add(new { sourceTimestamp = SourceTime(initial).ToString("O"), serverTimestamp = ServerTime(initial).ToString("O"), contract = "Source supplied as 2026-10-01T00:00:00Z; server timestamp observed without normalization or historian algorithm changes" }); });
|
||||
foreach (var ns in new[] { raw, uns })
|
||||
{
|
||||
await Case($"scalar-read-write-ns-{ns}", async () => { Require(Clr(await client.ReadValueAsync(ScalarId(ns), Ct)) is int, "Scalar lost Int32 type"); Require(StatusCode.IsGood(await client.WriteValueAsync(ScalarId(ns), 42, Ct)), "Scalar write failed"); Require((int)Clr(await client.ReadValueAsync(ScalarId(ns), Ct))! == 42, "Scalar round trip failed"); });
|
||||
await Case($"typed-array-read-write-ns-{ns}", async () => { Require(Clr(await client.ReadValueAsync(VectorId(ns), Ct)) is int[], "Array lost Int32[] type"); Require(StatusCode.IsGood(await client.WriteValueAsync(VectorId(ns), new[] { 7, 8, 9 }, Ct)), "Array write failed"); Require(((int[])Clr(await client.ReadValueAsync(VectorId(ns), Ct))!).SequenceEqual(new[] { 7, 8, 9 }), "Array round trip failed"); });
|
||||
}
|
||||
await Case("monitor-quality-timestamps", async () => { var seen = new ConcurrentQueue<DataValue>(); client.DataChanged += (_, e) => seen.Enqueue(e.Value); await client.SubscribeAsync(ScalarId(raw), 100, Ct); await client.WriteValueAsync(ScalarId(raw), 73, Ct); await Wait(() => seen.Any(v => Equals(Clr(v), 73))); var v = seen.Last(v => Equals(Clr(v), 73)); Require(StatusCode.IsGood(v.StatusCode) && SourceTime(v) > Epoch && ServerTime(v) > Epoch, "Bad monitor quality or missing timestamps"); Observations.Add(new { sourceTimestamp = SourceTime(v).ToString("O"), serverTimestamp = ServerTime(v).ToString("O"), status = v.StatusCode.ToString() }); await client.UnsubscribeAsync(ScalarId(raw), Ct); });
|
||||
await Case("event-filter-and-native-ack", async () => { var alarms = new ConcurrentQueue<AlarmEventArgs>(); client.AlarmEvent += (_, a) => alarms.Enqueue(a); await client.SubscribeAlarmsAsync(ObjectIds.Server, 100, Ct); await Wait(() => alarms.Any(a => a.Message == "matrix-alarm" && a.ActiveState && a.Severity == 800)); var alarm = alarms.Last(a => a.Message == "matrix-alarm"); Require(alarm.EventId?.Length > 0, "Missing event ID"); Require(StatusCode.IsGood(await client.AcknowledgeAlarmAsync(new NodeId(Alarm, raw).ToString(), alarm.EventId!, "matrix-ack", Ct)), "Ack refused"); await Wait(() => alarms.Any(a => a.Message == "matrix-alarm" && a.AckedState)); await client.UnsubscribeAlarmsAsync(Ct); });
|
||||
await Case("raw-history-CP-paging", async () => { var values = await client.HistoryReadRawAsync(ScalarId(raw), Epoch, Epoch.AddMinutes(1), 100, Ct); Require(values.Count == 25, "Expected 25 raw samples, got " + values.Count); Require(values.Select(v => Convert.ToInt32(Clr(v))).SequenceEqual(Enumerable.Range(0, 25)), "Raw page loss/duplicates/order"); });
|
||||
await Case("event-history-CP-paging", async () =>
|
||||
{
|
||||
var filter = new EventFilter();
|
||||
#if SDK2
|
||||
filter.SelectClauses += new SimpleAttributeOperand(ObjectTypeIds.BaseEventType, new QualifiedName("EventId")) { AttributeId = Attributes.Value };
|
||||
#else
|
||||
filter.SelectClauses.Add(new SimpleAttributeOperand {TypeDefinitionId=ObjectTypeIds.BaseEventType,BrowsePath=new QualifiedNameCollection {new QualifiedName("EventId")},AttributeId=Attributes.Value});
|
||||
#endif
|
||||
var events = await client.HistoryReadEventsAsync(new NodeId(Folder, raw), Epoch, Epoch.AddMinutes(1), filter, 100, Ct); Require(events.Count == 25, "Expected 25 event samples, got " + events.Count);
|
||||
#if SDK2
|
||||
var ids = events.Select(e => System.Text.Encoding.UTF8.GetString((byte[])ZB.MOM.WW.OtOpcUa.Client.Shared.Helpers.ValueConverter.ToClrObject(e.Fields[0])!));
|
||||
#else
|
||||
var ids=events.Select(e=>System.Text.Encoding.UTF8.GetString((byte[])e.Fields[0].Value));
|
||||
#endif
|
||||
Require(ids.SequenceEqual(Enumerable.Range(0, 25).Select(i => $"e{i:00}")), "Event history loss/duplicates/order across timestamp ties");
|
||||
});
|
||||
await Case("event-history-where-filter-positive-and-negative", async () =>
|
||||
{
|
||||
foreach (var type in new[] { ObjectTypeIds.BaseEventType, ObjectTypeIds.AuditEventType })
|
||||
{
|
||||
var filter = new EventFilter();
|
||||
#if SDK2
|
||||
filter.SelectClauses += new SimpleAttributeOperand(ObjectTypeIds.BaseEventType, new QualifiedName("EventId")) { AttributeId = Attributes.Value };
|
||||
filter.WhereClause.Push(FilterOperator.OfType, Variant.From(type));
|
||||
#else
|
||||
filter.SelectClauses.Add(new SimpleAttributeOperand(ObjectTypeIds.BaseEventType,new QualifiedName("EventId")){AttributeId=Attributes.Value});
|
||||
filter.WhereClause.Push(FilterOperator.OfType,new LiteralOperand(new Variant(type)));
|
||||
#endif
|
||||
var events = await client.HistoryReadEventsAsync(new NodeId(Folder, raw), Epoch, Epoch.AddMinutes(1), filter, 100, Ct);
|
||||
Require(events.Count == (type == ObjectTypeIds.BaseEventType ? 25 : 0), $"Where filter for {type} expected {(type == ObjectTypeIds.BaseEventType ? 25 : 0)} got {events.Count}");
|
||||
}
|
||||
});
|
||||
var unset = await client.ReadValueAsync(new NodeId(Folder + "/UnsetTimestamp", raw), Ct);
|
||||
Observations.Add(new { sourceTimestamp = SourceTime(unset).ToString("O"), serverTimestamp = ServerTime(unset).ToString("O"), timestampObservation = "Fixture source supplied DateTime.MinValue (unset). These are unmodified application/SDK wire observations; no historian algorithm or expected timestamp was changed." });
|
||||
await Benchmark(client, ScalarId(raw), VectorId(raw));
|
||||
await client.DisconnectAsync(Ct);
|
||||
}
|
||||
static OpcUaClientDriverOptions DriverOptions(string endpoint) => new()
|
||||
{
|
||||
EndpointUrl = endpoint,
|
||||
SecurityPolicy = OpcUaSecurityPolicy.Basic256Sha256,
|
||||
SecurityMode = OpcUaSecurityMode.SignAndEncrypt,
|
||||
AuthType = OpcUaAuthType.Username,
|
||||
Username = "apikey",
|
||||
Password = Password,
|
||||
AutoAcceptCertificates = false,
|
||||
RawTags = [new("scalar",JsonSerializer.Serialize(new {nodeId=$"nsu={RawUri};s={Scalar}"}),true),
|
||||
new("vector",JsonSerializer.Serialize(new {nodeId=$"nsu={RawUri};s={Vector}"}),true),
|
||||
new("uns",JsonSerializer.Serialize(new {nodeId=$"nsu={UnsUri};s=Eq/Scalar"}),true)],
|
||||
SessionName = "MatrixDriver",
|
||||
ApplicationUri = "urn:localhost:MatrixDriver"
|
||||
};
|
||||
static string IsolatedLocalData()
|
||||
{
|
||||
var local = Path.GetFullPath(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
|
||||
Require(local.StartsWith(Path.GetFullPath(Root) + Path.DirectorySeparatorChar, StringComparison.Ordinal), "Driver/CLI canonical PKI must be in task-owned root: " + local);
|
||||
return local;
|
||||
}
|
||||
static async Task Driver(string endpoint)
|
||||
{
|
||||
IsolatedLocalData();
|
||||
await using var driver = new OpcUaClientDriver(DriverOptions(endpoint), "matrix");
|
||||
await Case("driver-trusted-encrypted-API-key-connect", () => driver.InitializeAsync("{}", Ct));
|
||||
await Case("driver-raw-UNS-URI-scalar-and-array-read", async () =>
|
||||
{
|
||||
var values = await driver.ReadAsync(["scalar", "vector", "uns"], Ct);
|
||||
Require(values.Count == 3 && values.All(x => x.StatusCode == 0), "Read failure");
|
||||
Require(values[0].Value is int && values[1].Value is int[] && values[2].Value is int, "Lost scalar/array type");
|
||||
});
|
||||
await Case("driver-scalar-array-write-roundtrip", async () =>
|
||||
{
|
||||
var writes = await driver.WriteAsync([new WriteRequest("scalar", 91), new WriteRequest("vector", new[] { 11, 12, 13 })], Ct);
|
||||
Require(writes.All(x => x.StatusCode == 0), "Write failed");
|
||||
var values = await driver.ReadAsync(["scalar", "vector"], Ct);
|
||||
Require(Equals(values[0].Value, 91) && ((int[])values[1].Value!).SequenceEqual(new[] { 11, 12, 13 }), "Write round trip mismatch");
|
||||
});
|
||||
await Case("driver-monitor-quality-timestamps", async () =>
|
||||
{
|
||||
var seen = new ConcurrentQueue<DataValueSnapshot>();
|
||||
driver.OnDataChange += (_, e) => seen.Enqueue(e.Snapshot);
|
||||
var subscription = await driver.SubscribeAsync(["scalar"], TimeSpan.FromMilliseconds(100), Ct);
|
||||
await driver.WriteAsync([new WriteRequest("scalar", 92)], Ct);
|
||||
await Wait(() => seen.Any(x => Equals(x.Value, 92)));
|
||||
var value = seen.Last(x => Equals(x.Value, 92));
|
||||
Require(value.StatusCode == 0 && value.SourceTimestampUtc.HasValue && value.ServerTimestampUtc > Epoch, "Missing monitor quality/timestamps");
|
||||
Observations.Add(new { value.SourceTimestampUtc, value.ServerTimestampUtc, value.StatusCode });
|
||||
await driver.UnsubscribeAsync(subscription, Ct);
|
||||
});
|
||||
Observations.Add(new { capability = "driver-history", status = "not-applicable", evidence = "Driver has no IHistoryProvider/HistoryRead capability; .claude/rules/drivers.md #655. Shared/CLI own history." });
|
||||
await driver.ShutdownAsync(Ct);
|
||||
}
|
||||
static async Task Performance(string endpoint)
|
||||
{
|
||||
using var client = new OpcUaClientService();
|
||||
var info = await client.ConnectAsync(Settings(endpoint), Ct);
|
||||
Require(info.SecurityMode == "SignAndEncrypt" && info.SecurityPolicyUri == SecurityPolicies.Basic256Sha256, "Unexpected benchmark endpoint policy");
|
||||
var namespaces = (string[])Clr(await client.ReadValueAsync(VariableIds.Server_NamespaceArray, Ct))!;
|
||||
var raw = (ushort)Array.IndexOf(namespaces, RawUri);
|
||||
Require(raw > 0, "Missing benchmark raw namespace");
|
||||
Observations.Add(new {info.SecurityMode, info.SecurityPolicyUri, tieredCompilation=Environment.GetEnvironmentVariable("DOTNET_TieredCompilation"), scope="Two scalar/Int32[3] variables; zero data monitors during measurement; five fixture variables and one native condition"});
|
||||
await Case("warmed-production-client-read-write-measurement", () => Benchmark(client, new NodeId(Scalar, raw), new NodeId(Vector, raw)));
|
||||
await client.DisconnectAsync(Ct);
|
||||
}
|
||||
static async Task Benchmark(OpcUaClientService client, NodeId scalar, NodeId vector)
|
||||
{
|
||||
// Local diagnostic subset; excludes reconnect, history and load.
|
||||
var rounds = int.Parse(Environment.GetEnvironmentVariable("OPCUA_MATRIX_PERF_ROUNDS") ?? "3");
|
||||
var warmups = int.Parse(Environment.GetEnvironmentVariable("OPCUA_MATRIX_PERF_WARMUPS") ?? "10");
|
||||
var measured = int.Parse(Environment.GetEnvironmentVariable("OPCUA_MATRIX_PERF_MEASURED") ?? "50");
|
||||
Require(rounds > 0 && warmups >= 0 && measured > 0, "Invalid performance sample counts");
|
||||
for (int round = 1; round <= rounds; round++)
|
||||
{
|
||||
for (int warmup = 0; warmup < warmups; warmup++)
|
||||
{
|
||||
await client.ReadValueAsync(scalar, Ct); await client.WriteValueAsync(scalar, warmup, Ct);
|
||||
await client.ReadValueAsync(vector, Ct); await client.WriteValueAsync(vector, new[] { warmup, warmup + 1, warmup + 2 }, Ct);
|
||||
}
|
||||
var samples = new Dictionary<string, List<double>>();
|
||||
var before = System.Diagnostics.Process.GetCurrentProcess(); var cpu = before.TotalProcessorTime;
|
||||
var allocated = GC.GetTotalAllocatedBytes(); var gc = new[] { GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2) };
|
||||
foreach (var operation in new[] { "scalar-read", "scalar-write", "array-read", "array-write" })
|
||||
{
|
||||
var times = new List<double>();
|
||||
for (int i = 0; i < measured; i++)
|
||||
{
|
||||
var start = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
if (operation == "scalar-read") await client.ReadValueAsync(scalar, Ct);
|
||||
else if (operation == "scalar-write") Require(StatusCode.IsGood(await client.WriteValueAsync(scalar, i, Ct)), "Benchmark scalar write");
|
||||
else if (operation == "array-read") await client.ReadValueAsync(vector, Ct);
|
||||
else Require(StatusCode.IsGood(await client.WriteValueAsync(vector, new[] { i, i + 1, i + 2 }, Ct)), "Benchmark array write");
|
||||
times.Add(System.Diagnostics.Stopwatch.GetElapsedTime(start).TotalMilliseconds);
|
||||
}
|
||||
samples[operation] = times;
|
||||
}
|
||||
before.Refresh();
|
||||
Observations.Add(new
|
||||
{
|
||||
benchmarkRound = round,
|
||||
warmupOperationsPerKind = 10,
|
||||
operationsPerKind = 50,
|
||||
latencyMilliseconds = samples.ToDictionary(x => x.Key, x => new { p50 = x.Value.Order().ElementAt((int)Math.Ceiling(measured * 0.50) - 1), p95 = x.Value.Order().ElementAt((int)Math.Ceiling(measured * 0.95) - 1), p99 = x.Value.Order().ElementAt((int)Math.Ceiling(measured * 0.99) - 1), throughputOperationsPerSecond = measured * 1000.0 / x.Value.Sum() }),
|
||||
clientCpuMilliseconds = (before.TotalProcessorTime - cpu).TotalMilliseconds,
|
||||
clientAllocatedBytes = GC.GetTotalAllocatedBytes() - allocated, clientAllocatedBytesPerOperation = (GC.GetTotalAllocatedBytes() - allocated) / (4.0 * measured),
|
||||
clientWorkingSetBytes = before.WorkingSet64,
|
||||
clientPrivateBytes = before.PrivateMemorySize64,
|
||||
clientThreads = before.Threads.Count,
|
||||
clientHandles = before.HandleCount,
|
||||
collections = new[] { GC.CollectionCount(0) - gc[0], GC.CollectionCount(1) - gc[1], GC.CollectionCount(2) - gc[2] }
|
||||
});
|
||||
}
|
||||
}
|
||||
sealed class Auth : IOpcUaUserAuthenticator
|
||||
{
|
||||
public Task<OpcUaUserAuthResult> AuthenticateUserNameAsync(string username, string password, CancellationToken ct) => Task.FromResult(username == "apikey" && password == Password ? OpcUaUserAuthResult.AllowKey("matrix", [OpcUaDataPlaneRoles.WriteOperate, OpcUaDataPlaneRoles.AlarmAck], "matrix") : OpcUaUserAuthResult.Deny("Invalid ephemeral fixture key"));
|
||||
}
|
||||
sealed class Gateway : IOpcUaNodeWriteGateway { public Task<NodeWriteOutcome> WriteAsync(string id, object? value, AddressSpaceRealm realm, CancellationToken ct) => Task.FromResult(new NodeWriteOutcome(true, null)); }
|
||||
sealed class Source(string root) : IHistorianDataSource
|
||||
{
|
||||
void Log(string operation, int rows) => File.AppendAllText(Path.Combine(root, "history-requests.jsonl"), JsonSerializer.Serialize(new { operation, rows }) + "\n");
|
||||
public int MaxRawValuesPerRead => 7;
|
||||
public Task<CoreHistory> ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct)
|
||||
{ var all = Enumerable.Range(0, 25).Select(i => new DataValueSnapshot(i, 0, Epoch.AddSeconds(i), Epoch.AddSeconds(i))).Where(v => v.SourceTimestampUtc >= s && v.SourceTimestampUtc < e).ToList(); var rows = all.Take((int)(m == 0 ? 7 : m)).ToArray(); Log("raw", rows.Length); return Task.FromResult(new CoreHistory(rows, all.Count > rows.Length ? [1] : null)); }
|
||||
public Task<HistoricalEventsResult> ReadEventsAsync(string? source, DateTime s, DateTime e, int max, CancellationToken ct)
|
||||
{ var all = Enumerable.Range(0, 25).Select(i => new HistoricalEvent($"e{i:00}", "matrix", Epoch.AddSeconds(i / 2), Epoch, "matrix-history", 500)).Where(v => v.EventTimeUtc >= s && v.EventTimeUtc < e).ToList(); var rows = all.Take(Math.Min(7, max <= 0 ? 7 : max)).ToArray(); Log("events", rows.Length); return Task.FromResult(new HistoricalEventsResult(rows, all.Count > rows.Length ? [1] : null)); }
|
||||
public Task<CoreHistory> ReadProcessedAsync(string r, DateTime s, DateTime e, TimeSpan i, HistoryAggregateType a, CancellationToken ct) => NullHistorianDataSource.Instance.ReadProcessedAsync(r, s, e, i, a, ct);
|
||||
public Task<CoreHistory> ReadAtTimeAsync(string r, IReadOnlyList<DateTime> t, CancellationToken ct) => NullHistorianDataSource.Instance.ReadAtTimeAsync(r, t, ct);
|
||||
public HistorianHealthSnapshot GetHealthSnapshot() => NullHistorianDataSource.Instance.GetHealthSnapshot();
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
# Production OPC UA client lifecycle diagnostic
|
||||
|
||||
This standalone harness runs the unchanged production `OpcUaClientService`,
|
||||
`OpcUaClientDriver`, and `OpcUaClientDriverBrowser` in one persistent Linux client
|
||||
process against a separate process using the production SDK server and node
|
||||
manager. Both processes load OPC Foundation SDK 2.0.1. The server fixture is a
|
||||
local copy of the compatibility harness at commit `7b353d7b`; only this directory's
|
||||
copy has configurable lifetime and atomic public resource snapshots.
|
||||
|
||||
Run a short diagnostic from a checkout containing the SDK 2.0.1 changes:
|
||||
|
||||
```sh
|
||||
python3 scripts/validation/opcua-client-lifecycle/run.py \
|
||||
--duration 90 --output /tmp/otopcua-client-lifecycle-diagnostic-unique
|
||||
```
|
||||
|
||||
The coordinator must launch and own the full process separately:
|
||||
|
||||
```sh
|
||||
python3 scripts/validation/opcua-client-lifecycle/run.py \
|
||||
--full --duration 7200 --output /tmp/otopcua-client-lifecycle-full-unique
|
||||
```
|
||||
|
||||
The output path must not exist. `--full` rejects a duration below 7200 seconds;
|
||||
`fullTwoHourLifecycle` is true only after the client monotonic clock has actually
|
||||
elapsed at least 7200 seconds and every final check completes. A diagnostic pass
|
||||
cannot satisfy that gate. The outer process also has a duration plus 300-second
|
||||
hard deadline and forcibly removes its own container on timeout.
|
||||
|
||||
The launcher publishes this standalone project, then uses the locally cached
|
||||
official `mcr.microsoft.com/dotnet/sdk:10.0.103-noble` Linux arm64 image with
|
||||
`--network none`, no host ports and fresh task-owned PKI. `XDG_DATA_HOME` is the
|
||||
only storage environment override; it never changes `HOME`. A generated
|
||||
credential stays in the child process environment. The output root is mode 0700
|
||||
and contains generated private keys: **never commit the entire output directory**.
|
||||
Commit only the selected redacted JSON/log receipts under `evidence/`.
|
||||
|
||||
Preparation invokes bounded failing connections through each product's public
|
||||
API to create its own application certificate. It does not reflectively invoke
|
||||
configuration builders or replace the store. Before the live session, public DER
|
||||
certificates are exchanged explicitly among the three clients and server.
|
||||
Autoaccept is disabled on both ends. Negotiation must be Basic256Sha256
|
||||
SignAndEncrypt and API-key-shaped credentials must pass the fixture authenticator
|
||||
and production authority/ACL path. Existing own DER hashes must remain unchanged
|
||||
across all loop iterations.
|
||||
|
||||
Every cycle performs two Shared connect/read/write/subscribe/unsubscribe/
|
||||
disconnect rounds on the same object. It performs two driver initialize/read/
|
||||
write/subscribe/unsubscribe rounds, each followed by a live reinitialize with an
|
||||
active subscription, then shutdown. It opens, browses and disposes two production
|
||||
AdminUI browser sessions. Scalar Int32 and Int32[3] writes must roundtrip exactly,
|
||||
and both Shared and driver scalar data notifications must arrive.
|
||||
|
||||
The external harness reads ownership fields for diagnostics only. Fields are
|
||||
looked up by exact name, with missing/null layouts failing the run. It does not
|
||||
write reflected fields or change SDK state. For SDK 2.0.1.0 only, each captured
|
||||
`CertificateManager.m_disposed` boolean must be false while connected and true
|
||||
after release. An independently created, deliberately disposed SDK manager proves
|
||||
the before/after probe. All strong manager references are dropped each pass.
|
||||
Eight released managers are checked per cycle: two Shared, four driver (including
|
||||
live reinitialization), and two browser. Production session and data subscription
|
||||
collections must clear, and public Shared connection information and driver health
|
||||
must return to disconnected/Unknown. Collected weak references to disposed client,
|
||||
driver, browser and browse-session objects must have no survivors at samples.
|
||||
|
||||
After every cycle the server's public session/subscription/monitored-item counts
|
||||
must return to zero within a bounded settle period. Resource samples capture GC,
|
||||
managed allocation/retained bytes, RSS/private bytes, global FDs and task/PKI FDs.
|
||||
Owned-path FD matching canonicalizes absolute paths and uses a path-separator
|
||||
boundary. Sampling starts cold, then every ten cycles and at completion. RSS and
|
||||
FD observations require interpretation after warmup; they are not performance
|
||||
budgets or an automatic G4 acceptance decision.
|
||||
|
||||
Artifacts are `result.json`, `source.json`, `command.json`, `build.log`,
|
||||
`container.log`, and `task/{summary.json,resources.jsonl,sdk-assemblies.json,
|
||||
client.log,server.log,prepare.log,server/live-resources.json}`. A failure additionally
|
||||
writes a redacted `task/failure.txt`. The server uses a fixed application URI and
|
||||
the three client certificate stores are reused for the entire process lifetime.
|
||||
|
||||
This is a small local lifecycle gate, not the entire Task 12 gate. It does not
|
||||
cover certificate revocation, network stalls, node surgical edits/rebuild,
|
||||
continuation-point abandonment/churn, representative driver fanout/load,
|
||||
Windows install/publish, ScadaBridge or AVEVA interoperability. It makes no
|
||||
baseline/candidate performance or full G4 claim. The coordinator's separate server
|
||||
lifecycle harness supplies complementary SDK session/CP/server mutation coverage.
|
||||
|
||||
The checked-in 2026-11-10 diagnostic completed **91.104 seconds**, 68 cycles,
|
||||
136 Shared rounds, 136 driver rounds (plus 136 live driver reinitializations),
|
||||
136 browser sessions, and 544 verified released certificate managers. The final
|
||||
sample had zero surviving weak product objects and zero owned PKI FDs; server
|
||||
sessions/subscriptions/items returned to zero after every cycle. Warm samples
|
||||
showed 146–148 total FDs; the final managed heap was 4,737,536 bytes and RSS was
|
||||
181,018,624 bytes. The deliberate manager before/after control passed and
|
||||
`--full --duration 90` exited 2 before creating artifacts. This is diagnostic
|
||||
coverage only; the two-hour gate remains pending until a coordinator-owned full
|
||||
run completes. Receipts are under `evidence/2026-11-10-diagnostic/`.
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Disposable network-isolated container only. All PKI is owned by this fresh task root.
|
||||
set -euo pipefail
|
||||
mkdir -p /task/data /task/server
|
||||
export XDG_DATA_HOME=/task/data
|
||||
export OPCUA_LIFECYCLE_SERVER_SECONDS=$((LIFECYCLE_SECONDS + 300))
|
||||
dotnet /app/Lifecycle.dll prepare /task > /task/prepare.log 2>&1
|
||||
mkdir -p /task/server/server-pki/trusted/certs
|
||||
for pki in /task/data/shared-pki /task/data/OtOpcUa/pki /task/data/OtOpcUa/adminui-browse-pki; do
|
||||
cp "$pki"/own/certs/* /task/server/server-pki/trusted/certs/
|
||||
done
|
||||
dotnet /app/Lifecycle.dll server /task/server > /task/server.log 2>&1 &
|
||||
server_pid=$!
|
||||
cleanup() {
|
||||
touch /task/server/stop
|
||||
for attempt in $(seq 1 150); do
|
||||
kill -0 "$server_pid" 2>/dev/null || break
|
||||
sleep 0.1
|
||||
done
|
||||
kill "$server_pid" 2>/dev/null || true
|
||||
wait "$server_pid" || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
for attempt in $(seq 1 600); do
|
||||
test ! -f /task/server/ready.json || break
|
||||
kill -0 "$server_pid" || exit 1
|
||||
sleep 0.1
|
||||
done
|
||||
test -f /task/server/ready.json
|
||||
for pki in /task/data/shared-pki /task/data/OtOpcUa/pki /task/data/OtOpcUa/adminui-browse-pki; do
|
||||
mkdir -p "$pki/trusted/certs"
|
||||
cp /task/server/server-pki/own/certs/* "$pki/trusted/certs/"
|
||||
done
|
||||
endpoint=$(cat /task/server/endpoint.txt)
|
||||
dotnet /app/Lifecycle.dll run /task "$endpoint" "$LIFECYCLE_SECONDS" "$LIFECYCLE_MODE" > /task/client.log 2>&1
|
||||
@@ -1,63 +0,0 @@
|
||||
|
||||
Determining projects to restore...
|
||||
All projects are up-to-date for restore.
|
||||
ZB.MOM.WW.OtOpcUa.Analyzers -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/bin/Release/netstandard2.0/ZB.MOM.WW.OtOpcUa.Analyzers.dll
|
||||
ZB.MOM.WW.OtOpcUa.Core.Abstractions -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.Abstractions.dll
|
||||
ZB.MOM.WW.OtOpcUa.Commons -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Core/ZB.MOM.WW.OtOpcUa.Commons/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Commons.dll
|
||||
ZB.MOM.WW.OtOpcUa.Cluster -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Cluster.dll
|
||||
ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions.dll
|
||||
ZB.MOM.WW.OtOpcUa.Core.Scripting -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.Scripting.dll
|
||||
ZB.MOM.WW.OtOpcUa.Core.VirtualTags -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts.dll
|
||||
ZB.MOM.WW.OtOpcUa.Configuration -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Configuration.dll
|
||||
ZB.MOM.WW.OtOpcUa.Core -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Core/ZB.MOM.WW.OtOpcUa.Core/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.dll
|
||||
ZB.MOM.WW.OtOpcUa.Core.Scripting.Protocol -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Protocol/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.Scripting.Protocol.dll
|
||||
ZB.MOM.WW.OtOpcUa.Core.Scripting.OutOfProcess -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.OutOfProcess/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.Scripting.OutOfProcess.dll
|
||||
ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.dll
|
||||
ZB.MOM.WW.OtOpcUa.Security -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.Security/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Security.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Calculation.Contracts -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation.Contracts/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Calculation.Contracts.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.dll
|
||||
ZB.MOM.WW.OtOpcUa.ControlPlane -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.ControlPlane.dll
|
||||
ZB.MOM.WW.OtOpcUa.OpcUaServer -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.OpcUaServer.dll
|
||||
ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.dll
|
||||
ZB.MOM.WW.OtOpcUa.Runtime -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Runtime.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Transport -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Transport/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Transport.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser.dll
|
||||
ZB.MOM.WW.OtOpcUa.AdminUI -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.AdminUI.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.AbCip -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.AbCip.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.AbLegacy -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Calculation -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Calculation.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.FOCAS -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Galaxy -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Modbus -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Modbus.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Mqtt -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.MTConnect -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.S7 -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.S7.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.Sql -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Sql.dll
|
||||
ZB.MOM.WW.OtOpcUa.Driver.TwinCAT -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.dll
|
||||
ZB.MOM.WW.OtOpcUa.ScriptWorker -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.ScriptWorker/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.ScriptWorker.dll
|
||||
ZB.MOM.WW.OtOpcUa.Host -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.Host/bin/Release/net10.0/OtOpcUa.Host.dll
|
||||
ZB.MOM.WW.OtOpcUa.Client.Shared -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Client.Shared.dll
|
||||
Lifecycle -> /Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-client-lifecycle/bin/Release/net10.0/Lifecycle.dll
|
||||
Lifecycle -> /private/tmp/opcua2-production-client-diagnostic-20261110c/app/
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"argv": [
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--name",
|
||||
"otopcua-client-lifecycle-a2c271bbeb54",
|
||||
"--network",
|
||||
"none",
|
||||
"-e",
|
||||
"OPCUA_MATRIX_PASSWORD",
|
||||
"-e",
|
||||
"LIFECYCLE_SECONDS",
|
||||
"-e",
|
||||
"LIFECYCLE_MODE",
|
||||
"-v",
|
||||
"/private/tmp/opcua2-production-client-diagnostic-20261110c/app:/app:ro",
|
||||
"-v",
|
||||
"/private/tmp/opcua2-production-client-diagnostic-20261110c/task:/task",
|
||||
"mcr.microsoft.com/dotnet/sdk:10.0.103-noble",
|
||||
"bash",
|
||||
"/task/container.sh"
|
||||
],
|
||||
"timeoutSeconds": 390
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
usage: run.py [-h] --output OUTPUT [--duration DURATION] [--full]
|
||||
[--image IMAGE]
|
||||
run.py: error: --full requires duration >=7200; diagnostic duration must be positive
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
{"cycle":0,"elapsedSeconds":0,"utc":"2026-11-10T17:39:34.5140163Z","WorkingSet64":50884608,"PrivateMemorySize64":87019520,"managedBytes":243384,"allocatedBytes":307688,"gc":[2,2,2],"fdCount":66,"ownedRootFdCount":4,"ownedPkiFdCount":0,"survivingProductionObjects":0,"managerDisposalsVerified":0,"sharedPasses":0,"driverPasses":0,"browserPasses":0}
|
||||
{"cycle":10,"elapsedSeconds":13.7816334,"utc":"2026-11-10T17:39:48.307566Z","WorkingSet64":168873984,"PrivateMemorySize64":210952192,"managedBytes":4622440,"allocatedBytes":53385048,"gc":[5,4,4],"fdCount":146,"ownedRootFdCount":4,"ownedPkiFdCount":0,"survivingProductionObjects":0,"managerDisposalsVerified":80,"sharedPasses":20,"driverPasses":20,"browserPasses":20}
|
||||
{"cycle":20,"elapsedSeconds":26.955914,"utc":"2026-11-10T17:40:01.4729188Z","WorkingSet64":181080064,"PrivateMemorySize64":230060032,"managedBytes":4716120,"allocatedBytes":100286768,"gc":[8,6,6],"fdCount":146,"ownedRootFdCount":4,"ownedPkiFdCount":0,"survivingProductionObjects":0,"managerDisposalsVerified":160,"sharedPasses":40,"driverPasses":40,"browserPasses":40}
|
||||
{"cycle":30,"elapsedSeconds":40.3288294,"utc":"2026-11-10T17:40:14.8512936Z","WorkingSet64":181923840,"PrivateMemorySize64":230674432,"managedBytes":4624608,"allocatedBytes":146998480,"gc":[11,8,8],"fdCount":146,"ownedRootFdCount":4,"ownedPkiFdCount":0,"survivingProductionObjects":0,"managerDisposalsVerified":240,"sharedPasses":60,"driverPasses":60,"browserPasses":60}
|
||||
{"cycle":40,"elapsedSeconds":53.6437668,"utc":"2026-11-10T17:40:28.1646671Z","WorkingSet64":183074816,"PrivateMemorySize64":232931328,"managedBytes":4503968,"allocatedBytes":193703712,"gc":[14,10,10],"fdCount":148,"ownedRootFdCount":4,"ownedPkiFdCount":0,"survivingProductionObjects":0,"managerDisposalsVerified":320,"sharedPasses":80,"driverPasses":80,"browserPasses":80}
|
||||
{"cycle":50,"elapsedSeconds":66.9338459,"utc":"2026-11-10T17:40:41.4571518Z","WorkingSet64":184758272,"PrivateMemorySize64":232972288,"managedBytes":4503736,"allocatedBytes":240424880,"gc":[17,12,12],"fdCount":148,"ownedRootFdCount":4,"ownedPkiFdCount":0,"survivingProductionObjects":0,"managerDisposalsVerified":400,"sharedPasses":100,"driverPasses":100,"browserPasses":100}
|
||||
{"cycle":60,"elapsedSeconds":80.254683,"utc":"2026-11-10T17:40:54.7806201Z","WorkingSet64":180539392,"PrivateMemorySize64":233132032,"managedBytes":4504072,"allocatedBytes":287112440,"gc":[20,14,14],"fdCount":148,"ownedRootFdCount":4,"ownedPkiFdCount":0,"survivingProductionObjects":0,"managerDisposalsVerified":480,"sharedPasses":120,"driverPasses":120,"browserPasses":120}
|
||||
{"cycle":68,"elapsedSeconds":91.0947059,"utc":"2026-11-10T17:41:05.614513Z","WorkingSet64":180760576,"PrivateMemorySize64":233250816,"managedBytes":4739896,"allocatedBytes":324761520,"gc":[23,16,16],"fdCount":148,"ownedRootFdCount":4,"ownedPkiFdCount":0,"survivingProductionObjects":0,"managerDisposalsVerified":544,"sharedPasses":136,"driverPasses":136,"browserPasses":136}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"exitCode": 0,
|
||||
"containerElapsedSeconds": 93.73282008292153,
|
||||
"summary": {
|
||||
"status": "passed",
|
||||
"diagnosticPositiveControl": "exact SDK2 manager false-before/true-after DisposeAsync",
|
||||
"applicationCertificateHashes": [
|
||||
"data/OtOpcUa/adminui-browse-pki/own/certs/OtOpcUa AdminUI Browse [14579B861300836A2A96549E307DB4123778B0C5].der:89e21ac01a5582dd04df77add2fad19495c695b69f4a48a05d2c71283325503b",
|
||||
"data/OtOpcUa/pki/own/certs/ClientLifecycle [AF2D40052FCDDC9A8C09686E6E8E0F17CFF5A979].der:e9bdc63bc2cc0b7901389f958b2fd6bfaf5369f439dc996e152668a3fa3c381c",
|
||||
"data/shared-pki/own/certs/OtOpcUaClient [91C87A8D0AC11919E0AFD44457BF932F3587F1B0].der:b4f2018a43f161e18d25f7b8bb80590ccec13afcf7b9189f6d97e10a68b55602"
|
||||
],
|
||||
"requestedSeconds": 90,
|
||||
"elapsedSeconds": 91.1041217,
|
||||
"fullTwoHourLifecycle": false,
|
||||
"cycles": 68,
|
||||
"sharedPasses": 136,
|
||||
"driverPasses": 136,
|
||||
"browserPasses": 136,
|
||||
"managerDisposalsVerified": 544,
|
||||
"finalResources": {
|
||||
"cycle": 68,
|
||||
"elapsedSeconds": 91.1041218,
|
||||
"utc": "2026-11-10T17:41:05.6187359Z",
|
||||
"WorkingSet64": 181018624,
|
||||
"PrivateMemorySize64": 233250816,
|
||||
"managedBytes": 4737536,
|
||||
"allocatedBytes": 324891240,
|
||||
"gc": [
|
||||
25,
|
||||
18,
|
||||
18
|
||||
],
|
||||
"fdCount": 148,
|
||||
"ownedRootFdCount": 4,
|
||||
"ownedPkiFdCount": 0,
|
||||
"survivingProductionObjects": 0,
|
||||
"managerDisposalsVerified": 544,
|
||||
"sharedPasses": 136,
|
||||
"driverPasses": 136,
|
||||
"browserPasses": 136
|
||||
},
|
||||
"scope": "Production Shared connect/disconnect/reconnect; production driver Initialize/Reinitialize/Shutdown/dispose; production browser open/browse/dispose; scalar and Int32[3] read/write/subscribe/unsubscribe; strict mutual trusted DER/Basic256Sha256 SignAndEncrypt/API-key fixture. No network stalls, revocation, surgical edits/rebuild, CP abandonment or Windows/AVEVA/performance G4 claim."
|
||||
},
|
||||
"applicationCertificates": {
|
||||
"data/OtOpcUa/adminui-browse-pki/own/certs/OtOpcUa AdminUI Browse [14579B861300836A2A96549E307DB4123778B0C5].der": "89e21ac01a5582dd04df77add2fad19495c695b69f4a48a05d2c71283325503b",
|
||||
"data/OtOpcUa/pki/own/certs/ClientLifecycle [AF2D40052FCDDC9A8C09686E6E8E0F17CFF5A979].der": "e9bdc63bc2cc0b7901389f958b2fd6bfaf5369f439dc996e152668a3fa3c381c",
|
||||
"data/shared-pki/own/certs/OtOpcUaClient [91C87A8D0AC11919E0AFD44457BF932F3587F1B0].der": "b4f2018a43f161e18d25f7b8bb80590ccec13afcf7b9189f6d97e10a68b55602",
|
||||
"server/server-pki/own/certs/OtOpcUa.Matrix [29412ADF65A3E370B5CDCABE9386217B43E1C1C1].der": "c3beb0f1c0536635c7ceb0dcfc42c6220d78d4a38c00ef199c137a6ba36bb04c"
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "Opc.Ua.Core",
|
||||
"version": "2.0.1.0",
|
||||
"sha256": "9d0857aa6b7f3c49037d4ea557b4da8edf07e146069464481783e66c5c9cd35a"
|
||||
},
|
||||
{
|
||||
"name": "Opc.Ua.Types",
|
||||
"version": "2.0.1.0",
|
||||
"sha256": "c542f69882f4e8a4ebd5ccc28a3881d6967c7ffb6dcc3c56dc6c776f39c2ede1"
|
||||
},
|
||||
{
|
||||
"name": "Opc.Ua.Security.Certificates",
|
||||
"version": "2.0.1.0",
|
||||
"sha256": "7f4969aceb2f0db5328c6882cb4b117342f3b18905c38acdba600683860c4fd0"
|
||||
},
|
||||
{
|
||||
"name": "Opc.Ua.Core.Types",
|
||||
"version": "2.0.1.0",
|
||||
"sha256": "a7dd141b8dabe8848ff28c2fb8e16a85a0508e6a49e6b1a1c1e267175b99af96"
|
||||
},
|
||||
{
|
||||
"name": "Opc.Ua.Client",
|
||||
"version": "2.0.1.0",
|
||||
"sha256": "620da406fabdbc75d11f9827ea7e39d2d3c762471bd2264b0d11f1d6cd9247cd"
|
||||
},
|
||||
{
|
||||
"name": "Opc.Ua.Configuration",
|
||||
"version": "2.0.1.0",
|
||||
"sha256": "6ed6a594c93c3ff35d1a16539a9fe87c1e2640549cc2f48ed6bae333dc8bf0e7"
|
||||
}
|
||||
]
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"utc":"2026-11-10T17:41:05.3943073Z","sessions":0,"subscriptions":0,"items":0,"serverProcess":26}
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"endpoint":"opc.tcp://127.0.0.1:36237/OtOpcUa","process":26,"sdk":"2.0.1.0","rawUri":"https://zb.com/otopcua/raw","unsUri":"https://zb.com/otopcua/uns"}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"WorkingSet64": 232546304,
|
||||
"PrivateMemorySize64": 263188480,
|
||||
"HandleCount": 151,
|
||||
"threads": 17,
|
||||
"cpuMilliseconds": 11661.139,
|
||||
"allocatedBytes": 334834272,
|
||||
"collections": [
|
||||
9,
|
||||
1,
|
||||
0
|
||||
]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"commit": "7b353d7bae5311a2f0a33c0a82c88d2c5b49d76f",
|
||||
"root": "/Users/dohertj2/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa",
|
||||
"durationSeconds": 90,
|
||||
"mode": "diagnostic",
|
||||
"image": {
|
||||
"Id": "sha256:b04611ee9e1b7d36797012ed921962c57189cb4dbb3dff12afb8e158110f293b",
|
||||
"RepoDigests": [
|
||||
"mcr.microsoft.com/dotnet/sdk@sha256:e362a8dbcd691522456da26a5198b8f3ca1d7641c95624fadc5e3e82678bd08a"
|
||||
],
|
||||
"Architecture": "arm64",
|
||||
"Os": "linux"
|
||||
},
|
||||
"network": "none",
|
||||
"startedUtc": "2026-11-10T17:39:26Z",
|
||||
"scope": "actual unchanged production Shared/OpcUaClientDriver/AdminUI browser in a repeated same-process loop; SDK 2.0.1 only"
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"diagnosticPositiveControl": "exact SDK2 manager false-before/true-after DisposeAsync",
|
||||
"applicationCertificateHashes": [
|
||||
"data/OtOpcUa/adminui-browse-pki/own/certs/OtOpcUa AdminUI Browse [14579B861300836A2A96549E307DB4123778B0C5].der:89e21ac01a5582dd04df77add2fad19495c695b69f4a48a05d2c71283325503b",
|
||||
"data/OtOpcUa/pki/own/certs/ClientLifecycle [AF2D40052FCDDC9A8C09686E6E8E0F17CFF5A979].der:e9bdc63bc2cc0b7901389f958b2fd6bfaf5369f439dc996e152668a3fa3c381c",
|
||||
"data/shared-pki/own/certs/OtOpcUaClient [91C87A8D0AC11919E0AFD44457BF932F3587F1B0].der:b4f2018a43f161e18d25f7b8bb80590ccec13afcf7b9189f6d97e10a68b55602"
|
||||
],
|
||||
"requestedSeconds": 90,
|
||||
"elapsedSeconds": 91.1041217,
|
||||
"fullTwoHourLifecycle": false,
|
||||
"cycles": 68,
|
||||
"sharedPasses": 136,
|
||||
"driverPasses": 136,
|
||||
"browserPasses": 136,
|
||||
"managerDisposalsVerified": 544,
|
||||
"finalResources": {
|
||||
"cycle": 68,
|
||||
"elapsedSeconds": 91.1041218,
|
||||
"utc": "2026-11-10T17:41:05.6187359Z",
|
||||
"WorkingSet64": 181018624,
|
||||
"PrivateMemorySize64": 233250816,
|
||||
"managedBytes": 4737536,
|
||||
"allocatedBytes": 324891240,
|
||||
"gc": [
|
||||
25,
|
||||
18,
|
||||
18
|
||||
],
|
||||
"fdCount": 148,
|
||||
"ownedRootFdCount": 4,
|
||||
"ownedPkiFdCount": 0,
|
||||
"survivingProductionObjects": 0,
|
||||
"managerDisposalsVerified": 544,
|
||||
"sharedPasses": 136,
|
||||
"driverPasses": 136,
|
||||
"browserPasses": 136
|
||||
},
|
||||
"scope": "Production Shared connect/disconnect/reconnect; production driver Initialize/Reinitialize/Shutdown/dispose; production browser open/browse/dispose; scalar and Int32[3] read/write/subscribe/unsubscribe; strict mutual trusted DER/Basic256Sha256 SignAndEncrypt/API-key fixture. No network stalls, revocation, surgical edits/rebuild, CP abandonment or Windows/AVEVA/performance G4 claim."
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"diagnosticExitCode": 0,
|
||||
"fullDurationGuardExitCode": 2,
|
||||
"rawArtifactRoot": "/private/tmp/opcua2-production-client-diagnostic-20261110c",
|
||||
"shortRunDoesNotMeetTwoHourGate": true,
|
||||
"harnessFiles": [
|
||||
{
|
||||
"path": "scripts/validation/opcua-client-lifecycle/.gitignore",
|
||||
"sha256": "32dae3052f331ee34d628ef535709b301259a45df7c7522c4d35dcf49873f00b"
|
||||
},
|
||||
{
|
||||
"path": "scripts/validation/opcua-client-lifecycle/Lifecycle.cs",
|
||||
"sha256": "609b73b55c4ad2ed7dcd70f42dbc702cc233cd2f9ace8c686f8fe1cbb8471f3a"
|
||||
},
|
||||
{
|
||||
"path": "scripts/validation/opcua-client-lifecycle/Lifecycle.csproj",
|
||||
"sha256": "379ce48ae014f4a52c9e9096a3fce5046689475a08408f279e430463f5f19085"
|
||||
},
|
||||
{
|
||||
"path": "scripts/validation/opcua-client-lifecycle/ProcessHost.cs",
|
||||
"sha256": "89b6f518c9706753e7f62bda4f8312e3390cf8d0ced99d847e8df52d935720d6"
|
||||
},
|
||||
{
|
||||
"path": "scripts/validation/opcua-client-lifecycle/README.md",
|
||||
"sha256": "05cc5b39f58abd7381b3eec51e0fe18bc6de6f525430810ef21b4679fe1e5d20"
|
||||
},
|
||||
{
|
||||
"path": "scripts/validation/opcua-client-lifecycle/container.sh",
|
||||
"sha256": "899cea38d6a7e21448308dea8931a3d2fe0986d736eaf96e4769ef1637ed5aae"
|
||||
},
|
||||
{
|
||||
"path": "scripts/validation/opcua-client-lifecycle/run.py",
|
||||
"sha256": "8da86e2215fcaa883a9212b6dde0e897795efd5d75307af6318345b61c965ad8"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and run isolated actual production Shared/driver/browser lifecycle loops."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[2]
|
||||
|
||||
|
||||
def write(path, value):
|
||||
path.write_text(json.dumps(value, indent=2) + '\n')
|
||||
|
||||
|
||||
def certs(root):
|
||||
return {str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
for p in sorted(root.rglob('*.der')) if '/own/certs/' in str(p)}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output', required=True, type=Path, help='Fresh nonexistent task evidence root, includes private PKI; never commit entire output')
|
||||
parser.add_argument('--duration', type=int, default=90)
|
||||
parser.add_argument('--full', action='store_true', help='Requires >=7200 seconds of real completed client runtime')
|
||||
parser.add_argument('--image', default='mcr.microsoft.com/dotnet/sdk:10.0.103-noble')
|
||||
args = parser.parse_args()
|
||||
if args.duration <= 0 or (args.full and args.duration < 7200):
|
||||
parser.error('--full requires duration >=7200; diagnostic duration must be positive')
|
||||
output = args.output.resolve()
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
os.chmod(output, 0o700)
|
||||
build = output / 'app'
|
||||
task = output / 'task'
|
||||
task.mkdir()
|
||||
image = json.loads(subprocess.check_output(['docker', 'image', 'inspect', args.image, '--format', '{{json .}}'], text=True))
|
||||
if image['Architecture'] != 'arm64' or image['Os'] != 'linux':
|
||||
raise RuntimeError('Expected cached official Linux arm64 SDK image')
|
||||
write(output / 'source.json', {'commit': subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip(),
|
||||
'root': str(ROOT), 'durationSeconds': args.duration, 'mode': 'full' if args.full else 'diagnostic',
|
||||
'image': {k: image.get(k) for k in ('Id', 'RepoDigests', 'Architecture', 'Os')}, 'network': 'none',
|
||||
'startedUtc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
||||
'scope': 'actual unchanged production Shared/OpcUaClientDriver/AdminUI browser in a repeated same-process loop; SDK 2.0.1 only'})
|
||||
with (output / 'build.log').open('w') as log:
|
||||
subprocess.run(['dotnet', 'publish', str(HERE / 'Lifecycle.csproj'), '-c', 'Release', '-m:1', '--nologo', '-o', str(build)], stdout=log, stderr=subprocess.STDOUT, check=True, timeout=300)
|
||||
shutil.copy2(HERE / 'container.sh', task / 'container.sh')
|
||||
name = 'otopcua-client-lifecycle-' + uuid.uuid4().hex[:12]
|
||||
env = dict(os.environ, OPCUA_MATRIX_PASSWORD='otua_lifecycle_' + secrets.token_urlsafe(32),
|
||||
LIFECYCLE_SECONDS=str(args.duration), LIFECYCLE_MODE='full' if args.full else 'diagnostic')
|
||||
command = ['docker', 'run', '--rm', '--name', name, '--network', 'none',
|
||||
'-e', 'OPCUA_MATRIX_PASSWORD', '-e', 'LIFECYCLE_SECONDS', '-e', 'LIFECYCLE_MODE',
|
||||
'-v', str(build) + ':/app:ro', '-v', str(task) + ':/task', args.image, 'bash', '/task/container.sh']
|
||||
write(output / 'command.json', {'argv': command, 'timeoutSeconds': args.duration + 300})
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with (output / 'container.log').open('w') as log:
|
||||
result = subprocess.run(command, stdout=log, stderr=subprocess.STDOUT, env=env, timeout=args.duration + 300)
|
||||
except BaseException:
|
||||
subprocess.run(['docker', 'rm', '-f', name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=20)
|
||||
raise
|
||||
summary = json.loads((task / 'summary.json').read_text()) if (task / 'summary.json').exists() else None
|
||||
write(output / 'result.json', {'exitCode': result.returncode, 'containerElapsedSeconds': time.monotonic() - started,
|
||||
'summary': summary, 'applicationCertificates': certs(task)})
|
||||
if result.returncode or not summary or (args.full and not summary['fullTwoHourLifecycle']):
|
||||
raise RuntimeError('Lifecycle run failed; inspect task/failure.txt, client.log and server.log: ' + str(output))
|
||||
print(json.dumps({'output': str(output), 'summary': summary}, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,7 +0,0 @@
|
||||
__pycache__/
|
||||
bin/
|
||||
obj/
|
||||
private-fixture/
|
||||
*.pfx
|
||||
*.der
|
||||
*.nettrace
|
||||
@@ -1,26 +0,0 @@
|
||||
// Read-only managed metadata extraction; does not load or execute product assemblies.
|
||||
using System.Reflection.Metadata;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using System.Text.Json;
|
||||
var root = Path.GetFullPath(args[0]);
|
||||
var modules = new List<object>();
|
||||
foreach (var directory in args.Skip(1))
|
||||
{
|
||||
foreach (var path in Directory.EnumerateFiles(directory, "*.dll", SearchOption.AllDirectories).Order())
|
||||
{
|
||||
try
|
||||
{
|
||||
using var file = File.OpenRead(path);
|
||||
using var pe = new PEReader(file);
|
||||
if (!pe.HasMetadata) continue;
|
||||
var metadata = pe.GetMetadataReader();
|
||||
if (!metadata.IsAssembly) continue;
|
||||
var assembly = metadata.GetAssemblyDefinition();
|
||||
modules.Add(new { file = Path.GetRelativePath(root, path).Replace(Path.DirectorySeparatorChar, '/'),
|
||||
assembly = metadata.GetString(assembly.Name), version = assembly.Version.ToString(),
|
||||
mvid = metadata.GetGuid(metadata.GetModuleDefinition().Mvid).ToString("D") });
|
||||
}
|
||||
catch (BadImageFormatException) { /* Native DLLs have no CLR module identity. */ }
|
||||
}
|
||||
}
|
||||
Console.WriteLine(JsonSerializer.Serialize(modules));
|
||||
@@ -1,39 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text.Json;
|
||||
using Opc.Ua.Security.Certificates;
|
||||
var mode = args[0];
|
||||
var directory = args[1];
|
||||
Directory.CreateDirectory(directory);
|
||||
using var key = RSA.Create(2048);
|
||||
var request = new CertificateRequest("CN=NativeMemoryControl", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
var original = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
|
||||
var pfx = original.Export(X509ContentType.Pfx);
|
||||
// Certificate.From takes ownership of original; only wrapped disposes that native certificate.
|
||||
using var wrapped = Certificate.From(original);
|
||||
void Sample(int i, string kind)
|
||||
{
|
||||
var live = GC.GetTotalMemory(true);
|
||||
GC.WaitForPendingFinalizers();
|
||||
using var p = Process.GetCurrentProcess();
|
||||
p.Refresh();
|
||||
Console.WriteLine(JsonSerializer.Serialize(new { kind, i, pid = Environment.ProcessId, mode, live, rss = p.WorkingSet64, methods = JitInfo.GetCompiledMethodCount(), gc = GC.GetGCMemoryInfo().TotalCommittedBytes }));
|
||||
Console.Out.Flush();
|
||||
}
|
||||
Sample(0, "ready");
|
||||
while (!File.Exists(Path.Combine(directory, "go"))) Thread.Sleep(50);
|
||||
for (int i = 1; i <= 1000; i++)
|
||||
{
|
||||
using X509Certificate2 copy = mode switch
|
||||
{
|
||||
"pfx" => System.Security.Cryptography.X509Certificates.X509CertificateLoader.LoadPkcs12(pfx, null, X509KeyStorageFlags.Exportable),
|
||||
"sdk-copy" => wrapped.AsX509Certificate2(),
|
||||
"shared-copy" => (X509Certificate2)typeof(X509Certificate2).GetConstructor(new[] { typeof(X509Certificate) })!.Invoke(new object[] { original }),
|
||||
_ => System.Security.Cryptography.X509Certificates.X509CertificateLoader.LoadCertificate(original.RawData)
|
||||
};
|
||||
if (i % 100 == 0) Sample(i, "sample");
|
||||
}
|
||||
Sample(1000, "done");
|
||||
while (!File.Exists(Path.Combine(directory, "stop"))) Thread.Sleep(50);
|
||||
@@ -1,15 +0,0 @@
|
||||
// Read-only temporary diagnostic attachment. Requires Microsoft.Diagnostics.NETCore.Client from dotnet-trace.
|
||||
using System.Diagnostics.Tracing;
|
||||
using Microsoft.Diagnostics.NETCore.Client;
|
||||
|
||||
var seconds = int.Parse(args[1]);
|
||||
if (seconds < 1 || seconds > 300) throw new ArgumentOutOfRangeException(nameof(seconds), "Use a bounded 1..300 second diagnostic attachment");
|
||||
var client = new DiagnosticsClient(int.Parse(args[0]));
|
||||
var providers = new[] { new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Verbose, 0x18) };
|
||||
// Match the recorded experiment's API default (256 MiB capacity, not measured resident allocation).
|
||||
using var session = client.StartEventPipeSession(providers, requestRundown: false, circularBufferMB: 256);
|
||||
await using var file = File.Create(args[2]);
|
||||
var copy = session.EventStream.CopyToAsync(file);
|
||||
await Task.Delay(TimeSpan.FromSeconds(seconds));
|
||||
session.Stop();
|
||||
await copy;
|
||||
@@ -1,208 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using Opc.Ua.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
|
||||
var root = Path.GetFullPath(args[0]);
|
||||
Directory.CreateDirectory(root);
|
||||
var seconds = int.Parse(args[1]);
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(seconds + 120));
|
||||
var ct = timeout.Token;
|
||||
var clientPki = Path.Combine(root, "client-pki");
|
||||
var telemetry = DefaultTelemetry.Create(_ => { });
|
||||
var config = new ApplicationConfiguration(telemetry);
|
||||
config.ApplicationName = "NativeLifecycleControl";
|
||||
config.ApplicationUri = "urn:NativeLifecycleControl:" + Guid.NewGuid();
|
||||
config.ApplicationType = ApplicationType.Client;
|
||||
config.SecurityConfiguration = new SecurityConfiguration
|
||||
{
|
||||
ApplicationCertificate = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(clientPki, "own"), SubjectName = "CN=NativeLifecycleControl" },
|
||||
TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(clientPki, "issuer") },
|
||||
TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(clientPki, "trusted") },
|
||||
RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(clientPki, "rejected") },
|
||||
AutoAcceptUntrustedCertificates = true
|
||||
};
|
||||
config.ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 15000 };
|
||||
var app = new ApplicationInstance(telemetry) { ApplicationConfiguration = config };
|
||||
await config.ValidateAsync(ApplicationType.Client, ct);
|
||||
await app.CheckApplicationInstanceCertificatesAsync(true, null, ct);
|
||||
int port;
|
||||
using (var listener = new TcpListener(IPAddress.Loopback, 0))
|
||||
{
|
||||
listener.Start();
|
||||
port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
}
|
||||
var options = new OpcUaApplicationHostOptions { ApplicationName = "NativeLifecycleServer", ApplicationUri = "urn:NativeLifecycleServer:" + Guid.NewGuid(), OpcUaPort = port, PublicHostname = "127.0.0.1", PkiStoreRoot = Path.Combine(root, "server-pki"), AllowAnonymous = false, AutoAcceptUntrustedClientCertificates = true, EnabledSecurityProfiles = [OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt] };
|
||||
OpcUaApplicationHost host = null!;
|
||||
OtOpcUaSdkServer server = null!;
|
||||
async Task Boot()
|
||||
{
|
||||
server = new OtOpcUaSdkServer();
|
||||
host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance, new Auth());
|
||||
await host.StartAsync(server, ct);
|
||||
var n = server.NodeManager!;
|
||||
n.NodeWriteGateway = new Gateway();
|
||||
n.EnsureFolder("Control", null, "Control", AddressSpaceRealm.Raw);
|
||||
n.EnsureFolder("Equipment", null, "Equipment", AddressSpaceRealm.Uns);
|
||||
n.EnsureVariable("Control/Scalar", "Control", "Scalar", "Int32", true, AddressSpaceRealm.Raw);
|
||||
n.EnsureVariable("Control/Array", "Control", "Array", "Int16", true, AddressSpaceRealm.Raw, isArray: true, arrayLength: 4);
|
||||
n.EnsureVariable("Equipment/Scalar", "Equipment", "Scalar", "Int32", true, AddressSpaceRealm.Uns);
|
||||
for (var i = 0; i < 16; i++) n.EnsureVariable("Control/Browse" + i, "Control", "Browse" + i, "Int32", false, AddressSpaceRealm.Raw);
|
||||
n.MaterialiseAlarmCondition("Control/Alarm", "Control", "Alarm", "OffNormalAlarm", 500, AddressSpaceRealm.Raw, isNative: true);
|
||||
n.WriteValue("Control/Scalar", 0, 0, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
n.WriteValue("Control/Array", new short[] { 0, 1, 2, 3 }, 0, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
n.WriteValue("Equipment/Scalar", 0, 0, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
}
|
||||
async Task<ISession> Connect()
|
||||
{
|
||||
var t = DefaultTelemetry.Create(_ => { });
|
||||
var endpoint = await CoreClientUtils.SelectEndpointAsync(config, $"opc.tcp://127.0.0.1:{port}/OtOpcUa", true, t, ct);
|
||||
var factory = new DefaultSessionFactory(t);
|
||||
var identity = new UserIdentity("control", Encoding.UTF8.GetBytes(FixtureCredential.Value));
|
||||
return await factory.CreateAsync(config, new ConfiguredEndpoint(null, endpoint ?? throw new Exception("Missing endpoint"), EndpointConfiguration.Create(config)), false, false, "NativeLifecycleControl", 15000, identity, default, ct);
|
||||
}
|
||||
static DataValue Value(object v)
|
||||
{
|
||||
#if SDK2
|
||||
if (!VariantHelper.TryCastFrom(v, out var variant)) throw new Exception("Unsupported control value");
|
||||
return new DataValue(variant);
|
||||
#else
|
||||
return new DataValue(new Variant(v));
|
||||
#endif
|
||||
}
|
||||
static object? Clr(DataValue v)
|
||||
{
|
||||
#if SDK2
|
||||
return v.WrappedValue.AsBoxedObject(Variant.BoxingBehavior.Legacy);
|
||||
#else
|
||||
return v.Value;
|
||||
#endif
|
||||
}
|
||||
object? CertificateStats()
|
||||
{
|
||||
#if SDK2
|
||||
return new { created = Opc.Ua.Security.Certificates.Certificate.InstancesCreated, disposed = Opc.Ua.Security.Certificates.Certificate.InstancesDisposed, outstanding = Opc.Ua.Security.Certificates.Certificate.InstancesLeaked };
|
||||
#else
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
void Sample(int cycle, string kind)
|
||||
{
|
||||
var live = GC.GetTotalMemory(true);
|
||||
using var p = Process.GetCurrentProcess();
|
||||
p.Refresh();
|
||||
var m = GC.GetGCMemoryInfo();
|
||||
Console.WriteLine(JsonSerializer.Serialize(new { kind, cycle, pid = Environment.ProcessId, live, rss = p.WorkingSet64, gcCommitted = m.TotalCommittedBytes, gcHeap = m.HeapSizeBytes, fragmented = m.FragmentedBytes, jit = JitInfo.GetCompiledMethodCount(), jitIl = JitInfo.GetCompiledILBytes(), sdk = typeof(NodeId).Assembly.GetName().Version?.ToString(), scope = "matched encrypted public lifecycle subset; no history/revocation/proxy", assemblies = AppDomain.CurrentDomain.GetAssemblies().Length, dynamicAssemblies = AppDomain.CurrentDomain.GetAssemblies().Count(a => a.IsDynamic), certificate = CertificateStats(), timers = Timer.ActiveCount, threads = p.Threads.Count, sessions = server.CurrentInstance.SessionManager.GetSessions().Count, subscriptions = server.CurrentInstance.SubscriptionManager.GetSubscriptions().Count }));
|
||||
Console.Out.Flush();
|
||||
}
|
||||
int cycles = 0, restarts = 0;
|
||||
await Boot();
|
||||
var watch = Stopwatch.StartNew();
|
||||
Sample(0, "start");
|
||||
try
|
||||
{
|
||||
while (watch.Elapsed.TotalSeconds < seconds)
|
||||
{
|
||||
using (var owner = await Connect()) using (var other = await Connect())
|
||||
{
|
||||
var ns = (ushort)owner.NamespaceUris.GetIndex("https://zb.com/otopcua/raw");
|
||||
var scalar = new NodeId("Control/Scalar", ns);
|
||||
var array = new NodeId("Control/Array", ns);
|
||||
using var subscription = new Subscription(owner.DefaultSubscription) { PublishingInterval = 100 };
|
||||
owner.AddSubscription(subscription);
|
||||
await subscription.CreateAsync(ct);
|
||||
subscription.AddItem(new MonitoredItem(subscription.DefaultItem) { StartNodeId = scalar, AttributeId = Attributes.Value, SamplingInterval = 50 });
|
||||
subscription.AddItem(new MonitoredItem(subscription.DefaultItem) { StartNodeId = array, AttributeId = Attributes.Value, SamplingInterval = 50 });
|
||||
await subscription.ApplyChangesAsync(ct);
|
||||
var response = await owner.WriteAsync(null, [new WriteValue { NodeId = scalar, AttributeId = Attributes.Value, Value = Value(++cycles) }, new WriteValue { NodeId = array, AttributeId = Attributes.Value, Value = Value(new short[] { 1, 2, 3, 4 }) }], ct);
|
||||
if (response.Results.ToList().Any(s => StatusCode.IsBad(s))) throw new Exception("Control write denied");
|
||||
if (!Equals(Clr(await owner.ReadValueAsync(scalar, ct)), cycles)) throw new Exception("Scalar changed");
|
||||
if (!((short[])Clr(await owner.ReadValueAsync(array, ct))!).SequenceEqual(new short[] { 1, 2, 3, 4 })) throw new Exception("Array changed");
|
||||
var browse = await owner.BrowseAsync(null, null, 1, [new BrowseDescription { NodeId = new NodeId("Control", ns), BrowseDirection = BrowseDirection.Forward, ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, IncludeSubtypes = true, ResultMask = (uint)BrowseResultMask.All }], ct);
|
||||
await Task.Delay(1000, ct);
|
||||
await subscription.DeleteAsync(true, ct);
|
||||
await owner.CloseAsync(ct);
|
||||
await other.CloseAsync(ct);
|
||||
}
|
||||
if (server.CurrentInstance.SessionManager.GetSessions().Count != 0 || server.CurrentInstance.SubscriptionManager.GetSubscriptions().Count != 0) throw new Exception("Control retained sessions/subscriptions");
|
||||
Sample(cycles, "sample");
|
||||
if (cycles % 4 == 0)
|
||||
{
|
||||
using var parked = await Connect();
|
||||
using var sub = new Subscription(parked.DefaultSubscription) { PublishingInterval = 100 };
|
||||
parked.AddSubscription(sub);
|
||||
await sub.CreateAsync(ct);
|
||||
sub.AddItem(new MonitoredItem(sub.DefaultItem) { StartNodeId = new NodeId("Control/Scalar", (ushort)parked.NamespaceUris.GetIndex("https://zb.com/otopcua/raw")), AttributeId = Attributes.Value, SamplingInterval = 50 });
|
||||
await sub.ApplyChangesAsync(ct);
|
||||
await host.DisposeAsync();
|
||||
await Boot();
|
||||
restarts++;
|
||||
}
|
||||
}
|
||||
Console.WriteLine(JsonSerializer.Serialize(new { kind = "complete", cycles, restarts, seconds = watch.Elapsed.TotalSeconds, fullGate = false }));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await host.DisposeAsync();
|
||||
#if SDK2
|
||||
await app.DisposeAsync();
|
||||
#endif
|
||||
}
|
||||
#if SDK2
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
var certificateType = typeof(Opc.Ua.Security.Certificates.Certificate);
|
||||
var report = new Dictionary<string, object>();
|
||||
var trackingEnabled = Environment.GetEnvironmentVariable("OPCUA_CERTIFICATE_LEAK_TRACKING") == "1";
|
||||
if (trackingEnabled)
|
||||
{
|
||||
foreach (var name in new[] { "EnumerateLiveCertificates", "EnumerateUnreachableUndisposedCertificates" })
|
||||
{
|
||||
var method = certificateType.GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic)
|
||||
?? throw new InvalidOperationException("SDK2 diagnostic method missing: " + name);
|
||||
var values = method.Invoke(null, null) as IEnumerable
|
||||
?? throw new InvalidOperationException("SDK2 diagnostic method return type changed: " + name);
|
||||
var stacks = new List<string>();
|
||||
foreach (var value in values)
|
||||
{
|
||||
var field = value!.GetType().GetField(name.Contains("Unreachable") ? "Item2" : "Item4")
|
||||
?? throw new InvalidOperationException("SDK2 diagnostic tuple field missing: " + name);
|
||||
if (field.FieldType != typeof(string))
|
||||
throw new InvalidOperationException("SDK2 diagnostic stack field type changed");
|
||||
// Deliberately omit tuple Item1 (certificate identity).
|
||||
stacks.Add((string)field.GetValue(value)!);
|
||||
}
|
||||
report[name] = new
|
||||
{
|
||||
count = stacks.Count,
|
||||
allocations = stacks.GroupBy(x => x).OrderByDescending(g => g.Count()).Take(15)
|
||||
.Select(g => new { count = g.Count(), stack = g.Key }).ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
Console.WriteLine(JsonSerializer.Serialize(new { kind = "certificate-final-diagnostics", trackingEnabled, stats = CertificateStats(), report }));
|
||||
#endif
|
||||
class Auth : IOpcUaUserAuthenticator
|
||||
{
|
||||
public Task<OpcUaUserAuthResult> AuthenticateUserNameAsync(string user, string password, CancellationToken ct) => Task.FromResult(user == "control" && password == FixtureCredential.Value ? OpcUaUserAuthResult.Allow("control", [OpcUaDataPlaneRoles.WriteOperate]) : OpcUaUserAuthResult.Deny("Control credential refused"));
|
||||
}
|
||||
class Gateway : IOpcUaNodeWriteGateway
|
||||
{
|
||||
public Task<NodeWriteOutcome> WriteAsync(string n, object? v, AddressSpaceRealm r, CancellationToken ct) => Task.FromResult(new NodeWriteOutcome(true, null));
|
||||
}
|
||||
|
||||
static class FixtureCredential
|
||||
{
|
||||
internal static string Value { get; } = Environment.GetEnvironmentVariable("OPCUA_MEMORY_PASSWORD") ?? throw new InvalidOperationException("Set OPCUA_MEMORY_PASSWORD for the task-owned fixture.");
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
# SDK 2 lifecycle memory investigation
|
||||
|
||||
The required two-hour server lifecycle gate failed on macOS and Linux. The RSS
|
||||
limit remains **cycle-3 baseline + 128 MiB**; retained managed memory remains
|
||||
+64 MiB and descriptors +32. No threshold, production code, certificate provider,
|
||||
or session factory was changed by this investigation. These controls do not
|
||||
replace the full gate. Issue #799 remains open.
|
||||
|
||||
## Preserved full-workload failures
|
||||
|
||||
| Run | Cycles / restarts | Observation time | RSS / limit (bytes) | Outcome |
|
||||
|---|---:|---:|---:|---|
|
||||
| macOS `fa823378` | 250 / 62 | 1045.53 s | 365,215,744 / 363,331,584 | Failed |
|
||||
| macOS `e135b7c9` profile reproduction | 245 / 61 | 1024.14 s | 368,656,384 / 363,053,056 | Failed |
|
||||
| Linux full harness, recorded build `38098ce142ab8c382825aa754e0758071dd3a46b` | 248 / 61 | 903.508 s | 353,931,264 / 351,850,496 | Failed |
|
||||
| macOS `6b2d4b81` bounded profile | 432 / 108 | 1807.479 s | First rejection: 364,199,936 / 363,266,048 | Failed; first assertion rethrown |
|
||||
|
||||
The last macOS row requested 1,800 seconds. It first rejected RSS at cycle 253 /
|
||||
1055.482 seconds, continued recording explicitly rejected observations, then
|
||||
threw the original assertion before any completion record. Its final observation
|
||||
was RSS 380,059,648, live managed 41,364,096, GC committed 37,060,608, JIT
|
||||
compilations 81,535, descriptors 200, and zero sessions, subscriptions, monitored
|
||||
items, browse/history continuation points, and PKI descriptors. SDK cached stores
|
||||
remained two during operation. The test runner returned exit 1; the nonzero-execution guard passed (exit 0),
|
||||
with one executed test and zero skipped tests. There is no passing two-hour result here.
|
||||
|
||||
The first macOS run's reported PKI descriptor zero is invalid: `/var` and
|
||||
`/private/var` aliases were not canonicalized. Corrected positive-handle and timeout
|
||||
controls passed before subsequent profiles. Global descriptor counts and the RSS
|
||||
failure itself were independent of that defect. A Shouldly source-expression
|
||||
message incorrectly names `Descriptors` in the RSS failure; the actual numeric
|
||||
comparison is RSS, as retained before the assertion.
|
||||
|
||||
## What the controls demonstrate
|
||||
|
||||
`LifecycleControl.cs` uses unchanged published product DLLs and public APIs for
|
||||
both SDK generations. It opens two real encrypted **opc.tcp** sessions with
|
||||
Basic256Sha256 / SignAndEncrypt, resolves namespace URIs, reads/writes Int32 and
|
||||
Int16[] values, creates/deletes two data monitored items, abandons a browse
|
||||
continuation, closes sessions, and restarts a server with an active subscription
|
||||
every four cycles against the same task-owned PKI. Public session/subscription
|
||||
counts must return to zero. It has no historian, revocation, surgical-rebuild,
|
||||
paused-network, or private continuation-store coverage. It is a diagnostic subset,
|
||||
not TLS and not the full soak workload.
|
||||
|
||||
The original DLLs are the immutable 1.5.378.182 baseline publication associated
|
||||
with `83ed880e`; its assembly version alone is 1.5.378.0 and does not identify the
|
||||
patch. The candidate uses SDK 2.0.1 and the server implementation from `fa823378`.
|
||||
Neither baseline nor candidate product code was rebuilt for these controls.
|
||||
The controls were compiled once for each reference set, then those exact control
|
||||
artifacts were used on both operating systems. Selected DLL and input-artifact
|
||||
hashes are in `evidence.json`. Neither reference set's principal SDK/server DLLs
|
||||
has a managed native/ReadyToRun header.
|
||||
|
||||
| Ten-minute matched subset | Cycles / restarts | Final RSS | Change from cycle 3 | Final managed / change | Final JIT compilations |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| macOS original | 212 / 53 | 330,301,440 | +121,683,968 | 271,168,424 / +169,990,456 | 28,066 |
|
||||
| macOS SDK2 | 200 / 50 | 302,170,112 | +95,862,784 | 44,569,584 / +295,120 | 62,608 |
|
||||
| Linux original | 249 / 62 | 384,950,272 | +176,836,608 | 147,139,568 / +99,356,752 | 28,321 |
|
||||
| Linux SDK2 | 259 / 64 | 390,844,416 | +194,183,168 | 19,561,120 / +736 | 71,529 |
|
||||
|
||||
Both ten-minute controls exited zero because their lifecycle checks passed;
|
||||
`fullGate=false` is explicit. They do **not** enforce the full harness memory
|
||||
assertions. In particular, both Linux subset RSS deltas exceed 128 MiB and the
|
||||
original retains managed memory/timers. These observations do not waive either
|
||||
failed full gate. They are not proof of an SDK1 defect or an SDK2 improvement.
|
||||
Both control branches explicitly delete subscriptions, close normal sessions,
|
||||
and dispose sessions/subscriptions. Parked resources intentionally encounter
|
||||
server shutdown and are then disposed. Each version calls its unchanged product
|
||||
host's DisposeAsync. The baseline host only calls ApplicationInstance.StopAsync;
|
||||
that original ApplicationInstance exposes no disposal interface. The SDK2 branch
|
||||
also uses its new ApplicationInstance.DisposeAsync ownership API. Per-connection
|
||||
DefaultTelemetry factories are left to the same ownership pattern on both
|
||||
branches, separately probed above. Thus measured original growth belongs to this
|
||||
published-product/fixture lifecycle combination; its allocation owner has not
|
||||
been isolated. These are one matched run per platform, concurrent processes,
|
||||
with different runtime versions; they are not performance distributions.
|
||||
|
||||
The thirty-minute Linux comparison uses the same immutable artifacts and image,
|
||||
fresh independent fixtures, and SDK2 certificate allocation tracking enabled
|
||||
before the first certificate (the same environment is passed to both versions).
|
||||
Both finished with exit zero and `fullGate=false`:
|
||||
|
||||
| Thirty-minute Linux subset | Cycles / restarts | Final RSS | Final live managed | GC committed | JIT compilations | Active timers |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| Original | 736 / 184 | 540,057,600 | 342,626,200 | 408,399,872 | 29,177 | 926 |
|
||||
| SDK2 | 768 / 192 | 334,180,352 | 19,248,984 | 42,983,424 | 72,087 | 19 |
|
||||
|
||||
Observed runtime was 1802.542 seconds original / 1804.523 seconds SDK2. The SDK2
|
||||
RSS fell from 390,782,976 at the 632.7-second checkpoint to 334,180,352 at the last
|
||||
observation; live managed stayed near 20 MB. After disposal and final collection,
|
||||
SDK2 wrapper counters were 19,272 created = 19,272 disposed, with zero live or
|
||||
unreachable-undisposed wrappers under enabled tracking. These finite results
|
||||
support post-transition saturation of this subset, not a two-hour full-workload
|
||||
pass or a guarantee of indefinite stability.
|
||||
|
||||
### Native and runtime observations
|
||||
|
||||
- During the macOS bounded profile, `Security::AppleDatabase` counts increased
|
||||
from 278 at cycle 52 / 211 seconds to 1,953 at cycle 370 / 1546 seconds. These
|
||||
native objects continued accumulating while managed memory stayed near 40 MiB.
|
||||
- The single-owner certificate probe makes a self-signed RSA fixture in memory,
|
||||
then disposes each of 1,000 copies/imports. On macOS, public-only and
|
||||
shared-handle copies leave AppleDatabase at 2; PFX imports and SDK
|
||||
`Certificate.AsX509Certificate2` private-key copies grow it from 2 to 1,002.
|
||||
A separate PFX process retained that count through a 180-second idle hold.
|
||||
`Certificate.From(original)` owns the original; the checked-in control has no
|
||||
second owner. All four corrected single-owner modes were rerun successfully.
|
||||
- The same isolated Linux probe ended at approximately 49 MB RSS for public or
|
||||
shared copies, 61 MB for PFX imports, and 69 MB for the SDK copy. Those are
|
||||
endpoint RSS observations including different JIT/GC costs, not evidence of a
|
||||
Linux native-object leak or a complete attribution.
|
||||
- SDK2 public certificate counters after the Linux ten-minute lifecycle run were
|
||||
6,495 created = 6,495 disposed. A separate stack-tracked macOS lifecycle run
|
||||
ended 591 = 591, zero live and zero unreachable-undisposed wrappers. Allocation
|
||||
tracking is opt-in; empty internal reports without that opt-in are not proof.
|
||||
- The Linux ten-minute native snapshots show SDK2 JIT `doublemapper` resident
|
||||
mappings increasing from about 65.4 to 106.7 MiB, anonymous writable mappings
|
||||
from 84.6 to 121.3 MiB, and `[heap]` around 19.2 MiB. Original JIT mappings were
|
||||
about 33.3 to 37.7 MiB. Mapping totals are descriptive categories, not exclusive
|
||||
object allocations: anonymous memory also contains GC regions, stacks, and
|
||||
native allocator arenas. RSS includes file/shared pages and can fall with
|
||||
reclamation. JIT counters are cumulative compilations, not unique live methods.
|
||||
- 10,000 disposed versus undisposed `DefaultTelemetry` factories ended at similar
|
||||
RSS (~63.4 MB) and live managed memory (~0.43 MB), with no timer growth. This
|
||||
does not justify changing host telemetry ownership to fix this failure.
|
||||
|
||||
Exact SDK tag source shows private-key PFX copying in nonvirtual
|
||||
`Certificate.AsX509Certificate2`. A 60-second macOS sampled-thread-time trace
|
||||
instead found the dominant sampled import path through
|
||||
`CertificateIdentifierResolver.LoadPrivateKeyCoreAsync` →
|
||||
`DirectoryCertificateStore.LoadPrivateKeyAsync` → `X509CertificateLoader.LoadPkcs12`.
|
||||
Inclusive sampled thread-time was about 4.79 seconds for store load and 2.99 seconds
|
||||
for PKCS12, versus 8.5 milliseconds for AsX509. These are neither exclusive CPU
|
||||
costs nor call counts. ApplicationInstance already normalizes its validation
|
||||
input to public DER. The validation copy delegate is internal, the manager sealed,
|
||||
and no safe narrow application copy-policy seam was identified. Changing session
|
||||
factory, certificate exportability, or key storage would alter behavior and was
|
||||
not used as a workaround.
|
||||
|
||||
A major JIT/RSS step appeared near 60–64 restarts in several workloads. This is a
|
||||
hypothesis about runtime/code-cache warmup, not a finding that all RSS growth is
|
||||
benign. A separate read-only EventPipe attachment observed the thirty-minute
|
||||
Linux candidate around that transition using `Microsoft-Windows-DotNETRuntime`,
|
||||
Verbose, keywords `0x18` (Loader + JIT), no rundown, 180 seconds. The transition was UTC 18:51:02.049–18:54:02.062. Factory JIT events
|
||||
occurred UTC 18:52:48.249–18:53:21.434: 6,690 distinct method tokens from one
|
||||
module, each once. The module is Opc.Ua.Core.Types.dll; metadata declares 8,442
|
||||
static methods on OpcUaExtensions. The observed set is a subset, so this does
|
||||
not prove every possible factory or the complete canonical workload is warmed.
|
||||
Method-load events totalled 51,327,536 native code bytes, including 51,161,724
|
||||
OptimizedTier1 bytes and 49,141,484 bytes in generated factories. Only 50
|
||||
reflection InvokeStub JIT starts occurred; the transition trace reports zero lost events. MethodSize sums count loaded code
|
||||
versions, not resident/PSS allocation bytes. An additional 180-second attachment
|
||||
after the transition checks continuing factory compilation. The second attachment (UTC 19:01:44.116–19:04:44.127) recorded 85 JIT starts,
|
||||
zero lost events, zero Opc.Ua starts, zero generated-factory starts, and zero generated-factory
|
||||
loaded bytes. Only 18 ordinary Tier1 method loads (5,720 bytes) and 20
|
||||
instrumented Tier1 loads (5,800 bytes) occurred. This supports a finite generated
|
||||
factory burst in the observed subset; it does not prove complete canonical
|
||||
workload saturation. The capture API's default circular-buffer capacity was 256 MiB; that is a
|
||||
capacity setting, not measured resident overhead. Attachments can allocate event
|
||||
buffers and perturb execution. Both prior full failures and the ten-minute
|
||||
control were uninstrumented by these JIT attachments, and the thirty-minute final
|
||||
interval is unattached. No captured native-code byte total is claimed to equal an
|
||||
RSS delta. Raw traces/heaps are not committed.
|
||||
|
||||
## Reproduce the diagnostic subset
|
||||
|
||||
Requires Python 3 and .NET 10. `run.py` references existing DLL directories,
|
||||
compiles only the control, records SHA-256 provenance, and creates a fresh local
|
||||
artifact directory. First reference directory wins duplicate filenames. Use the
|
||||
actual baseline publications, rather than restoring a different package version.
|
||||
No production project or package file is changed.
|
||||
|
||||
```bash
|
||||
python3 scripts/validation/opcua-lifecycle-memory/run.py lifecycle \
|
||||
--references "$baselineHostDlls" --references "$baselineClientDlls" \
|
||||
--output /tmp/opcua-memory-original --seconds 600
|
||||
python3 scripts/validation/opcua-lifecycle-memory/run.py lifecycle --sdk2 \
|
||||
--references "$candidateDlls" --output /tmp/opcua-memory-sdk2 --seconds 600 \
|
||||
--certificate-tracking
|
||||
```
|
||||
|
||||
Fixture credentials are generated in the runner environment and not logged.
|
||||
PKI material stays under each local `private-fixture` directory. Do not publish
|
||||
those directories or unredacted process/native logs. These controls grant a
|
||||
fixture user WriteOperate and auto-accept task-owned certificates; they are
|
||||
local test fixtures, not deployment configurations.
|
||||
|
||||
For a matched Linux pair, first build both with `--build-only` into fresh
|
||||
`/tmp/opcua-memory-original` and `/tmp/opcua-memory-sdk2` output directories, then:
|
||||
|
||||
```bash
|
||||
python3 scripts/validation/opcua-lifecycle-memory/linux_pair.py \
|
||||
--baseline /tmp/opcua-memory-original/bin/Release/net10.0 \
|
||||
--candidate /tmp/opcua-memory-sdk2/bin/Release/net10.0 \
|
||||
--output /tmp/opcua-memory-linux-pair --seconds 1800 \
|
||||
--image sha256:b04611ee9e1b7d36797012ed921962c57189cb4dbb3dff12afb8e158110f293b
|
||||
```
|
||||
|
||||
The runner uses separate task-owned `--network none` containers, fresh PKI,
|
||||
read-only executable mounts, bounded shutdown, and periodic read-only `/proc`
|
||||
snapshots. It records child and container exit statuses independently. The image
|
||||
is Ubuntu 24.04 arm64, .NET SDK 10.0.103 / runtime 10.0.3. The Mac runs used
|
||||
macOS 26.5.2 arm64, SDK 10.0.403 / runtime 10.0.14. Runtime and OS differences are
|
||||
explicit; do not attribute all cross-platform differences to SDK code.
|
||||
|
||||
On macOS, run each certificate mode with a fresh output directory:
|
||||
|
||||
```bash
|
||||
python3 scripts/validation/opcua-lifecycle-memory/run.py certificate --sdk2 \
|
||||
--references "$candidateDlls" --output /tmp/opcua-memory-pfx --mode pfx
|
||||
```
|
||||
|
||||
Other modes are `sdk-copy`, `public`, and `shared-copy`. The last intentionally
|
||||
uses the old copy constructor by reflection as a diagnostic control. It is not
|
||||
proposed production code. The control exports no fixture key/certificate data.
|
||||
`TelemetryControl.cs` is available through `run.py telemetry --sdk2` with modes
|
||||
`dispose` or `undisposed`. `summarize.py` emits Linux mapping categories without
|
||||
file paths from local `*-smaps.txt` files. All controls are opt-in and outside the
|
||||
ordinary solution/test graph.
|
||||
|
||||
Optional trace reproduction uses the installed dotnet-trace 9.0.661903 managed
|
||||
libraries, without adding a product dependency:
|
||||
|
||||
```bash
|
||||
python3 scripts/validation/opcua-lifecycle-memory/build_trace_tools.py \
|
||||
--libraries "$diagnosticToolLibraries" --output /tmp/opcua-memory-trace-tools
|
||||
# Run capture in the same process namespace / user as the task-owned target.
|
||||
dotnet /tmp/opcua-memory-trace-tools/capture/bin/Release/net10.0/capture.dll \
|
||||
"$targetPid" 180 /tmp/opcua-memory-local.nettrace
|
||||
dotnet /tmp/opcua-memory-trace-tools/summary/bin/Release/net10.0/summary.dll \
|
||||
/tmp/opcua-memory-local.nettrace > /tmp/opcua-memory-method-summary.json
|
||||
```
|
||||
|
||||
Capture is read-only and opt-in; raw traces remain local. The summary exports
|
||||
static method names and aggregate counts rather than certificate identities,
|
||||
arguments, or process mappings. The original transition parser also retained
|
||||
method tokens/modules locally to cross-check name-key uniqueness.
|
||||
|
||||
## Prospective saturation experiment (not executed)
|
||||
|
||||
The observed finite factory burst supports a separately labeled readiness
|
||||
experiment; it does not authorize moving the acceptance baseline. A reviewed
|
||||
candidate proposal is exactly 384 canonical lifecycle cycles / 96 every-four-cycle
|
||||
restarts, then one fixed settling cycle, all within 1,800 seconds. No early finish,
|
||||
extension, or RSS-selected endpoint is permitted. Both fixed readiness windows
|
||||
321–352 and 353–384 must contain zero generated-factory JIT/method-load events, zero SDK assembly
|
||||
loads, and zero lost events. Other JIT/native-code totals remain review evidence,
|
||||
not a claim that every runtime path is warmed. Trace capture is bounded and identical for both versions, then
|
||||
detached/drained before the settling cycle. Ownership, FD, PKI, store, and logical
|
||||
resource checks remain active throughout; original cold budget rejections remain
|
||||
recorded. Any later full gate requires at least 7,200 additional seconds after a
|
||||
prospectively approved baseline. No such warmed full gate has been run here.
|
||||
|
||||
The count is beyond the observed 60–64-restart transition and projects to about
|
||||
1,606 seconds from the Mac canonical run, but completion within the fixed cutoff
|
||||
is not guaranteed under load. Failure to complete or satisfy readiness is a failed
|
||||
experiment, not permission to extend. The full canonical test currently depends
|
||||
on SDK2 value APIs and read-only private SDK2 continuation/store instrumentation.
|
||||
An original-SDK adapter proving identical history, revocation, surgical-edit,
|
||||
and stalled-network operation counts is a prerequisite for a matched canonical
|
||||
experiment; the public subset does not provide that proof. Continued Mac native
|
||||
inventory growth remains an independent unresolved gate, even if JIT saturates.
|
||||
|
||||
## Evidence and limits
|
||||
|
||||
`evidence.json` contains sanitized numeric endpoints, native inventory counts,
|
||||
and hashes of retained diagnostic inputs. Full artifacts remain in the originating
|
||||
worktrees or `/tmp/opcua2-rss-investigation`; no PKI, certificate identifiers,
|
||||
credentials, raw traces, unredacted heap/process dumps, or complete test logs are
|
||||
checked in. Existing failures were preserved untouched.
|
||||
|
||||
Supported progress is a reproduced RSS failure, bounded logical/managed
|
||||
inventories in SDK2, demonstrated macOS PFX native retention, and measured
|
||||
cross-platform JIT/anonymous-memory growth. A complete allocation attribution and
|
||||
safe production correction remain unresolved. The server two-hour gate remains
|
||||
failed; a separate long-running client-only process is a different scope.
|
||||
|
||||
Package verification: baseline and candidate control builds, both optional trace
|
||||
tools, and certificate/telemetry controls built with zero warnings/errors. Local
|
||||
control smoke runs and the fresh Linux pair smoke exited zero. Python compilation
|
||||
and whitespace checks passed. One reused-output smoke failed because a manual
|
||||
incremental build used a different `/tmp` path alias and its runtimeconfig was
|
||||
missing; fresh canonical outputs passed, and the Linux runner now rejects an
|
||||
incomplete output directory before launch. These smoke checks validate tooling
|
||||
only and do not substitute for either long run.
|
||||
@@ -1,120 +0,0 @@
|
||||
# Candidate saturation experiment — review before launch
|
||||
|
||||
This is a prospective diagnostic protocol, not an acceptance run or a replacement
|
||||
for the failed cold server soak. Production code, the cycle-3 resource baseline,
|
||||
and the +64 MiB managed / +128 MiB RSS / +32 descriptor caps remain unchanged.
|
||||
Only `OTOPCUA_SDK2_SOAK_MODE=saturation` selects this behavior; the existing
|
||||
`diagnostic`, `profile`, and `full` modes keep their existing stopping rules.
|
||||
|
||||
The fixed canonical phase performs exactly 384 lifecycle cycles, with the
|
||||
existing active-resource restart after every fourth cycle: 96 completed restarts.
|
||||
It then performs exactly one settling cycle, without another restart. The phase,
|
||||
including observer handshakes, must finish within 1,800 seconds. Initial fixture
|
||||
startup has a separate 120-second bound. There is no early completion, extension,
|
||||
RSS-selected endpoint, budget reset, or two-hour/full-pass flag. Cleanup remains
|
||||
bounded independently of the phase deadline.
|
||||
|
||||
All canonical operations remain in use: encrypted opc.tcp sessions, namespace URI
|
||||
binding, scalar/typed-array read/write, subscriptions, browse abandonment, history
|
||||
continuation churn/release/cross-session/close, surgical changes/rebuilds, key
|
||||
revocation, paused network reads/recovery, and same-PKI server restart. All
|
||||
ownership, continuation, PKI-descriptor, store and node inventory checks remain
|
||||
active. Cold budget rejection is latched at its original observation and rethrown,
|
||||
even if diagnostic readiness later succeeds. A rejected readiness receipt does
|
||||
not become a passing test or a new resource baseline.
|
||||
|
||||
## Observer boundaries
|
||||
|
||||
A fresh task-owned control directory and UUID bind every request/receipt to this
|
||||
run and target PID. Files are published atomically; existing, malformed, foreign,
|
||||
or incomplete receipts fail closed. The observer is a separate diagnostic process
|
||||
using public `DiagnosticsClient` / EventPipe APIs. Product dependencies are
|
||||
unchanged. A separate short positive control first invokes a named non-SDK method
|
||||
with NoInlining/NoOptimization after attaching and requires its JIT-start count in
|
||||
the parsed receipt. This proves CLR provider delivery/classification independently
|
||||
of custom marker delivery. That probe is never invoked by the canonical experiment.
|
||||
|
||||
One continuous trace covers both fixed windows, including their boundary gap:
|
||||
|
||||
| Marker | Before/after operation | Completed restarts |
|
||||
|---|---|---:|
|
||||
| window1-start | Before cycle 321 | 80 |
|
||||
| window1-end | After cycle 352 and its restart | 88 |
|
||||
| window2-start | Before cycle 353 | 88 |
|
||||
| window2-end | After cycle 384 and its restart | 96 |
|
||||
|
||||
The target waits for the observer's active receipt and enabled marker provider
|
||||
before its first window operation. The parser requires exactly these four
|
||||
markers, the correct UUID and cycle/restart values, and strictly ordered
|
||||
timestamps. A continuous veto also covers events outside the marker ranges
|
||||
within the attached interval. Both windows and the entire attached interval
|
||||
require zero `Opc.Ua.OpcUaExtensions` JIT starts and method-load events, zero new
|
||||
`Opc.Ua.*` assembly-load events, and zero lost events. Other JIT counts and loaded
|
||||
native method-byte sums are reported for review; they do not establish that all
|
||||
runtime paths are warmed. Method-size sums do not equal resident memory.
|
||||
|
||||
Runtime capture is Verbose `Microsoft-Windows-DotNETRuntime`, keywords `0x18`
|
||||
(Loader + JIT), plus Informational `OtOpcUa-Sdk2-Saturation` markers, no rundown,
|
||||
and a prospectively fixed **16 MiB** circular-buffer capacity. This is a new
|
||||
experiment and does not reinterpret the earlier 256 MiB-capacity traces.
|
||||
|
||||
After restart 96, the target requests stop and waits. The observer must stop its
|
||||
session, drain the event stream, close it, parse the entire trace, and atomically
|
||||
publish a PID/run-bound drained receipt. Its Stop/copy/dispose/parse stages have
|
||||
10/10/5/10-second bounds. Target receipt waits are bounded at 45 seconds and remain
|
||||
inside the 1,800-second phase deadline. Observer faults propagate immediately.
|
||||
The target also verifies its marker provider is disabled before the settling
|
||||
cycle. Missing markers, no events, event loss, or malformed stream cannot prove
|
||||
readiness. A parser failure produces a failed observer receipt, not empty counts.
|
||||
|
||||
The `saturation-result` separates readiness, cold budget rejection, fixed operation
|
||||
completion and verified shutdown ownership. It always records `acceptance=false`.
|
||||
It is emitted before rethrowing the original cold assertion. The launcher retains
|
||||
nonzero test/container exits and records diagnostic readiness separately.
|
||||
|
||||
## Prepare immutable Linux artifacts
|
||||
|
||||
Commit the reviewed source first. Preparation refuses a dirty checkout and
|
||||
publishes fresh, separate app/observer artifacts for `linux-arm64`, self-contained
|
||||
.NET **10.0.14**, without trimming or single-file publishing. It never starts the
|
||||
experiment. The bundle records source revision, source hashes, exact publish
|
||||
command, fixed protocol, image digest and SHA-256 of every app/observer/tool file.
|
||||
It requires the landed HistorianGateway Client/Contracts 1.0.0 dependency graph
|
||||
and records assembly versions/module MVIDs by reading PE metadata without loading
|
||||
or executing product assemblies.
|
||||
|
||||
```bash
|
||||
python3 scripts/validation/opcua-lifecycle-memory/prepare_saturation.py \
|
||||
--output /tmp/opcua-saturation-reviewed \
|
||||
--libraries "$diagnosticToolLibraries" \
|
||||
--image sha256:b04611ee9e1b7d36797012ed921962c57189cb4dbb3dff12afb8e158110f293b
|
||||
```
|
||||
|
||||
`diagnosticToolLibraries` is the installed dotnet-trace 9.0.661903
|
||||
`tools/net8.0/any` directory. No product package reference is added. Artifact
|
||||
preparation and review are prerequisites; root and Astra must review the actual
|
||||
code, control tests and receipt before launch. The coordinator owns the long
|
||||
execution.
|
||||
|
||||
```bash
|
||||
python3 /tmp/opcua-saturation-reviewed/tools/launch_saturation.py \
|
||||
--bundle /tmp/opcua-saturation-reviewed --receipt-sha256 "$reviewedReceiptHash" \
|
||||
--output /tmp/opcua-saturation-run --verify-only
|
||||
# After root/Astra review, root runs the same command without --verify-only.
|
||||
```
|
||||
|
||||
The launcher rechecks every immutable file before and after the run. It creates a
|
||||
fresh offline task-owned container, writable output/PKI directories, and read-only
|
||||
app/observer/tool mounts. Descriptor controls run first in a separate process.
|
||||
TRX guards require two descriptor tests and one canonical test actually executed,
|
||||
with no skipped results. The launcher verifies the actual target and observer
|
||||
mapped their own self-contained `libcoreclr.so`; the older SDK-image controller
|
||||
runtime is not the target runtime. A 2,040-second outer timeout includes startup,
|
||||
phase and bounded cleanup; it does not extend the 1,800-second phase.
|
||||
|
||||
No long saturation run has been launched during implementation. Existing cold
|
||||
failures remain untouched. No original-SDK full-workload adapter, warmed
|
||||
acceptance baseline, additional two-hour server run, or production correction is
|
||||
part of this change. The separately demonstrated Mac native certificate-related
|
||||
inventory growth remains unresolved even if generated-factory compilation
|
||||
saturates.
|
||||
@@ -1,133 +0,0 @@
|
||||
// Diagnostic-only observer. The target remains the unchanged canonical lifecycle test process.
|
||||
using System.Diagnostics.Tracing;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Diagnostics.NETCore.Client;
|
||||
using Microsoft.Diagnostics.Tracing;
|
||||
|
||||
const string protocol = "sdk2-saturation-v1";
|
||||
const string markerProvider = "OtOpcUa-Sdk2-Saturation";
|
||||
var control = Path.GetFullPath(args[0]);
|
||||
var runId = args[1];
|
||||
if (!Guid.TryParseExact(runId, "D", out _)) throw new InvalidDataException("Malformed run identity");
|
||||
using var lifetime = new CancellationTokenSource(TimeSpan.FromSeconds(1980));
|
||||
var ct = lifetime.Token;
|
||||
try
|
||||
{
|
||||
var request = await WaitJson("attach-request.json", ct);
|
||||
using (request)
|
||||
{
|
||||
Identity(request.RootElement);
|
||||
Require(request.RootElement.GetProperty("firstCycle").GetInt32() == 321
|
||||
&& request.RootElement.GetProperty("completedRestarts").GetInt32() == 80, "Invalid attach boundary");
|
||||
var targetPid = request.RootElement.GetProperty("targetPid").GetInt32();
|
||||
Require(targetPid > 0 && targetPid != Environment.ProcessId, "Invalid target process");
|
||||
var providers = new[]
|
||||
{
|
||||
new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Verbose, 0x18),
|
||||
new EventPipeProvider(markerProvider, EventLevel.Informational)
|
||||
};
|
||||
var client = new DiagnosticsClient(targetPid);
|
||||
var session = await Task.Run(() => client.StartEventPipeSession(providers, requestRundown: false, circularBufferMB: 16), ct)
|
||||
.WaitAsync(TimeSpan.FromSeconds(10), ct);
|
||||
var trace = Path.Combine(control, "readiness.nettrace");
|
||||
await using (var output = new FileStream(trace, FileMode.CreateNew, FileAccess.Write, FileShare.Read))
|
||||
{
|
||||
var copy = session.EventStream.CopyToAsync(output, ct);
|
||||
await Publish("active.json", new { protocol, runId, targetPid, observerPid = Environment.ProcessId, active = true });
|
||||
using var stop = await WaitJson("detach-request.json", ct);
|
||||
Identity(stop.RootElement);
|
||||
Require(stop.RootElement.GetProperty("targetPid").GetInt32() == targetPid
|
||||
&& stop.RootElement.GetProperty("lastCycle").GetInt32() == 384
|
||||
&& stop.RootElement.GetProperty("completedRestarts").GetInt32() == 96, "Invalid detach boundary");
|
||||
await Task.Run(session.Stop, ct).WaitAsync(TimeSpan.FromSeconds(10), ct);
|
||||
await copy.WaitAsync(TimeSpan.FromSeconds(10), ct);
|
||||
await output.FlushAsync(ct);
|
||||
}
|
||||
await Task.Run(session.Dispose, ct).WaitAsync(TimeSpan.FromSeconds(5), ct);
|
||||
var summary = await Task.Run(() => Parse(trace, runId), ct).WaitAsync(TimeSpan.FromSeconds(10), ct);
|
||||
// Published only after Stop, stream drain, disposal and complete parsing. Never just a file-exists ACK.
|
||||
await Publish("drained.json", new
|
||||
{
|
||||
protocol, runId, targetPid, observerPid = Environment.ProcessId, active = false, drained = true,
|
||||
summary.markersValid, summary.eventsLost, summary.sdkFactoryJit, summary.sdkFactoryLoads,
|
||||
summary.sdkAssemblyLoads, summary.windows, summary.otherJit, summary.loadedNativeBytes, summary.runtimeControlJit,
|
||||
configuration = new { runtimeProvider = "Microsoft-Windows-DotNETRuntime", level = "Verbose", keywords = "0x18",
|
||||
markerProvider, circularBufferMiB = 16, requestRundown = false },
|
||||
acceptance = false
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
await Publish("observer-failed.json", new { protocol, runId, exceptionType = error.GetType().FullName });
|
||||
// An IPC/Stop worker can still be blocked. The parent also bounds and reaps this separate process.
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
void Identity(JsonElement value) => Require(value.GetProperty("protocol").GetString() == protocol
|
||||
&& value.GetProperty("runId").GetString() == runId, "Mismatched observer identity");
|
||||
static void Require(bool condition, string message) { if (!condition) throw new InvalidDataException(message); }
|
||||
async Task<JsonDocument> WaitJson(string name, CancellationToken token)
|
||||
{
|
||||
var path = Path.Combine(control, name);
|
||||
while (!File.Exists(path)) await Task.Delay(25, token);
|
||||
return JsonDocument.Parse(await File.ReadAllTextAsync(path, token));
|
||||
}
|
||||
async Task Publish(string name, object value)
|
||||
{
|
||||
var temporary = Path.Combine(control, name + ".tmp");
|
||||
await using (var stream = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write))
|
||||
await JsonSerializer.SerializeAsync(stream, value);
|
||||
File.Move(temporary, Path.Combine(control, name));
|
||||
}
|
||||
static TraceResult Parse(string path, string expectedRun)
|
||||
{
|
||||
var runtimeControlJit = 0;
|
||||
var markers = new List<Marker>();
|
||||
var events = new List<Observed>();
|
||||
using var source = new EventPipeEventSource(path);
|
||||
source.Clr.MethodJittingStarted += data =>
|
||||
{
|
||||
if (data.MethodNamespace == "ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.Sdk2LifecycleSoakTests"
|
||||
&& data.MethodName == "SaturationProtocolRuntimeProbe") runtimeControlJit++;
|
||||
events.Add(new(data.TimeStampRelativeMSec,
|
||||
data.MethodNamespace == "Opc.Ua.OpcUaExtensions" ? "factory-jit" : "other-jit", 0));
|
||||
};
|
||||
source.Clr.MethodLoadVerbose += data => events.Add(new(data.TimeStampRelativeMSec,
|
||||
data.MethodNamespace == "Opc.Ua.OpcUaExtensions" ? "factory-load" : "other-load", data.MethodSize));
|
||||
source.Clr.LoaderAssemblyLoad += data =>
|
||||
{
|
||||
if (data.FullyQualifiedAssemblyName.StartsWith("Opc.Ua.", StringComparison.Ordinal))
|
||||
events.Add(new(data.TimeStampRelativeMSec, "sdk-assembly", 0));
|
||||
};
|
||||
source.Dynamic.All += data =>
|
||||
{
|
||||
if (data.ProviderName == "OtOpcUa-Sdk2-Saturation" && data.EventName == "Boundary")
|
||||
markers.Add(new(data.TimeStampRelativeMSec, (string)data.PayloadByName("runId"),
|
||||
(string)data.PayloadByName("name"), (int)data.PayloadByName("cycle"), (int)data.PayloadByName("restarts")));
|
||||
};
|
||||
source.Process();
|
||||
(string Name, int Cycle, int Restarts)[] expected =
|
||||
[ ("window1-start", 321, 80), ("window1-end", 352, 88),
|
||||
("window2-start", 353, 88), ("window2-end", 384, 96) ];
|
||||
var valid = markers.Count == 4 && markers.Select((marker, index) =>
|
||||
marker.Run == expectedRun && (marker.Name, marker.Cycle, marker.Restarts) == expected[index]).All(x => x)
|
||||
&& markers.Zip(markers.Skip(1)).All(pair => pair.First.Ms < pair.Second.Ms);
|
||||
var windows = Enumerable.Range(0, 2).Select(index =>
|
||||
{
|
||||
var selected = valid ? events.Where(x => x.Ms >= markers[index * 2].Ms && x.Ms <= markers[index * 2 + 1].Ms).ToArray() : [];
|
||||
return new Window($"window{index + 1}", 321 + index * 32, 352 + index * 32, 80 + index * 8, 88 + index * 8,
|
||||
selected.Count(x => x.Kind == "factory-jit"), selected.Count(x => x.Kind == "factory-load"),
|
||||
selected.Count(x => x.Kind == "sdk-assembly"));
|
||||
}).ToArray();
|
||||
// Totals include the whole attached interval, including marker gaps, startup and stopping the collector.
|
||||
return new TraceResult(valid, source.EventsLost, events.Count(x => x.Kind == "factory-jit"),
|
||||
events.Count(x => x.Kind == "factory-load"), events.Count(x => x.Kind == "sdk-assembly"), windows,
|
||||
events.Count(x => x.Kind == "other-jit"), events.Sum(x => x.Bytes), runtimeControlJit);
|
||||
}
|
||||
record Marker(double Ms, string Run, string Name, int Cycle, int Restarts);
|
||||
record Observed(double Ms, string Kind, long Bytes);
|
||||
record Window(string name, int firstCycle, int lastCycle, int startRestarts, int endRestarts,
|
||||
int sdkFactoryJit, int sdkFactoryLoads, int sdkAssemblyLoads);
|
||||
record TraceResult(bool markersValid, int eventsLost, int sdkFactoryJit, int sdkFactoryLoads,
|
||||
int sdkAssemblyLoads, Window[] windows, int otherJit, long loadedNativeBytes, int runtimeControlJit);
|
||||
@@ -1,22 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Opc.Ua;
|
||||
var dispose = args[0] == "dispose";
|
||||
for (int i = 0; i <= 10000; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
var t = DefaultTelemetry.Create(_ => { });
|
||||
t.LoggerFactory.CreateLogger("TelemetryControl").LogDebug("control");
|
||||
if (dispose) ((IDisposable)t).Dispose();
|
||||
}
|
||||
if (i % 250 == 0)
|
||||
{
|
||||
var live = GC.GetTotalMemory(true);
|
||||
using var p = Process.GetCurrentProcess();
|
||||
p.Refresh();
|
||||
Console.WriteLine(JsonSerializer.Serialize(new { i, dispose, live, rss = p.WorkingSet64, methods = JitInfo.GetCompiledMethodCount(), il = JitInfo.GetCompiledILBytes(), gc = GC.GetGCMemoryInfo().TotalCommittedBytes, threads = p.Threads.Count, timers = Timer.ActiveCount }));
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// Redacted aggregate summary. Keep raw traces local.
|
||||
using Microsoft.Diagnostics.Tracing;
|
||||
using System.Text.Json;
|
||||
|
||||
var starts = new Dictionary<string, int>();
|
||||
var tiers = new Dictionary<string, int>();
|
||||
var nativeBytes = new Dictionary<string, long>();
|
||||
var factoryNames = new Dictionary<string, int>();
|
||||
var factoryTokens = new HashSet<int>();
|
||||
var factoryModules = new HashSet<long>();
|
||||
var firstFactoryMs = double.PositiveInfinity;
|
||||
var lastFactoryMs = 0d;
|
||||
var factoryNativeBytes = 0L;
|
||||
var sdkAssemblyLoads = 0;
|
||||
using var source = new EventPipeEventSource(args[0]);
|
||||
source.Clr.MethodJittingStarted += data =>
|
||||
{
|
||||
var name = data.MethodNamespace + "." + data.MethodName;
|
||||
starts[name] = starts.GetValueOrDefault(name) + 1;
|
||||
if (!name.StartsWith("Opc.Ua.OpcUaExtensions.", StringComparison.Ordinal)) return;
|
||||
factoryNames[name] = factoryNames.GetValueOrDefault(name) + 1;
|
||||
factoryTokens.Add(data.MethodToken);
|
||||
factoryModules.Add(data.ModuleID);
|
||||
firstFactoryMs = Math.Min(firstFactoryMs, data.TimeStampRelativeMSec);
|
||||
lastFactoryMs = Math.Max(lastFactoryMs, data.TimeStampRelativeMSec);
|
||||
};
|
||||
source.Clr.MethodLoadVerbose += data =>
|
||||
{
|
||||
var tier = data.OptimizationTier.ToString();
|
||||
tiers[tier] = tiers.GetValueOrDefault(tier) + 1;
|
||||
nativeBytes[tier] = nativeBytes.GetValueOrDefault(tier) + data.MethodSize;
|
||||
if (data.MethodNamespace == "Opc.Ua.OpcUaExtensions") factoryNativeBytes += data.MethodSize;
|
||||
};
|
||||
source.Clr.LoaderAssemblyLoad += data =>
|
||||
{
|
||||
if (data.FullyQualifiedAssemblyName.StartsWith("Opc.Ua.", StringComparison.Ordinal)) sdkAssemblyLoads++;
|
||||
};
|
||||
source.Process();
|
||||
Console.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
traceStartUtc = source.SessionStartTime.ToUniversalTime(),
|
||||
traceEndUtc = source.SessionEndTime.ToUniversalTime(),
|
||||
eventsLost = source.EventsLost,
|
||||
sdkAssemblyLoads,
|
||||
jittingStarts = starts.Values.Sum(),
|
||||
// Name keys omit generic signatures; token/module counts independently identify the factory set.
|
||||
distinctNameKeys = starts.Count,
|
||||
opcUaJittingStarts = starts.Where(x => x.Key.StartsWith("Opc.Ua.", StringComparison.Ordinal)).Sum(x => x.Value),
|
||||
invokeStubStarts = starts.Where(x => x.Key.StartsWith("dynamicClass.InvokeStub", StringComparison.Ordinal)).Sum(x => x.Value),
|
||||
tiers,
|
||||
summedLoadedNativeMethodBytes = nativeBytes,
|
||||
factory = new
|
||||
{
|
||||
starts = factoryNames.Values.Sum(), distinctNames = factoryNames.Count,
|
||||
distinctTokens = factoryTokens.Count, moduleCount = factoryModules.Count,
|
||||
everyNameOnce = factoryNames.Values.All(x => x == 1),
|
||||
firstMs = double.IsFinite(firstFactoryMs) ? (double?)firstFactoryMs : null,
|
||||
lastMs = factoryNames.Count == 0 ? (double?)null : lastFactoryMs,
|
||||
summedLoadedNativeMethodBytes = factoryNativeBytes,
|
||||
examples = factoryNames.Keys.Take(10).ToArray()
|
||||
}
|
||||
}, new JsonSerializerOptions { WriteIndented = true }));
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the opt-in saturation observer without changing product dependencies."""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def build(output, libraries, linux):
|
||||
output = output.resolve()
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
project = ET.Element("Project", Sdk="Microsoft.NET.Sdk")
|
||||
properties = ET.SubElement(project, "PropertyGroup")
|
||||
for key, value in {"OutputType": "Exe", "TargetFramework": "net10.0", "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")
|
||||
ET.SubElement(items, "Compile", Include=str(Path(__file__).resolve().parent / "SaturationObserver.cs"))
|
||||
for name in ["Microsoft.Diagnostics.NETCore.Client", "Microsoft.Diagnostics.Tracing.TraceEvent", "Microsoft.Diagnostics.FastSerialization"]:
|
||||
path = (libraries / (name + ".dll")).resolve(strict=True)
|
||||
reference = ET.SubElement(items, "Reference", Include=name)
|
||||
ET.SubElement(reference, "HintPath").text = str(path)
|
||||
project_path = output / "observer.csproj"
|
||||
ET.indent(project)
|
||||
ET.ElementTree(project).write(project_path, encoding="unicode")
|
||||
command = ["dotnet", "publish" if linux else "build", str(project_path), "-c", "Release", "-m:2", "--nologo"]
|
||||
if linux:
|
||||
command += ["-r", "linux-arm64", "--self-contained", "true", "-p:RuntimeFrameworkVersion=10.0.14",
|
||||
"-p:PublishSingleFile=false", "-p:PublishTrimmed=false", "-p:UseAppHost=true", "-o", str(output / "published")]
|
||||
with (output / "build.log").open("w") as log:
|
||||
result = subprocess.run(command, stdout=log, stderr=subprocess.STDOUT)
|
||||
(output / "build-command.json").write_text(json.dumps(command, indent=2) + "\n")
|
||||
if result.returncode:
|
||||
raise subprocess.CalledProcessError(result.returncode, command)
|
||||
return output / ("published" if linux else "bin/Release/net10.0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
parser.add_argument("--libraries", required=True, type=Path)
|
||||
parser.add_argument("--linux-self-contained", action="store_true")
|
||||
args = parser.parse_args()
|
||||
print(build(args.output, args.libraries, args.linux_self_contained))
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build optional local capture/summary tools using the user's installed dotnet-trace libraries."""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--libraries", required=True, type=Path, help="dotnet-trace tools/net8.0/any directory")
|
||||
parser.add_argument("--output", required=True, type=Path, help="Fresh local output directory")
|
||||
args = parser.parse_args()
|
||||
args.output = args.output.resolve()
|
||||
args.output.mkdir(parents=True, exist_ok=False)
|
||||
for name, source, references in [
|
||||
("capture", "EventPipeCapture.cs", ["Microsoft.Diagnostics.NETCore.Client"]),
|
||||
("summary", "TraceSummary.cs", ["Microsoft.Diagnostics.Tracing.TraceEvent", "Microsoft.Diagnostics.FastSerialization", "Microsoft.Diagnostics.NETCore.Client"])
|
||||
]:
|
||||
output = args.output / name
|
||||
output.mkdir()
|
||||
project = ET.Element("Project", Sdk="Microsoft.NET.Sdk")
|
||||
properties = ET.SubElement(project, "PropertyGroup")
|
||||
for key, value in {"OutputType": "Exe", "TargetFramework": "net10.0", "ImplicitUsings": "enable",
|
||||
"EnableDefaultCompileItems": "false"}.items():
|
||||
ET.SubElement(properties, key).text = value
|
||||
items = ET.SubElement(project, "ItemGroup")
|
||||
ET.SubElement(items, "FrameworkReference", Include="Microsoft.AspNetCore.App")
|
||||
ET.SubElement(items, "Compile", Include=str(Path(__file__).resolve().parent / source))
|
||||
for reference in references:
|
||||
dll = (args.libraries / (reference + ".dll")).resolve(strict=True)
|
||||
item = ET.SubElement(items, "Reference", Include=reference)
|
||||
ET.SubElement(item, "HintPath").text = str(dll)
|
||||
path = output / (name + ".csproj")
|
||||
ET.indent(project)
|
||||
ET.ElementTree(project).write(path, encoding="unicode")
|
||||
with (output / "build.log").open("w") as log:
|
||||
result = subprocess.run(["dotnet", "build", str(path), "-c", "Release", "-m:2"], stdout=log, stderr=subprocess.STDOUT)
|
||||
if result.returncode:
|
||||
raise SystemExit(result.returncode)
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Diagnostic only. The published xUnit apphost and observer are self-contained .NET 10.0.14.
|
||||
set -euo pipefail
|
||||
mkdir -p /task/tmp /task/data /task/results /task/control
|
||||
export TMPDIR=/task/tmp XDG_DATA_HOME=/task/data
|
||||
export OTOPCUA_SDK2_SOAK=1 OTOPCUA_SDK2_SOAK_MODE=saturation OTOPCUA_SDK2_SOAK_SECONDS=1800
|
||||
export OTOPCUA_SDK2_SOAK_OUTPUT=/task/resources.jsonl
|
||||
export OTOPCUA_SDK2_SATURATION_CONTROL=/task/control
|
||||
export OPCUA_CERTIFICATE_LEAK_TRACKING=1
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1
|
||||
[[ $(uname -s) == Linux && $(uname -m) == aarch64 && -d /proc/self/fd ]]
|
||||
dotnet --info > /task/controller-runtime.txt
|
||||
vstest_pid= observer_pid=
|
||||
cleanup() {
|
||||
for child in "$vstest_pid" "$observer_pid"; do
|
||||
if [[ -n "$child" ]] && kill -0 "$child" 2>/dev/null; then kill "$child" 2>/dev/null || true; fi
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
dotnet vstest /app/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.dll \
|
||||
--TestCaseFilter:'FullyQualifiedName~Sdk2LifecycleSoakTests.Descriptor_probe_' \
|
||||
--ResultsDirectory:/task/results '--Logger:trx;LogFileName=descriptor-controls.trx' > /task/descriptor-controls.log 2>&1
|
||||
/observer/observer /task/control "$OTOPCUA_SDK2_SATURATION_RUN_ID" > /task/observer.log 2>&1 &
|
||||
observer_pid=$!
|
||||
dotnet vstest /app/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.dll \
|
||||
--TestCaseFilter:'FullyQualifiedName=ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.Sdk2LifecycleSoakTests.Repeated_lifecycles_return_resources_to_a_steady_baseline' \
|
||||
--ResultsDirectory:/task/results '--Logger:trx;LogFileName=saturation.trx' > /task/test.log 2>&1 &
|
||||
vstest_pid=$!
|
||||
observed=0
|
||||
for attempt in $(seq 1 1200); do
|
||||
if [[ -s /task/resources.jsonl ]]; then
|
||||
target_pid=$(head -n 1 /task/resources.jsonl | sed -n 's/.*"processId":\([0-9]*\),.*/\1/p')
|
||||
[[ -n "$target_pid" ]]
|
||||
target_exe=$(readlink "/proc/$target_pid/exe")
|
||||
[[ "$target_exe" == /app/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests ]]
|
||||
printf 'targetPid=%s\nactualExecutable=%s\n' "$target_pid" "$target_exe" > /task/target-runtime.txt
|
||||
awk '$6 ~ /\/(libcoreclr.so|libhostfxr.so|libhostpolicy.so|System.Private.CoreLib.dll)$/ {print $6}' "/proc/$target_pid/maps" | sort -u >> /task/target-runtime.txt
|
||||
grep -Fx /app/libcoreclr.so /task/target-runtime.txt > /dev/null
|
||||
[[ $(readlink "/proc/$observer_pid/exe") == /observer/observer ]]
|
||||
awk '$6 ~ /\/libcoreclr.so$/ {print $6}' "/proc/$observer_pid/maps" | sort -u > /task/observer-runtime.txt
|
||||
grep -Fx /observer/libcoreclr.so /task/observer-runtime.txt > /dev/null
|
||||
observed=1
|
||||
break
|
||||
fi
|
||||
kill -0 "$vstest_pid" 2>/dev/null || break
|
||||
sleep 0.1
|
||||
done
|
||||
[[ "$observed" == 1 ]]
|
||||
set +e
|
||||
wait "$vstest_pid"
|
||||
test_status=$?
|
||||
printf '%s\n' "$test_status" > /task/test-exit-status.txt
|
||||
for attempt in $(seq 1 400); do
|
||||
kill -0 "$observer_pid" 2>/dev/null || break
|
||||
sleep 0.1
|
||||
done
|
||||
if kill -0 "$observer_pid" 2>/dev/null; then kill "$observer_pid"; fi
|
||||
wait "$observer_pid"
|
||||
observer_status=$?
|
||||
printf '%s\n' "$observer_status" > /task/observer-exit-status.txt
|
||||
# Preserve a cold-cap failure even when diagnostic readiness succeeds.
|
||||
if [[ "$test_status" != 0 ]]; then exit "$test_status"; fi
|
||||
exit "$observer_status"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,114 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Launch only a previously reviewed immutable candidate saturation bundle. Never an acceptance run."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def verify(root, expected):
|
||||
receipt_path = root / "receipt.json"
|
||||
if digest(receipt_path) != expected:
|
||||
raise ValueError("Receipt hash differs from the reviewed artifact")
|
||||
receipt = json.loads(receipt_path.read_text())
|
||||
for name, value in receipt["files"].items():
|
||||
if digest(root / name) != value:
|
||||
raise ValueError("Immutable bundle file changed: " + name)
|
||||
require(receipt["protocol"] == "sdk2-saturation-v1" and receipt["acceptance"] is False, "Invalid diagnostic protocol")
|
||||
require((receipt["canonicalCycles"], receipt["completedRestarts"], receipt["settlingCycles"], receipt["maximumPhaseSeconds"]) == (384, 96, 1, 1800), "Changed prospective limits")
|
||||
return receipt
|
||||
|
||||
|
||||
def trx(path, minimum):
|
||||
document = ET.parse(path)
|
||||
counters = document.find(".//{*}Counters")
|
||||
if counters is None:
|
||||
raise ValueError("Missing TRX counters")
|
||||
executed = int(counters.attrib.get("executed", "0"))
|
||||
skipped = max(int(counters.attrib.get("notExecuted", "0")),
|
||||
sum(result.get("outcome") == "NotExecuted" for result in document.findall("./{*}Results/{*}UnitTestResult")))
|
||||
if executed < minimum or skipped:
|
||||
raise ValueError("Required diagnostic tests were skipped or not executed")
|
||||
return {"executed": executed, "skipped": skipped}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--bundle", type=Path, required=True)
|
||||
parser.add_argument("--receipt-sha256", required=True, help="Hash reviewed by root and Astra before launch")
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--verify-only", action="store_true")
|
||||
options = parser.parse_args()
|
||||
root = options.bundle.resolve(strict=True)
|
||||
receipt = verify(root, options.receipt_sha256)
|
||||
if options.verify_only:
|
||||
print("Immutable bundle verified; no experiment launched")
|
||||
return 0
|
||||
output = options.output.resolve()
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
run_id = str(uuid.uuid4())
|
||||
container = "opcua-saturation-" + uuid.uuid4().hex[:12]
|
||||
command = ["docker", "run", "--rm", "--name", container, "--network", "none",
|
||||
"-e", "OTOPCUA_SOAK_BUILD_SHA=" + receipt["sourceRevision"],
|
||||
"-e", "OTOPCUA_SDK2_SATURATION_RUN_ID=" + run_id,
|
||||
"-v", str(root / "app") + ":/app:ro", "-v", str(root / "observer") + ":/observer:ro",
|
||||
"-v", str(root / "tools") + ":/tools:ro", "-v", str(output) + ":/task",
|
||||
receipt["image"], "bash", "/tools/container_saturation.sh"]
|
||||
(output / "launch.json").write_text(json.dumps({"command": command, "receiptSha256": options.receipt_sha256,
|
||||
"sourceRevision": receipt["sourceRevision"], "acceptance": False}, indent=2) + "\n")
|
||||
start = time.monotonic()
|
||||
try:
|
||||
with (output / "container.log").open("w") as log:
|
||||
process = subprocess.run(command, stdout=log, stderr=subprocess.STDOUT, timeout=2040)
|
||||
status = process.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
status = 124
|
||||
finally:
|
||||
subprocess.run(["docker", "stop", "--time", "10", container], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=20)
|
||||
(output / "container-exit-status.txt").write_text(str(status) + "\n")
|
||||
results = {"acceptance": False, "containerExitStatus": status, "wallSeconds": time.monotonic() - start,
|
||||
"sourceRevision": receipt["sourceRevision"], "receiptSha256": options.receipt_sha256}
|
||||
try:
|
||||
results["descriptorTests"] = trx(output / "results/descriptor-controls.trx", 2)
|
||||
results["experimentTests"] = trx(output / "results/saturation.trx", 1)
|
||||
rows = [json.loads(line) for line in (output / "resources.jsonl").read_text().splitlines()]
|
||||
header = rows[0]
|
||||
require(header["mode"] == "saturation" and header["requestedSeconds"] == 1800 and header["build"] == receipt["sourceRevision"], "Wrong executed source or mode")
|
||||
require(not any(row["kind"] == "complete" for row in rows), "Saturation must never emit an ordinary completion result")
|
||||
outcome = [row for row in rows if row["kind"] == "saturation-result"]
|
||||
require(len(outcome) == 1, "Missing unique diagnostic result")
|
||||
outcome = outcome[0]
|
||||
require((outcome["canonicalCycles"], outcome["settlingCycles"], outcome["restarts"]) == (384, 1, 96), "Incomplete fixed work")
|
||||
require(outcome["phaseSeconds"] <= 1800 and outcome["shutdownOwnershipVerified"], "Cutoff or cleanup proof failed")
|
||||
require(outcome["acceptance"] is False, "This is not an acceptance run")
|
||||
results["diagnosticOutcome"] = outcome
|
||||
observer_status = int((output / "observer-exit-status.txt").read_text())
|
||||
test_status = int((output / "test-exit-status.txt").read_text())
|
||||
require(observer_status == 0, "Observer failed")
|
||||
require((test_status != 0) == (outcome["coldBudgetRejected"] or not outcome["readiness"]), "Cold-cap exit was not preserved")
|
||||
results["protocolCompleted"] = True
|
||||
except Exception as error:
|
||||
results["protocolCompleted"] = False
|
||||
results["validationErrorType"] = type(error).__name__
|
||||
status = status or 1
|
||||
verify(root, options.receipt_sha256)
|
||||
(output / "run-receipt.json").write_text(json.dumps(results, indent=2) + "\n")
|
||||
print(json.dumps(results, indent=2))
|
||||
return status
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,113 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run two already-built public lifecycle controls in isolated, offline Linux containers."""
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
def latest_sample(path):
|
||||
# A concurrently-written final line may be incomplete. All complete rows remain in the source log.
|
||||
for row in reversed(path.read_text().splitlines()):
|
||||
try:
|
||||
value = json.loads(row)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if value.get("kind") == "sample":
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def run(options):
|
||||
options.output = options.output.resolve()
|
||||
options.output.mkdir(parents=True, exist_ok=False)
|
||||
processes = []
|
||||
environment = os.environ.copy()
|
||||
environment["OPCUA_MEMORY_PASSWORD"] = secrets.token_urlsafe(32)
|
||||
# Both receive the same environment. SDK1 ignores the SDK2 diagnostic opt-in.
|
||||
environment["OPCUA_CERTIFICATE_LEAK_TRACKING"] = "1"
|
||||
started = time.monotonic()
|
||||
try:
|
||||
for label, directory in [("baseline", options.baseline), ("candidate", options.candidate)]:
|
||||
directory = directory.resolve(strict=True)
|
||||
for required in ["control.dll", "control.deps.json", "control.runtimeconfig.json"]:
|
||||
if not (directory / required).is_file():
|
||||
raise FileNotFoundError("Use a complete, canonically-built control output: missing " + required)
|
||||
output = options.output / label
|
||||
output.mkdir()
|
||||
container = "opcua-memory-" + label + "-" + uuid.uuid4().hex[:10]
|
||||
shell = ('dotnet /app/control.dll /task/private-fixture ' + str(options.seconds) +
|
||||
' > /task/samples.jsonl 2>&1 & child=$!; echo "$child" > /task/dotnet.pid; '
|
||||
'wait "$child"; status=$?; echo "$status" > /task/exit-status.txt; exit "$status"')
|
||||
command = ["docker", "run", "--rm", "--name", container, "--network", "none",
|
||||
"--env", "OPCUA_MEMORY_PASSWORD", "--env", "OPCUA_CERTIFICATE_LEAK_TRACKING",
|
||||
"-v", str(directory) + ":/app:ro", "-v", str(output) + ":/task",
|
||||
options.image, "bash", "-c", shell]
|
||||
manifest = {"argv": command, "fullGate": False, "seconds": options.seconds,
|
||||
"environmentNamesOnly": ["OPCUA_MEMORY_PASSWORD", "OPCUA_CERTIFICATE_LEAK_TRACKING"],
|
||||
"startedUtc": datetime.now(timezone.utc).isoformat(),
|
||||
"runtimeFiles": {p.name: hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
for p in sorted(directory.iterdir()) if p.is_file()}}
|
||||
(output / "command.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
stream = (output / "container.log").open("w")
|
||||
process = subprocess.Popen(command, env=environment, stdout=stream, stderr=subprocess.STDOUT)
|
||||
processes.append((label, container, output, stream, process))
|
||||
next_sample = started + 60
|
||||
while any(process.poll() is None for _, _, _, _, process in processes):
|
||||
if time.monotonic() - started > options.seconds + 240:
|
||||
raise TimeoutError("Diagnostic controls exceeded bounded shutdown allowance")
|
||||
if time.monotonic() >= next_sample:
|
||||
for label, container, output, _, process in processes:
|
||||
pid_file = output / "dotnet.pid"
|
||||
if process.poll() is not None or not pid_file.exists():
|
||||
continue
|
||||
pid = pid_file.read_text().strip()
|
||||
if not pid.isdecimal():
|
||||
raise RuntimeError("Unexpected task-owned process id")
|
||||
stamp = str(int(time.time()))
|
||||
latest = latest_sample(output / "samples.jsonl")
|
||||
with (output / "observations.jsonl").open("a") as observed:
|
||||
observed.write(json.dumps({"utc": datetime.now(timezone.utc).isoformat(),
|
||||
"seconds": time.monotonic() - started,
|
||||
"sample": latest}) + "\n")
|
||||
for name in ["smaps", "smaps_rollup", "status"]:
|
||||
with (output / (stamp + "-" + name + ".txt")).open("w") as destination:
|
||||
result = subprocess.run(["docker", "exec", container, "cat", "/proc/" + pid + "/" + name],
|
||||
stdout=destination, stderr=subprocess.STDOUT, timeout=15)
|
||||
if result.returncode and process.poll() is None:
|
||||
raise RuntimeError(label + " live process diagnostic read failed")
|
||||
next_sample += 60
|
||||
time.sleep(.5)
|
||||
statuses = []
|
||||
for label, _, output, _, process in processes:
|
||||
status = process.wait()
|
||||
(output / "docker-exit-status.txt").write_text(str(status) + "\n")
|
||||
print(label + " exit " + str(status), flush=True)
|
||||
statuses.append(status)
|
||||
return 0 if all(status == 0 for status in statuses) else 1
|
||||
finally:
|
||||
for _, container, _, stream, process in processes:
|
||||
if process.poll() is None:
|
||||
subprocess.run(["docker", "stop", "--time", "10", container], stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, timeout=20)
|
||||
process.wait(timeout=20)
|
||||
stream.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--baseline", type=Path, required=True, help="Already-built control output directory")
|
||||
parser.add_argument("--candidate", type=Path, required=True, help="Already-built control output directory")
|
||||
parser.add_argument("--output", type=Path, required=True, help="New local diagnostic directory")
|
||||
parser.add_argument("--image", required=True, help="Immutable Linux .NET10 image digest")
|
||||
parser.add_argument("--seconds", type=int, default=1800)
|
||||
options = parser.parse_args()
|
||||
if not 1 <= options.seconds <= 1800 or not options.image.startswith("sha256:"):
|
||||
parser.error("Use an immutable image digest and duration 1..1800; never a full-soak gate")
|
||||
raise SystemExit(run(options))
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare a fresh immutable candidate bundle. This command never launches the experiment."""
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
from build_saturation_observer import build
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--libraries", type=Path, required=True)
|
||||
parser.add_argument("--image", required=True)
|
||||
options = parser.parse_args()
|
||||
source = Path(__file__).resolve().parent
|
||||
repo = source.parents[2]
|
||||
revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip()
|
||||
if subprocess.check_output(["git", "status", "--porcelain"], cwd=repo, text=True).strip():
|
||||
raise RuntimeError("Commit and review the exact source before preparing immutable artifacts")
|
||||
if not options.image.startswith("sha256:"):
|
||||
raise ValueError("Use an immutable Linux .NET SDK image digest")
|
||||
root = options.output.resolve()
|
||||
root.mkdir(parents=True, exist_ok=False)
|
||||
project = repo / "tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.csproj"
|
||||
command = ["dotnet", "publish", str(project), "-c", "Release", "-r", "linux-arm64", "--self-contained", "true",
|
||||
"-p:RuntimeFrameworkVersion=10.0.14", "-p:PublishSingleFile=false", "-p:PublishTrimmed=false", "-p:UseAppHost=true",
|
||||
"--artifacts-path", str(root / "build"), "-o", str(root / "app"), "-m:2", "--nologo"]
|
||||
with (root / "publish.log").open("w") as log:
|
||||
subprocess.run(command, cwd=repo, stdout=log, stderr=subprocess.STDOUT, check=True)
|
||||
observer = build(root / "observer-build", options.libraries, True)
|
||||
shutil.copytree(observer, root / "observer")
|
||||
tools = root / "tools"
|
||||
tools.mkdir()
|
||||
for name in ["launch_saturation.py", "container_saturation.sh", "SATURATION.md"]:
|
||||
shutil.copy2(source / name, tools / name)
|
||||
manifest = {str(path.relative_to(root)): sha(path) for directory in ["app", "observer", "tools"]
|
||||
for path in sorted((root / directory).rglob("*")) if path.is_file()}
|
||||
for config in ["app/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.runtimeconfig.json", "observer/observer.runtimeconfig.json"]:
|
||||
runtime = json.loads((root / config).read_text())
|
||||
included = runtime["runtimeOptions"]["includedFrameworks"]
|
||||
if not included or any(value["version"] != "10.0.14" for value in included):
|
||||
raise RuntimeError("Published participant is not the prospectively fixed self-contained runtime")
|
||||
dependencies = json.loads((root / "app/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.deps.json").read_text())["libraries"]
|
||||
gateway_libraries = [name for name in dependencies if name.startswith("ZB.MOM.WW.HistorianGateway.")]
|
||||
if not gateway_libraries or any(name.rsplit("/", 1)[-1] != "1.0.0" for name in gateway_libraries):
|
||||
raise RuntimeError("The reviewed candidate requires the landed HistorianGateway 1.0.0 graph")
|
||||
metadata = root / "module-info-build"
|
||||
metadata.mkdir()
|
||||
(metadata / "module-info.csproj").write_text(
|
||||
'<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType>'
|
||||
'<TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings>'
|
||||
'<EnableDefaultCompileItems>false</EnableDefaultCompileItems></PropertyGroup>'
|
||||
'<ItemGroup><Compile Include="' + str(source / "ArtifactModuleInfo.cs") + '" /></ItemGroup></Project>')
|
||||
with (metadata / "build.log").open("w") as log:
|
||||
subprocess.run(["dotnet", "build", str(metadata / "module-info.csproj"), "-c", "Release", "-m:2"],
|
||||
stdout=log, stderr=subprocess.STDOUT, check=True)
|
||||
modules = json.loads(subprocess.check_output(["dotnet", str(metadata / "bin/Release/net10.0/module-info.dll"),
|
||||
str(root), str(root / "app"), str(root / "observer")], text=True))
|
||||
receipt = {"protocol": "sdk2-saturation-v1", "sourceRevision": revision, "image": options.image,
|
||||
"rid": "linux-arm64", "runtimeVersion": "10.0.14", "selfContained": True,
|
||||
"canonicalCycles": 384, "completedRestarts": 96, "settlingCycles": 1, "maximumPhaseSeconds": 1800,
|
||||
"windows": [[321, 352], [353, 384]], "observerBufferMiB": 16, "runtimeKeywords": "0x18", "rundown": False,
|
||||
"acceptance": False, "preparedUtc": datetime.now(timezone.utc).isoformat(), "publishCommand": command,
|
||||
"sdkLibraries": [name for name in dependencies if name.startswith("OPCFoundation.")],
|
||||
"gatewayLibraries": gateway_libraries, "managedModules": modules,
|
||||
"testSourceSha256": sha(project.parent / "Sdk2LifecycleSoakTests.cs"),
|
||||
"observerSourceSha256": sha(source / "SaturationObserver.cs"), "files": manifest}
|
||||
receipt_path = root / "receipt.json"
|
||||
receipt_path.write_text(json.dumps(receipt, indent=2) + "\n")
|
||||
print(json.dumps({"bundle": str(root), "receiptSha256": sha(receipt_path), "sourceRevision": revision,
|
||||
"launched": False}, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,134 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build opt-in controls against existing published DLLs; never rebuild product code."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("kind", choices=["lifecycle", "certificate", "telemetry"])
|
||||
parser.add_argument("--references", type=Path, action="append", required=True,
|
||||
help="Existing published DLL directory; first directory wins duplicate names")
|
||||
parser.add_argument("--output", type=Path, required=True, help="New local artifact directory")
|
||||
parser.add_argument("--sdk2", action="store_true")
|
||||
parser.add_argument("--seconds", type=int, default=600)
|
||||
parser.add_argument("--mode", help="Certificate: public/pfx/shared-copy/sdk-copy; telemetry: dispose/undisposed")
|
||||
parser.add_argument("--build-only", action="store_true")
|
||||
parser.add_argument("--certificate-tracking", action="store_true", help="SDK2 opt-in allocation-stack diagnostics")
|
||||
options = parser.parse_args()
|
||||
if options.kind != "lifecycle" and not options.sdk2:
|
||||
parser.error("certificate and telemetry controls require --sdk2")
|
||||
if not 1 <= options.seconds <= 1800:
|
||||
parser.error("Diagnostic duration must be 1..1800 seconds; this is never a full soak")
|
||||
options.output = options.output.resolve()
|
||||
options.output.mkdir(parents=True, exist_ok=False)
|
||||
names = {"lifecycle": "LifecycleControl.cs", "certificate": "CertificateControl.cs", "telemetry": "TelemetryControl.cs"}
|
||||
source = Path(__file__).resolve().parent / names[options.kind]
|
||||
references = {}
|
||||
for directory in options.references:
|
||||
directory = directory.resolve(strict=True)
|
||||
for dll in sorted(directory.glob("*.dll")):
|
||||
references.setdefault(dll.name, dll)
|
||||
if "Opc.Ua.Core.dll" not in references:
|
||||
parser.error("References must contain actual OPC UA managed assemblies")
|
||||
project = ET.Element("Project", Sdk="Microsoft.NET.Sdk")
|
||||
properties = ET.SubElement(project, "PropertyGroup")
|
||||
for key, value in {"OutputType": "Exe", "TargetFramework": "net10.0", "ImplicitUsings": "enable",
|
||||
"Nullable": "enable", "EnableDefaultCompileItems": "false",
|
||||
"DefineConstants": "SDK2" if options.sdk2 else ""}.items():
|
||||
ET.SubElement(properties, key).text = value
|
||||
items = ET.SubElement(project, "ItemGroup")
|
||||
ET.SubElement(items, "FrameworkReference", Include="Microsoft.AspNetCore.App")
|
||||
ET.SubElement(items, "Compile", Include=str(source))
|
||||
for name, path in references.items():
|
||||
reference = ET.SubElement(items, "Reference", Include=path.stem)
|
||||
ET.SubElement(reference, "HintPath").text = str(path)
|
||||
project_path = options.output / "control.csproj"
|
||||
ET.indent(project)
|
||||
ET.ElementTree(project).write(project_path, encoding="unicode")
|
||||
manifest = {"kind": options.kind, "sdk2": options.sdk2, "seconds": options.seconds,
|
||||
"fullSoakGate": False, "source": {source.name: digest(source)},
|
||||
"referencePrecedence": "first directory wins duplicate names",
|
||||
"references": {name: digest(path) for name, path in references.items()}}
|
||||
(options.output / "provenance.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
with (options.output / "build.log").open("w") as log:
|
||||
result = subprocess.run(["dotnet", "build", str(project_path), "-c", "Release", "-m:2"],
|
||||
stdout=log, stderr=subprocess.STDOUT)
|
||||
if result.returncode:
|
||||
return result.returncode
|
||||
if options.build_only:
|
||||
return 0
|
||||
executable = options.output / "bin/Release/net10.0/control.dll"
|
||||
environment = os.environ.copy()
|
||||
environment["OPCUA_MEMORY_PASSWORD"] = secrets.token_urlsafe(32)
|
||||
if options.certificate_tracking:
|
||||
environment["OPCUA_CERTIFICATE_LEAK_TRACKING"] = "1"
|
||||
if options.kind == "lifecycle":
|
||||
command = ["dotnet", str(executable), str(options.output / "private-fixture"), str(options.seconds)]
|
||||
with (options.output / "samples.jsonl").open("w") as log:
|
||||
result = subprocess.run(command, env=environment, stdout=log, stderr=subprocess.STDOUT,
|
||||
timeout=options.seconds + 180)
|
||||
status = result.returncode
|
||||
elif options.kind == "telemetry":
|
||||
if options.mode not in ["dispose", "undisposed"]:
|
||||
parser.error("Telemetry mode must be dispose or undisposed")
|
||||
with (options.output / "samples.jsonl").open("w") as log:
|
||||
result = subprocess.run(["dotnet", str(executable), options.mode], env=environment,
|
||||
stdout=log, stderr=subprocess.STDOUT, timeout=180)
|
||||
status = result.returncode
|
||||
else:
|
||||
if sys.platform != "darwin":
|
||||
parser.error("Native certificate control currently requires macOS heap/vmmap")
|
||||
if options.mode not in ["public", "pfx", "shared-copy", "sdk-copy"]:
|
||||
parser.error("Certificate mode must be public, pfx, shared-copy or sdk-copy")
|
||||
status = certificate_control(executable, options.output, options.mode, environment)
|
||||
(options.output / "exit-status.txt").write_text(str(status) + "\n")
|
||||
return status
|
||||
|
||||
|
||||
def certificate_control(executable, output, mode, environment):
|
||||
# Native snapshots contain process metadata; retain locally, do not publish them unredacted.
|
||||
with (output / "samples.jsonl").open("w") as log:
|
||||
process = subprocess.Popen(["dotnet", str(executable), mode, str(output)], env=environment,
|
||||
stdout=log, stderr=subprocess.STDOUT)
|
||||
try:
|
||||
deadline = time.monotonic() + 180
|
||||
while not (output / "samples.jsonl").stat().st_size:
|
||||
if process.poll() is not None or time.monotonic() > deadline:
|
||||
raise RuntimeError("Certificate control failed before ready")
|
||||
time.sleep(.05)
|
||||
for label in ["before", "after"]:
|
||||
if label == "after":
|
||||
(output / "go").touch()
|
||||
while True:
|
||||
rows = (output / "samples.jsonl").read_text().splitlines()
|
||||
if rows and json.loads(rows[-1]).get("kind") == "done":
|
||||
break
|
||||
if process.poll() is not None or time.monotonic() > deadline:
|
||||
raise RuntimeError("Certificate control did not finish")
|
||||
time.sleep(.1)
|
||||
with (output / ("heap-" + label + ".txt")).open("w") as log_heap:
|
||||
subprocess.run(["/usr/bin/heap", "--noContent", "-s", str(process.pid)],
|
||||
stdout=log_heap, stderr=subprocess.STDOUT, timeout=20, check=True)
|
||||
(output / "stop").touch()
|
||||
return process.wait(timeout=10)
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.terminate()
|
||||
process.wait(timeout=10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Small negative controls for artifact tampering and VSTest's misleading skipped counter."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from launch_saturation import trx, verify
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="opcua-saturation-tools-") as temporary:
|
||||
root = Path(temporary)
|
||||
artifact = root / "immutable"
|
||||
artifact.write_text("control")
|
||||
receipt = {"protocol": "sdk2-saturation-v1", "acceptance": False, "canonicalCycles": 384,
|
||||
"completedRestarts": 96, "settlingCycles": 1, "maximumPhaseSeconds": 1800,
|
||||
"files": {"immutable": hashlib.sha256(artifact.read_bytes()).hexdigest()}}
|
||||
path = root / "receipt.json"
|
||||
path.write_text(json.dumps(receipt))
|
||||
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
verify(root, digest)
|
||||
artifact.write_text("tampered")
|
||||
try:
|
||||
verify(root, digest)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
raise RuntimeError("Changed artifact was accepted")
|
||||
xml = root / "skipped.trx"
|
||||
xml.write_text('<TestRun xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010"><Results>'
|
||||
'<UnitTestResult outcome="NotExecuted" /></Results><ResultSummary>'
|
||||
'<Counters executed="1" notExecuted="0" /></ResultSummary></TestRun>')
|
||||
try:
|
||||
trx(xml, 1)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
raise RuntimeError("Direct skipped result was hidden by the counter")
|
||||
print("Artifact verification, tamper rejection and direct skipped-result controls passed (3/3)")
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Emit mapping-category totals without path names from local Linux /proc/PID/smaps."""
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
HEADER = re.compile(r"^[0-9a-f]+-[0-9a-f]+\s+(\S+)\s+\S+\s+\S+\s+\S+\s*(.*)$")
|
||||
METRIC = re.compile(r"^(Size|Rss|Pss|Private_Dirty|Private_Clean|Anonymous):\s+(\d+) kB$")
|
||||
|
||||
|
||||
def summarize(path):
|
||||
groups = defaultdict(lambda: defaultdict(int))
|
||||
group = None
|
||||
for line in path.read_text().splitlines():
|
||||
header = HEADER.match(line)
|
||||
if header:
|
||||
permissions, name = header.groups()
|
||||
if "doublemapper" in name:
|
||||
group = "jit-doublemapper"
|
||||
elif name == "[heap]":
|
||||
group = "native-heap"
|
||||
elif name.startswith("[stack"):
|
||||
group = "stack"
|
||||
elif not name:
|
||||
group = "anonymous-exec" if "x" in permissions else "anonymous-write" if "w" in permissions else "anonymous-read"
|
||||
elif name.endswith(".dll"):
|
||||
group = "managed-assembly-files"
|
||||
elif ".so" in name:
|
||||
group = "native-library-files"
|
||||
elif name.startswith("["):
|
||||
group = "other-kernel-mappings"
|
||||
else:
|
||||
group = "other-files"
|
||||
elif group and (metric := METRIC.match(line)):
|
||||
groups[group][metric[1]] += int(metric[2]) * 1024
|
||||
return dict(groups)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("files", nargs="+", type=Path)
|
||||
args = parser.parse_args()
|
||||
print(json.dumps([{"sample": index, "groups": summarize(path)} for index, path in enumerate(args.files)], indent=2))
|
||||
@@ -1,93 +0,0 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using Opc.Ua.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.DeploymentCache;
|
||||
|
||||
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(45));
|
||||
var ct = deadline.Token;
|
||||
var mode = args[0];
|
||||
var root = Path.GetFullPath(args[1]);
|
||||
if (mode == "seed")
|
||||
{
|
||||
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{ ["LocalDb:Path"] = Path.Combine(root, "localdb.db") }).Build();
|
||||
await using var provider = new ServiceCollection().AddLogging().AddOtOpcUaLocalDb(config).BuildServiceProvider();
|
||||
var presented = Environment.GetEnvironmentVariable("SMOKE_API_KEY") ?? throw new InvalidOperationException("Missing task credential.");
|
||||
if (!ApiKeySecret.TryParse(presented, out var id, out var secret)) throw new InvalidOperationException("Invalid task credential format.");
|
||||
var pepper = Environment.GetEnvironmentVariable("Security__ApiKeys__Pepper") ?? throw new InvalidOperationException("Missing task pepper.");
|
||||
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
|
||||
{
|
||||
Clusters = new[] { new { ClusterId = "smoke", Name = "Smoke" } },
|
||||
RawFolders = new[] { new { RawFolderId = "smoke", Name = "Smoke", ClusterId = "smoke" } },
|
||||
ApiKeys = new[] { new { ApiKeyId = id, Name = "publish-smoke", Principal = "publish-smoke",
|
||||
Enabled = true, SecretHash = ApiKeySecret.ComputeHash(secret, pepper), CreatedUtc = DateTime.UtcNow } },
|
||||
NodeAcls = new[] { new { NodeAclId = "smoke-read", ClusterId = "smoke", Principal = "publish-smoke",
|
||||
ScopeKind = 0, ScopeId = (string?)null, PermissionFlags = 7 } },
|
||||
});
|
||||
var revision = Convert.ToHexStringLower(SHA256.HashData(artifact));
|
||||
await provider.GetRequiredService<IDeploymentArtifactCache>().StoreAsync("smoke", ZB.MOM.WW.OtOpcUa.Commons.Types.DeploymentId.NewId().ToString(), revision, artifact, ct);
|
||||
Console.WriteLine(JsonSerializer.Serialize(new { mode, result = "seeded-through-public-cache", revision }));
|
||||
return;
|
||||
}
|
||||
|
||||
var telemetry = DefaultTelemetry.Create(_ => { });
|
||||
var clientName = mode == "prepare-cli" ? "OtOpcUaClient" : "PublishSmokeClient";
|
||||
var security = new SecurityConfiguration
|
||||
{
|
||||
ApplicationCertificate = new CertificateIdentifier { StoreType = CertificateStoreType.Directory,
|
||||
StorePath = Path.Combine(root, "own"), SubjectName = "CN=" + clientName },
|
||||
TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(root, "issuer") },
|
||||
TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(root, "trusted") },
|
||||
RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(root, "rejected") },
|
||||
AutoAcceptUntrustedCertificates = false,
|
||||
};
|
||||
var configuration = new ApplicationConfiguration(telemetry)
|
||||
{
|
||||
ApplicationName = clientName, ApplicationUri = "urn:localhost:" + clientName,
|
||||
ApplicationType = ApplicationType.Client, SecurityConfiguration = security,
|
||||
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60_000 },
|
||||
};
|
||||
await using var application = new ApplicationInstance(telemetry)
|
||||
{ ApplicationName = configuration.ApplicationName, ApplicationType = ApplicationType.Client, ApplicationConfiguration = configuration };
|
||||
await configuration.ValidateAsync(ApplicationType.Client, ct);
|
||||
await application.CheckApplicationInstanceCertificatesAsync(true, lifeTimeInMonths: 12, ct);
|
||||
if (mode is "prepare" or "prepare-cli")
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(new { mode, result = "prepared", autoAccept = security.AutoAcceptUntrustedCertificates }));
|
||||
return;
|
||||
}
|
||||
var endpoint = args[2];
|
||||
var expected = args[3];
|
||||
var credential = Environment.GetEnvironmentVariable("SMOKE_API_KEY") ?? throw new InvalidOperationException("Missing task credential.");
|
||||
try
|
||||
{
|
||||
var selected = await CoreClientUtils.SelectEndpointAsync(configuration, endpoint, true, telemetry, ct)
|
||||
?? throw new InvalidOperationException("No secure endpoint advertised.");
|
||||
if (selected.SecurityMode != MessageSecurityMode.SignAndEncrypt || selected.SecurityPolicyUri != SecurityPolicies.Basic256Sha256)
|
||||
throw new InvalidOperationException("Unexpected negotiated security policy.");
|
||||
using var session = await new DefaultSessionFactory(telemetry).CreateAsync(configuration,
|
||||
new ConfiguredEndpoint(null, selected, EndpointConfiguration.Create(configuration)), false, true,
|
||||
"PublishSmoke", 60_000, new UserIdentity("apikey", Encoding.UTF8.GetBytes(credential)), default, ct);
|
||||
if (expected != "allow") throw new InvalidOperationException("A denied test credential or peer was admitted.");
|
||||
var response = await session.ReadAsync(null, 0, TimestampsToReturn.Both,
|
||||
[new ReadValueId { NodeId = VariableIds.Server_ServerStatus_State, AttributeId = Attributes.Value }], ct);
|
||||
if (response.Results.Count != 1 || StatusCode.IsBad(response.Results[0].StatusCode)) throw new InvalidOperationException("Server status read failed.");
|
||||
Console.WriteLine(JsonSerializer.Serialize(new { mode, expected, result = "allowed", policy = selected.SecurityPolicyUri,
|
||||
securityMode = selected.SecurityMode.ToString(), status = response.Results[0].StatusCode.ToString(), autoAccept = false }));
|
||||
await session.CloseAsync(ct);
|
||||
}
|
||||
catch (ServiceResultException error) when (expected != "allow")
|
||||
{
|
||||
var accepted = expected == "key-deny"
|
||||
? new[] { StatusCodes.BadUserAccessDenied, StatusCodes.BadIdentityTokenRejected, StatusCodes.BadIdentityTokenInvalid }
|
||||
: new[] { StatusCodes.BadCertificateUntrusted, StatusCodes.BadSecurityChecksFailed, StatusCodes.BadSecureChannelClosed };
|
||||
if (!accepted.Contains(error.StatusCode)) throw;
|
||||
Console.WriteLine(JsonSerializer.Serialize(new { mode, expected, result = "denied", status = error.StatusCode.ToString(), autoAccept = false }));
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../../src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,70 +0,0 @@
|
||||
# Actual Host publish smoke
|
||||
|
||||
From an isolated candidate checkout, publish the actual Host and CLI for Linux ARM64
|
||||
and Windows x64 using the install recipe's self-contained setting. Do not use a
|
||||
checkout that another process is building, and choose a new artifact directory.
|
||||
|
||||
```bash
|
||||
task_output=/absolute/fresh/publish-smoke
|
||||
test ! -e "$task_output" || exit 2
|
||||
mkdir -p "$task_output"
|
||||
for rid in linux-arm64 win-x64; do
|
||||
dotnet publish src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj \
|
||||
-c Release -r "$rid" --self-contained true \
|
||||
-p:PublishSingleFile=false -p:PublishTrimmed=false -m:1 \
|
||||
-o "$task_output/$rid/host" || exit
|
||||
dotnet publish src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.OtOpcUa.Client.CLI.csproj \
|
||||
-c Release -r "$rid" --self-contained true \
|
||||
-p:PublishSingleFile=false -p:PublishTrimmed=false -m:1 \
|
||||
-o "$task_output/$rid/cli" || exit
|
||||
done
|
||||
dotnet publish scripts/validation/opcua-publish-smoke/PublishProbe.csproj \
|
||||
-c Release -m:1 -o "$task_output/probe" || exit
|
||||
python3 scripts/validation/opcua-publish-smoke/runtime.py --artifacts "$task_output"
|
||||
```
|
||||
|
||||
The cached `mcr.microsoft.com/dotnet/sdk:10.0.103-noble` image must be ARM64 for this
|
||||
Linux artifact. The SDK image supplies the small probe's framework; the actual
|
||||
Host and CLI are launched through their published self-contained apphosts. There
|
||||
is no Native AOT, trimming, or single-file transformation.
|
||||
|
||||
The runtime script creates a fresh private state directory, ephemeral keys and
|
||||
loopback ports. A temporary preparation container seeds the LocalDb cache through
|
||||
public production registration/cache APIs. The artifact contains an empty driver
|
||||
configuration, one raw folder, and a real HMAC API key with read/browse/subscribe
|
||||
grants. No production authentication seam is replaced. The actual Host uses
|
||||
`Cluster:Roles=driver`, `ConfigSource:Mode=FetchAndCache`, an unreachable loopback
|
||||
central endpoint, and task-only LocalDb, secrets SQLite and GUID PKI paths. Its
|
||||
single-node Akka seed is local. Every container has `--network none`; no fleet
|
||||
endpoint, ConfigDb, existing container, volume, or PKI is used.
|
||||
|
||||
One task-owned container runs the actual Host as its entrypoint, stops cleanly,
|
||||
then starts the same executable with the same state and PKI again. Each boot must
|
||||
serve healthy readiness, liveness and active endpoints, admit a trusted
|
||||
Basic256Sha256 SignAndEncrypt API-key client, reject a wrong key, and allow an
|
||||
actual published CLI read of ServerStatus.State with Good quality. The first boot
|
||||
also rejects an untrusted server and an untrusted client. The two certificate
|
||||
thumbprints must match. Both stops must exit zero without an OOM kill. An explicit
|
||||
execution guard requires two boots and all six strict SDK probe cases.
|
||||
|
||||
The probe retains its public SDK ApplicationInstance throughout its session and
|
||||
disposes it afterward. Peer autoaccept is false. The CLI's existing configuration
|
||||
sets autoaccept true and has no strict-validation flag, so its successful reads
|
||||
are not counted as strict peer-validation coverage. Its certificate is prepared
|
||||
with its normal application name and URI using public SDK APIs and trusted by the
|
||||
server before startup. A closed-port CLI preflight is an expected nonzero setup
|
||||
check, not a passing connection case.
|
||||
|
||||
`commands.json`, per-case logs, health JSON and `summary.json` retain actual exits,
|
||||
results and certificate identity. Secrets are supplied through a mode-0600 task
|
||||
environment file, never written into recorded arguments, and the file is removed
|
||||
at the end. Raw process output and this run's appended rolling-file output are
|
||||
checked for task secrets before redacted copies are saved. Only the captured,
|
||||
task-labelled container is removed; private bind-mounted state remains local for
|
||||
inspection and must not be redistributed with release artifacts.
|
||||
|
||||
Windows cross-publish and PE/native/dependency inspection can run on macOS.
|
||||
Windows service installation and start/stop remain a separate operator gate on
|
||||
an approved Windows environment. The Wonder-box remote-execution hold still
|
||||
applies: no WinRM, psbridge, or replacement remote-execution channel. This smoke
|
||||
does not complete backend, load, failover, or two-hour soak validation.
|
||||
@@ -1,152 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Smoke actual self-contained Linux Host/CLI artifacts with task-only PKI and LocalDb."""
|
||||
import argparse, base64, datetime, hashlib, json, os, re, secrets, shutil, subprocess, time, uuid
|
||||
from pathlib import Path
|
||||
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--artifacts', required=True, type=Path)
|
||||
p.add_argument('--image', default='mcr.microsoft.com/dotnet/sdk:10.0.103-noble')
|
||||
a = p.parse_args(); out = a.artifacts.resolve()
|
||||
run = out / ('runtime-' + uuid.uuid4().hex); run.mkdir(exist_ok=False)
|
||||
state = run / 'state'; state.mkdir(); os.chmod(state, 0o700)
|
||||
identifier = uuid.uuid4().hex[:12]
|
||||
web, ua, akka = [20000 + secrets.randbelow(10000) for _ in range(3)]
|
||||
while len({web, ua, akka}) != 3: web, ua, akka = [20000 + secrets.randbelow(10000) for _ in range(3)]
|
||||
key = 'otua_' + secrets.token_hex(8) + '_' + secrets.token_urlsafe(32)
|
||||
pepper, fetch = secrets.token_urlsafe(32), secrets.token_urlsafe(32)
|
||||
master = base64.b64encode(secrets.token_bytes(32)).decode()
|
||||
env = {
|
||||
'ASPNETCORE_URLS': f'http://127.0.0.1:{web}', 'ASPNETCORE_ENVIRONMENT': 'Production',
|
||||
'Cluster__Roles__0': 'driver',
|
||||
'Cluster__SystemName': 'otopcua', 'Cluster__Hostname': '127.0.0.1',
|
||||
'Cluster__PublicHostname': '127.0.0.1', 'Cluster__Port': str(akka),
|
||||
'Cluster__SeedNodes__0': f'akka.tcp://otopcua@127.0.0.1:{akka}',
|
||||
'ConfigSource__Mode': 'FetchAndCache', 'ConfigSource__CentralFetchEndpoints__0': 'http://127.0.0.1:1',
|
||||
'ConfigSource__ApiKey': fetch, 'ConfigSource__FetchTimeoutSeconds': '1',
|
||||
'LocalDb__Path': '/state/localdb.db', 'Secrets__SqlitePath': '/state/secrets.db',
|
||||
'ZB_SECRETS_MASTER_KEY': master, 'Security__ApiKeys__Pepper': pepper,
|
||||
'Security__Ldap__Enabled': 'false', 'Security__Acl__Enabled': 'false',
|
||||
'OpcUa__ApplicationName': 'PublishSmoke-' + identifier,
|
||||
'OpcUa__ApplicationUri': 'urn:localhost:PublishSmoke:' + identifier,
|
||||
'OpcUa__PublicHostname': 'localhost', 'OpcUa__OpcUaPort': str(ua),
|
||||
'OpcUa__PkiStoreRoot': '/state/server-pki-' + identifier,
|
||||
'OpcUa__AllowAnonymous': 'false', 'OpcUa__AutoAcceptUntrustedClientCertificates': 'false',
|
||||
'OpcUa__EnabledSecurityProfiles__0': 'Basic256Sha256SignAndEncrypt',
|
||||
'SMOKE_API_KEY': key, 'XDG_DATA_HOME': '/state/cli-data', 'HOME': '/state/home',
|
||||
}
|
||||
envfile = run / 'private.env'; envfile.write_text(''.join(f'{k}={v}\n' for k,v in env.items())); os.chmod(envfile, 0o600)
|
||||
redactions = [key, key.split('_', 2)[2], pepper, fetch, master]
|
||||
def clean(text):
|
||||
for secret in redactions: text = text.replace(secret, '<ephemeral-secret>')
|
||||
return text
|
||||
records = []
|
||||
raw_secret_findings = []
|
||||
rolling_root = out / 'linux-arm64/host/logs'
|
||||
rolling_offsets = {p: p.stat().st_size for p in rolling_root.glob('*') if p.is_file()}
|
||||
def command(args, name, expected=0, timeout=90):
|
||||
start = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
result = subprocess.run(args, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout)
|
||||
raw_secret_findings.append({'log': name, 'occurrences': sum(result.stdout.count(secret) for secret in redactions)})
|
||||
(run / (name + '.log')).write_text(clean(result.stdout))
|
||||
records.append({'name': name, 'utc': start, 'argv': [clean(str(x)) for x in args], 'exit': result.returncode})
|
||||
(run / 'commands.json').write_text(json.dumps(records, indent=2))
|
||||
if expected is not None and result.returncode != expected:
|
||||
raise RuntimeError(f'{name}: exit {result.returncode}; see {run / (name + ".log")}')
|
||||
return result
|
||||
mounts = ['--mount', f'type=bind,src={out},dst=/work', '--mount', f'type=bind,src={state},dst=/state']
|
||||
base = ['docker','run','--rm','--network','none','--label','codex.task=opcua-publish-smoke','--env-file',str(envfile),*mounts,'-w','/work/linux-arm64/host',a.image]
|
||||
probe = ['dotnet','/work/probe/PublishProbe.dll']
|
||||
cli = ['/work/linux-arm64/cli/ZB.MOM.WW.OtOpcUa.Client.CLI']
|
||||
server_pki = state / ('server-pki-' + identifier)
|
||||
container = None; summary = {'run': str(run), 'image': a.image, 'ports': {'http':web,'ua':ua,'akka':akka}, 'boots':[], 'cases':[]}
|
||||
def copy_public(source, target):
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
certs = list(source.glob('*.der'))
|
||||
if not certs: raise RuntimeError(f'No public certificates at {source}')
|
||||
for cert in certs: shutil.copy2(cert, target / cert.name)
|
||||
def inspect(): return json.loads(subprocess.check_output(['docker','inspect',container], text=True))[0]
|
||||
def save_logs(name):
|
||||
r = subprocess.run(['docker','logs',container],text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
|
||||
raw_secret_findings.append({'log': name, 'occurrences': sum(r.stdout.count(secret) for secret in redactions)})
|
||||
(run / (name + '.log')).write_text(clean(r.stdout))
|
||||
def exec_probe(name, pki, expected, credential=None):
|
||||
args = ['docker','exec']
|
||||
if credential: args += ['-e','SMOKE_API_KEY=' + credential]
|
||||
args += [container,*probe,'probe',pki,f'opc.tcp://localhost:{ua}/OtOpcUa',expected]
|
||||
r = command(args,name)
|
||||
payload = json.loads(r.stdout.strip().splitlines()[-1]); summary['cases'].append({'name':name,**payload})
|
||||
try:
|
||||
command([*base,*probe,'seed','/state'],'seed')
|
||||
command([*base,*probe,'prepare','/state/client-pki'],'prepare-trusted-client')
|
||||
command([*base,*probe,'prepare','/state/untrusted-client-pki'],'prepare-untrusted-client')
|
||||
command([*base,*cli,'--help'],'cli-help')
|
||||
command([*base,*probe,'prepare-cli','/state/cli-data/OtOpcUaClient/pki'],'prepare-cli-certificate')
|
||||
failed = command([*base,*cli,'read','--url','opc.tcp://localhost:1/OtOpcUa','--security','encrypt','--username','apikey','--password',key,'--node','i=2259'],'cli-prepare-on-closed-port',expected=None)
|
||||
if failed.returncode == 0: raise RuntimeError('Closed-port CLI fixture preparation unexpectedly succeeded.')
|
||||
copy_public(state/'client-pki/own/certs',server_pki/'trusted/certs')
|
||||
copy_public(state/'cli-data/OtOpcUaClient/pki/own/certs',server_pki/'trusted/certs')
|
||||
name = 'otopcua-sdk2-publish-' + identifier
|
||||
result = command(['docker','create','--name',name,'--network','none','--label','codex.task=opcua-publish-smoke','--env-file',str(envfile),*mounts,'-w','/work/linux-arm64/host',a.image,'./OtOpcUa.Host'],'container-create')
|
||||
container = result.stdout.strip()
|
||||
summary['container'] = container
|
||||
for boot in [1,2]:
|
||||
command(['docker','start',container],f'boot{boot}-start')
|
||||
deadline = time.monotonic() + 90
|
||||
health = None
|
||||
while time.monotonic() < deadline:
|
||||
if not inspect()['State']['Running']:
|
||||
save_logs(f'boot{boot}-host'); raise RuntimeError('Full Host exited before health readiness.')
|
||||
r = subprocess.run(['docker','exec',container,'curl','-fsS',f'http://127.0.0.1:{web}/health/ready'],text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
|
||||
if r.returncode == 0:
|
||||
health = r.stdout; break
|
||||
time.sleep(1)
|
||||
if health is None:
|
||||
save_logs(f'boot{boot}-host'); raise RuntimeError('Full Host readiness did not become healthy.')
|
||||
if json.loads(health).get('status') != 'Healthy': raise RuntimeError('Readiness is degraded or unhealthy.')
|
||||
(run/f'boot{boot}-ready.json').write_text(health)
|
||||
command(['docker','exec',container,'curl','-fsS',f'http://127.0.0.1:{web}/healthz'],f'boot{boot}-live')
|
||||
command(['docker','exec',container,'curl','-fsS',f'http://127.0.0.1:{web}/health/active'],f'boot{boot}-active')
|
||||
certs = list((server_pki/'own/certs').glob('*.der'))
|
||||
if len(certs) != 1: raise RuntimeError('Expected exactly one application certificate.')
|
||||
cert_sha256 = hashlib.sha256(certs[0].read_bytes()).hexdigest()
|
||||
thumbprint = subprocess.check_output(['openssl','x509','-inform','DER','-in',str(certs[0]),'-noout','-fingerprint','-sha1'],text=True).strip()
|
||||
copy_public(server_pki/'own/certs',state/'client-pki/trusted/certs')
|
||||
copy_public(server_pki/'own/certs',state/'untrusted-client-pki/trusted/certs')
|
||||
exec_probe(f'boot{boot}-trusted-key','/state/client-pki','allow')
|
||||
exec_probe(f'boot{boot}-wrong-key','/state/client-pki','key-deny',key+'-wrong')
|
||||
if boot == 1:
|
||||
hold = state/'hold-server-trust'; shutil.move(str(state/'client-pki/trusted/certs'),hold)
|
||||
(state/'client-pki/trusted/certs').mkdir()
|
||||
try: exec_probe('untrusted-server','/state/client-pki','peer-deny')
|
||||
finally:
|
||||
shutil.rmtree(state/'client-pki/trusted/certs'); shutil.move(str(hold),state/'client-pki/trusted/certs')
|
||||
exec_probe('untrusted-client','/state/untrusted-client-pki','peer-deny')
|
||||
r = command(['docker','exec',container,*cli,'read','--url',f'opc.tcp://localhost:{ua}/OtOpcUa','--security','encrypt','--username','apikey','--password',key,'--node','i=2259'],f'boot{boot}-actual-cli-read')
|
||||
if not re.search(r'^Status:\s+Good(?:\s|$)', r.stdout, re.MULTILINE): raise RuntimeError('Actual CLI did not report a Good server status read.')
|
||||
command(['docker','stop','--time','30',container],f'boot{boot}-stop',timeout=45)
|
||||
stopped = inspect()['State']; save_logs(f'boot{boot}-host')
|
||||
if stopped['Running'] or stopped['ExitCode'] != 0 or stopped['OOMKilled']: raise RuntimeError('Full Host did not shut down cleanly.')
|
||||
summary['boots'].append({'boot':boot,'certificate_sha256':cert_sha256,'thumbprint':thumbprint,'exit':stopped['ExitCode'],'oomKilled':stopped['OOMKilled'],'health_ready':health})
|
||||
if summary['boots'][0]['thumbprint'] != summary['boots'][1]['thumbprint']: raise RuntimeError('Application certificate changed between boots.')
|
||||
summary['certificate_preserved'] = True
|
||||
for path in rolling_root.glob('*'):
|
||||
if not path.is_file(): continue
|
||||
text = path.read_bytes()[rolling_offsets.get(path, 0):].decode(errors='replace')
|
||||
raw_secret_findings.append({'log': 'rolling-' + path.name, 'occurrences': sum(text.count(secret) for secret in redactions)})
|
||||
(run / ('rolling-' + path.name)).write_text(clean(text))
|
||||
if len(summary['boots']) != 2 or len(summary['cases']) != 6: raise RuntimeError('Required smoke cases did not all execute.')
|
||||
if any(x['occurrences'] for x in raw_secret_findings): raise RuntimeError('Task secret appeared in raw application output; see recorded occurrence counts.')
|
||||
summary['result'] = 'passed'
|
||||
finally:
|
||||
if container:
|
||||
save_logs('host-final')
|
||||
owned = inspect()
|
||||
if owned['Config']['Labels'].get('codex.task') != 'opcua-publish-smoke': raise RuntimeError('Task container ownership changed.')
|
||||
command(['docker','rm','-f',container],'container-cleanup')
|
||||
logs = '\n'.join(x.read_text(errors='replace') for x in run.glob('*.log'))
|
||||
summary['task_secret_occurrences_in_saved_logs'] = sum(logs.count(secret) for secret in redactions)
|
||||
summary['raw_log_secret_findings'] = raw_secret_findings
|
||||
summary['completed_utc'] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
(run/'summary.json').write_text(json.dumps(summary,indent=2))
|
||||
envfile.unlink(missing_ok=True)
|
||||
print(json.dumps(summary,indent=2))
|
||||
@@ -1,35 +0,0 @@
|
||||
# Independent scripted-alarm scope baseline
|
||||
|
||||
This SDK 1.5.378.182-only source probe records the existing opaque-ID behavior
|
||||
tracked by [#797](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/797).
|
||||
It is deliberately outside the candidate solution and is not a runtime adapter.
|
||||
|
||||
Use a clean, separate checkout at original application commit
|
||||
`83ed880e4471344deaf008da9bd028c7e170bdc7`. Confirm its package pins before
|
||||
running. Copy **only** `ScriptedScopeProbe.cs` temporarily into that checkout's
|
||||
`tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/`, refusing to
|
||||
overwrite an existing file. Run:
|
||||
|
||||
```sh
|
||||
dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests \
|
||||
-c Release --filter FullyQualifiedName~BaselineScriptedScopeProbe \
|
||||
--logger trx --results-directory /absolute/fresh-baseline-evidence
|
||||
```
|
||||
|
||||
Preserve the test exit and run the baseline checkout's
|
||||
`scripts/ci/assert-not-all-skipped.sh` with `MIN_EXECUTED=1` against the fresh TRX.
|
||||
Remove only the temporary probe you copied after the run, keeping the source
|
||||
and TRX evidence here/outside the checkout. Verify baseline tracked files remain
|
||||
unchanged. Never load this assembly or old SDK into the candidate process.
|
||||
|
||||
The real session has no application-wide roles. The production-valid opaque
|
||||
scripted ID is denied by an Equipment grant, routes nothing, and is then allowed
|
||||
by an explicit Cluster grant. A passing probe pins **both** outcomes; it does not
|
||||
claim narrow equipment authorization works. The independent candidate counterpart
|
||||
is `Sdk2OpaqueScriptedScopeBaselineTests`.
|
||||
|
||||
Do not make the ID slash-qualified to get a positive result: the current UI
|
||||
rejects `/`, and that would hide the production scope-resolution defect. A future
|
||||
fix requires reviewed ownership/scope binding without changing opaque IDs or
|
||||
widening permissions. This upgrade preserves the baseline denial and links the
|
||||
separate defect.
|
||||
@@ -1,44 +0,0 @@
|
||||
// Deliberately compiled only in a temporary SDK1.5 baseline integration checkout; never in the candidate solution.
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
using static ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.ServerEventRig;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
|
||||
|
||||
public sealed class BaselineScriptedScopeProbe
|
||||
{
|
||||
[Fact]
|
||||
public async Task Opaque_scripted_ID_is_denied_by_its_equipment_grant_but_allowed_by_a_cluster_grant()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await using var rig = await StartAsync("BaselineScriptedScope", encrypted: false, ct);
|
||||
rig.KeyRoles = [];
|
||||
const NodePermissions permissions = NodePermissions.ReadOnly | NodePermissions.AlarmAcknowledge | NodePermissions.MethodCall;
|
||||
rig.Enforce(true, [Key()], new NodeAcl
|
||||
{
|
||||
NodeAclId = "narrow", ClusterId = ServerEventRig.Cluster, Principal = KeyPrincipal,
|
||||
ScopeKind = NodeAclScopeKind.Equipment, ScopeId = $"eq-{EqA}", PermissionFlags = permissions,
|
||||
});
|
||||
rig.MaterialiseScripted();
|
||||
var routed = 0;
|
||||
rig.Nm.AlarmCommandRouter = _ => routed++;
|
||||
using var session = await rig.ConnectAsync(anonymous: false, ct);
|
||||
rig.Project(false, new AlarmConditionSnapshot(true, true, true, true, AlarmShelvingKind.Unshelved, 500, "scope"));
|
||||
var condition = rig.Nm.TryGetAlarmCondition(ScriptedAlarmId, AddressSpaceRealm.Uns).ShouldNotBeNull();
|
||||
var eventId = condition.EventId.ShouldNotBeNull().Value;
|
||||
var denied = await Should.ThrowAsync<ServiceResultException>(() => session.CallAsync(condition.NodeId,
|
||||
MethodIds.AcknowledgeableConditionType_Acknowledge, ct, new object[] { eventId, new LocalizedText("denied") }));
|
||||
denied.StatusCode.ShouldBe(StatusCodes.BadUserAccessDenied);
|
||||
routed.ShouldBe(0);
|
||||
rig.Enforce(true, [Key()], ClusterGrant(KeyPrincipal, "cluster-positive", permissions));
|
||||
await session.CallAsync(condition.NodeId, MethodIds.AcknowledgeableConditionType_Acknowledge, ct,
|
||||
new object[] { eventId, new LocalizedText("allowed") });
|
||||
routed.ShouldBe(1);
|
||||
await session.CloseAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -1,374 +0,0 @@
|
||||
#pragma warning disable OTOPCUA0001 // This wire harness deliberately exercises public driver capabilities directly.
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.Shared;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
|
||||
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.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
|
||||
using AlarmEventArgs = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.AlarmEventArgs;
|
||||
using WriteRequest = ZB.MOM.WW.OtOpcUa.Core.Abstractions.WriteRequest;
|
||||
using SharedSecurityMode = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.SecurityMode;
|
||||
using CoreHistory = ZB.MOM.WW.OtOpcUa.Core.Abstractions.HistoryReadResult;
|
||||
|
||||
internal static class ProcessHost
|
||||
{
|
||||
static readonly DateTime Epoch = new(2026, 10, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
const string RawUri = "https://zb.com/otopcua/raw", UnsUri = "https://zb.com/otopcua/uns";
|
||||
const string Folder = "Matrix/dev", Scalar = Folder + "/Scalar", Vector = Folder + "/Vector", Alarm = Folder + "/Alarm";
|
||||
static readonly CancellationToken Ct = new CancellationTokenSource(TimeSpan.FromMinutes(4)).Token;
|
||||
static string Password => Environment.GetEnvironmentVariable("OPCUA_MATRIX_PASSWORD") ?? throw new InvalidOperationException("Missing ephemeral credential");
|
||||
static readonly List<object> Cases = [];
|
||||
static readonly List<object> Observations = [];
|
||||
static int Failures;
|
||||
static string Root = "";
|
||||
static object? Clr(DataValue v)
|
||||
{
|
||||
#if SDK2
|
||||
return ZB.MOM.WW.OtOpcUa.Client.Shared.Helpers.ValueConverter.ToClrObject(v.WrappedValue);
|
||||
#else
|
||||
return v.Value;
|
||||
#endif
|
||||
}
|
||||
static DateTime SourceTime(DataValue value)
|
||||
{
|
||||
#if SDK2
|
||||
return value.SourceTimestamp.ToDateTime();
|
||||
#else
|
||||
return value.SourceTimestamp;
|
||||
#endif
|
||||
}
|
||||
static DateTime ServerTime(DataValue value)
|
||||
{
|
||||
#if SDK2
|
||||
return value.ServerTimestamp.ToDateTime();
|
||||
#else
|
||||
return value.ServerTimestamp;
|
||||
#endif
|
||||
}
|
||||
static void Require(bool yes, string message) { if (!yes) throw new InvalidOperationException(message); }
|
||||
static async Task Case(string name, Func<Task> action)
|
||||
{
|
||||
try { await action(); Cases.Add(new { name, status = "passed" }); }
|
||||
catch (Exception e) { Failures++; Cases.Add(new { name, status = "failed", error = e.ToString() }); }
|
||||
}
|
||||
static async Task Wait(Func<bool> predicate)
|
||||
{
|
||||
for (int i = 0; i < 200; i++) { if (predicate()) return; await Task.Delay(50, Ct); }
|
||||
Require(false, "Notification deadline exceeded");
|
||||
}
|
||||
static ConnectionSettings Settings(string endpoint) => new()
|
||||
{
|
||||
EndpointUrl = endpoint,
|
||||
Username = "apikey",
|
||||
Password = Password,
|
||||
SecurityMode = SharedSecurityMode.SignAndEncrypt,
|
||||
AutoAcceptCertificates = false,
|
||||
CertificateStorePath = Environment.GetEnvironmentVariable("OPCUA_MATRIX_CLIENT_PKI") ?? Path.Combine(Root, "client-pki")
|
||||
};
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
Root = Path.GetFullPath(args[1]); Directory.CreateDirectory(Root);
|
||||
try
|
||||
{
|
||||
switch (args[0])
|
||||
{
|
||||
case "local-data-path": Console.WriteLine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)); return 0;
|
||||
case "prepare":
|
||||
var type = typeof(OpcUaClientService).Assembly.GetType("ZB.MOM.WW.OtOpcUa.Client.Shared.Adapters.DefaultApplicationConfigurationFactory")!;
|
||||
var factory = Activator.CreateInstance(type, true)!;
|
||||
await (Task<ApplicationConfiguration>)type.GetMethod("CreateAsync")!.Invoke(factory, [Settings("opc.tcp://127.0.0.1:1"), Ct])!;
|
||||
Console.WriteLine("PREPARED"); return 0;
|
||||
case "prepare-driver": await PrepareDriver(); return 0;
|
||||
case "driver": await Driver(args[2]); break;
|
||||
case "server": await Serve(); return 0;
|
||||
case "client": await Client(args[2]); break;
|
||||
case "performance": await Performance(args[2]); break;
|
||||
default: throw new ArgumentException("Expected prepare/server/client");
|
||||
}
|
||||
}
|
||||
catch (Exception e) { Failures++; Cases.Add(new { name = "process", status = "failed", error = e.ToString() }); }
|
||||
File.WriteAllText(Path.Combine(Root, "result.json"), JsonSerializer.Serialize(new { sdk = typeof(NodeId).Assembly.GetName().Version?.ToString(), process = Environment.ProcessId, cases = Cases, observations = Observations, failures = Failures, loadedSdkAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name!.StartsWith("Opc.Ua", StringComparison.Ordinal)).Select(a => new { name = a.GetName().Name, version = a.GetName().Version?.ToString(), path = a.Location }) }, new JsonSerializerOptions { WriteIndented = true }));
|
||||
return Failures == 0 ? 0 : 1;
|
||||
}
|
||||
static async Task Serve()
|
||||
{
|
||||
var options = new OpcUaApplicationHostOptions
|
||||
{
|
||||
ApplicationName = "OtOpcUa.Matrix",
|
||||
ApplicationUri = $"urn:OtOpcUa.Matrix.{Guid.NewGuid():N}",
|
||||
PublicHostname = "127.0.0.1",
|
||||
PkiStoreRoot = Path.Combine(Root, "server-pki"),
|
||||
AllowAnonymous = false,
|
||||
AutoAcceptUntrustedClientCertificates = false,
|
||||
EnabledSecurityProfiles = Environment.GetEnvironmentVariable("OPCUA_MATRIX_SERVER_SECURITY") == "none"
|
||||
? [OpcUaSecurityProfile.None] : [OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt]
|
||||
};
|
||||
using (var listener = new TcpListener(IPAddress.Loopback, 0)) { listener.Start(); options.OpcUaPort = ((IPEndPoint)listener.LocalEndpoint).Port; }
|
||||
var server = new OtOpcUaSdkServer();
|
||||
await using var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance, new Auth());
|
||||
await host.StartAsync(server, Ct);
|
||||
var nm = server.NodeManager!; var sink = new SdkAddressSpaceSink(nm);
|
||||
var authority = new DataPlaneAuthorityState();
|
||||
authority.Apply("matrix", "rev1", [new ApiKey { ApiKeyId = "matrix", Name = "matrix", SecretHash = "fixture", Principal = "matrix", Enabled = true, CreatedUtc = DateTime.UtcNow.AddDays(-1) }],
|
||||
[new NodeAcl { NodeAclId = "all", ClusterId = "matrix", Principal = "matrix", ScopeKind = NodeAclScopeKind.Cluster, PermissionFlags = (NodePermissions)int.MaxValue }], new Dictionary<AclScopeRef, IReadOnlyList<NodeAclPath>>());
|
||||
nm.DataPlaneAuthority = authority;
|
||||
nm.Authorizer = new DataPlaneAuthorizer(authority, new TriePermissionEvaluator(authority.Tries), true, NullLogger.Instance, TimeProvider.System);
|
||||
var history = new Source(Root); nm.HistorianDataSource = history;
|
||||
sink.EnsureFolder(Folder, null, "dev", AddressSpaceRealm.Raw);
|
||||
sink.EnsureFolder("Eq", null, "Eq", AddressSpaceRealm.Uns);
|
||||
foreach (var realm in new[] { AddressSpaceRealm.Raw, AddressSpaceRealm.Uns })
|
||||
{
|
||||
var scalar = realm == AddressSpaceRealm.Raw ? Scalar : "Eq/Scalar";
|
||||
var vector = realm == AddressSpaceRealm.Raw ? Vector : "Eq/Vector";
|
||||
var parent = realm == AddressSpaceRealm.Raw ? Folder : "Eq";
|
||||
sink.EnsureVariable(scalar, parent, "Scalar", "Int32", true, realm, "matrix.scalar");
|
||||
sink.EnsureVariable(vector, parent, "Vector", "Int32", true, realm, isArray: true, arrayLength: 3);
|
||||
sink.WriteValue(scalar, 17, 0, Epoch, realm); sink.WriteValue(vector, new[] { 1, 2, 3 }, 0, Epoch, realm);
|
||||
}
|
||||
sink.AddReference("Eq/Scalar", AddressSpaceRealm.Uns, Scalar, AddressSpaceRealm.Raw);
|
||||
sink.MaterialiseAlarmCondition(Alarm, Folder, "Alarm", "OffNormalAlarm", 800, AddressSpaceRealm.Raw, isNative: true);
|
||||
sink.WireAlarmNotifiers(Alarm, AddressSpaceRealm.Raw, ["Eq"], AddressSpaceRealm.Uns);
|
||||
sink.EnsureVariable(Folder + "/UnsetTimestamp", Folder, "UnsetTimestamp", "Int32", false, AddressSpaceRealm.Raw);
|
||||
sink.WriteValue(Folder + "/UnsetTimestamp", 3, 0, DateTime.MinValue, AddressSpaceRealm.Raw);
|
||||
nm.NodeWriteGateway = new Gateway();
|
||||
var state = new AlarmConditionSnapshot(true, false, true, true, AlarmShelvingKind.Unshelved, 800, "matrix-alarm");
|
||||
nm.NativeAlarmAckRouter = _ => Task.Run(() => { state = state with { Acknowledged = true }; sink.WriteAlarmCondition(Alarm, state, DateTime.UtcNow, AddressSpaceRealm.Raw); });
|
||||
File.WriteAllText(Path.Combine(Root, "endpoint.txt"), $"opc.tcp://127.0.0.1:{options.OpcUaPort}/OtOpcUa");
|
||||
File.WriteAllText(Path.Combine(Root, "ready.json"), JsonSerializer.Serialize(new { endpoint = $"opc.tcp://127.0.0.1:{options.OpcUaPort}/OtOpcUa", process = Environment.ProcessId, sdk = typeof(NodeId).Assembly.GetName().Version?.ToString(), rawUri = RawUri, unsUri = UnsUri }));
|
||||
int tick = 0;
|
||||
while (!File.Exists(Path.Combine(Root, "stop")))
|
||||
{
|
||||
sink.WriteAlarmCondition(Alarm, state with { Message = ++tick % 2 == 0 ? "matrix-alarm" : "matrix-tick" }, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
await Task.Delay(1000, Ct);
|
||||
}
|
||||
using var process = System.Diagnostics.Process.GetCurrentProcess();
|
||||
File.WriteAllText(Path.Combine(Root, "server-resources.json"), JsonSerializer.Serialize(new { process.WorkingSet64, process.PrivateMemorySize64, process.HandleCount, threads=process.Threads.Count, cpuMilliseconds=process.TotalProcessorTime.TotalMilliseconds, allocatedBytes=GC.GetTotalAllocatedBytes(), collections=new[]{GC.CollectionCount(0),GC.CollectionCount(1),GC.CollectionCount(2)} }, new JsonSerializerOptions {WriteIndented=true}));
|
||||
}
|
||||
static async Task Client(string endpoint)
|
||||
{
|
||||
using var client = new OpcUaClientService();
|
||||
await Case("trusted-SignAndEncrypt-API-key", async () => { var info = await client.ConnectAsync(Settings(endpoint), Ct); Require(info.SecurityMode == "SignAndEncrypt" && info.SecurityPolicyUri == SecurityPolicies.Basic256Sha256, "Unexpected negotiated policy"); Observations.Add(new { info.SecurityMode, info.SecurityPolicyUri }); });
|
||||
if (!client.IsConnected) return;
|
||||
var namespaces = (string[])Clr(await client.ReadValueAsync(VariableIds.Server_NamespaceArray, Ct))!;
|
||||
var raw = (ushort)Array.IndexOf(namespaces, RawUri); var uns = (ushort)Array.IndexOf(namespaces, UnsUri);
|
||||
NodeId ScalarId(ushort ns) => new(ns == raw ? Scalar : "Eq/Scalar", ns);
|
||||
NodeId VectorId(ushort ns) => new(ns == raw ? Vector : "Eq/Vector", ns);
|
||||
await Case("raw-UNS-URI-browse", async () =>
|
||||
{
|
||||
Require(raw > 0 && uns > 0, "Missing namespace URI");
|
||||
var rawChildren = await client.BrowseAsync(new NodeId(Folder, raw), Ct);
|
||||
var unsChildren = await client.BrowseAsync(new NodeId("Eq", uns), Ct);
|
||||
Observations.Add(new { rawChildren, unsChildren });
|
||||
// Supplement the production Shared results with unfiltered wire-reference metadata.
|
||||
var adapter = typeof(OpcUaClientService).GetField("_session", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(client)!;
|
||||
var sdkSession = (Opc.Ua.Client.Session)adapter.GetType().GetField("_session", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(adapter)!;
|
||||
var (_, _, rawReferences) = await sdkSession.BrowseAsync(null, null, new NodeId(Folder, raw), 0u,
|
||||
BrowseDirection.Forward, ReferenceTypeIds.References, true, 0u);
|
||||
Observations.Add(new { rawReferences = (rawReferences.ToArray() ?? []).Select(reference => new { nodeId = reference.NodeId.ToString(), browseName = reference.BrowseName.ToString(), nodeClass = reference.NodeClass.ToString(), typeDefinition = reference.TypeDefinition.ToString(), referenceType = reference.ReferenceTypeId.ToString(), reference.IsForward }).ToArray() });
|
||||
var expectedRaw = new[] { Scalar, Vector, Alarm, Folder + "/UnsetTimestamp" }.Select(id => new NodeId(id, raw).ToString());
|
||||
Require(expectedRaw.All(id => rawChildren.Any(child => child.NodeId == id)), "Missing raw children: " + string.Join(",", expectedRaw.Where(id => !rawChildren.Any(child => child.NodeId == id))));
|
||||
Require(new[] { "Eq/Scalar", "Eq/Vector" }.Select(id => new NodeId(id, uns).ToString()).All(id => unsChildren.Any(child => child.NodeId == id)), "Missing UNS scalar/array children");
|
||||
});
|
||||
await Case("initial-quality-explicit-source-and-server-timestamps", async () => { var initial = await client.ReadValueAsync(ScalarId(raw), Ct); Require(StatusCode.IsGood(initial.StatusCode), "Bad initial quality"); Require(SourceTime(initial) == Epoch && ServerTime(initial) > Epoch, "Lost explicit initial timestamps"); Observations.Add(new { sourceTimestamp = SourceTime(initial).ToString("O"), serverTimestamp = ServerTime(initial).ToString("O"), contract = "Source supplied as 2026-10-01T00:00:00Z; server timestamp observed without normalization or historian algorithm changes" }); });
|
||||
foreach (var ns in new[] { raw, uns })
|
||||
{
|
||||
await Case($"scalar-read-write-ns-{ns}", async () => { Require(Clr(await client.ReadValueAsync(ScalarId(ns), Ct)) is int, "Scalar lost Int32 type"); Require(StatusCode.IsGood(await client.WriteValueAsync(ScalarId(ns), 42, Ct)), "Scalar write failed"); Require((int)Clr(await client.ReadValueAsync(ScalarId(ns), Ct))! == 42, "Scalar round trip failed"); });
|
||||
await Case($"typed-array-read-write-ns-{ns}", async () => { Require(Clr(await client.ReadValueAsync(VectorId(ns), Ct)) is int[], "Array lost Int32[] type"); Require(StatusCode.IsGood(await client.WriteValueAsync(VectorId(ns), new[] { 7, 8, 9 }, Ct)), "Array write failed"); Require(((int[])Clr(await client.ReadValueAsync(VectorId(ns), Ct))!).SequenceEqual(new[] { 7, 8, 9 }), "Array round trip failed"); });
|
||||
}
|
||||
await Case("monitor-quality-timestamps", async () => { var seen = new ConcurrentQueue<DataValue>(); client.DataChanged += (_, e) => seen.Enqueue(e.Value); await client.SubscribeAsync(ScalarId(raw), 100, Ct); await client.WriteValueAsync(ScalarId(raw), 73, Ct); await Wait(() => seen.Any(v => Equals(Clr(v), 73))); var v = seen.Last(v => Equals(Clr(v), 73)); Require(StatusCode.IsGood(v.StatusCode) && SourceTime(v) > Epoch && ServerTime(v) > Epoch, "Bad monitor quality or missing timestamps"); Observations.Add(new { sourceTimestamp = SourceTime(v).ToString("O"), serverTimestamp = ServerTime(v).ToString("O"), status = v.StatusCode.ToString() }); await client.UnsubscribeAsync(ScalarId(raw), Ct); });
|
||||
await Case("event-filter-and-native-ack", async () => { var alarms = new ConcurrentQueue<AlarmEventArgs>(); client.AlarmEvent += (_, a) => alarms.Enqueue(a); await client.SubscribeAlarmsAsync(ObjectIds.Server, 100, Ct); await Wait(() => alarms.Any(a => a.Message == "matrix-alarm" && a.ActiveState && a.Severity == 800)); var alarm = alarms.Last(a => a.Message == "matrix-alarm"); Require(alarm.EventId?.Length > 0, "Missing event ID"); Require(StatusCode.IsGood(await client.AcknowledgeAlarmAsync(new NodeId(Alarm, raw).ToString(), alarm.EventId!, "matrix-ack", Ct)), "Ack refused"); await Wait(() => alarms.Any(a => a.Message == "matrix-alarm" && a.AckedState)); await client.UnsubscribeAlarmsAsync(Ct); });
|
||||
await Case("raw-history-CP-paging", async () => { var values = await client.HistoryReadRawAsync(ScalarId(raw), Epoch, Epoch.AddMinutes(1), 100, Ct); Require(values.Count == 25, "Expected 25 raw samples, got " + values.Count); Require(values.Select(v => Convert.ToInt32(Clr(v))).SequenceEqual(Enumerable.Range(0, 25)), "Raw page loss/duplicates/order"); });
|
||||
await Case("event-history-CP-paging", async () =>
|
||||
{
|
||||
var filter = new EventFilter();
|
||||
#if SDK2
|
||||
filter.SelectClauses += new SimpleAttributeOperand(ObjectTypeIds.BaseEventType, new QualifiedName("EventId")) { AttributeId = Attributes.Value };
|
||||
#else
|
||||
filter.SelectClauses.Add(new SimpleAttributeOperand {TypeDefinitionId=ObjectTypeIds.BaseEventType,BrowsePath=new QualifiedNameCollection {new QualifiedName("EventId")},AttributeId=Attributes.Value});
|
||||
#endif
|
||||
var events = await client.HistoryReadEventsAsync(new NodeId(Folder, raw), Epoch, Epoch.AddMinutes(1), filter, 100, Ct); Require(events.Count == 25, "Expected 25 event samples, got " + events.Count);
|
||||
#if SDK2
|
||||
var ids = events.Select(e => System.Text.Encoding.UTF8.GetString((byte[])ZB.MOM.WW.OtOpcUa.Client.Shared.Helpers.ValueConverter.ToClrObject(e.Fields[0])!));
|
||||
#else
|
||||
var ids=events.Select(e=>System.Text.Encoding.UTF8.GetString((byte[])e.Fields[0].Value));
|
||||
#endif
|
||||
Require(ids.SequenceEqual(Enumerable.Range(0, 25).Select(i => $"e{i:00}")), "Event history loss/duplicates/order across timestamp ties");
|
||||
});
|
||||
await Case("event-history-where-filter-positive-and-negative", async () =>
|
||||
{
|
||||
foreach (var type in new[] { ObjectTypeIds.BaseEventType, ObjectTypeIds.AuditEventType })
|
||||
{
|
||||
var filter = new EventFilter();
|
||||
#if SDK2
|
||||
filter.SelectClauses += new SimpleAttributeOperand(ObjectTypeIds.BaseEventType, new QualifiedName("EventId")) { AttributeId = Attributes.Value };
|
||||
filter.WhereClause.Push(FilterOperator.OfType, Variant.From(type));
|
||||
#else
|
||||
filter.SelectClauses.Add(new SimpleAttributeOperand(ObjectTypeIds.BaseEventType,new QualifiedName("EventId")){AttributeId=Attributes.Value});
|
||||
filter.WhereClause.Push(FilterOperator.OfType,new LiteralOperand(new Variant(type)));
|
||||
#endif
|
||||
var events = await client.HistoryReadEventsAsync(new NodeId(Folder, raw), Epoch, Epoch.AddMinutes(1), filter, 100, Ct);
|
||||
Require(events.Count == (type == ObjectTypeIds.BaseEventType ? 25 : 0), $"Where filter for {type} expected {(type == ObjectTypeIds.BaseEventType ? 25 : 0)} got {events.Count}");
|
||||
}
|
||||
});
|
||||
var unset = await client.ReadValueAsync(new NodeId(Folder + "/UnsetTimestamp", raw), Ct);
|
||||
Observations.Add(new { sourceTimestamp = SourceTime(unset).ToString("O"), serverTimestamp = ServerTime(unset).ToString("O"), timestampObservation = "Fixture source supplied DateTime.MinValue (unset). These are unmodified application/SDK wire observations; no historian algorithm or expected timestamp was changed." });
|
||||
await Benchmark(client, ScalarId(raw), VectorId(raw));
|
||||
await client.DisconnectAsync(Ct);
|
||||
}
|
||||
static OpcUaClientDriverOptions DriverOptions(string endpoint) => new()
|
||||
{
|
||||
EndpointUrl = endpoint,
|
||||
SecurityPolicy = OpcUaSecurityPolicy.Basic256Sha256,
|
||||
SecurityMode = OpcUaSecurityMode.SignAndEncrypt,
|
||||
AuthType = OpcUaAuthType.Username,
|
||||
Username = "apikey",
|
||||
Password = Password,
|
||||
AutoAcceptCertificates = false,
|
||||
RawTags = [new("scalar",JsonSerializer.Serialize(new {nodeId=$"nsu={RawUri};s={Scalar}"}),true),
|
||||
new("vector",JsonSerializer.Serialize(new {nodeId=$"nsu={RawUri};s={Vector}"}),true),
|
||||
new("uns",JsonSerializer.Serialize(new {nodeId=$"nsu={UnsUri};s=Eq/Scalar"}),true)],
|
||||
SessionName = "MatrixDriver",
|
||||
ApplicationUri = "urn:localhost:MatrixDriver"
|
||||
};
|
||||
static string IsolatedLocalData()
|
||||
{
|
||||
var local = Path.GetFullPath(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
|
||||
Require(local.StartsWith(Path.GetFullPath(Root) + Path.DirectorySeparatorChar, StringComparison.Ordinal), "Driver/CLI canonical PKI must be in task-owned root: " + local);
|
||||
return local;
|
||||
}
|
||||
static async Task PrepareDriver()
|
||||
{
|
||||
var local = IsolatedLocalData();
|
||||
await using var driver = new OpcUaClientDriver(DriverOptions("opc.tcp://127.0.0.1:1"), "matrix");
|
||||
// The production configuration method creates the application's own certificate without
|
||||
// attempting a session. Reflection is confined to preparation; all wire operations below
|
||||
// call the unchanged public driver API.
|
||||
var type = typeof(OpcUaClientDriver);
|
||||
var hook = Activator.CreateInstance(type.GetNestedType("CertValidationHook", BindingFlags.NonPublic)!, true)!;
|
||||
await (Task<ApplicationConfiguration>)type.GetMethod("BuildApplicationConfigurationAsync", BindingFlags.Instance | BindingFlags.NonPublic)!.Invoke(driver, [hook, Ct])!;
|
||||
File.WriteAllText(Path.Combine(Root, "driver-pki.txt"), Path.Combine(local, "OtOpcUa", "pki"));
|
||||
}
|
||||
static async Task Driver(string endpoint)
|
||||
{
|
||||
IsolatedLocalData();
|
||||
await using var driver = new OpcUaClientDriver(DriverOptions(endpoint), "matrix");
|
||||
await Case("driver-trusted-encrypted-API-key-connect", () => driver.InitializeAsync("{}", Ct));
|
||||
await Case("driver-raw-UNS-URI-scalar-and-array-read", async () =>
|
||||
{
|
||||
var values = await driver.ReadAsync(["scalar", "vector", "uns"], Ct);
|
||||
Require(values.Count == 3 && values.All(x => x.StatusCode == 0), "Read failure");
|
||||
Require(values[0].Value is int && values[1].Value is int[] && values[2].Value is int, "Lost scalar/array type");
|
||||
});
|
||||
await Case("driver-scalar-array-write-roundtrip", async () =>
|
||||
{
|
||||
var writes = await driver.WriteAsync([new WriteRequest("scalar", 91), new WriteRequest("vector", new[] { 11, 12, 13 })], Ct);
|
||||
Require(writes.All(x => x.StatusCode == 0), "Write failed");
|
||||
var values = await driver.ReadAsync(["scalar", "vector"], Ct);
|
||||
Require(Equals(values[0].Value, 91) && ((int[])values[1].Value!).SequenceEqual(new[] { 11, 12, 13 }), "Write round trip mismatch");
|
||||
});
|
||||
await Case("driver-monitor-quality-timestamps", async () =>
|
||||
{
|
||||
var seen = new ConcurrentQueue<DataValueSnapshot>();
|
||||
driver.OnDataChange += (_, e) => seen.Enqueue(e.Snapshot);
|
||||
var subscription = await driver.SubscribeAsync(["scalar"], TimeSpan.FromMilliseconds(100), Ct);
|
||||
await driver.WriteAsync([new WriteRequest("scalar", 92)], Ct);
|
||||
await Wait(() => seen.Any(x => Equals(x.Value, 92)));
|
||||
var value = seen.Last(x => Equals(x.Value, 92));
|
||||
Require(value.StatusCode == 0 && value.SourceTimestampUtc.HasValue && value.ServerTimestampUtc > Epoch, "Missing monitor quality/timestamps");
|
||||
Observations.Add(new { value.SourceTimestampUtc, value.ServerTimestampUtc, value.StatusCode });
|
||||
await driver.UnsubscribeAsync(subscription, Ct);
|
||||
});
|
||||
Observations.Add(new { capability = "driver-history", status = "not-applicable", evidence = "Driver has no IHistoryProvider/HistoryRead capability; .claude/rules/drivers.md #655. Shared/CLI own history." });
|
||||
await driver.ShutdownAsync(Ct);
|
||||
}
|
||||
static async Task Performance(string endpoint)
|
||||
{
|
||||
using var client = new OpcUaClientService();
|
||||
var info = await client.ConnectAsync(Settings(endpoint), Ct);
|
||||
Require(info.SecurityMode == "SignAndEncrypt" && info.SecurityPolicyUri == SecurityPolicies.Basic256Sha256, "Unexpected benchmark endpoint policy");
|
||||
var namespaces = (string[])Clr(await client.ReadValueAsync(VariableIds.Server_NamespaceArray, Ct))!;
|
||||
var raw = (ushort)Array.IndexOf(namespaces, RawUri);
|
||||
Require(raw > 0, "Missing benchmark raw namespace");
|
||||
Observations.Add(new {info.SecurityMode, info.SecurityPolicyUri, tieredCompilation=Environment.GetEnvironmentVariable("DOTNET_TieredCompilation"), scope="Two scalar/Int32[3] variables; zero data monitors during measurement; five fixture variables and one native condition"});
|
||||
await Case("warmed-production-client-read-write-measurement", () => Benchmark(client, new NodeId(Scalar, raw), new NodeId(Vector, raw)));
|
||||
await client.DisconnectAsync(Ct);
|
||||
}
|
||||
static async Task Benchmark(OpcUaClientService client, NodeId scalar, NodeId vector)
|
||||
{
|
||||
// Local diagnostic subset; excludes reconnect, history and load.
|
||||
var rounds = int.Parse(Environment.GetEnvironmentVariable("OPCUA_MATRIX_PERF_ROUNDS") ?? "3");
|
||||
var warmups = int.Parse(Environment.GetEnvironmentVariable("OPCUA_MATRIX_PERF_WARMUPS") ?? "10");
|
||||
var measured = int.Parse(Environment.GetEnvironmentVariable("OPCUA_MATRIX_PERF_MEASURED") ?? "50");
|
||||
Require(rounds > 0 && warmups >= 0 && measured > 0, "Invalid performance sample counts");
|
||||
for (int round = 1; round <= rounds; round++)
|
||||
{
|
||||
for (int warmup = 0; warmup < warmups; warmup++)
|
||||
{
|
||||
await client.ReadValueAsync(scalar, Ct); await client.WriteValueAsync(scalar, warmup, Ct);
|
||||
await client.ReadValueAsync(vector, Ct); await client.WriteValueAsync(vector, new[] { warmup, warmup + 1, warmup + 2 }, Ct);
|
||||
}
|
||||
var samples = new Dictionary<string, List<double>>();
|
||||
var before = System.Diagnostics.Process.GetCurrentProcess(); var cpu = before.TotalProcessorTime;
|
||||
var allocated = GC.GetTotalAllocatedBytes(); var gc = new[] { GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2) };
|
||||
foreach (var operation in new[] { "scalar-read", "scalar-write", "array-read", "array-write" })
|
||||
{
|
||||
var times = new List<double>();
|
||||
for (int i = 0; i < measured; i++)
|
||||
{
|
||||
var start = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
if (operation == "scalar-read") await client.ReadValueAsync(scalar, Ct);
|
||||
else if (operation == "scalar-write") Require(StatusCode.IsGood(await client.WriteValueAsync(scalar, i, Ct)), "Benchmark scalar write");
|
||||
else if (operation == "array-read") await client.ReadValueAsync(vector, Ct);
|
||||
else Require(StatusCode.IsGood(await client.WriteValueAsync(vector, new[] { i, i + 1, i + 2 }, Ct)), "Benchmark array write");
|
||||
times.Add(System.Diagnostics.Stopwatch.GetElapsedTime(start).TotalMilliseconds);
|
||||
}
|
||||
samples[operation] = times;
|
||||
}
|
||||
before.Refresh();
|
||||
Observations.Add(new
|
||||
{
|
||||
benchmarkRound = round,
|
||||
warmupOperationsPerKind = 10,
|
||||
operationsPerKind = 50,
|
||||
latencyMilliseconds = samples.ToDictionary(x => x.Key, x => new { p50 = x.Value.Order().ElementAt((int)Math.Ceiling(measured * 0.50) - 1), p95 = x.Value.Order().ElementAt((int)Math.Ceiling(measured * 0.95) - 1), p99 = x.Value.Order().ElementAt((int)Math.Ceiling(measured * 0.99) - 1), throughputOperationsPerSecond = measured * 1000.0 / x.Value.Sum() }),
|
||||
clientCpuMilliseconds = (before.TotalProcessorTime - cpu).TotalMilliseconds,
|
||||
clientAllocatedBytes = GC.GetTotalAllocatedBytes() - allocated, clientAllocatedBytesPerOperation = (GC.GetTotalAllocatedBytes() - allocated) / (4.0 * measured),
|
||||
clientWorkingSetBytes = before.WorkingSet64,
|
||||
clientPrivateBytes = before.PrivateMemorySize64,
|
||||
clientThreads = before.Threads.Count,
|
||||
clientHandles = before.HandleCount,
|
||||
collections = new[] { GC.CollectionCount(0) - gc[0], GC.CollectionCount(1) - gc[1], GC.CollectionCount(2) - gc[2] }
|
||||
});
|
||||
}
|
||||
}
|
||||
sealed class Auth : IOpcUaUserAuthenticator
|
||||
{
|
||||
public Task<OpcUaUserAuthResult> AuthenticateUserNameAsync(string username, string password, CancellationToken ct) => Task.FromResult(username == "apikey" && password == Password ? OpcUaUserAuthResult.AllowKey("matrix", [OpcUaDataPlaneRoles.WriteOperate, OpcUaDataPlaneRoles.AlarmAck], "matrix") : OpcUaUserAuthResult.Deny("Invalid ephemeral fixture key"));
|
||||
}
|
||||
sealed class Gateway : IOpcUaNodeWriteGateway { public Task<NodeWriteOutcome> WriteAsync(string id, object? value, AddressSpaceRealm realm, CancellationToken ct) => Task.FromResult(new NodeWriteOutcome(true, null)); }
|
||||
sealed class Source(string root) : IHistorianDataSource
|
||||
{
|
||||
void Log(string operation, int rows) => File.AppendAllText(Path.Combine(root, "history-requests.jsonl"), JsonSerializer.Serialize(new { operation, rows }) + "\n");
|
||||
public int MaxRawValuesPerRead => 7;
|
||||
public Task<CoreHistory> ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct)
|
||||
{ var all = Enumerable.Range(0, 25).Select(i => new DataValueSnapshot(i, 0, Epoch.AddSeconds(i), Epoch.AddSeconds(i))).Where(v => v.SourceTimestampUtc >= s && v.SourceTimestampUtc < e).ToList(); var rows = all.Take((int)(m == 0 ? 7 : m)).ToArray(); Log("raw", rows.Length); return Task.FromResult(new CoreHistory(rows, all.Count > rows.Length ? [1] : null)); }
|
||||
public Task<HistoricalEventsResult> ReadEventsAsync(string? source, DateTime s, DateTime e, int max, CancellationToken ct)
|
||||
{ var all = Enumerable.Range(0, 25).Select(i => new HistoricalEvent($"e{i:00}", "matrix", Epoch.AddSeconds(i / 2), Epoch, "matrix-history", 500)).Where(v => v.EventTimeUtc >= s && v.EventTimeUtc < e).ToList(); var rows = all.Take(Math.Min(7, max <= 0 ? 7 : max)).ToArray(); Log("events", rows.Length); return Task.FromResult(new HistoricalEventsResult(rows, all.Count > rows.Length ? [1] : null)); }
|
||||
public Task<CoreHistory> ReadProcessedAsync(string r, DateTime s, DateTime e, TimeSpan i, HistoryAggregateType a, CancellationToken ct) => NullHistorianDataSource.Instance.ReadProcessedAsync(r, s, e, i, a, ct);
|
||||
public Task<CoreHistory> ReadAtTimeAsync(string r, IReadOnlyList<DateTime> t, CancellationToken ct) => NullHistorianDataSource.Instance.ReadAtTimeAsync(r, t, ct);
|
||||
public HistorianHealthSnapshot GetHealthSnapshot() => NullHistorianDataSource.Instance.GetHealthSnapshot();
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
# OPC UA SDK process compatibility
|
||||
|
||||
Run from a dedicated candidate checkout with .NET 10, Python 3, PowerShell 7,
|
||||
and Docker. Supply the unchanged original application's existing `host/` and
|
||||
`client/` publish directories. The output directory must not already exist.
|
||||
|
||||
```powershell
|
||||
./scripts/e2e/test-opcua-sdk-compatibility.ps1 `
|
||||
-BaselineRoot /absolute/original-checkout `
|
||||
-BaselineArtifacts /absolute/original-publishes `
|
||||
-CandidateRoot /absolute/dedicated-candidate-checkout `
|
||||
-OutputDirectory /absolute/fresh-evidence-directory
|
||||
```
|
||||
|
||||
The baseline console references the exact published production assemblies;
|
||||
it does not rebuild or edit the original application. The candidate console
|
||||
references the production host, Client.Shared and OPC UA driver projects, and
|
||||
the candidate CLI is separately published. Different SDK versions always run
|
||||
in different processes. Source revisions, SDK assemblies, SHA-256 hashes,
|
||||
commands, per-case results, actual namespace URIs, negotiated Shared security,
|
||||
loaded client SDK assemblies, timestamps and backend request logs are retained.
|
||||
|
||||
The required Shared legs are original→candidate, candidate→original and
|
||||
candidate→candidate. A fourth original→original leg provides a reference for
|
||||
both the application contract and the small performance sample. Every Shared
|
||||
leg checks the exact scalar, array and condition browse identities, both
|
||||
namespaces by URI, typed scalar/Int32[3] read/write, monitored Good quality and
|
||||
source/server timestamps, the production alarm event filter and native
|
||||
acknowledgment, and complete raw/event history across continuation points.
|
||||
Event-history results include timestamp ties, exact IDs/order and positive and
|
||||
negative `OfType` predicates. The historian is a deterministic fixture behind
|
||||
the production server's session-bound continuation store; these results do
|
||||
not certify any SQL Server, QuestDB or AVEVA backend.
|
||||
|
||||
Server, Shared client and driver require trusted Basic256Sha256
|
||||
SignAndEncrypt peers, with certificate autoaccept disabled. Public certificates
|
||||
are exchanged before sessions open. The test authenticator accepts one exact,
|
||||
random, API-key-shaped credential from the process environment; production
|
||||
ACL, write and condition-method checks use the resulting key identity. This
|
||||
fixture does not replace cryptographic API-key-authenticator coverage.
|
||||
|
||||
The driver and actual CLI run in a disposable `--network none` Linux container,
|
||||
alongside a separate baseline or candidate server process. PKI lives only in
|
||||
the mounted task directory; no host profile PKI or fleet endpoint is touched.
|
||||
The driver hardcodes `LocalApplicationData/OtOpcUa/pki`, so Linux's task-local
|
||||
`XDG_DATA_HOME` supplies isolation. The helper refuses a driver store outside
|
||||
its task root. CLI uses its own task-local canonical store. CLI currently
|
||||
forces `AutoAcceptCertificates=true` and exposes no strict-validation flag
|
||||
(`CommandBase.CreateConnectionSettings`); the exchanged peers and encrypted
|
||||
server remain in use, but CLI is not counted as strict client validation.
|
||||
Driver history is N/A because the application removed that capability (#655);
|
||||
Shared and CLI execute history reads instead.
|
||||
|
||||
Three measured rounds follow ten warmup operations per kind in each round:
|
||||
50 scalar reads, scalar writes, array reads and array writes per kind. The
|
||||
sample records nearest-rank p50/p95/p99, sequential throughput, client CPU,
|
||||
allocated bytes, GC collections, working set, private bytes, threads and
|
||||
handles. This is a small local diagnostic on a shared development host. It
|
||||
has no production fan-out, reconnect, queue, backpressure, backend or soak
|
||||
performance claim and does not establish Gate G4.
|
||||
|
||||
A separately published `server` helper can be used by other local consumer
|
||||
checks. Supply a fresh directory and `OPCUA_MATRIX_PASSWORD` in the process
|
||||
environment, then launch `dotnet ProcessHost.dll server <directory>`.
|
||||
`ready.json`/`endpoint.txt` identify the ephemeral loopback port; touching
|
||||
`<directory>/stop` shuts it down. Its default is strict SignAndEncrypt only.
|
||||
`OPCUA_MATRIX_SERVER_SECURITY=none` is an explicit fixture-only override for
|
||||
checking an existing unsecured consumer subset; the matrix runner clears that
|
||||
override. Anonymous is disabled in either mode. Secure clients must place
|
||||
their public certificates under `server-pki/trusted/certs` before server start
|
||||
and trust the resulting `server-pki/own/certs` certificate.
|
||||
|
||||
Explicitly supplied timestamps are asserted. Unset source timestamps are
|
||||
recorded separately as observations, with their original application/SDK wire
|
||||
values. The harness does not change historian algorithms or normalize the
|
||||
unset-timestamp sentinel to manufacture parity.
|
||||
|
||||
For the supplementary sustained warmup experiment, use the built consoles from
|
||||
one completed matrix run:
|
||||
|
||||
```sh
|
||||
python3 scripts/validation/opcua-sdk-compatibility/performance.py \
|
||||
--baseline-harness /absolute/run/build-baseline/bin/Release/net10.0/ProcessHost.dll \
|
||||
--candidate-harness /absolute/run/build-candidate/bin/Release/net10.0/ProcessHost.dll \
|
||||
--output /absolute/fresh-performance-directory
|
||||
```
|
||||
|
||||
This runs three fresh baseline/candidate process pairs serially, with 1,000
|
||||
warmup and 2,000 measured operations per kind in each process. Both server and
|
||||
client receive `DOTNET_TieredCompilation=0` for this experiment only; the
|
||||
application's shipped configuration is unchanged. The recorded budget is a
|
||||
15% increase in the median of the three p95 samples for any operation kind.
|
||||
Exit code 2 means a diagnostic budget regression, while process failures
|
||||
remain errors. Even an experiment within that budget does not certify the
|
||||
complete production performance matrix. The allocation-per-operation field
|
||||
is the average across all four operation kinds in a measured workload.
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs only inside the disposable, network-isolated task container.
|
||||
set -euo pipefail
|
||||
mkdir -p /task/driver/data /task/cli/data /task/server
|
||||
XDG_DATA_HOME=/task/driver/data dotnet /candidate/ProcessHost.dll prepare-driver /task/driver > /task/driver-prepare.log 2>&1
|
||||
OPCUA_MATRIX_CLIENT_PKI=/task/cli/data/OtOpcUaClient/pki XDG_DATA_HOME=/task/cli/data dotnet /candidate/ProcessHost.dll prepare /task/cli > /task/cli-prepare.log 2>&1
|
||||
mkdir -p /task/server/server-pki/trusted/certs
|
||||
cp /task/driver/data/OtOpcUa/pki/own/certs/* /task/server/server-pki/trusted/certs/
|
||||
cp /task/cli/data/OtOpcUaClient/pki/own/certs/* /task/server/server-pki/trusted/certs/
|
||||
dotnet /server/ProcessHost.dll server /task/server > /task/server.log 2>&1 &
|
||||
server_pid=$!
|
||||
cleanup() {
|
||||
touch /task/server/stop
|
||||
wait "$server_pid" || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
for attempt in $(seq 1 600); do
|
||||
test ! -f /task/server/ready.json || break
|
||||
kill -0 "$server_pid" || exit 1
|
||||
sleep 0.1
|
||||
done
|
||||
test -f /task/server/ready.json
|
||||
endpoint=$(cat /task/server/endpoint.txt)
|
||||
mkdir -p /task/driver/data/OtOpcUa/pki/trusted/certs /task/cli/data/OtOpcUaClient/pki/trusted/certs
|
||||
cp /task/server/server-pki/own/certs/* /task/driver/data/OtOpcUa/pki/trusted/certs/
|
||||
cp /task/server/server-pki/own/certs/* /task/cli/data/OtOpcUaClient/pki/trusted/certs/
|
||||
result=0
|
||||
XDG_DATA_HOME=/task/driver/data dotnet /candidate/ProcessHost.dll driver /task/driver "$endpoint" > /task/driver.log 2>&1 || result=1
|
||||
# CLI currently autoaccepts by product design; it has no strict-validation flag.
|
||||
# Its public certificates are nevertheless explicitly exchanged in this fixture.
|
||||
XDG_DATA_HOME=/task/cli/data dotnet /cli/ZB.MOM.WW.OtOpcUa.Client.CLI.dll read --url "$endpoint" --security signandencrypt --username apikey --password "$OPCUA_MATRIX_PASSWORD" --node 'ns=2;s=Matrix/dev/Scalar' > /task/cli-read.log 2>&1 || result=1
|
||||
XDG_DATA_HOME=/task/cli/data dotnet /cli/ZB.MOM.WW.OtOpcUa.Client.CLI.dll historyread --url "$endpoint" --security signandencrypt --username apikey --password "$OPCUA_MATRIX_PASSWORD" --node 'ns=2;s=Matrix/dev/Scalar' --start 2026-10-01T00:00:00Z --end 2026-10-01T00:01:00Z --max 100 > /task/cli-raw-history.log 2>&1 || result=1
|
||||
XDG_DATA_HOME=/task/cli/data dotnet /cli/ZB.MOM.WW.OtOpcUa.Client.CLI.dll historyread --url "$endpoint" --security signandencrypt --username apikey --password "$OPCUA_MATRIX_PASSWORD" --node 'ns=2;s=Matrix/dev' --start 2026-10-01T00:00:00Z --end 2026-10-01T00:01:00Z --max 100 --events --fields EventId,Message > /task/cli-event-history.log 2>&1 || result=1
|
||||
exit "$result"
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Id": "sha256:b04611ee9e1b7d36797012ed921962c57189cb4dbb3dff12afb8e158110f293b",
|
||||
"RepoDigests": [
|
||||
"mcr.microsoft.com/dotnet/sdk@sha256:e362a8dbcd691522456da26a5198b8f3ca1d7641c95624fadc5e3e82678bd08a"
|
||||
],
|
||||
"Architecture": "arm64",
|
||||
"Os": "linux"
|
||||
}
|
||||
-534
@@ -1,534 +0,0 @@
|
||||
{
|
||||
"scope": "Early short3round,10warmup+50measured/kind, shared-host diagnostic only. This source run had insufficient alarm browse assertions and is superseded as functional evidence by run-10. Preserve these original timing observations without claiming the earlier functional matrix passed the full browse contract.",
|
||||
"source": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-07/matrix.json",
|
||||
"measurements": [
|
||||
{
|
||||
"name": "baseline-client-to-baseline-server",
|
||||
"sdk": "1.5.378.0",
|
||||
"observations": [
|
||||
{
|
||||
"benchmarkRound": 1,
|
||||
"warmupOperationsPerKind": 10,
|
||||
"operationsPerKind": 50,
|
||||
"latencyMilliseconds": {
|
||||
"scalar-read": {
|
||||
"p50": 0.1949,
|
||||
"p95": 0.2802,
|
||||
"p99": 0.3099,
|
||||
"throughputOperationsPerSecond": 4941.39505465183
|
||||
},
|
||||
"scalar-write": {
|
||||
"p50": 0.2127,
|
||||
"p95": 0.2761,
|
||||
"p99": 0.4201,
|
||||
"throughputOperationsPerSecond": 4527.919149475669
|
||||
},
|
||||
"array-read": {
|
||||
"p50": 0.1511,
|
||||
"p95": 0.2183,
|
||||
"p99": 0.3511,
|
||||
"throughputOperationsPerSecond": 6323.190935073475
|
||||
},
|
||||
"array-write": {
|
||||
"p50": 0.1346,
|
||||
"p95": 0.2007,
|
||||
"p99": 0.2762,
|
||||
"throughputOperationsPerSecond": 6981.777560566919
|
||||
}
|
||||
},
|
||||
"clientCpuMilliseconds": 51.315,
|
||||
"clientAllocatedBytes": 1317880,
|
||||
"clientWorkingSetBytes": 134643712,
|
||||
"clientPrivateBytes": 0,
|
||||
"clientThreads": 18,
|
||||
"clientHandles": 0,
|
||||
"collections": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"benchmarkRound": 2,
|
||||
"warmupOperationsPerKind": 10,
|
||||
"operationsPerKind": 50,
|
||||
"latencyMilliseconds": {
|
||||
"scalar-read": {
|
||||
"p50": 0.1667,
|
||||
"p95": 0.2128,
|
||||
"p99": 0.2603,
|
||||
"throughputOperationsPerSecond": 5854.595271828857
|
||||
},
|
||||
"scalar-write": {
|
||||
"p50": 0.1705,
|
||||
"p95": 0.2398,
|
||||
"p99": 0.2455,
|
||||
"throughputOperationsPerSecond": 5681.043494068992
|
||||
},
|
||||
"array-read": {
|
||||
"p50": 0.1645,
|
||||
"p95": 0.2415,
|
||||
"p99": 7.8687,
|
||||
"throughputOperationsPerSecond": 3070.70607815561
|
||||
},
|
||||
"array-write": {
|
||||
"p50": 0.1646,
|
||||
"p95": 0.2192,
|
||||
"p99": 0.2517,
|
||||
"throughputOperationsPerSecond": 5982.650314089142
|
||||
}
|
||||
},
|
||||
"clientCpuMilliseconds": 45.783,
|
||||
"clientAllocatedBytes": 1318464,
|
||||
"clientWorkingSetBytes": 136298496,
|
||||
"clientPrivateBytes": 0,
|
||||
"clientThreads": 18,
|
||||
"clientHandles": 0,
|
||||
"collections": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"benchmarkRound": 3,
|
||||
"warmupOperationsPerKind": 10,
|
||||
"operationsPerKind": 50,
|
||||
"latencyMilliseconds": {
|
||||
"scalar-read": {
|
||||
"p50": 0.1606,
|
||||
"p95": 0.2057,
|
||||
"p99": 0.2141,
|
||||
"throughputOperationsPerSecond": 6152.7102688734385
|
||||
},
|
||||
"scalar-write": {
|
||||
"p50": 0.1645,
|
||||
"p95": 0.1888,
|
||||
"p99": 0.1986,
|
||||
"throughputOperationsPerSecond": 6050.338818973862
|
||||
},
|
||||
"array-read": {
|
||||
"p50": 0.1262,
|
||||
"p95": 0.1857,
|
||||
"p99": 2.3447,
|
||||
"throughputOperationsPerSecond": 5717.945199213212
|
||||
},
|
||||
"array-write": {
|
||||
"p50": 0.1297,
|
||||
"p95": 0.1718,
|
||||
"p99": 0.1826,
|
||||
"throughputOperationsPerSecond": 7597.05234369065
|
||||
}
|
||||
},
|
||||
"clientCpuMilliseconds": 43.613,
|
||||
"clientAllocatedBytes": 1296800,
|
||||
"clientWorkingSetBytes": 137920512,
|
||||
"clientPrivateBytes": 0,
|
||||
"clientThreads": 18,
|
||||
"clientHandles": 0,
|
||||
"collections": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "baseline-client-to-candidate-server",
|
||||
"sdk": "1.5.378.0",
|
||||
"observations": [
|
||||
{
|
||||
"benchmarkRound": 1,
|
||||
"warmupOperationsPerKind": 10,
|
||||
"operationsPerKind": 50,
|
||||
"latencyMilliseconds": {
|
||||
"scalar-read": {
|
||||
"p50": 0.2427,
|
||||
"p95": 0.2798,
|
||||
"p99": 0.3337,
|
||||
"throughputOperationsPerSecond": 4066.528404700906
|
||||
},
|
||||
"scalar-write": {
|
||||
"p50": 0.2384,
|
||||
"p95": 0.2922,
|
||||
"p99": 0.2955,
|
||||
"throughputOperationsPerSecond": 4237.647258242225
|
||||
},
|
||||
"array-read": {
|
||||
"p50": 0.2082,
|
||||
"p95": 0.2556,
|
||||
"p99": 0.3692,
|
||||
"throughputOperationsPerSecond": 4715.602040912563
|
||||
},
|
||||
"array-write": {
|
||||
"p50": 0.1895,
|
||||
"p95": 0.295,
|
||||
"p99": 0.3612,
|
||||
"throughputOperationsPerSecond": 4881.811347282295
|
||||
}
|
||||
},
|
||||
"clientCpuMilliseconds": 55.468,
|
||||
"clientAllocatedBytes": 1325592,
|
||||
"clientWorkingSetBytes": 134545408,
|
||||
"clientPrivateBytes": 0,
|
||||
"clientThreads": 19,
|
||||
"clientHandles": 0,
|
||||
"collections": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"benchmarkRound": 2,
|
||||
"warmupOperationsPerKind": 10,
|
||||
"operationsPerKind": 50,
|
||||
"latencyMilliseconds": {
|
||||
"scalar-read": {
|
||||
"p50": 0.2064,
|
||||
"p95": 0.293,
|
||||
"p99": 0.3812,
|
||||
"throughputOperationsPerSecond": 4710.004992605292
|
||||
},
|
||||
"scalar-write": {
|
||||
"p50": 0.2025,
|
||||
"p95": 0.3077,
|
||||
"p99": 0.4019,
|
||||
"throughputOperationsPerSecond": 4707.1671326762125
|
||||
},
|
||||
"array-read": {
|
||||
"p50": 0.1626,
|
||||
"p95": 0.2309,
|
||||
"p99": 0.2487,
|
||||
"throughputOperationsPerSecond": 5908.2100486836525
|
||||
},
|
||||
"array-write": {
|
||||
"p50": 0.1752,
|
||||
"p95": 0.243,
|
||||
"p99": 0.2937,
|
||||
"throughputOperationsPerSecond": 5501.336824848437
|
||||
}
|
||||
},
|
||||
"clientCpuMilliseconds": 45.733,
|
||||
"clientAllocatedBytes": 1326672,
|
||||
"clientWorkingSetBytes": 136249344,
|
||||
"clientPrivateBytes": 0,
|
||||
"clientThreads": 19,
|
||||
"clientHandles": 0,
|
||||
"collections": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"benchmarkRound": 3,
|
||||
"warmupOperationsPerKind": 10,
|
||||
"operationsPerKind": 50,
|
||||
"latencyMilliseconds": {
|
||||
"scalar-read": {
|
||||
"p50": 0.1973,
|
||||
"p95": 0.2547,
|
||||
"p99": 0.2738,
|
||||
"throughputOperationsPerSecond": 4988.62593287305
|
||||
},
|
||||
"scalar-write": {
|
||||
"p50": 0.2038,
|
||||
"p95": 0.2312,
|
||||
"p99": 0.2664,
|
||||
"throughputOperationsPerSecond": 4974.035534509858
|
||||
},
|
||||
"array-read": {
|
||||
"p50": 0.16,
|
||||
"p95": 0.1892,
|
||||
"p99": 2.4703,
|
||||
"throughputOperationsPerSecond": 4757.46446174047
|
||||
},
|
||||
"array-write": {
|
||||
"p50": 0.195,
|
||||
"p95": 0.2245,
|
||||
"p99": 0.2277,
|
||||
"throughputOperationsPerSecond": 5199.775369704029
|
||||
}
|
||||
},
|
||||
"clientCpuMilliseconds": 46.975,
|
||||
"clientAllocatedBytes": 1286632,
|
||||
"clientWorkingSetBytes": 137936896,
|
||||
"clientPrivateBytes": 0,
|
||||
"clientThreads": 19,
|
||||
"clientHandles": 0,
|
||||
"collections": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "candidate-client-to-baseline-server",
|
||||
"sdk": "2.0.1.0",
|
||||
"observations": [
|
||||
{
|
||||
"benchmarkRound": 1,
|
||||
"warmupOperationsPerKind": 10,
|
||||
"operationsPerKind": 50,
|
||||
"latencyMilliseconds": {
|
||||
"scalar-read": {
|
||||
"p50": 0.2241,
|
||||
"p95": 0.2949,
|
||||
"p99": 0.4007,
|
||||
"throughputOperationsPerSecond": 4283.2055510343935
|
||||
},
|
||||
"scalar-write": {
|
||||
"p50": 0.2197,
|
||||
"p95": 0.2889,
|
||||
"p99": 0.3367,
|
||||
"throughputOperationsPerSecond": 4384.887922264707
|
||||
},
|
||||
"array-read": {
|
||||
"p50": 0.1518,
|
||||
"p95": 0.2545,
|
||||
"p99": 0.3429,
|
||||
"throughputOperationsPerSecond": 6052.902366684823
|
||||
},
|
||||
"array-write": {
|
||||
"p50": 0.1445,
|
||||
"p95": 0.2006,
|
||||
"p99": 0.2097,
|
||||
"throughputOperationsPerSecond": 6667.466762678188
|
||||
}
|
||||
},
|
||||
"clientCpuMilliseconds": 63.388,
|
||||
"clientAllocatedBytes": 673504,
|
||||
"clientWorkingSetBytes": 128729088,
|
||||
"clientPrivateBytes": 0,
|
||||
"clientThreads": 20,
|
||||
"clientHandles": 0,
|
||||
"collections": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"benchmarkRound": 2,
|
||||
"warmupOperationsPerKind": 10,
|
||||
"operationsPerKind": 50,
|
||||
"latencyMilliseconds": {
|
||||
"scalar-read": {
|
||||
"p50": 0.1828,
|
||||
"p95": 0.2139,
|
||||
"p99": 0.2324,
|
||||
"throughputOperationsPerSecond": 5446.563762921972
|
||||
},
|
||||
"scalar-write": {
|
||||
"p50": 0.1544,
|
||||
"p95": 0.2086,
|
||||
"p99": 8.8284,
|
||||
"throughputOperationsPerSecond": 3001.903206633005
|
||||
},
|
||||
"array-read": {
|
||||
"p50": 0.1853,
|
||||
"p95": 0.227,
|
||||
"p99": 0.2436,
|
||||
"throughputOperationsPerSecond": 5281.448384404939
|
||||
},
|
||||
"array-write": {
|
||||
"p50": 0.19,
|
||||
"p95": 0.2219,
|
||||
"p99": 0.2799,
|
||||
"throughputOperationsPerSecond": 5251.163132633879
|
||||
}
|
||||
},
|
||||
"clientCpuMilliseconds": 63.984,
|
||||
"clientAllocatedBytes": 674328,
|
||||
"clientWorkingSetBytes": 129597440,
|
||||
"clientPrivateBytes": 0,
|
||||
"clientThreads": 20,
|
||||
"clientHandles": 0,
|
||||
"collections": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"benchmarkRound": 3,
|
||||
"warmupOperationsPerKind": 10,
|
||||
"operationsPerKind": 50,
|
||||
"latencyMilliseconds": {
|
||||
"scalar-read": {
|
||||
"p50": 0.1501,
|
||||
"p95": 0.1844,
|
||||
"p99": 0.196,
|
||||
"throughputOperationsPerSecond": 6574.016855779218
|
||||
},
|
||||
"scalar-write": {
|
||||
"p50": 0.1424,
|
||||
"p95": 0.1684,
|
||||
"p99": 0.1927,
|
||||
"throughputOperationsPerSecond": 6859.181013786954
|
||||
},
|
||||
"array-read": {
|
||||
"p50": 0.1601,
|
||||
"p95": 0.1993,
|
||||
"p99": 0.2257,
|
||||
"throughputOperationsPerSecond": 6130.155460742485
|
||||
},
|
||||
"array-write": {
|
||||
"p50": 0.1835,
|
||||
"p95": 0.2002,
|
||||
"p99": 0.2039,
|
||||
"throughputOperationsPerSecond": 5506.911173522769
|
||||
}
|
||||
},
|
||||
"clientCpuMilliseconds": 58.499,
|
||||
"clientAllocatedBytes": 674224,
|
||||
"clientWorkingSetBytes": 130433024,
|
||||
"clientPrivateBytes": 0,
|
||||
"clientThreads": 20,
|
||||
"clientHandles": 0,
|
||||
"collections": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "candidate-client-to-candidate-server",
|
||||
"sdk": "2.0.1.0",
|
||||
"observations": [
|
||||
{
|
||||
"benchmarkRound": 1,
|
||||
"warmupOperationsPerKind": 10,
|
||||
"operationsPerKind": 50,
|
||||
"latencyMilliseconds": {
|
||||
"scalar-read": {
|
||||
"p50": 0.2968,
|
||||
"p95": 0.3548,
|
||||
"p99": 0.4229,
|
||||
"throughputOperationsPerSecond": 3258.4115895183427
|
||||
},
|
||||
"scalar-write": {
|
||||
"p50": 0.2754,
|
||||
"p95": 0.3423,
|
||||
"p99": 0.4283,
|
||||
"throughputOperationsPerSecond": 3531.7718192862994
|
||||
},
|
||||
"array-read": {
|
||||
"p50": 0.2407,
|
||||
"p95": 0.2708,
|
||||
"p99": 0.3132,
|
||||
"throughputOperationsPerSecond": 4129.194229038145
|
||||
},
|
||||
"array-write": {
|
||||
"p50": 0.27,
|
||||
"p95": 0.3783,
|
||||
"p99": 0.3875,
|
||||
"throughputOperationsPerSecond": 3553.1047028893854
|
||||
}
|
||||
},
|
||||
"clientCpuMilliseconds": 73.878,
|
||||
"clientAllocatedBytes": 685360,
|
||||
"clientWorkingSetBytes": 128188416,
|
||||
"clientPrivateBytes": 0,
|
||||
"clientThreads": 19,
|
||||
"clientHandles": 0,
|
||||
"collections": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"benchmarkRound": 2,
|
||||
"warmupOperationsPerKind": 10,
|
||||
"operationsPerKind": 50,
|
||||
"latencyMilliseconds": {
|
||||
"scalar-read": {
|
||||
"p50": 0.2477,
|
||||
"p95": 0.2772,
|
||||
"p99": 0.2965,
|
||||
"throughputOperationsPerSecond": 4027.580873823946
|
||||
},
|
||||
"scalar-write": {
|
||||
"p50": 0.2425,
|
||||
"p95": 0.2885,
|
||||
"p99": 0.357,
|
||||
"throughputOperationsPerSecond": 4128.137384412154
|
||||
},
|
||||
"array-read": {
|
||||
"p50": 0.2063,
|
||||
"p95": 0.2431,
|
||||
"p99": 0.2776,
|
||||
"throughputOperationsPerSecond": 4806.07487864661
|
||||
},
|
||||
"array-write": {
|
||||
"p50": 0.261,
|
||||
"p95": 0.3474,
|
||||
"p99": 0.3773,
|
||||
"throughputOperationsPerSecond": 3704.060391000616
|
||||
}
|
||||
},
|
||||
"clientCpuMilliseconds": 68.193,
|
||||
"clientAllocatedBytes": 674904,
|
||||
"clientWorkingSetBytes": 129024000,
|
||||
"clientPrivateBytes": 0,
|
||||
"clientThreads": 19,
|
||||
"clientHandles": 0,
|
||||
"collections": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"benchmarkRound": 3,
|
||||
"warmupOperationsPerKind": 10,
|
||||
"operationsPerKind": 50,
|
||||
"latencyMilliseconds": {
|
||||
"scalar-read": {
|
||||
"p50": 0.247,
|
||||
"p95": 0.3,
|
||||
"p99": 0.3212,
|
||||
"throughputOperationsPerSecond": 4017.096763826847
|
||||
},
|
||||
"scalar-write": {
|
||||
"p50": 0.211,
|
||||
"p95": 0.2719,
|
||||
"p99": 0.2778,
|
||||
"throughputOperationsPerSecond": 4647.185664361664
|
||||
},
|
||||
"array-read": {
|
||||
"p50": 0.2318,
|
||||
"p95": 0.262,
|
||||
"p99": 0.291,
|
||||
"throughputOperationsPerSecond": 4246.789427193043
|
||||
},
|
||||
"array-write": {
|
||||
"p50": 0.2428,
|
||||
"p95": 0.2903,
|
||||
"p99": 0.3024,
|
||||
"throughputOperationsPerSecond": 4139.312708517878
|
||||
}
|
||||
},
|
||||
"clientCpuMilliseconds": 67.128,
|
||||
"clientAllocatedBytes": 682744,
|
||||
"clientWorkingSetBytes": 129843200,
|
||||
"clientPrivateBytes": 0,
|
||||
"clientThreads": 19,
|
||||
"clientHandles": 0,
|
||||
"collections": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
-792
@@ -1,792 +0,0 @@
|
||||
[
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/client/Opc.Ua.Client.dll",
|
||||
"sha256": "fd000b186f4bc5dbd9947e876f96c178e8c4259ee3e8717d672a4ac2f4c3a058",
|
||||
"bytes": 852096
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/client/Opc.Ua.Configuration.dll",
|
||||
"sha256": "30fb5142f13a3b3083c0484bcdedaec2efe2f50e57f10ec6a61257ca5359aa7e",
|
||||
"bytes": 85120
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/client/Opc.Ua.Core.dll",
|
||||
"sha256": "602e85316943ad7ebae33a23d60a69a6ea50d91250ad0556906b816435daa37c",
|
||||
"bytes": 7945344
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/client/Opc.Ua.Security.Certificates.dll",
|
||||
"sha256": "a2a05c64348f6f8aa60de0fe27d8e5e22d12e09b1ba6172ca34b3773620e394c",
|
||||
"bytes": 79488
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/client/Opc.Ua.Types.dll",
|
||||
"sha256": "c5c5e36cf6381444b408fe1b51726b17deca74173764cd62ccee954b3c8c2718",
|
||||
"bytes": 1127552
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/client/ZB.MOM.WW.OtOpcUa.Client.CLI.deps.json",
|
||||
"sha256": "8756470c547137f40ef9962dfa6f40c6dc2a1801cd397524dce7127fb79b70b9",
|
||||
"bytes": 12175
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/client/ZB.MOM.WW.OtOpcUa.Client.CLI.dll",
|
||||
"sha256": "38ff27df4c1389d2cf97a7ec1fe206733f02c3664b29c4fe18586ede225a53ca",
|
||||
"bytes": 91648
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/client/ZB.MOM.WW.OtOpcUa.Client.CLI.runtimeconfig.json",
|
||||
"sha256": "9950d4583cfc9a106857c0d5b8a75b53e8774852b57b80bc3823a74d44a4d87f",
|
||||
"bytes": 330
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/client/ZB.MOM.WW.OtOpcUa.Client.Shared.dll",
|
||||
"sha256": "326e14df7bf2a7602637fbff36988c3dd4be38de936da24a1b4287c0ab685be6",
|
||||
"bytes": 111616
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/Opc.Ua.Client.dll",
|
||||
"sha256": "fd000b186f4bc5dbd9947e876f96c178e8c4259ee3e8717d672a4ac2f4c3a058",
|
||||
"bytes": 852096
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/Opc.Ua.Configuration.dll",
|
||||
"sha256": "30fb5142f13a3b3083c0484bcdedaec2efe2f50e57f10ec6a61257ca5359aa7e",
|
||||
"bytes": 85120
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/Opc.Ua.Core.dll",
|
||||
"sha256": "602e85316943ad7ebae33a23d60a69a6ea50d91250ad0556906b816435daa37c",
|
||||
"bytes": 7945344
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/Opc.Ua.Security.Certificates.dll",
|
||||
"sha256": "a2a05c64348f6f8aa60de0fe27d8e5e22d12e09b1ba6172ca34b3773620e394c",
|
||||
"bytes": 79488
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/Opc.Ua.Server.dll",
|
||||
"sha256": "d8d9706dea577858325730cd89896826ed82f7bfcf9503ccd0260311663a69f4",
|
||||
"bytes": 642176
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/Opc.Ua.Types.dll",
|
||||
"sha256": "c5c5e36cf6381444b408fe1b51726b17deca74173764cd62ccee954b3c8c2718",
|
||||
"bytes": 1127552
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/OtOpcUa.Host.deps.json",
|
||||
"sha256": "f47fb25d83e40a5c1d96c063f1d63b02a04c222a10554220df58cac896654ea9",
|
||||
"bytes": 153279
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/OtOpcUa.Host.dll",
|
||||
"sha256": "e88fc71f59e69e15c3191a782c29ce44127e320c7560ddd26ce1759787806f98",
|
||||
"bytes": 193024
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/OtOpcUa.Host.runtimeconfig.json",
|
||||
"sha256": "eff38a9750f7cca2407b1d639a963d56ffea92088e6da971bbbfa74e3dcd7224",
|
||||
"bytes": 540
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.AdminUI.dll",
|
||||
"sha256": "1ae11959c22bc53a648b91c6c1ae41e4a5d717e396bd645eae4fb08dc1633597",
|
||||
"bytes": 2152448
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Cluster.dll",
|
||||
"sha256": "d6bc21641172d832004b62b8bb76aeb40622a943be2172f394e5c6333c04ef57",
|
||||
"bytes": 67584
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Commons.dll",
|
||||
"sha256": "be97a0b975afde7ec4ebf0b10de53c26f212ceca1f6a3ae090e40ebfde304413",
|
||||
"bytes": 231424
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Configuration.dll",
|
||||
"sha256": "195dab9f7a9f80c45902e653ff361116a307d8110a3f87baad814bea57689b84",
|
||||
"bytes": 542720
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.ControlPlane.dll",
|
||||
"sha256": "36954f51b09930df414675e5a0c038933ad2b27cd6560bf797d99801c02b30a1",
|
||||
"bytes": 179200
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Core.Abstractions.dll",
|
||||
"sha256": "b0f4e332ced57eae3ab1dcc282c073f51ea0c545a381aef50479ee58f0c23efc",
|
||||
"bytes": 124416
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.dll",
|
||||
"sha256": "a61a6ea68236dd4ed86ab4e74e4d632c5e260103aa5ebd92f4f995eb9f170b22",
|
||||
"bytes": 56320
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.dll",
|
||||
"sha256": "f1e429c0b8fcab8216b9107440008b92eea1236ff1225e79580ad70f51d56431",
|
||||
"bytes": 83968
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions.dll",
|
||||
"sha256": "ea24898698c8351bc9cf20fcce725dccc93ccf05ef3af7783f73854cc2033c31",
|
||||
"bytes": 13312
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Core.Scripting.OutOfProcess.dll",
|
||||
"sha256": "cf44009a720a00bf9192374d371c7c76e75afbe62cc216b5c1be913b92679127",
|
||||
"bytes": 135168
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Core.Scripting.Protocol.dll",
|
||||
"sha256": "07800332d0842b93c3987fca1dc10a1390bcb16f44567610d867513ad9fe17e0",
|
||||
"bytes": 117248
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Core.Scripting.dll",
|
||||
"sha256": "9637b7ffb4984f54bc54363ff2d81b6a3401f0cb7eeed8694db626ef41d54d5b",
|
||||
"bytes": 121856
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.dll",
|
||||
"sha256": "107aaf2ef2d7704e320dabc1072bc50217faabb2d1488e66c91b1f756b440c45",
|
||||
"bytes": 11264
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Core.dll",
|
||||
"sha256": "34a4d7c0dddf4efa5fae91a3798c2f1c08b992faf4a827a9e05ed1338c6e4720",
|
||||
"bytes": 124928
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.dll",
|
||||
"sha256": "315dcc540d304c9f1d5b48caa25e5f081569f1a41aa8784cf371c1d6cef7322d",
|
||||
"bytes": 123392
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts.dll",
|
||||
"sha256": "d7b34798587d0939cc78eb69d9df1d617c1c75a61b8dd11a5308958a43a8c4f1",
|
||||
"bytes": 24576
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.AbCip.dll",
|
||||
"sha256": "73073627fcdc8079df645478d6717576975117c3b21cfa7e9422f1e3bc5f4e44",
|
||||
"bytes": 214016
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts.dll",
|
||||
"sha256": "5c66d76dd455c42f0c5b0aed11240fa31e1bdbd2f0a8368d8f41976977e93502",
|
||||
"bytes": 30720
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.dll",
|
||||
"sha256": "cbf92a935d86596a9d4dbeeaa625e304c2045851a0367c3a6b0de03bf78bd6fa",
|
||||
"bytes": 136704
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Calculation.Contracts.dll",
|
||||
"sha256": "29c079414497b30debe8d3ecd7e77bdc4eb16c4fc076e675680a2e880e54b6c2",
|
||||
"bytes": 10240
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Calculation.dll",
|
||||
"sha256": "132e36042a4691cc8e252ab9e9d8c90592fcd897959f533aa37e33f193cf00c8",
|
||||
"bytes": 57344
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts.dll",
|
||||
"sha256": "2bdbf743194193d5a7bc323912e2c7f740cc21633154a4a3cc8e034fd15bb7e8",
|
||||
"bytes": 38912
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.dll",
|
||||
"sha256": "17c28f27fdecd09a46b971b19655bdb933f84990e91bb80c7092c344a0ca974f",
|
||||
"bytes": 300032
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser.dll",
|
||||
"sha256": "f2796d323c222708c28cd0a4289c46db31ec3b6537da7f5cd260fc76f8abdc76",
|
||||
"bytes": 19456
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts.dll",
|
||||
"sha256": "780a11f69194713e2f87e74277e99882ee3518b2824551b45cd04e3b9bdf6648",
|
||||
"bytes": 24576
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.dll",
|
||||
"sha256": "a8e19baa5a736544d914f942d526a5cc038f6b982451bfa82a77365a159434e4",
|
||||
"bytes": 224768
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.dll",
|
||||
"sha256": "400b1896e55d08781231efeba302596299df2b00b2f48afd43d46c22d59b515c",
|
||||
"bytes": 129536
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.dll",
|
||||
"sha256": "8e91a8cce804023639a4bdb2dc60f9fc950992882e50786d45ceeb0f38ed1f68",
|
||||
"bytes": 167424
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.dll",
|
||||
"sha256": "2746b8e9c03b5b8c330152803eee91cf2069f0e6c074d70d03017b1f1f1a2398",
|
||||
"bytes": 51712
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.dll",
|
||||
"sha256": "94b9bcd4506427a44158e0b299a5ae7bcee14fd40b85cc9f63420e4631aa1a6f",
|
||||
"bytes": 143872
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts.dll",
|
||||
"sha256": "6582151085ddea7d319b990c2c58057b6ba5a49860593229b3642dff527ae204",
|
||||
"bytes": 19456
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.dll",
|
||||
"sha256": "c9670c5d169056563294673839ca63321a72a438c519263be708b55f3fc818f9",
|
||||
"bytes": 214016
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.dll",
|
||||
"sha256": "ed07d04709372ef804f465995431b15acdef791f66c8130ab7282372728f7cda",
|
||||
"bytes": 25088
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts.dll",
|
||||
"sha256": "724873ac2f6c08f307a150c5575cc3c022d7e367124ed924c0c7899b0e9575a5",
|
||||
"bytes": 39424
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Modbus.dll",
|
||||
"sha256": "db1e3e7c79dc1a46e1d4ffaa5ea1aac9ed73814d7f06cbfa855ebb3b27a8810d",
|
||||
"bytes": 131584
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser.dll",
|
||||
"sha256": "ace18db746c30307e67efa3acd68e915cc2b62b0a85bdba833df4ea7c85b5f6c",
|
||||
"bytes": 48128
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts.dll",
|
||||
"sha256": "24e795bd8027d7b3ca3d95630b73be6d7cef774243357b01850f145b6066dcb9",
|
||||
"bytes": 139776
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Transport.dll",
|
||||
"sha256": "bec0cecc0bbb06b4715d7b909ccf4d357420e120b2bff3c10ee9f59f71044927",
|
||||
"bytes": 12800
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.dll",
|
||||
"sha256": "da64eacb80ddca9cd0a40295bba2c8ff81ee9c68ccaf32cbd329ae854375deb7",
|
||||
"bytes": 195072
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.dll",
|
||||
"sha256": "7e2ac501136d24a3d0ad6bdae2e4a47460439ad8ab78af538bbccca2e38e5695",
|
||||
"bytes": 35840
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.dll",
|
||||
"sha256": "c15e6db37681cbbef8684d5301e1bba54186ae503aaf55d1c2d06a0567ab28fe",
|
||||
"bytes": 20480
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session.dll",
|
||||
"sha256": "910604abd4daa6196c8fdcd2db085873ee5a4c27b5a67c359590071117c6e4dd",
|
||||
"bytes": 16384
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.dll",
|
||||
"sha256": "cd689b0ec777820d91f914abfd9e4a921143a46df84ee43d0214c66f74225ac0",
|
||||
"bytes": 144384
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts.dll",
|
||||
"sha256": "6fe895674318dc42c0c3cc74266831862d3fe2f13098c26e7d826baf79d7fc47",
|
||||
"bytes": 29696
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.S7.dll",
|
||||
"sha256": "d23a7ac62d781f0c14dd7c9d4f51b712c13599ee78b9b6d302d30dfd5e77da37",
|
||||
"bytes": 124928
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.dll",
|
||||
"sha256": "c341d49bf100e0617d9c4e7d24f63d8affd1bc0fe2edce15e51cc14366fa2207",
|
||||
"bytes": 43008
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts.dll",
|
||||
"sha256": "f0a0cc852116f739247422a0427550e52c938b27d1a8b4e685f45a19a23635ab",
|
||||
"bytes": 38912
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect.dll",
|
||||
"sha256": "7d78655da50c07ac049cd687c59f2a7b6eb2c491543bd4089ffbe60fb805205f",
|
||||
"bytes": 40448
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.Sql.dll",
|
||||
"sha256": "6ec1a9bc7443c1d7b8084ee2fc5e67cbfd2a8d61add303a980cef78a46cb5c67",
|
||||
"bytes": 144384
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts.dll",
|
||||
"sha256": "96db9e5d162b19914cf0ba55c1408c1c9271c80c1a1a12604e684f5785de4b96",
|
||||
"bytes": 17408
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.dll",
|
||||
"sha256": "c24cfc897bde6c0eb65b9958bad952814ed02a27fe25a55c416c1ba61be2c261",
|
||||
"bytes": 188928
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.OpcUaServer.dll",
|
||||
"sha256": "ee1f704fa1574b77bf56cef415c47c33a4428c6852db52bb1443d6765d092cb1",
|
||||
"bytes": 363008
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Runtime.dll",
|
||||
"sha256": "50e752383ae81ef6bb23e360fafe9162be0d1dcb7dd2d8731f16bce762834dd4",
|
||||
"bytes": 539648
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/83ed880e/baseline/host/ZB.MOM.WW.OtOpcUa.Security.dll",
|
||||
"sha256": "a88b1577d1e4dd580e012e7673e31d5363c3d9a79fb415bef4c3d63849678a4d",
|
||||
"bytes": 52736
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-baseline-oracle/OtOpcUa/Directory.Packages.props",
|
||||
"sha256": "7e6459447843b93bc94fbc1f3eeb1c695b5c46c08aeb94debe86a7d419c63221",
|
||||
"bytes": 18829
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/Directory.Packages.props",
|
||||
"sha256": "31a4cac1c205f5b9e67119357c77751b8ab999f6c5f350da68784614040c8a93",
|
||||
"bytes": 19000
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-baseline/bin/Release/net10.0/ProcessHost.dll",
|
||||
"sha256": "841f83aeb3a32d49c06518a98963743d720015bba41ae3138649126ce3a6af1c",
|
||||
"bytes": 94720
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/Opc.Ua.Client.dll",
|
||||
"sha256": "620da406fabdbc75d11f9827ea7e39d2d3c762471bd2264b0d11f1d6cd9247cd",
|
||||
"bytes": 2442880
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/Opc.Ua.Configuration.dll",
|
||||
"sha256": "6ed6a594c93c3ff35d1a16539a9fe87c1e2640549cc2f48ed6bae333dc8bf0e7",
|
||||
"bytes": 117376
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/Opc.Ua.Core.Schema.dll",
|
||||
"sha256": "51cfad48657d95d38074bb88be29aea6f5b361a07397247d58838d066b99772e",
|
||||
"bytes": 135168
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/Opc.Ua.Core.Types.dll",
|
||||
"sha256": "a7dd141b8dabe8848ff28c2fb8e16a85a0508e6a49e6b1a1c1e267175b99af96",
|
||||
"bytes": 12703872
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/Opc.Ua.Core.dll",
|
||||
"sha256": "9d0857aa6b7f3c49037d4ea557b4da8edf07e146069464481783e66c5c9cd35a",
|
||||
"bytes": 2549376
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/Opc.Ua.Security.Certificates.dll",
|
||||
"sha256": "7f4969aceb2f0db5328c6882cb4b117342f3b18905c38acdba600683860c4fd0",
|
||||
"bytes": 118400
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/Opc.Ua.Server.dll",
|
||||
"sha256": "930e714a2f6e15d393840bcdc95e1afa8012bc740c3738a655f958142721864e",
|
||||
"bytes": 3332224
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/Opc.Ua.Types.dll",
|
||||
"sha256": "c542f69882f4e8a4ebd5ccc28a3881d6967c7ffb6dcc3c56dc6c776f39c2ede1",
|
||||
"bytes": 1792640
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/OtOpcUa.Host.deps.json",
|
||||
"sha256": "c289766446d61a6bf15574fdde982a8c44b6faaabb38ad718522f63987a58d69",
|
||||
"bytes": 179492
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/OtOpcUa.Host.dll",
|
||||
"sha256": "a4ac1663a8af08aec150e02c5a9590e6fd346d051256ebbd9ee16019f2ecc6a2",
|
||||
"bytes": 193024
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/OtOpcUa.Host.runtimeconfig.json",
|
||||
"sha256": "eff38a9750f7cca2407b1d639a963d56ffea92088e6da971bbbfa74e3dcd7224",
|
||||
"bytes": 540
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ProcessHost.deps.json",
|
||||
"sha256": "cb76c2da9266dd11b61483fc9425d8a7ed3e25c83b1179ef60bfbd559b7db77c",
|
||||
"bytes": 165039
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ProcessHost.dll",
|
||||
"sha256": "e025fc21ac3559c3796ec639f0ef7b9c3026ccbbdaa4783e14b8930eae3a2043",
|
||||
"bytes": 95232
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ProcessHost.runtimeconfig.json",
|
||||
"sha256": "bdbb6b43e509fbbab6c78b7004c4debf1e9791a648a08f730ff5997f71ef7099",
|
||||
"bytes": 508
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.AdminUI.dll",
|
||||
"sha256": "0f76616b8ddccdb681a9e576f7d031a58b0f5e257d579f58b2143679c27afbe6",
|
||||
"bytes": 2152448
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Client.Shared.dll",
|
||||
"sha256": "49729f0e97e752a65fa2f8debb4ea5505a7ccdfe9ca1321ae6bf8ce5323a7049",
|
||||
"bytes": 120320
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Cluster.dll",
|
||||
"sha256": "4ea4a506d94ecd032b41e0e3643f2d407e9904759c892ffa14a28483ad82862e",
|
||||
"bytes": 67584
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Commons.dll",
|
||||
"sha256": "b77b9502c43e126cdb730b535dbd01c76f9f4ad4c79765ba6e7ea447938e3834",
|
||||
"bytes": 231424
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Configuration.dll",
|
||||
"sha256": "f7f91c501acd81f7bf2d71740f2141d2cc134248388d2f43f78bb16fbf2e7c37",
|
||||
"bytes": 542720
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.ControlPlane.dll",
|
||||
"sha256": "54484082b7d785727c1bbb4cd430542e2c101f31801ef3fe113b99efc8e347c2",
|
||||
"bytes": 179200
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.Abstractions.dll",
|
||||
"sha256": "c4be146d49102962324a2e4af71f17b2804e6003a82954733dd6302632e5e249",
|
||||
"bytes": 124416
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.dll",
|
||||
"sha256": "71d20c01ccafb8327ee474f1133a7b265d4252872ac9f6fb0b78fe2c8d6e636f",
|
||||
"bytes": 56320
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.dll",
|
||||
"sha256": "3ad5440cb4c1644cad844c50a28ac9c0f9534795a6a553df446fb701c34c191a",
|
||||
"bytes": 83968
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions.dll",
|
||||
"sha256": "88d9ee543a6a8525a4074ecb9524ce5c095e1bbba8017bde980964cb53da447f",
|
||||
"bytes": 13312
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.Scripting.OutOfProcess.dll",
|
||||
"sha256": "c889dd89aad72e130439e916371ec4e63f0abbf10f7934e42caf504d5227aadb",
|
||||
"bytes": 135168
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.Scripting.Protocol.dll",
|
||||
"sha256": "6b21c826e93a1f8d942f2ee101d522d2566b8e8fd8b2ef223b4ca75f679d7118",
|
||||
"bytes": 117248
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.Scripting.dll",
|
||||
"sha256": "f36de3c774ece202f34288c7bee877cbee3d14a688ae8b07aede00ba016f5b57",
|
||||
"bytes": 121856
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.dll",
|
||||
"sha256": "21893da38bf83afe3cb90f6a4d8caed6bb5db837fecc0c819f88ea7e17c73a27",
|
||||
"bytes": 11264
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Core.dll",
|
||||
"sha256": "7aec5cb22b6a9939044502eaedf188a5e95887057b6cc7a70b0444ff83f1802b",
|
||||
"bytes": 124928
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.dll",
|
||||
"sha256": "86db4c0cbca35c13f9ffaa258ecc54e15fb05c4b5fbda991909f00793a26a1dc",
|
||||
"bytes": 123392
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts.dll",
|
||||
"sha256": "862f63ab4499d1e32cd5a3d7245dca4158398a7d546d5a882be37cb9e4738de8",
|
||||
"bytes": 24576
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.AbCip.dll",
|
||||
"sha256": "bd5d38bc81fef735143ba58d2158e1cb37b2b9998e32ebb7765a1eea6111c8c5",
|
||||
"bytes": 214016
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts.dll",
|
||||
"sha256": "a0b596bcd65014cafac896c17d1247e0c7cfef076952aee7096d90377ee3aedf",
|
||||
"bytes": 30720
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.dll",
|
||||
"sha256": "fdf201aaccbaf10b53eb07f0b627e8ffd3955ec78b3e446be85c03e1ad7ff687",
|
||||
"bytes": 136704
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Calculation.Contracts.dll",
|
||||
"sha256": "36033badc6dcfa646592dfe00aa184c657b24f7d73f17c4574853727275c320f",
|
||||
"bytes": 10240
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Calculation.dll",
|
||||
"sha256": "6124ecc4dae44b0f4012f03d3583c86ce4d0766205386ff7c70f319c77220e87",
|
||||
"bytes": 57344
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts.dll",
|
||||
"sha256": "150a79b5d38205d573399e29a2924eeb62c17261729c82bbf6c606a769f03467",
|
||||
"bytes": 38912
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.dll",
|
||||
"sha256": "b45e29a1d892ae00e914839dfe1001c6eb15ce14af1f9800a0ff48a5517d8dd6",
|
||||
"bytes": 300032
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser.dll",
|
||||
"sha256": "87f8d77edc62c8d1c60e69620b297b1cef75b91355c871aae707e3f05f30e267",
|
||||
"bytes": 19456
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts.dll",
|
||||
"sha256": "4eaad55f82495fedcf82a9be3157c748d2fc6b6f59eb7ea41bff88b18332dd51",
|
||||
"bytes": 24576
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.dll",
|
||||
"sha256": "e336053cc6fd0cb57615d2333c9ea99a2a9acac9273c8927af82c3fc551157ac",
|
||||
"bytes": 224768
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.dll",
|
||||
"sha256": "e6f7ebfb5b8078a336601a7603a094ac743c4adbf02bb311dfef29d2328eeb65",
|
||||
"bytes": 129536
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.dll",
|
||||
"sha256": "55e7cf55c8fece377cdef598f901e4347b2da13ab7feb265623866c08bad477f",
|
||||
"bytes": 167424
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.dll",
|
||||
"sha256": "b033a90e65f7b67a8ab7ef5c614019a54c12dd5590fc9eae17fca4b736918373",
|
||||
"bytes": 51712
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.dll",
|
||||
"sha256": "685e8961b23190b745568955a34cca1462dffce15007c25a2265f44fd1224436",
|
||||
"bytes": 143872
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts.dll",
|
||||
"sha256": "a592d1481ea662ef368d07f706e70779a1f879126101f17042ce270c5faa66c3",
|
||||
"bytes": 19456
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.dll",
|
||||
"sha256": "1ca66a30106c4b9476eed6b766ae78a59cbf6b408814e2cce9f1d7fd68773762",
|
||||
"bytes": 214016
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.dll",
|
||||
"sha256": "2cb7c0cd79d399bce99f66d40fedbc002aad0d1204c252c1f62a7edd83e73b09",
|
||||
"bytes": 25088
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts.dll",
|
||||
"sha256": "a8fd87c6a32b45b91d1c6e12c597916a20d5c254d137a7f8d9bb6cfe473ca674",
|
||||
"bytes": 39424
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Modbus.dll",
|
||||
"sha256": "84723c305417dd10440345acefb50c33f47d55b34543584abf7d9235ad13df86",
|
||||
"bytes": 131584
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser.dll",
|
||||
"sha256": "a5c4262f0a0d378d99a89016d6b7fa927632ff5d99bcc5de52a1ceb359ad2d5c",
|
||||
"bytes": 48128
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts.dll",
|
||||
"sha256": "f4328140545f1c8e2226865ceac9d309b8f55bbee0ed5e9713836f16bc0aff63",
|
||||
"bytes": 139776
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Transport.dll",
|
||||
"sha256": "b592a854bb997781a1f1d316a615f629cb3fb08dad683f2fdbff2867210cc43b",
|
||||
"bytes": 12800
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.dll",
|
||||
"sha256": "4d63899c81316a0d9f856b59759e97dd9b52d53b00ae5fd57a2cdfc916fc73ea",
|
||||
"bytes": 195072
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.dll",
|
||||
"sha256": "a21d73fbd9dfd60ebf4ac56a961e172473f54e55727dc1ad856991c9e1ba756c",
|
||||
"bytes": 36352
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.dll",
|
||||
"sha256": "2e45346638a6814dc6af3f8c99ea448bd2538267baf645453e818fe702265157",
|
||||
"bytes": 20480
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session.dll",
|
||||
"sha256": "a9052366f2c27e485bf9a1d6c3150459f54aa1c510b63d01d99a9270ff95f3bc",
|
||||
"bytes": 19968
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.dll",
|
||||
"sha256": "3bbe213152adee004934b4fbb612957b6891461f69c20e55459b5a03660a2915",
|
||||
"bytes": 147968
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts.dll",
|
||||
"sha256": "0610e0f7ba83fb3c3768e4909fb1d2fa0d698608c58606b31235b782813b65ca",
|
||||
"bytes": 29696
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.S7.dll",
|
||||
"sha256": "f1300ad64b7ddfde76078e9f174a10875a6e7f6a5bdf5d20ae3cdf1c94aecee9",
|
||||
"bytes": 124928
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.dll",
|
||||
"sha256": "0965780c6b2c8f14884e98e1e8f89d7d54d3d74473987cf63f5b97f98b147fef",
|
||||
"bytes": 43008
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts.dll",
|
||||
"sha256": "b705643ccd5e85e11bd585d8258c9d4bb78268bd5664ec20a7310cd92a955751",
|
||||
"bytes": 38912
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect.dll",
|
||||
"sha256": "3ba6f0217710d3c69b1cd7baaaa99a1b43fae3b0d27da66efba6f0c58475fd91",
|
||||
"bytes": 40448
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.Sql.dll",
|
||||
"sha256": "17b472fe49541f794dccdc2490cda075fc746a515195dd5cee9b1250ef7e702c",
|
||||
"bytes": 144384
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts.dll",
|
||||
"sha256": "6b0ea052040c218b70cf2d31c437be7dc474ce6e000395259a9ce4055fd1ce0f",
|
||||
"bytes": 17408
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.dll",
|
||||
"sha256": "ecdb122f3800af6ebb07421f0987e9e1561645a10bd05c86000974ce6c898060",
|
||||
"bytes": 188928
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.OpcUaServer.dll",
|
||||
"sha256": "40a65a2124fb917d6c5fe38e648ff3c5db9fb96672fd9ff1233992bd5cc17abc",
|
||||
"bytes": 420864
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Runtime.dll",
|
||||
"sha256": "a66420c048d83ad43f923fb3b97a07078df570d56a1bb5b2ce8406d6f9cbb4a0",
|
||||
"bytes": 539648
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ZB.MOM.WW.OtOpcUa.Security.dll",
|
||||
"sha256": "daf52ae4d37a05f00819d837456d1db8191cd721794a12363a4424af46f0801e",
|
||||
"bytes": 52736
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli/Opc.Ua.Client.dll",
|
||||
"sha256": "620da406fabdbc75d11f9827ea7e39d2d3c762471bd2264b0d11f1d6cd9247cd",
|
||||
"bytes": 2442880
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli/Opc.Ua.Configuration.dll",
|
||||
"sha256": "6ed6a594c93c3ff35d1a16539a9fe87c1e2640549cc2f48ed6bae333dc8bf0e7",
|
||||
"bytes": 117376
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli/Opc.Ua.Core.Schema.dll",
|
||||
"sha256": "51cfad48657d95d38074bb88be29aea6f5b361a07397247d58838d066b99772e",
|
||||
"bytes": 135168
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli/Opc.Ua.Core.Types.dll",
|
||||
"sha256": "a7dd141b8dabe8848ff28c2fb8e16a85a0508e6a49e6b1a1c1e267175b99af96",
|
||||
"bytes": 12703872
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli/Opc.Ua.Core.dll",
|
||||
"sha256": "9d0857aa6b7f3c49037d4ea557b4da8edf07e146069464481783e66c5c9cd35a",
|
||||
"bytes": 2549376
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli/Opc.Ua.Security.Certificates.dll",
|
||||
"sha256": "7f4969aceb2f0db5328c6882cb4b117342f3b18905c38acdba600683860c4fd0",
|
||||
"bytes": 118400
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli/Opc.Ua.Types.dll",
|
||||
"sha256": "c542f69882f4e8a4ebd5ccc28a3881d6967c7ffb6dcc3c56dc6c776f39c2ede1",
|
||||
"bytes": 1792640
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli/ZB.MOM.WW.OtOpcUa.Client.CLI.deps.json",
|
||||
"sha256": "e1a36de862f9d940bfb55e09db02461bc20f2bd12dad30c8a8bca81ac38be40a",
|
||||
"bytes": 37379
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli/ZB.MOM.WW.OtOpcUa.Client.CLI.dll",
|
||||
"sha256": "bf1029417276643c6e7b632b882d3fc87c611e52f9e2fdd428e94d2e11f0513e",
|
||||
"bytes": 92160
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli/ZB.MOM.WW.OtOpcUa.Client.CLI.runtimeconfig.json",
|
||||
"sha256": "9950d4583cfc9a106857c0d5b8a75b53e8774852b57b80bc3823a74d44a4d87f",
|
||||
"bytes": 330
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli/ZB.MOM.WW.OtOpcUa.Client.Shared.dll",
|
||||
"sha256": "49729f0e97e752a65fa2f8debb4ea5505a7ccdfe9ca1321ae6bf8ce5323a7049",
|
||||
"bytes": 120320
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/scripts/validation/opcua-sdk-compatibility/ProcessHost.cs",
|
||||
"sha256": "724bca278fadcb44d086888415df7608acc9be56443682d4d5a08377f1aaccef",
|
||||
"bytes": 30352
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/scripts/validation/opcua-sdk-compatibility/container-leg.sh",
|
||||
"sha256": "1f5d1158572379b8a6304c3a3134a78c112043940f5bce468621677f7da3ef9e",
|
||||
"bytes": 2589
|
||||
},
|
||||
{
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/scripts/validation/opcua-sdk-compatibility/run.py",
|
||||
"sha256": "b94c880c0828b3bf9b330e142077dd5ef256cf6a27b037fd48a44361975fe5b6",
|
||||
"bytes": 12587
|
||||
}
|
||||
]
|
||||
-202
@@ -1,202 +0,0 @@
|
||||
[
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"build",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-baseline/ProcessHost.csproj",
|
||||
"-c",
|
||||
"Release",
|
||||
"-m:1",
|
||||
"--nologo"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-baseline/build.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"build",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/ProcessHost.csproj",
|
||||
"-c",
|
||||
"Release",
|
||||
"-m:1",
|
||||
"--nologo"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/build.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"publish",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.OtOpcUa.Client.CLI.csproj",
|
||||
"-c",
|
||||
"Release",
|
||||
"-o",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli",
|
||||
"-m:1",
|
||||
"--nologo"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli-build.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-baseline/bin/Release/net10.0/ProcessHost.dll",
|
||||
"prepare",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/baseline-client-to-baseline-server/shared"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/baseline-client-to-baseline-server/prepare.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-baseline/bin/Release/net10.0/ProcessHost.dll",
|
||||
"server",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/baseline-client-to-baseline-server/server"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/baseline-client-to-baseline-server/server.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-baseline/bin/Release/net10.0/ProcessHost.dll",
|
||||
"client",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/baseline-client-to-baseline-server/shared",
|
||||
"opc.tcp://127.0.0.1:55610/OtOpcUa"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/baseline-client-to-baseline-server/shared.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-baseline/bin/Release/net10.0/ProcessHost.dll",
|
||||
"prepare",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/baseline-client-to-candidate-server/shared"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/baseline-client-to-candidate-server/prepare.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ProcessHost.dll",
|
||||
"server",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/baseline-client-to-candidate-server/server"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/baseline-client-to-candidate-server/server.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-baseline/bin/Release/net10.0/ProcessHost.dll",
|
||||
"client",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/baseline-client-to-candidate-server/shared",
|
||||
"opc.tcp://127.0.0.1:55642/OtOpcUa"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/baseline-client-to-candidate-server/shared.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ProcessHost.dll",
|
||||
"prepare",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-client-to-baseline-server/shared"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-client-to-baseline-server/prepare.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-baseline/bin/Release/net10.0/ProcessHost.dll",
|
||||
"server",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-client-to-baseline-server/server"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-client-to-baseline-server/server.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ProcessHost.dll",
|
||||
"client",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-client-to-baseline-server/shared",
|
||||
"opc.tcp://127.0.0.1:55671/OtOpcUa"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-client-to-baseline-server/shared.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ProcessHost.dll",
|
||||
"prepare",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-client-to-candidate-server/shared"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-client-to-candidate-server/prepare.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ProcessHost.dll",
|
||||
"server",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-client-to-candidate-server/server"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-client-to-candidate-server/server.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ProcessHost.dll",
|
||||
"client",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-client-to-candidate-server/shared",
|
||||
"opc.tcp://127.0.0.1:55697/OtOpcUa"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-client-to-candidate-server/shared.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--name",
|
||||
"otopcua-sdk2-matrix-222d08944aba",
|
||||
"--network",
|
||||
"none",
|
||||
"-e",
|
||||
"OPCUA_MATRIX_PASSWORD",
|
||||
"-v",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0:/candidate:ro",
|
||||
"-v",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-baseline/bin/Release/net10.0:/server:ro",
|
||||
"-v",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli:/cli:ro",
|
||||
"-v",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-driver-cli-to-baseline-server:/task",
|
||||
"mcr.microsoft.com/dotnet/sdk:10.0.103-noble",
|
||||
"bash",
|
||||
"/task/run.sh"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-driver-cli-to-baseline-server/container.log"
|
||||
},
|
||||
{
|
||||
"argv": [
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--name",
|
||||
"otopcua-sdk2-matrix-00f1df5c1613",
|
||||
"--network",
|
||||
"none",
|
||||
"-e",
|
||||
"OPCUA_MATRIX_PASSWORD",
|
||||
"-v",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0:/candidate:ro",
|
||||
"-v",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0:/server:ro",
|
||||
"-v",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-cli:/cli:ro",
|
||||
"-v",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-driver-cli-to-candidate-server:/task",
|
||||
"mcr.microsoft.com/dotnet/sdk:10.0.103-noble",
|
||||
"bash",
|
||||
"/task/run.sh"
|
||||
],
|
||||
"log": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/candidate-driver-cli-to-candidate-server/container.log"
|
||||
}
|
||||
]
|
||||
-1828
File diff suppressed because it is too large
Load Diff
-17
@@ -1,17 +0,0 @@
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":5}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"endpoint":"opc.tcp://127.0.0.1:55610/OtOpcUa","process":35791,"sdk":"1.5.378.0","rawUri":"https://zb.com/otopcua/raw","unsUri":"https://zb.com/otopcua/uns"}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"WorkingSet64": 231833600,
|
||||
"PrivateMemorySize64": 0,
|
||||
"HandleCount": 0,
|
||||
"threads": 31,
|
||||
"cpuMilliseconds": 1531.714,
|
||||
"allocatedBytes": 156774688,
|
||||
"collections": [
|
||||
20,
|
||||
8,
|
||||
2
|
||||
]
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":5}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"endpoint":"opc.tcp://127.0.0.1:55642/OtOpcUa","process":35814,"sdk":"2.0.1.0","rawUri":"https://zb.com/otopcua/raw","unsUri":"https://zb.com/otopcua/uns"}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"WorkingSet64": 201850880,
|
||||
"PrivateMemorySize64": 0,
|
||||
"HandleCount": 0,
|
||||
"threads": 31,
|
||||
"cpuMilliseconds": 2559.57,
|
||||
"allocatedBytes": 47810656,
|
||||
"collections": [
|
||||
5,
|
||||
1,
|
||||
0
|
||||
]
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":5}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"endpoint":"opc.tcp://127.0.0.1:55671/OtOpcUa","process":35841,"sdk":"1.5.378.0","rawUri":"https://zb.com/otopcua/raw","unsUri":"https://zb.com/otopcua/uns"}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"WorkingSet64": 229752832,
|
||||
"PrivateMemorySize64": 0,
|
||||
"HandleCount": 0,
|
||||
"threads": 30,
|
||||
"cpuMilliseconds": 1699.059,
|
||||
"allocatedBytes": 157134200,
|
||||
"collections": [
|
||||
20,
|
||||
8,
|
||||
2
|
||||
]
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":7}
|
||||
{"operation":"raw","rows":5}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
{"operation":"events","rows":7}
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"endpoint":"opc.tcp://127.0.0.1:55697/OtOpcUa","process":35866,"sdk":"2.0.1.0","rawUri":"https://zb.com/otopcua/raw","unsUri":"https://zb.com/otopcua/uns"}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"WorkingSet64": 201097216,
|
||||
"PrivateMemorySize64": 0,
|
||||
"HandleCount": 0,
|
||||
"threads": 30,
|
||||
"cpuMilliseconds": 2626.114,
|
||||
"allocatedBytes": 47846920,
|
||||
"collections": [
|
||||
5,
|
||||
1,
|
||||
0
|
||||
]
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
Event history for ns=2;s=Matrix/dev (2026-10-01 00:00 -> 2026-10-01 00:01)
|
||||
|
||||
EventId | Message
|
||||
653030 | matrix-history
|
||||
653031 | matrix-history
|
||||
653032 | matrix-history
|
||||
653033 | matrix-history
|
||||
653034 | matrix-history
|
||||
653035 | matrix-history
|
||||
653036 | matrix-history
|
||||
653037 | matrix-history
|
||||
653038 | matrix-history
|
||||
653039 | matrix-history
|
||||
653130 | matrix-history
|
||||
653131 | matrix-history
|
||||
653132 | matrix-history
|
||||
653133 | matrix-history
|
||||
653134 | matrix-history
|
||||
653135 | matrix-history
|
||||
653136 | matrix-history
|
||||
653137 | matrix-history
|
||||
653138 | matrix-history
|
||||
653139 | matrix-history
|
||||
653230 | matrix-history
|
||||
653231 | matrix-history
|
||||
653232 | matrix-history
|
||||
653233 | matrix-history
|
||||
653234 | matrix-history
|
||||
|
||||
25 events returned.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user