mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-22 17:50:03 +08:00
fix/encoder-stack-depth-json-flake
96 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
28f1401324 |
Server: cut request-path lock contention and allocation; add load harness (#4707)
Profiling the reference server under load showed most request-path CPU spent spinning on contended locks, plus several avoidable allocations per request or per node. This PR removes them, switches the reference server to Server GC, and adds the load harness used to measure it. ## Results Paired A/B against `master` (3 alternating rounds, medians, 20 sessions, workstation GC on both sides, 24-core machine): | Scenario | Throughput | p50 | p90 | Server CPU / op | Server alloc / op | Lock contention | |---|---|---|---|---|---|---| | Read, 1 value | **+45%** (94k → 137k req/s) | −20% | −46% | −37% | −39% | −94% | | Read, 100 values | **+40%** | −26% | −17% | −24% | −25% | +130% ¹ | | Read, 100 values, SignAndEncrypt | **+48%** | −36% | −20% | −30% | −26% | +106% ¹ | | Browse (Server object) | **+33%** | −31% | −12% | −13% | −14% | −40% | | Write, 100 values | +13% | −12% | −4% | −10% | −19% | −23% | | Data-change subscriptions | ±0 ² | ≈ | ≈ | −14% | −17% | −29% | ¹ The faster server spends more time in the remaining per-node locks. ² Throughput is bounded by the writer rate in this scenario. Separately, Server GC instead of workstation GC doubles Read-100 throughput (21k → 44k req/s) and cuts p90 from 2.1 ms to 0.6 ms, because workstation GC paused the server for about half the wall time under load. The working set grows from ~210 MB to ~430 MB. ## Changes - **`ObjectPool<T>`**: lock-free slot array. `ConcurrentBag.Count` took every per-thread lock on each `Return` of the pooled response value-task source, once per request. - **`FairRequestQueue`**: - drops redundant gate entries: the fail-fast pre-checks and the stopped check read volatile fields and are re-checked under the gate; the owner count is folded into selection; - coalesces wake pulses with a pending flag, so the bounded signal channel's lock is only taken when no token is outstanding. Wake-count semantics in the tests are unchanged. - **`DefaultServerResourceIsolationProvider`**: - compares peer addresses without `GetAddressBytes()` allocations; - builds the unsecured verification context only when a classifier is configured. - **TCP listener / channel**: caches the certificate DER copies per channel, keyed on the certificate instance. `X509Certificate2.RawData` returns a fresh copy on every access, and the listener read it for every request. - **`EndpointBase`**: resolves the request `ActivitySource` once instead of `Assembly.GetCallingAssembly()` per request. - **Node managers**: - one `ServerSystemContext` copy per request (cached on the `OperationContext`) for per-node callbacks: permission metadata, node metadata, view membership, method lookup; - removes a discarded copy in `GetReferenceDescriptionAsync`; - per-request operation caches are plain `Dictionary` instead of a `ConcurrentDictionary` with 24 lock objects; - the validation cache is built in one pass; - fixes zero-capacity error lists. - **`ResourceManager`**: translation lookups read an immutable snapshot that is rebuilt after writes. Browse serialised on the global lock for every display name. - **`MonitoredItem`**: shares the default `DataChangeFilter` instead of allocating one per sampled value. - **Reference server**: `ServerGarbageCollection=true`; new "Garbage collector mode" section in `docs/ServerScalability.md`. - **`perf/`**: `ServerLoadHarness` (out-of-process server plus load driver, server-side CPU/GC/alloc/contention deltas, optional `dotnet-trace` capture), `TraceAnalyzer` (TraceEvent-based CPU/allocation/contention aggregation), and paired A/B scripts. Not part of `UA.slnx`. ### Behaviour notes for custom node managers - The `IDictionary<NodeId, NodeState>` operation cache handed to overrides is now a `Dictionary`. It was a `ConcurrentDictionary`, but only one request flow uses it. - Within one request, per-node callbacks of a node manager share one `ServerSystemContext` copy. ### Later commits on this PR - **Permission checks without `NodeMetadata`** (`9068684c3`): the role-permission and access-restriction validators run on an internal `PermissionMetadata` struct; `AsyncCustomNodeManager` fills it on the stack from the same attributes and namespace defaults. Browse's reference filter reads 3 attributes per target instead of 13. Read 100 values: 120.6 → 95.7 KB/request, ~21k → 26k req/s; Browse 67.6 → 51.8 KB, 26.7k → 29.8k req/s. - **Read-only channel evidence** (`936b28d76`, `dc638ab9f`): `SecureChannelContext.ClientChannelCertificate`, `ServerChannelCertificate` and `ChannelThumbprint` are `ByteString`, so the cached per-channel certificate buffer is shared without a copy and cannot be altered by handlers or classifiers. **Public API change**, see the new section in `docs/MigrationGuide.md`. The isolation provider's revalidation snapshot still copies. - **Session lookup** (`936b28d76`): `SessionManager.TryGetSessionContext` takes the candidate snapshot without the bindings lock; the validating check under the lock is unchanged (2 instead of 4 global lock acquisitions per request). - Unit tests for `ObjectPool`, the per-request system context cache, the translation snapshot, `SecureChannelContext` null semantics and a stopped `FairRequestQueue`; `perf/` follows the repository conventions. ## Testing - `Opc.Ua.Server.Tests` (net10.0): 8386 passed before the `ByteString` change. After it, 13 secure-session and isolation tests failed; both causes are fixed in `dc638ab9f` and those tests (25 with their variants) pass. A full rerun of the Server, Sessions, Core, Redundancy, LDS and GDS suites on the final commit is in progress. - Full solution builds for net10.0 and net48; `dotnet format whitespace` clean. ### o6-automation opcua-benchmarks (sampling, deep pipelines) Replicated both tests of the [o6-automation benchmark](https://github.com/o6-automation/opcua-benchmarks) in the harness (`--scenario counters`, and `read` with 10 clients × N outstanding), paired against `origin/master`: | Test | master | this PR | |---|---|---| | Subscription sampling, 100 Hz, every item ≥ 99 % of samples, 1 item | FAIL (75–94 %) | PASS (100 %) | | same, 1024 items | FAIL (64–73 %) | PASS (99.9 %) | | same, capacity | — | between 4096 and 6144 items (~410–520k samples/s) | | Read, 10 clients × 100 outstanding, None | stalls (0–1.7k req/s) | 121–131k req/s | | Read, 10 clients × 100 outstanding, SignAndEncrypt | stalls (0 req/s) | 89–91k req/s | | Read, 10 clients × 1000 outstanding | channels faulted (`BadTcpNotEnoughResources`) | 0 errors, ~45–54k req/s | - **Fixed-rate sampling** (`30b769d13`): the sampling loop waited one interval after each sample, adding timer overshoot and sampling time to every cycle; samples now have absolute deadlines. - **Async response send** (`cdac5c5ca`): `SendResponse` blocked thread-pool workers on the channel gate; with many requests in flight the receive loop that was handed the gate waited for a pool thread and the channel stalled. The TCP, Kestrel TCP and WebSocket listeners now use `SendResponseAsync`. - **Backpressure instead of channel fault** (`191cd2ba7`): server channels stop reading while more than 100 responses are queued and resume at 50, instead of faulting the channel. Client channels keep reading. At 10,000 outstanding requests throughput is ~50k req/s with or without the limit engaged, so the remaining gap to the 100-per-client case is not the backpressure. ## Not changed - `SentMessageQueue` deep-clones every `NotificationMessage`. Retained messages return their pooled payloads on acknowledgement while the response copy may still be queued for send, and a client can acknowledge a sequence number before receiving it; the clone keeps a recycled payload from reaching another session's response. Removing it needs a send-completion hook from the transport. - Generated responses allocate a default `ResponseHeader` that is replaced (~2-3 % of per-request allocation). Fixing it means making structure fields of every generated type lazily initialized, a generator-wide change to default values, equality and encoding. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
61edaa1ed0 |
Remove netstandard2.1 and net472 target frameworks (#4624)
# Description Drops support for **netstandard2.1** and **.NET Framework 4.7.2**. Libraries now target `net48`, `net8.0`, `net9.0` and `net10.0`. `Opc.Ua.Types` and `Opc.Ua.SourceGeneration.Core` still also target `netstandard2.0`, but only because the Roslyn source generators load them. There is no general netstandard2.0 support. **Preprocessor directives** (112 files) - Every `#if`/`#elif` mentioning `NETSTANDARD2_1*`, `NET472*`, `NET_STANDARD_TESTS`, `SKIP_ECC_CERTIFICATE_REQUEST_SIGNING` or `NETSTANDARD*` was evaluated against the remaining TFM set (net48, net8/9/10, plus netstandard2.0 inside `Opc.Ua.Types`/`tools`) and simplified. Dead branches were removed. - Example: `NETSTANDARD2_1 || NET8_0_OR_GREATER` → `NET8_0_OR_GREATER`. - Example: `NET8_0_OR_GREATER || NET472_OR_GREATER` → removed (always true). - Netstandard-only code was removed: `UnixFileModeShim`, the CRDT reflection path in `ReplicatedGossipOptions.CreateReaderOptions`, and the netstandard2.1-only test variants. **Custom test logic** - `targets.props` drops the `net472`, `netstandard2.0` and `netstandard2.1` `CustomTestTarget` profiles, `NetStandardTests` and `SKIP_ECC_CERTIFICATE_REQUEST_SIGNING`. Supported values are now `net48`, `net8.0`, `net9.0`, `net10.0`. - The `NET_STANDARD_TESTS` define is removed from 29 test projects. - The "build netstandard shells as net10.0" overrides are removed from 18 projects. - `RestrictForLegacyTfm` now only applies to `net48`. - `tests/customtest.bat` is updated to the new values. **Build / packaging / CI** - MSBuild TFM conditions are simplified: Kestrel package choice, MQTTnet v4 (net48 only), BouncyCastle, Di/OpenUsd/Vision/Stress test excludes, and `System.IO.FileSystem.AccessControl` (dropped from Core). - The MigrationAnalyzer nuspec drops its net472/netstandard2.1 lib groups. The Azure signing lists are updated. - `get-ci-matrix.ps1` removes the net472/netstandard test profiles (including the PR-gating `linux-netstandard2.1`) and the matching build legs. **Breaking API change (2.0 preview):** removed `HttpsTransportListener.IsWssTransportSupported` and `ReplicatedGossipOptions.IsGossipStateDecodingSupported`. They only returned `false` on netstandard2.1 and are now always `true`. The test guards that used them were removed as well. **Docs** - NuGet READMEs, `README.md`, `DeveloperGuide.md`, `PubSub.md`, the certificate/ECC/identity docs and the 2.0 migration guide (plus its `.agents` skill mirror) are updated. - The migration guide notes that 4.7.2 consumers must move to 4.8 and netstandard2.1 consumers to net8.0+. ### Reviewer notes - The `System.IO.FileSystem.AccessControl` `PackageVersion` pin is left in `Directory.Packages.props`. It has no direct reference any more, but transitive pinning is on, so it was kept to avoid shifting resolved versions. - The `#if` rewrite was done mechanically by a truth-table evaluator. The diff in the `.cs` files is limited to directive lines, removed dead branches, and stale-comment updates. ### Verified locally - `dotnet build UA.slnx -p:CustomTestTarget=net48`: 0 errors. - `dotnet build UA.slnx -p:CustomTestTarget=net10.0`: 0 errors. - Default multi-target builds (no `CustomTestTarget`) of Opc.Ua.Types, Bindings.Https, PubSub.Mqtt and Redundancy.Server succeeded. - net10.0 tests: - Opc.Ua.Redundancy.Server.Tests: 840/840 passed. - Opc.Ua.Types.Tests: 11363 passed, 1 skipped. - ModelDependencyScanner tests: 20/20 passed. - The full test suite was **not** run locally. ## Related Issues - None ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [ ] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2ef6628902 |
Restrict private key permissions on the netstandard2.1 build (#4542)
# Description `DirectoryCertificateStore` only restricted private key directories and files (and only checked existing directories for private access) under `NET8_0_OR_GREATER || NET472_OR_GREATER`. The **netstandard2.1** asset of `Opc.Ua.Core` — used by .NET Core 3.1–7, Mono and Unity consumers — therefore wrote private keys with default (typically world-readable) permissions, and `HasPrivateAccess` always returned `false`. The first run of the new GitHub nightly ([run 36017168717](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/36017168717)) surfaced this in `test-windows-netstandard2.1 (3/12)` and `test-linux-netstandard2.1 (3/12)`: - Linux: `ExistingDirectoryPermissionFailureIsAcceptedOnlyForPrivateAccessAsync` — store directory not restricted to `0700`. - Windows: the same test's unsafe-access cases did not throw `UnauthorizedAccessException`, and `NewPrivateKeyPreservesInheritedAccessToExistingKeysAsync` lost access for trusted identities. The tests were correct; this fixes the product code. ## Changes - **Windows:** reference `System.IO.FileSystem.AccessControl` 5.0.0 for `netstandard2.1` only, so the existing ACL code compiles for every Core target; the `#if` guards around it are removed. ⚠️ New package dependency for the netstandard2.1 target. - **Unix:** new internal `UnixFileModeShim` (netstandard2.1 only) sets `0700`/`0600` through a libc `chmod` `DllImport` (same pattern as `Opc.Ua.PubSub.Eth`). No reflection, so it stays NativeAOT compatible. - netstandard2.1 cannot read Unix modes portably (the `stat` layout is platform specific), so `HasPrivateAccess` fails closed there. `chmod` only fails for a caller that neither owns the path nor is root, and that caller could not use a `0700` path anyway. ## Testing - `dotnet test tests/Opc.Ua.Core.Tests -c Release -p:CustomTestTarget=netstandard2.1 -f net8.0 --filter FullyQualifiedName~DirectoryCertificateStoreTests` on Windows: 34/34 passed (the 3 CI failures included). - Same filter with `CustomTestTarget=net10.0`: 34/34 passed. - Linux netstandard2.1 not run locally (no WSL); relies on CI. ## Related Issues - Fixes the netstandard2.1 nightly failures introduced with #4503. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [ ] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
23be306c65 |
Update NuGet dependencies (#4536)
## Summary - update NuGet dependencies to their latest compatible stable releases while retaining documented compatibility, native, security, and mocking pins - align the nbgv CLI with the central `Nerdbank.GitVersioning` version (3.10.94) and fail explicitly on tool installation/update errors - update the synchronized OpenUSD family to `0.14.0-alpha`; adapt the Vision blank-frame fixture to the new `RenderDiagnosticsState` constructor argument - retain `Microsoft.CodeAnalysis` API packages at the existing 5.0/4.14 host bands: Roslyn 5.9 requires Immutable/Metadata 10.0.1+, while the retained older compiler-host closure is intentionally pinned to 9.0.0 ## Dependency updates - `Dekaf` 1.17.0 → 1.21.1 - legacy ASP.NET Core 2.3 dependencies → 2.3.13; net8/net9 authentication and WebApi test packages → 8.0.31/9.0.20 - `Microsoft.CodeAnalysis.Analyzers` 4.14.0 → 5.9.0 - `TUnit` 1.68.0 → 1.69.0 - `Avalonia` / `Avalonia.Desktop` 12.1.2 → 12.1.3 - schema-test `System.Collections.Immutable` overrides 10.0.11 → 10.0.12 - `AzureSignTool` 3.0.0 → 7.0.1 and `NuGetKeyVaultSignTool` 3.0.45 → 3.2.3 - eight direct OpenUSD packages 0.12.0-alpha → 0.14.0-alpha ## Validation - `dotnet restore UA.slnx --disable-parallel` - clean `UA.slnx` Release build with `CustomTestTarget=net10.0` - `UA.slnx` Release builds with `net48` and `netstandard2.1` - OpenUSD and Vision test suites on net8/net9/net10: 882/882/921 and 484 passed + 1 environment skip on each target - full `Opc.Ua.Tools.Tests` net10.0: 963 passed, 1 skipped - source-generator package validation against a locally packed release package set - updated signing tools installed in an isolated directory; existing `sign` CLI options verified without credentials - nbgv 3.10.94 bootstrap exercised in simulated GitHub CI; disposable committed fixture verified canonical and case-invalid release refs with a four-component version ## Limits No protected signing/publication, Linux/macOS native rendering, or full stress/nightly suites were run locally. |
||
|
|
7bd90d1ed7 |
Update dependencies and re-enable macOS NativeAOT (#4493)
## Description Updates unpinned NuGet dependencies to current compatible stable versions, pins the repository and CI to .NET SDK 10.0.401 (.NET 10.0.12), and re-enables the macOS ARM64 NativeAOT lane. The existing intentional pins remain unchanged, including Moq, compatibility/security remediation pins, Roslyn compatibility versions, coordinated OpenUSD packages, and per-TFM MQTTnet/ASP.NET Core bands. The .NET 10.0.12 `Microsoft.DotNet.ILCompiler` package provenance was verified against the runtime relocation-anchor fix required for the Apple linker failure. ## Related Issues - Fixes #4443 ## Validation - `dotnet restore UA.slnx`: passed - Targeted .NET 10 build: passed - Core schema tests: 160 passed - Main NativeAOT tests: 178 passed - Historian NativeAOT tests: 2 passed - MCP NativeAOT tests: 3 passed - `git diff --check`: passed ## Checklist - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [ ] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran all tests locally using the UA.slnx solution against at least .NET Framework and .NET 10, and all passed. - [ ] I fixed all failing and flaky tests in the CI pipelines and all CodeQL warnings. - [ ] I have addressed all PR feedback received. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com> |
||
|
|
7fe8b30cc3 |
Publish migration plugin marketplace and update NuGet dependencies (#4358)
# Description - Replace the Azure DevOps continuous-preview feed with the GitHub Packages NuGet feed, including its classic PAT `read:packages` requirement, while retaining nuget.org for official public 2.0 preview releases. - Document one-shot MCP execution with `dotnet tool exec` and `dnx`. - Make this repository registerable as the `opcua-dotnet` Copilot marketplace and package `opcua-v20-migration` as an installable plugin. - Bundle the 15 thematic migration documents for offline plugin use and add a CI check that keeps the bundle and marketplace metadata synchronized. - Align the migration analyzer guidance with current master, including the new `UA0030` analyzer while retaining `UA0029` as a runtime-shim-only marker with reserved diagnostic metadata. - Restrict generated collection-shim type discovery to consumer source declarations before the exact `System.<Type>` / `Opc.Ua.<Type>` metadata fallback, with regression coverage for dependency-only metadata. - Update 52 unpinned NuGet package IDs across 60 central entries, plus two schema-test overrides, while preserving documented compatibility and security pins. ## Related Issues - Fixes #4330 ## Validation - `dotnet build UA.slnx -c Release --no-restore`: 0 warnings, 0 errors. - Migration analyzer tests across net472, net48, net8.0, net9.0, and net10.0: 795 passed, 0 failed, 0 skipped. - The migration plugin bundle, manifests, PowerShell, XML, links, and isolated marketplace installation validate successfully. - The direct and transitive NuGet vulnerability audit reports no known vulnerabilities. - A full solution test run was completed before merging current master. Its remaining failures were existing locale, path-length, GPU, shared-state, and flaky integration failures. Persistent cases reproduced on the pre-update commit, while isolated reruns cleared the dependency-relevant Core, GDS, Sessions, Subscriptions, and durable-queue failures. ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e0ecb0e9-e5ef-4b2f-9922-41a7d42b0372 |
||
|
|
4eccafbe2f |
Remove AAS projects and samples (#4359)
# Description Remove the Asset Administration Shell projects and samples from the mainline repository while preserving the complete implementation and history on the `aas` branch. This change: - removes the `Opc.Ua.Aas`, `Opc.Ua.Aas.Client`, `Opc.Ua.Aas.Server`, and `Opc.Ua.Aas.WoT` projects; - removes the AAS test project, AOT coverage, sample, and documentation; - removes AAS entries from `UA.slnx`, the expected package list, and related build configuration; - removes the now-unused `System.IO.Packaging` package version; and - updates remaining documentation and comments so they no longer advertise removed AAS functionality. The preservation branch `aas` points to the original `master` commit before this removal. ## Related Issues - None. ## Validation - `dotnet restore UA.slnx` - `dotnet build UA.slnx -c Release --no-restore` - Repository-wide sweep for dangling AAS project, package, file, and documentation references The full Release build completed with zero errors. It reported 31 existing analyzer warnings in files unrelated to this change. ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. (Not applicable: this removes a feature and its tests.) - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
cef83550cb |
OPC UA Vision: a vision-guided bin-picking demo an LLM agent can see and drive (#4235)
# Description Implements **OPC UA — Vision** and uses it to build the demo the companion specifications were drafted for: an eye-in-hand camera watches a bin of parts, a language model *looks at the frame*, decides which part matches the instruction, and commands the robot through Robot Intent — with the OpenUSD viewer showing the arm move and the bin empty out. > **Draft, and stacked.** [#4195](https://github.com/OPCFoundation/UA-.NETStandard/pull/4195) has merged and master is merged in, so what is left on top of `master` is the Vision work plus the unmerged `marcschier/ai-model-management` branch, which the demo needs for the model/dataset/deployment provenance. Review the Vision commits; the AI Model Management work lands with its own PR. ## What is here **The specification, implemented.** `Opc.Ua.Vision` is generated from the spec's own `Opc.Ua.Vision.NodeSet2.xml`, vendored byte-identically (SHA-256 `79C592C6…`, verified against a fresh download and stored LF-preserved like the Robot Intent NodeSet). `Opc.Ua.Vision.Server` builds the §4.2 discovery object, sensors, the coordinate-frame tree, calibrations, media endpoints, inference pipelines and the §9 feedback methods, with rendering and inference behind injectable providers. `Opc.Ua.Vision.Client` is built on the generated ObjectType proxies and composes transforms across the frame tree, which is what turns a detection in camera coordinates into a pose a robot can act on. **Two perception paths behind one contract**, which is exactly what `InferenceLocationEnum` is for. `OnServer` is a deterministic detector derived from the stage's ground truth — no model, no network, no GPU, so CI can exercise the whole loop. `EdgeOffServer` is the agent: it sees the frame over MCP and calls `SubmitDetections`, and the Server publishes results it did not compute. A client reads `DetectionResultType` identically either way. **The agent's eyes.** `tools/Opc.Ua.Mcp.Vision` adds 22 `vision_*` tools, which with the four shared connection tools makes the `vision` profile 26; `vision_get_frame` returns the camera image as an MCP `ImageContentBlock`, so the model looks at pixels rather than reading a description of them. Tool profiles now compose — `--profile vision,robotics` yields 62 tools — so one agent can both see and act without dragging in the full 136-tool catalogue. **The cell.** `samples/Robotics/BinPickingCell` follows the spec's own Robotics-Vision Addendum: the `world → robot_base → flange → gripper_tcp` frame tree with `camera_eih` on the flange, the `HandEye` extrinsics and the `Intrinsics612x512` intrinsics — the addendum's 2448×2048 calibration scaled to the 4×4-binned grid the camera actually delivers. `samples/Robotics/BinPickingClient` hosts the composed MCP catalogue and optionally the viewer. **Documentation.** `docs/Vision.md` (~800 lines) covers the model, hosting, the build context, every topology builder, both perception paths, feedback, the MCP profile and the limitations; linked from `docs/README.md`, with the profile table in `docs/McpServer.md` and READMEs for both samples. ## Evidence, not assertions Each of these was measured on a running system rather than assumed: | Claim | Evidence | |---|---| | The camera sees the bin | 612×512 frame with the bin and all five parts in view — red `(212,47,47)`, green `(61,193,71)`, blue `(54,85,218)`, yellow `(215,196,58)`, orange `(209,119,55)` | | An agent can act on what it sees | Over MCP, `vision_get_frame` returns a 612×512 PNG that decodes from base64 to 1,254,051 bytes, the descriptor beside the detections says 612×512, and all five `BoundingBox2D` centres land **inside** the frame on the pixel colour their own detection claims | | The geometry is right | A detection composed `camera_eih → world` lands on the authored position, residual **0.0000 m** — on both the on-server and off-server paths | | The detector tracks the world | Placing a part drops the next scan from 5 detections to 4, with that part absent | | The robot obeys the agent | Scripted run took command authority and completed a pick and a place, both `Succeeded` | | Tool catalogues are honest | Counted from a running server: vision 26, robotics 40, `vision,robotics` 62, full 136, **zero duplicates** | | Every node is browsable by NodeId | Eight representative NodeIds — both folders, a pipeline, a frame, the sensor, its Calibrations folder, the HandEye calibration, the Media folder — all `Good` | That tool-count check matters because the profile tests collapse names into a `HashSet` and *cannot see* a duplicate registration — so the new tests count registrations, not names. ## Things that were wrong, and are not now Writing the tests found three defects of one kind — code that answered instead of refusing. A zero-norm quaternion was silently rewritten to identity, so a caller with an all-zero orientation got a confident wrong grasp pose. `SubmitCorrection` rewrote a null `ResultId` to empty and called the sink anyway. A bad `StageIdentifier` **killed the host process** inside native `UsdStage.Open`, via an exception .NET cannot catch. All three now refuse loudly and are pinned by tests. **Nodes that existed but could not be reached.** The node manager indexes the Vision root before configurators run, so everything the fluent builder grafted on afterwards — both folders, the pipelines with their Results and Feedback, the frames, the sensors with their optics, calibrations and media endpoints — was never added to the index. Browsing *forward from a parent* worked, because that walks `NodeState.Children` in memory; browsing any of those nodes *by its own NodeId* returned `BadNodeIdUnknown`, which is how an ordinary client and the MCP discovery tools navigate. Registration is now deferred and flushed after each configurator. The first fix covered only the DI path and left the documented non-DI fallback broken in exactly the same way, so `ConfigureVisionAsync` now exists and registers what it builds. That defect survived a green suite because every test browsed children. The new tests assert by NodeId, and were checked against the broken code — with the flush removed, they fail. **Every Method was uncallable.** The Server passed 426 unit tests while not one of its Methods could be invoked over a real session — `RunInference` returned `Bad_TooManyArguments`, and so did every other Method that takes arguments, which is all of them but `StartContinuous` and `Stop`. Three independent faults, each sufficient on its own: the Method nodes were never created (they are Optional children, and the dispatcher guards every attachment with a null check, so it silently attached nothing); nothing declared the arguments, so the stack concluded none were expected; and `MethodDeclarationId` pointed at a synthesised NodeId rather than the NodeSet declaration a client calls with, so the lookup missed the instance Method entirely. The builder now creates the Methods, the `Results` folder and the `Feedback` object when a provider or sink is configured, declares every signature, and names the right declaration. Unit tests could not have caught this: they invoke the handler delegates directly and never go through `MethodState.Call`. It took an end-to-end test over a real session. Relatedly, children created by the generated `CreateOrReplace` helpers carry no `ReferenceTypeId`, so nothing could reach them — fixed once in the build context rather than at the several dozen call sites. **And one where I was wrong.** Earlier in this branch I reversed my own agents' decision and made `SubmitDetections` accept an empty detection set, reasoning that "I looked and the bin is empty" is a correct observation and refusing it forces a correct agent to invent a detection. The reasoning is sound; it is also an argument *against* the specification rather than an implementation of it. §9.5 is normative and explicit — its StatusCode table lists `Bad_InvalidArgument` for "`Detections` empty; or `SubmitCorrection` supplies both or neither corrected array". Worse, **none of §9.5's argument rules were enforced anywhere** — the dispatcher forwarded every submission straight to the feedback sink, so conformance depended on whichever sink a host installed. They are Server obligations, so they now live in the dispatcher, ahead of the sink, proven by strict mocks that would throw if the sink were consulted. The usability problem is real and does not go away by conforming, so it is raised where it can be fixed: [opcua-drafts#70](https://github.com/marcschier/opcua-drafts/issues/70). ## Tracking the updated drafts The Vision, Robot Intent and AI Model Management NodeSets on `opcua-drafts` `main` have moved, partly because of feedback this branch raised. All three are re-vendored byte-identically — verified by comparing git blob hashes against the upstream files rather than by eye — and the implementations follow. **Two statements that were previously inexpressible now have a wire representation.** `SubmitDetections` takes `SceneIsEmpty` and `SubmitCorrection` takes `RetractAll`, so an agent can report "I examined this frame and there is nothing in it" — the terminating condition of the bin-picking task — and can retract a false positive by correcting a result down to nothing. Both are checked in both directions, because the flag is exactly what separates a deliberate empty observation from a lost payload: an empty array without the flag is refused, and the flag with an array attached is refused as two contradictory claims about one frame. Implemented in the dispatcher, the client, the MCP tools the agent drives, and the sample cell. Also absorbed: `LampType` and `LightingMode` became enums (§66), `ProfileName` became `DefaultProfileName` on the endpoint (distinct from the `ProfileName` a caller passes to `GetStreamEndpoint`, which keeps its name), AI `Invoke`/`InvokeAsync` gained `PayloadUri` under the same exactly-one rule, and AI `ListModels` gained a `ContinuationPoint` — because `MaxResults` alone puts everything past the bound permanently out of reach. **OpenUSD is on `0.9.0-alpha`**, verified on the running cell rather than by restore alone: D3D12 backend, 612×512 frame, the bin and all five parts in view and detected — the same result `0.8.0-alpha` produced. The one local workaround that survives is `VisionMethodArguments`, and its reason has changed rather than gone away: the builder materialises these Methods as Optional children through `CreateOrReplace`, which constructs the state object directly and never runs the generated factory that now carries the signatures. Checked rather than assumed — removing the declarations fails five of the eight `VisionMethodSurfaceTests`. ## Review feedback Three threads, all addressed in `0fe2803a3`: - **The AI Model Management work is now a library family, not a sample.** `src/Opc.Ua.AI` (model), `.Inference` (`IInferenceBackend` and backends), `.Server` (node manager) and `.Client` (discovery, reads, calls, artefact transfer), in the same shape as the Robotics and Vision families; the samples become `samples/AI/ModelManagementServer` and `ModelManagementClient`. The client library needed real work rather than relocation: its browsing surface was `private` inside the sample's scenario runner, so moving the file alone would have produced a library nobody could call. - **No cloud-vendor SDK is referenced anywhere.** Inference goes through `Microsoft.Extensions.AI` — an `IChatClient` is implemented by hosted services and on-device runtimes alike, which is exactly the property clause 8.1 asserts. `Azure.Identity` is gone: workload identity is not an Azure feature, every platform projects a token into a file and each SDK reads that file behind its own API, so the resolver reads it directly. The comment claiming `Microsoft.Extensions.AI` would force a `System.Text.Json` bump was stale — it pins 10.0.10, which this repository already centralises on. - **`BinPickingCell` and `BinPickingClient` moved into `samples/Robotics/`**, which is where they belonged: the cell was already linking the arm, its kinematics and both USD assets out of `IntentEnabledRobot`. ## Upstream Work that belonged elsewhere went elsewhere rather than being worked around locally: - **`openusd-dotnet` [#13](https://github.com/marcschier/openusd-dotnet/pull/13)** — merged and shipped in `0.8.0-alpha`. `SilkFrameCapture.Capture` rendered only the first capture of a session and silently returned a blank frame for every one after it. Also filed [#14](https://github.com/marcschier/openusd-dotnet/issues/14) (no camera-by-prim API) and [#15](https://github.com/marcschier/openusd-dotnet/issues/15) (skipped tests reported as passed — which fooled me for several runs). Both are fixed; this branch takes `0.8.0-alpha` and deletes two workarounds, including 143 lines of hand-rolled projection maths. - **`opcua-drafts` [#66](https://github.com/marcschier/opcua-drafts/issues/66)–[#69](https://github.com/marcschier/opcua-drafts/issues/69)** — a close read of the spec produced 22 candidate ambiguities; 12 were discarded as misreadings with clause citations and the remaining 10 filed. One is concrete: five members exist in the NodeId CSV that no clause documents. - **`opcua-drafts` [#70](https://github.com/marcschier/opcua-drafts/issues/70)** — §9.5 could not express an empty observation or a false-positive retraction. **Adopted**, with the explicit-flag resolution argued for: `SubmitDetections` gained `SceneIsEmpty`, `SubmitCorrection` gained `RetractAll`, and the corrected-array rule relaxed from *exactly one* to *at most one*. - **`opcua-drafts` [#71](https://github.com/marcschier/opcua-drafts/issues/71)** — the Vision NodeSet browse-named all 13 `InputArguments` / `OutputArguments` Properties in the Vision namespace rather than the base namespace, so a stack could not find them and every Method in the specification was uncallable. **Fixed**; this branch now tracks the corrected artefact. ## The defect underneath it all The end-to-end suite was removed earlier in this branch because nine of its fourteen tests failed on one symptom: a client could not open a pipeline's `Feedback` object or enumerate its `Results`, even though browsing the same parent listed both. Restoring it meant explaining that, and the explanation was not in Vision. `RelativePathElement` initialised **`IsInverse` to `true`**. A caller that sets a `ReferenceTypeId` and a `TargetName` — which reads as *"the child reached by this Reference"* — was therefore asking the Server for the **inverse** Reference, and got `Bad_NoMatch` with nothing in the request to suggest why. `ObjectTypeClient.ResolveChildNodeIdAsync` is such a caller, and it is what **every source-generated Optional-child accessor in the stack** is built on — the accessor whose own documentation cites `AlarmConditionTypeClient.GetShelvingStateAsync` as an example. Vision is merely where it surfaced, because Vision puts two Optional children on the path an off-Server agent must take. The sibling helper in `StateMachineTypeClientExtensions` already set `IsInverse = false` by hand, which is the shape of a trap rather than a default. The default is now forward, `ResolveChildNodeIdAsync` says so explicitly, and the existing test that had recorded `true` as *expected* now pins `false` with the reasoning. Of the forty `RelativePathElement` construction sites in the tree, that accessor was **the only one relying on the default at all** — every deliberate caller already set it. Two further defects were found underneath it, both real and both fixed: - The generated `CreateOrReplace` helpers construct a child state object directly and leave **`TypeDefinitionId`** unset as well as `ReferenceTypeId`. A child referenced by nothing cannot be browsed; one with no type definition is a malformed Object that a client filtering by type silently skips — which is exactly what `EnumerateResultsAsync` does. Normalisation moved to `VisionNodeManager`, so it also covers the case the builder cannot see: a result published **at runtime** by an inference provider, long after the address space was built. That path produced results a client could list but not read. - `Opc.Ua.AI` shipped without an `AssemblyInfo`, so the solution built with six `CA1014` warnings. I had initially "fixed" this in the Vision client with Browse-based fallbacks, on the strength of a probe that appeared to show `TranslateBrowsePathsToNodeIds` failing even for `Server → ServerStatus`. That probe was wrong: it used an unqualified `ReferenceTypeIds`, which inside a `Opc.Ua.Vision.*` namespace binds to *Vision's* `ReferenceTypeIds` and yields a nonsense NodeId. Chasing that to the end is what turned up the real cause. **The fallbacks are gone** — they would have hidden a stack-wide bug behind a Vision-shaped workaround, and one of them matched on browse name while ignoring the namespace index. `tests/Opc.Ua.Vision.Intent.Tests` is restored and back in `UA.slnx`, passing **14/14** on its own merit rather than through a client-side workaround. A registration test now pins the pipeline's `Feedback` and `Results` children, which nothing covered before. ## Running it, and what that found The demo's premise is that a language model **looks at the bin and reasons about what it sees**. Running it end to end showed it could not, for reasons no test covered, because every one of them lived between components. **The sample did not compile.** Two `CS8604` nullable errors with `TreatWarningsAsErrors`, so neither the cell server nor the demo could be started at all. Worth recording that my first explanation was wrong: CI *does* build this project - the Linux leg's `net10.0` pass compiles it, log-confirmed - so this is not a coverage gap in the workflow, and no workflow change was made. **`Pick` and `Place` never moved the arm.** They were a delay plus a gripper action, so the operations reported `Succeeded` while nothing travelled. They now solve IK once and interpolate in joint space. **And the parts never moved**: the arm swung but the bin stayed full, because nothing tied the held object to the world state. The connector now creates 11 monitored items where it created 6, and a scripted run delivers `SeqNo=151` across 10 messages where it delivered `SeqNo=3` across 1. **The frame was smuggled through a string field.** The media provider put a base64 *data URI* of the PNG into `VisionImageReferenceDataType.Uri` - a field meant to be a *reference*, while the bytes were already returned in the inline `ByteString`. Every response therefore shipped the image twice, and a 1,311,400-byte PNG became a 1,748,536-character string against a `MaxStringLength` of 65,535: **26.7x over**. That is why raising `MaxByteStringLength`, `MaxArrayLength` and `MaxMessageSize` never helped - none of them govern a `String`. Separately the MCP tool assigned raw bytes where `ImageContentBlock` requires base64, so the wire carried `"data":"�PNG..."` - every non-UTF-8 byte replaced by U+FFFD, destroying the image and inflating the response to 6.8 MB. **The camera was aimed at nothing.** The arm's home pose, the flange scan pose the Vision model declared and the joint angles in the USD stage were three independent claims about where the camera was, and they disagreed - the declared flange orientation pointed the view 86 degrees away from the projection camera. Solving them as one thing exposed two further constraints that are easy to miss: the camera has to sit off the tool axis or it photographs its own gripper, and the IK branch has to be elbow-back or a link parks under the camera and fills the frame with the arm's own upper arm. A third was a regression this branch introduced and `Opc.Ua.Robotics.Tests` caught - aiming a straight-down camera from a point on the base's own X-Z plane puts the wrist on the J4/J6 singularity, so every motion away from home fails to solve. A 15-degree camera roll, free because the camera still looks straight down, gets 25 degrees clear, which is the margin the poses before this branch had. **The frame and the detections were different images.** The sensor declared 2448x2048, the clip endpoint 1280x1024, and the renderer produced 640x512 - not even the same aspect ratio. The ground-truth detector projects through the declared intrinsics, so its boxes lived in 2448x2048 space while an agent was handed a 640x512 picture: *"pick the red cube you can see"* arrived with coordinates that pointed off the image. Everything now agrees at 612x512, an exact 4x4 bin of the native sensor with the intrinsics scaled to match; the native size stays in the model and serial number, where it describes the hardware rather than the image. **`LatestClip` was created and never written**, so it reported `Bad_NoDataAvailable` for the life of the Server and a consumer following the model - read the published frame, call the method only if there is none - never got a frame. The dispatcher now publishes a clip it has just encoded, for every Vision server rather than this sample alone. One genuine API gap turned up on the way: `OpcUaServerOptions` exposes `MaxByteStringLength`, `MaxArrayLength` and `MaxMessageSize` and **no `MaxStringLength`**, so a Server that legitimately needs a long string could not raise the limit through the hosting API at all. Added, with tests. It is not the fix for anything above - that was a design bug - but the missing knob was real. ## Not finished - **Two projects sit below the repository's 80% line coverage.** `Opc.Ua.Vision.OpenUsd` is at 71.1% — what is left only executes with a real OpenUSD stage and plugin tree, which is not on every build agent. `Opc.Ua.AI.Inference` is now at **86.7%**, up from 12.9%: the code this branch *wrote* is covered by 25 tests, and `RestChatCompletionsBackend` — previously the whole of the shortfall — is covered by 18 more against a stubbed `HttpMessageHandler`, exercising success, refusal, throttling, retry-after, timeout and cancellation without a network. Everything else clears the bar — `Opc.Ua.Vision.Server` 92.1%, `Opc.Ua.AI` 91.4%, `.Vision.Client` 88.1%, `Opc.Ua.Vision` 85.7%, `Opc.Ua.AI.Server` 80.1%, all measured with coverlet. The CI coverage gate reports itself as advisory and non-blocking. - **`SamplesCollected` is not counted here, and `docs/Vision.md` now says so.** Section 9.4 requires a Server to count a negative example (`SceneIsEmpty` / `RetractAll` carrying a `GroundTruthLabel`) exactly as it counts one carrying geometry. `VisionNodeManager` does not, and structurally cannot: `SamplesCollected` is a property of `LearningJobType`, which the *AI Model Management* companion defines, and Vision reaches the job through a `NodeId` value rather than a Reference **precisely so this model takes no dependency on the model that defines it**. What the Server does guarantee is that the negative example survives the hop intact — both flags are carried verbatim to `IVisionFeedbackSink` — so a host that binds a learning job has everything it needs to satisfy the clause on the counter it owns. Stated in the limitations rather than left implied. ## Related Issues Was stacked on #4195, now merged. No tracking issue yet for the Vision implementation itself — happy to open one if you would like the design recorded as an ADR before this goes further. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. — *Types 8562, Server 4072, Client 2123, InformationModel 1063, Robotics 540, Tools 466, Vision 442, AI 74 and the restored Vision+Intent end-to-end suite 14/14, all green on net10.0. Only `.OpenUsd` (71.1%) is below the 80% bar — see Not finished.* - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. — *`dotnet build UA.slnx -c Release`: 0 errors, 0 warnings.* - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. — *net48: Types 8555, Client 2126, Vision 385 green. Two net48 Server tests fail (`CreateUserManagementSeamBindsTheModelToTheServerAsync`, and `DurableDataValueQueueVerifyReferenceBatchingAsync` only under parallel load); both were confirmed **pre-existing** by re-running them with the `IsInverse` change reverted, which fails identically.* - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. — *The net48 build legs failed because the restored end-to-end project used `ValueTask.FromResult` / `string.Create`, which do not exist on .NET Framework; fixed in `fb57522da` and verified building on every TFM. `DisposeDrainsHeldConnectionCallbackBeforeListenerDisposal` failed once on the Ubuntu Client leg — a 13 ms timing-sensitive reverse-connect disposal test that passes locally 2123/2123 and touches no browse paths.* - [ ] I have addressed **all** PR feedback received. ## Typed Vision-guided Robotics MCP workflows The branch now also makes the demo workflow first-class in the Full MCP profile: - replaces stringified Robotics intent and mission inputs with typed request schemas and exact name-or-NodeId selectors; - adds bounded operation/mission paging, stable step-to-operation identity, retained mission discovery, and `robotics_wait_mission`; - makes `vision_run_inference` return bounded typed detection, inspection, or segmentation summaries in one call; - adds same-session `robotics_vision_pick` to select a detection and submit Pick or Pick/Place work without implicitly taking authority, retrying, cancelling, or waiting; and - starts Publish workers for classic subscriptions so an independently launched OpenUSD viewer receives live robot and part updates. The migration guide documents the intentionally breaking MCP request-schema changes. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa Copilot-Session: a248a589-9a20-4372-868e-5d347e57001b Copilot-Session: 99d1ab42-f83a-41ab-bf3e-85bf81fadace Copilot-Session: bcbc8ac9-5abd-42a8-8bac-e10533790e2e |
||
|
|
397db603db |
Add OPC UA for Asset Administration Shell: the published OPC 30270 and v3 draft (#4244)
# Description Implements the **Asset Administration Shell** companion specifications on top of `marcschier/wot-type-binding`, in two halves that a Server chooses between: * **AAS V3** — the [draft](https://github.com/marcschier/opcua-drafts/tree/main/companion-specs/AAS) (`http://opcfoundation.org/UA/I4AAS/v3/`), all sixteen conformance units: the metamodel, the registry built on the abstract xRegistry base, generational materialization, federation, packages with digest-bound integrity, the Digital Product Passport vocabulary, and the Annex F bridge to WoT Thing Descriptions. * **AAS V2** — the published [OPC 30270](https://reference.opcfoundation.org/specs/OPC-30270/full) (`http://opcfoundation.org/UA/I4AAS/`), which maps the AAS V2.0.1 metamodel: object model, ingestion of XML, JSON and AASX, materialization, server and client. The two are different metamodel generations rather than two revisions of one model — V2 keeps `Asset` as a first-class Identifiable, `identification.idType`, Views and ordered collections, none of which survive into V3 — so they are **alternatives**. A Server hosts one or the other; this is documented rather than enforced at startup. Each is named for the generation it maps (`Opc.Ua.Aas.V2`, `Opc.Ua.Aas.V3`, `AddAasV2Server`, `AddAasV3Server`), and what genuinely serves both carries no marker and sits at the root of `Opc.Ua.Aas`: the deterministic NodeId encoding and idShortPath conventions, `AasOptional<T>`, the digest rule, the materialization diagnostics, the AASX container walk, and the value and operation providers, which are keyed on `NodeId` and `Variant` and so carry no version at all. ## What is in here **New libraries**: `Opc.Ua.Aas`, `Opc.Ua.Aas.Server`, `Opc.Ua.Aas.Client`, `Opc.Ua.Aas.WoT`, plus `samples/Aas` and `docs/Aas.md`. **Enabling changes to existing code**, each needed by the above: * The OPC UA `Decimal` DataType added to `Opc.Ua.Types`, with its Part 6 §5.1.10 wire encoding. `BinaryDecoder` gained one internal member, `TryReadRemainingBodyBytes`, because a trailing field with no self-describing length cannot otherwise be read; `Decimal.Decode` is its only caller. Part 6 Table 3 types the unscaled `Value` as an **OctetString whose byte count is `Length` − 2**, not a length-prefixed `ByteString`, so it carries no length of its own — `DecimalEncodingTests` pins the body byte-for-byte against that mistake, and against the `Length ≤ 2` values the same table calls invalid. * The xRegistry projection engine extracted into `Opc.Ua.XRegistry.Server` and `Opc.Ua.WotCon.Server` migrated onto it (−848/+387 lines), with the WoT Connectivity tests passing **unedited**. * Two source-generator fixes, both of which made a model impossible to build (see below). ## Defects found and fixed along the way Running the stack end to end, rather than trusting per-area test suites, surfaced defects that no unit test caught. The ones worth a maintainer's attention: * **The generator emitted non-terminating constructors.** A structure field with no declared default was initialized with `new T()`, so a self-referential DataType (`AASReferenceDataType.referredSemanticId`) recursed until the stack overflowed and `AddOpcUaAas` could not load the model at all. The test host *died* rather than failing. Only the field that closes a cycle is now left null. * **The generator omitted `new` on a redeclared placeholder.** `AASOrderedSubmodelElementCollectionType` redeclares the `SubmodelElement` slot of its base type, and the generated `Add` method hid the base one without saying so, so the V2 model would not compile. No existing model could have hit either defect — both are build failures, so anything that builds today is unaffected. * **The metamodel half had never worked outside its unit tests**: the emitted NodeSet declared no reference-type aliases and was rejected by the loader; the Mandatory `Invoke` Method was never emitted, because a NodeSet is imported as written rather than instantiated from its type; the runtime bound at the authored namespace index instead of the one the Server assigns; and `MethodDeclarationId` was addressed in the wrong namespace. Every materializer test inspected the in-memory object graph, which cannot see whether the graph is loadable — two tests now import it exactly as `RuntimeNodeSetNodeManager` does. * **The client could not see any Property member.** It browsed `HasComponent` with subtypes, but `HasProperty` is a sibling under `Aggregates`, so reading a value failed and invoking failed argument validation. * **Every repeated registry write forked a duplicate group and resource** instead of adding a version, because the identifier was re-derived against a sibling set that already contained the entity itself. This also made the persistence bounds unreachable. * `.gitignore`'s `**/packages/*` silently swallowed the whole package-integrity implementation and its tests, which would have vanished on a fresh clone. ## Specification findings Nine findings against the V3 draft are fixed in [marcschier/opcua-drafts#75](https://github.com/marcschier/opcua-drafts/pull/75). The substantive one: clause 6.1.5's absent-versus-empty rule was **unsatisfiable** for five container fields that materialize as parent components with no node of their own, which made `AAS-LosslessRoundTrip` unachievable. OPC 30270 contradicts itself twice. Both are recorded in `AasV2ConformanceUnits.cs` and the documentation rather than smoothed over: Table 83 spells the unit `I4AAS MultiLangaugeProperty` where Table 85 spells it correctly, and Table 85 lists an `I4AAS Security` unit that Table 83 never defines. Table 84, which would assign profile URIs, is empty, so `ServerProfileArray` gains no I4AAS entry. ## Known limitation Calling an `Operation` **with arguments** answers `BadTooManyArguments`. Method resolution works from both the instance NodeId and the `MethodDeclarationId`, and the argument definitions are published, but `NodeState.AddChild` does not route through `MethodState.FindChild`, so a Method imported from a NodeSet leaves its typed `InputArguments` null. Closing this means changing how the shared NodeSet import claims typed children, which affects every consumer of runtime NodeSets and is out of scope here. It is documented in `docs/Aas.md` §11, the conformance matrix marks `AAS-OperationInvoke` partial rather than claiming it, and `samples/Aas` exercises the path and reports the status rather than working around it. V2 is unaffected — its `Operation` is an OptionalPlaceholder with no declared arguments. ## Related Issues - No tracking issue yet. Opening as a **draft** for design discussion first, per the ADR guidance below; happy to open one and link it once the shape is agreed. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. ### Verification | Check | Result | |---|---| | AAS tests | **670 passed, 0 failed** on net472, net48, net8.0, net9.0, net10.0 | | Coverage | Client 80.8 %, Server 81.2 %, `Opc.Ua.Aas` 82.0 %, WoT 82.4 % — all above the 80 % gate | | Libraries | `Opc.Ua.Aas`, `.Server`, `.Client`, `.WoT` build clean on every target framework, zero warnings | | Regression | `Opc.Ua.Types` 8677, `Opc.Ua.WotCon` 1094, `Opc.Ua.XRegistry` 198, `Opc.Ua.Server` 4050 — all green | | Sample | `samples/Aas` builds warning-free and runs end to end | | Merge validation | Solution builds clean on net10.0 and net48; WoT Types 504, WotCon 1111, AAS 670, XRegistry 198 tests passed | | CI | All 99 checks green on `ff9298e44`; only the CLA signature status is outstanding | ### Review pass A full review against both specifications, and for API consistency and correctness, produced twelve fixes. Each one has a test that fails without it. | Defect | Where | Why it mattered | |---|---|---| | V2 projection aborted on **every** `File` read from a document | `AasV2EnvironmentRuntime` | The runtime bound `File/Value` unconditionally, the materializer emitted it only with content, and the builder throws `BadNotFound` for an unknown NodeId — aborting the whole registration. No reader ever populates `AasFile.File`, so no AAS V2 document could be hosted at all. | | `aas-suppl` anchored on the wrong part | `AasxPackageReaderCore`, `AasxPackageWriter` | IDTA and the Package Explorer anchor it on the environment part; we used the origin part. Every real `.aasx` returned zero supplementary files, silently. Affects both halves. | | Federation failed closed on pooled connections | `AasFederation` | `ConnectCallback` fires only for new connections, so the second fetch to an origin — and all of net472/net48/netstandard2.1 — failed with a security-flavoured error. The peer is now validated *inside* the callback, which makes reuse safe by construction. | | Malformed `Decimal` escaped as `ArgumentOutOfRangeException` | `BinaryDecoder` | A declared body shorter than `Scale` produced a negative read length. Two of the three test cases had been passing for the wrong reason. | | V2 and V3 cross-wired their value providers | `OpcUaAasServerBuilderExtensions` | Both registered `IAasValueProvider`; whichever was added first served both AddressSpaces. V2 now has its own contract, so co-hosting is harmless rather than silently wrong. `EnvironmentFolder` was also bound and ignored on the V2 path. | | Export filter skipped three nesting kinds | `AasEnvironmentExporter` | Content under `Entity`, `AnnotatedRelationshipElement` and `Operation` was exported unevaluated *and* stamped with a Digest presenting it as complete (clause 6.5.10). List authorization paths also shifted after a denial. | | Concealment held at the Method but not in the projection | `AasRegistryModel` | `GetSubmodel` answered `BadNotFound` while an anonymous Browse revealed the node, its identifier, semanticId and digest (clause 6.5.7). | | File handles were unsynchronized and unowned | `AasElementFileManager` | The async node manager holds no lock, so concurrent Calls raced a bare `Dictionary`; sequential handles let any Session read, seek or commit another's. Writes were also unbounded. | | Projection host was unimplementable outside the stack | `AasEnvironmentProjection` | The handle demanded a type with an internal constructor — our own tests used `FormatterServices.GetUninitializedObject` to get round it. Now mirrors WotCon's interface-typed registration. | | Embedded `FileType` omitted 7 of 10 Mandatory members | `AasEnvironmentMaterializer` (V2) | OPC 10000-3 6.4.4 requires them all; without `Size` and `OpenCount` a Client cannot size a read or detect an abandoned handle. | | Round-trip oracle ignored most of the model | `AasRoundTripComparer` | `Blob`, `File`, `ReferenceElement` and `BasicEventElement` had no case arm, so their fields compared equal by falling off the switch; `valueType`, `semanticId`, `entityType` and `Key.type` were never read. The clause 6.4 losslessness claim rested on it. | Known, unchanged: calling an `Operation` with arguments answers `BadTooManyArguments`, because `NodeState.AddChild` does not route through `MethodState.FindChild`. Documented in `docs/Aas.md` §11. Not addressed in this pass, and worth a maintainer decision: OPC 30270 §9.2 says all NodeIds of Nodes not defined in the companion specification **shall not** use the standard namespaces, and Table 87 marks a vendor-specific instance namespace Mandatory. Both halves currently mint instances in the companion namespace. Separating them touches NodeId minting, the runtime and the client together, so it is better done deliberately than folded into a review pass. This is a large change and touches shared code (`Opc.Ua.Types`, the source generator, the xRegistry projection). It is deliberately a draft: the enabling changes are separable if maintainers would prefer them split into their own PRs. The latest `master` is merged into this branch in `0dd0ff0e1`. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8 Copilot-Session: c3d88950-4a65-4ff6-87d2-1f545a875c9b Copilot-Session: b8ab3763-b9bd-46ae-b0ef-045db26e5782 |
||
|
|
aad9e9b1c1 |
Document Opc.Ua.Core.Diagnostics trim/AOT exclusion and suppress reflection IL2075 warnings (#4249)
## Description
`Opc.Ua.Core.Diagnostics` has two trim/AOT defects that are invisible
today because the assembly is not marked `IsAotCompatible`:
`NicCaptureSource.StartAsync` carries
`[RequiresDynamicCode]`/`[RequiresUnreferencedCode]` that its interface
member `ICaptureSource.StartAsync` lacks (IL2046/IL3051), and
`ServiceCallReassembler` reflects over `IEncodeable` properties by name
(IL2075 ×3), which under trimming silently returns `null` instead of
failing loudly.
### NicCaptureSource / ICaptureSource mismatch
- Left as-is: `ICaptureSource` stays unannotated since most capture
sources don't need the SharpPcap dynamic-loading requirement, and
`NicCaptureSource` genuinely does.
- Documented explicitly in `Opc.Ua.Core.Diagnostics.csproj` that the
project is deliberately **not** `IsAotCompatible`/`IsTrimmable` for this
reason, mirroring the existing pattern in
`Opc.Ua.Mcp.Diagnostics.csproj`.
### ServiceCallReassembler reflection
- Verified the reflection-based property lookup is intentional (avoids
allocating via `ToArray()` just to get a count; covered by existing
tests).
- Added `[UnconditionalSuppressMessage]` with justification and a TODO
to `GetCollectionCount`/`GetPropertyString`, pointing at the
source-generated-accessor approach suggested as the long-term fix,
instead of leaving the reflection silently unannotated.
```csharp
// TODO: Replace these string-keyed property lookups with a source-generated
// accessor ... until then, GetType().GetProperty(...) here is unavoidable
// and is suppressed rather than silently left unannotated.
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern",
Justification = "...")]
private static int? GetCollectionCount(IEncodeable message, string propertyName)
```
Verified by temporarily enabling `IsAotCompatible` locally: all 3
IL2075s resolve, leaving only the accepted/documented `NicCaptureSource`
IL2046/IL3051 mismatch.
## Related Issues
## Checklist
- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [ ] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes #4236
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
|
||
|
|
64d2834b2f |
Confine capture artifact paths to the configured base folder (#4247)
## What `CaptureSessionManager.StartAsync` validates the caller-supplied `SessionFolder` against the configured capture base folder, then copies `PcapFilePath` and `KeyLogFilePath` from the same request through **without validating them**. Both reach the filesystem: | Property | Sink | Effect | | --- | --- | --- | | `PcapFilePath` (write) | `PcapFileWriter.OpenCurrentFile` — `FileMode.Create`, after `EnsureParentDirectoryExists` | truncates and overwrites the named file, creating parent directories | | `PcapFilePath` (read) | `ReplayCaptureSource` → `PcapFormatter.FormatAsync` → `File.ReadAllBytesAsync` | returns the named file's bytes to the caller | | `KeyLogFilePath` | `OpenOrCreate` + seek to end | appends key material to the named file | `InProcessCaptureSource.ResolveArtifactPath` returns a rooted path unchanged and `Path.Combine`s a relative one without normalizing, so `..` traversal is not contained either. Where those values arrive from outside the process — the MCP `start_capture` tool passes a `StartCaptureRequest` straight through — a caller can name any path the process can open, in either direction. ## Why this looks like an oversight The containment already exists and is applied consistently everywhere else in this package. `PacketDecodeTools.DecodePcapWithKeysAsync` and `PacketReplayTools.ReplayPcapAsync` both run caller paths through `McpCapturePath.ResolveAndValidate(path, allowedRoot)` before use. `start_capture` is the one entry point that skips it. ## Change Adds `ValidateAndResolveArtifactPath` beside the existing `ValidateAndResolveSessionFolder` and applies it to both artifact properties. Relative paths are resolved against the validated session folder, preserving the `StartCaptureRequest` contract, and the result is then confined to the configured capture base folder. Containment comparisons are case-insensitive on Windows and case-sensitive on non-Windows platforms. Null and empty paths pass through untouched so sources keep deriving their default names inside the session folder. ## Tests Five added to `SessionFolderValidationTests`: - rejects a rooted `PcapFilePath` outside the base folder - rejects `..` traversal in `PcapFilePath` - rejects a rooted `KeyLogFilePath` outside the base folder - accepts artifact paths inside the base folder and resolves relative paths beneath the session folder - rejects a case-variant sibling path on Linux Each was verified to **fail without the change**. MCP capture, decode, and replay fixtures now place valid session artifacts beneath the configured capture root while preserving explicit outside-root rejection coverage. `SessionFolderValidationTests` (11/11) and the complete `Opc.Ua.Tools.Tests` suite (430/430) pass locally. ## Note This has security relevance for hosts that expose the MCP diagnostics tools, so it was also reported to `securityteam@opcfoundation.org` per `SECURITY.md`. Happy to close this and handle it privately instead if you would prefer. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b8ab3763-b9bd-46ae-b0ef-045db26e5782 |
||
|
|
466c5cb982 |
Robot intent follow-ups: multi-controller executors, spec fixes, and an MCP tool library (#4195)
Follow-ups to #4165, plus the work that followed from re-checking this branch against the specification. Everything started as a defect found by review or by probing the running code; the specification is unchanged except where noted. The TLS certificate lifetime fix (#4196) and the OpenUSD work (#4198) were both merged into this branch. #4196 has since landed on master, so it no longer appears in this diff. #4198 was closed in favour of this PR and its content is here. The MCP trim/AOT work (#4228), the `DataGenerator` null `NodeId` fix (#4227) and the NodeSet external-parent import fix (#4229) have since been merged in as well. All three are closed in favour of this PR and their content is here, described under *Making the MCP libraries trim and AOT clean*, *A generator that returned the value you opted out of* and *A NodeSet parent that lived outside the import batch* below. ## A two-arm cell could not be built `IntentExecution` carried no controller identity, and there was effectively one `IIntentExecutor` per server. An executor handed an execution therefore could not tell **which arm** it was for, so a cell with two `IntentControllerType` instances — an ordinary configuration — was not expressible through the builder or DI. Both halves are addressed: - `IntentExecution` now carries `ControllerId` (`NodeId`, the OPC UA identity) and `ControllerName`, so one shared executor can discriminate; - DI gained per-controller registration — `AddRobotIntentExecutor<TExecutor>(controllerBrowseName)` and an instance overload — so two genuinely different executors can coexist. Resolution order is `WithExecutor` → per-controller DI registration → the single DI executor → a build-time throw. There is still **no silent fallback** to a rejecting executor; that was a Critical defect fixed in #4165 and it stays fixed. `IIntentExecutor` is unchanged, so this is purely additive. ## Declarations that were quietly untrue - **`Accepts<T>` silently discarded a redeclaration.** Declaring an intent type twice — say to narrow `cancelSupported` on the second call — kept the first, wider declaration and said nothing. That is the same class of defect as the capability-honesty problems #4165 fixed (`PauseSupported` declaring a pause the host could not perform; `Aborting` degrading silently to `Buffered`). Identical redeclarations remain idempotent; conflicting ones now throw `BadInvalidArgument`. - **`RI-Interop-40010` assumed Annex B item 3 rather than checking it.** It now requires exact set equality between the OPC 40010 `TaskProgramName` values and the Robot Intent `ProgramType.ProgramId` values. Both sides are readable from the address space without commanding the robot, which is what makes the requirement structural under §12.1. ## Two things the specification requires that this did not do Re-checking the implementation against the merged draft revision turned up two genuine violations. Both are fixed, and the NodeSet is untouched by either. - **Every graphed mission reported `Failed` on success.** `MissionRules.SelectTransition` returned `null` for two different situations — a graph *terminus*, which is the normal success path, and a branch point whose conditions all evaluated false — and the caller collapsed both into `Failed`/`NoTransition`. A three-way result (`Terminus` / `Selected` / `NoConditionMatched`) separates them, so a mission that runs to its end now completes. The failure reason surfaces through the inherited Part 10 result container rather than a new member. - **Facet URIs were never published.** An upstream `may` became a `shall` in the revision this branch adopts. `RobotIntentConformanceUris.Facets` held 31 URIs with **zero** production references — the set existed but nothing emitted it. The published set is now derived from `RobotIntentFacetCalculator`, the same authority that already computed `SupportedFacets`, rather than from a second hand-maintained list that could drift; a reflection test pins the two together. ## An easy client mistake `RequestAuthorityAsync` returns a lease the caller must remember to inspect. Forgetting makes every later submission fail with `ControlNotOwned` for reasons that are invisible at the call site. `RequireAuthorityAsync` throws instead, and names the current owner. `RequestAuthorityAsync` is untouched — inspecting and branching is legitimate. ## Letting an agent drive the robot New `tools/Opc.Ua.Mcp.Robotics` gives a language model the intent API as MCP tools: 36 across discovery, monitoring, direct control and missions, behind a `Robotics` tool profile, composable into any MCP host exactly as the existing OPC UA tool packages are. The `IntentViewerClient` sample hosts them behind `--mcp` (stdio by default, HTTP when `--view`, since MCP stdio and the in-process viewer would otherwise share stdout), and the sample gained a pallet scene so an agent can pick and stack parts and watch the result in the OpenUSD viewer. All the scene logic is server-side — no viewer or connector changes. Four gaps in the client API had to be filled first, since the tools are a thin layer over it and could not have been written honestly without them: `ReadStateAsync`, `ListOperationsAsync`/`ListMissionsAsync`, controller-level `CancelIntent`/`CancelAll`/`Pause`/`Resume`/`Retry`/`ReleaseControl`, and `WaitForCompletionAsync`. The tools refuse rather than paper over: a refusal from the server is returned with its `IntentFailureEnum` and never retried, and command authority is never acquired as a side effect of a tool call. **One defect this surfaced:** `--profile robotics` shipped 36 tools that could not do anything. Every robotics tool resolves a named OPC UA session and only the connection tools can open one, so with no `Connect` in the catalogue the whole profile failed. A test had pinned the defect in place, asserting the profile did *not* contain `Connect` on the line below the `diagnostics` assertion requiring it. Each package now declares its own requirement in its registration extension rather than the host special-casing it, so an application embedding `Opc.Ua.Mcp.Robotics` directly gets a usable catalogue without reproducing the host's pairing. `Full` still takes the connection tools from the core package, so they are never registered twice. The replacement test states the invariant — a profile exposing session-scoped tools must expose the tools that open a session — rather than the instance. ## Making the MCP libraries trim and AOT clean *Merged from #4228.* `Opc.Ua.Mcp.Core` is now `IsAotCompatible` on `net10.0`, and the reflection the analysers then reported is gone. Turning the analysers on is the point: until now they were not running on these libraries at all — including on the tool library this branch adds. - **`OpcUaJsonHelper.Serialize`** went through `JsonSerializer` (IL2026 / IL3050). The conversion helpers in that class already reduce every OPC UA value to a closed set — `null`, the primitive scalars, `string`, `Dictionary<string, object?>` and lists thereof — so the JSON is now written directly with a `Utf8JsonWriter`. Output is unchanged, and that is asserted rather than assumed: `OpcUaJsonHelperTests.SerializeMatchesTheReflectionBasedSerializer` compares the new writer byte for byte against `JsonSerializer` for every shape the helpers emit, including all numeric widths, the custom-formatted scalars, nested sequences and strings that force the encoder to escape. One deliberate contract change: arbitrary object graphs are no longer accepted. Serializing them needs reflection, so `Serialize` now throws `NotSupportedException` naming the type instead of silently stringifying it. No call site in the repository passes anything outside the closed set. - **`McpSchemaFilters`** used `JsonSerializer.SerializeToElement` on a `JsonObject`; it now parses the node's own text, which needs no reflection. `Opc.Ua.Mcp.Diagnostics` is deliberately **not** marked. It depends on SharpPcap and on the reflective service-call dissection in `Opc.Ua.Core.Diagnostics`, neither of which is trim safe — which is why that package does not opt in either. Two improvements are kept regardless: `PacketDecodeTools` serializes `DecodedServiceCall` through a source-generated `JsonSerializerContext`, and `PacketCaptureTools.ListInterfaces` now carries the `RequiresUnreferencedCode` / `RequiresDynamicCode` annotations that `NetworkInterfaceEnumerator.ListLocalInterfaces` already declares, so the constraint is visible to callers instead of implicit. Nothing is suppressed. Marking an assembly `IsAotCompatible` only proves its own IL is clean; it says nothing about whether the library still functions once reflection is gone. So `Opc.Ua.Aot.Tests` references `Opc.Ua.Mcp.Core` and exercises it with JSON reflection disabled, stating what is and is not true today: - tool **results** serialize without reflection — this would fail outright if the `Utf8JsonWriter` change regressed; - tool **registration** still requires reflection, because the MCP SDK builds each tool's input schema by asking `JsonSchemaExporter` for a `JsonTypeInfo` of every parameter type. Without a source-generated context covering the tool signatures — and with the injected `OpcUaSessionManager` being described rather than treated as a service — that throws `NotSupportedException`. The second test asserts the throw. It documents the remaining gap and turns red the moment someone closes it, rather than leaving it to be found in a published binary. The package README says the same thing in prose. **Two pre-existing issues this surfaced, both outside this PR:** pulling `Opc.Ua.Mcp.Diagnostics` into an AOT publish reports `IL2046` / `IL3051` on `NicCaptureSource.StartAsync`, which is annotated `RequiresUnreferencedCode` / `RequiresDynamicCode` while the `ICaptureSource.StartAsync` it implements is not, and `IL2075` on `ServiceCallReassembler.GetCollectionCount` / `GetPropertyString`, which call `Type.GetProperty` on the result of `GetType()`. Happy to file these separately if useful. ## A generator that returned the value you opted out of *Merged from #4227. Fixes #4226.* `GetRandomNodeId(useBoundaryValues: false)` could still return `NodeId.Null` — one of the boundary values the same method offers when boundary values *are* requested. A caller that opted out should not receive one, and `GetRandomExpandedNodeId` builds on it and inherited the same behaviour. The `IdType.Opaque` branch is the practical path: `GetRandomByteString()` draws its length with `NextInt32(MaxStringLength)`, which is inclusive of zero, and a zero-length opaque identifier in namespace 0 satisfies `NodeId.IsNull`. With three namespaces and `MaxStringLength` at 10 that is roughly **one call in 130**. A numeric identifier of `0` in namespace 0 is null too, at around one in four billion. The draw is now repeated while the result is null and boundary values were not requested. Guarding the result rather than the individual branches covers every identifier type, including the numeric case, rather than only the one that happens to be reachable today. The `useBoundaryValues: true` path is untouched, so boundary values remain available when they are asked for. Four tests in `tests/Opc.Ua.Core.Tests/Types/DataGeneratorTests.cs` loop 20,000 draws, because at one call in 130 a single draw proves nothing. Against the **unmodified** generator they fail on draw 93 and draw 1346. Two of the four exist to keep the fix honest rather than to test the bug: `GetRandomNodeIdStillProducesEveryIdentifierType` catches a "fix" that simply stopped emitting opaque identifiers, which would satisfy the null assertions while silently narrowing the generator, and `GetRandomNodeIdWithBoundaryValuesStillYieldsThem` confirms the leak was closed rather than the feature removed. **How it was found:** an intermittent downstream test failure. A test server initialising a static `NodeId`-valued variable via `TestDataSystem.ReadValue` occasionally stored `NodeId.Null`; the encoder correctly writes that as JSON `null`, and a consumer asserting a value was present failed about one run in five — never reproducibly in isolation. It only surfaced under load because `UnsecureRandom.Shared` is a fixed-seed, process-global generator shared between `DataGenerator` and OPC UA transport jitter, so the number of draws preceding address-space initialisation depends on what else is running in the process. ## A NodeSet parent that lived outside the import batch *Merged from #4229.* `UANodeSetHelpers.LinkParentChildRelationships` builds its lookup table from **only the nodes in the current import batch**, links a child when the parent is found — and then cleared `instance.Handle` **unconditionally**, which is exactly where `Import` had stashed the authored `ParentNodeId`. A node whose parent lives in another NodeSet, or is owned by another NodeManager, therefore lost that parent **silently** at import, and nothing downstream could recover it. `RuntimeNodeSetNodeManager.CreateAddressSpaceAsync` already has a step that wires references to external node managers; the parent link never reached it. Such a parent is now recorded rather than discarded, and exposed through `UANodeSet.TryGetUnresolvedParentNodeId`. A parent that resolved normally inside the batch is **not** recorded, so a caller does not re-wire references that already exist. The record lives in a `ConditionalWeakTable`, so it does not keep an imported node alive and does not widen `NodeState` — whose `Handle` is a general-purpose slot a caller may legitimately use for its own purposes, and so is not a safe place to leave the value. `RuntimeNodeSetNodeManager` consumes it to report the one case that cannot be repaired automatically. In NodeSet2 the `ParentNodeId` attribute is a *hint*; the authoritative edge is the entry under `<References>`, which the reverse-reference pass already turns into an external reference. A node that names an out-of-manager parent but carries **no such Reference** materializes with no path from that parent, and no ReferenceType can be inferred to synthesise one — so it is reported as an authoring defect rather than guessed at or silently accepted. This is deliberately **not** a fix for a specific address-space symptom. It removes a silent loss of authored information and gives callers the means to act on it. Whether any particular orphaned-node report is caused by this path, or by namespace-index translation elsewhere, still needs separate diagnosis. **Changed while merging.** As authored in #4229 the accessor was `GetUnresolvedParentNodeId`, returning `NodeId?`. `NodeId` is a readonly struct implementing `INullable`, so that was a `System.Nullable<NodeId>` wrapped around a type that already carries its own null representation — which this repository excludes in favour of an `IsNull` check, and it asks for `TryGet` over casting on struct types. The tests correspondingly asserted `Is.Null` against a struct, which the testing rules also exclude. It is now `bool TryGetUnresolvedParentNodeId(NodeState, out NodeId)`, yielding `NodeId.Null` when there is no unresolved parent. The API is unshipped — it arrives with this branch — so nothing depended on the previous shape. Both tests were re-verified by mutation after the rewrite: disabling the record in `LinkParentChildRelationships` still fails `ImportKeepsAParentThatIsNotInTheSameBatch`. ## Dependencies - **OpenUSD to `0.7.0-alpha`.** All 11 issues we filed upstream are fixed, so the workarounds they justified are gone and the parts of the pump and generator samples that were blocked on them are implemented. - **ModelContextProtocol to `2.1.0`** across all five MCP tool libraries and the executable. No code changes were needed — the surface we use is unchanged in 2.x — but central package management required a cascade of pins, each commented where it sits. Both transports were verified against a running server rather than assumed. MCP 2.x is stateless (no `initialize`, no `Mcp-Session-Id`); an OPC UA session is application state addressed by a caller-supplied name, which fits that model but does assume process affinity, and that assumption is now written down rather than implied. ## Infrastructure The `code coverage` gate was measuring nothing on this branch and reporting success. It is fixed and now enforced, which immediately exposed real gaps — most usefully `RoboticsIntentJson`, which parses agent-supplied JSON (untrusted input from a language model) and had no tests at all. It is now at full line coverage in CI. Patch coverage is **90.20%** against a 75% floor. The `netstandard2.1` build leg was also failing. The MCP projects build as empty shells under a legacy `CustomTestTarget`, and the viewer sample referenced them on the target framework alone, so it tried to resolve a net10.0 shell from net8.0. The sample now derives MCP support from whether those projects are actually being built. ## From review A review pass over the branch found two real defects, both fixed. `RoboticsIntentJson` parses JSON written by a language model, so every malformed shape it can be handed is reachable input; it indexed pose arrays without checking their length and enumerated properties without checking their kind, so a short or wrongly-typed array escaped as `IndexOutOfRangeException` or `InvalidOperationException` rather than the argument error the tool descriptions promise the agent. Separately, `IntentOperationHandle` treated a zero `QueuePosition` as "not observed" and discarded it, so a handle kept reporting the last queued position after the server had said the operation left the queue. The pose validation immediately caught a latent test defect: the pallet scenario interpolated doubles into JSON with the current culture, so under a comma-decimal culture `[0.12, 0.16, 0.28]` became a six-element array. It passed only because that submission is expected to be refused for want of authority, so the wrong coordinates were never used. A third review finding — that a default-constructed `RobotIntentControllerState` hands out a null `NodeId` — was checked and rejected: `NodeId`, `QualifiedName` and `LocalizedText` are readonly structs here, so `default` is the null value and `IsNull` is `true`. The tests asserting that invariant are kept. A second review pass over the merged branch found two more, both fixed and both about an error surfacing as the wrong kind of failure rather than about a wrong result: - **Malformed agent JSON did not arrive as an argument error.** `RoboticsIntentJson` turns every *validation* failure into an `ArgumentException` — a wrong root kind, a missing property, a short pose array — but three `JsonDocument.Parse` calls and the NodeId parse were unguarded. A syntax error escaped as `JsonException` and an unparseable NodeId as `ServiceResultException`, which are the two shapes a language model is most likely to produce. That is the same defect class as the pose/kind checking above: a caller separating bad input from a genuine fault had to know to catch three unrelated exception types. Parsing now goes through one helper that reports a syntax error as an `ArgumentException` naming the argument, keeping the `JsonException` as the inner exception so the position is not lost, and the NodeId goes through `NodeId.TryParse` rather than `Parse`, collapsing its two failure modes into one. `MalformedIntentJsonIsRejected` had asserted `InstanceOf<JsonException>`, pinning the behaviour rather than the intended contract; it now asserts `ArgumentException` like every neighbouring test, and three tests cover the newly guarded paths. - **`ConditionalWeakTable.Add` throws on a duplicate key.** Not reachable today, but `TryGetUnresolvedParentNodeId` documents `Handle` as a general-purpose slot a caller may use for its own purposes — and a caller doing exactly that before re-importing into the same collection would take a hard throw from inside the import pipeline. Removing first makes the most recent import win. `AddOrUpdate` would say this more directly but does not exist on `netstandard2.0`. ## Deliberately not fixed here `FastenIntentDataType.Joint` is refused when the Server exposes no OPC 40450/40451 model, and a client cannot discover that before calling. That is **not fixable in this repository**: §5.4.2 makes `Joint` the defining mechanism while §11.3 says the parameters otherwise stand alone, and `IntentCapabilityDataType` declares capabilities at intent-type granularity with no member-level mechanism anywhere in the model. It is a specification gap, raised as S4 on [marcschier/opcua-drafts#60](https://github.com/marcschier/opcua-drafts/pull/60) rather than worked around with a bespoke mechanism the spec does not define. ## Validation `dotnet build UA.slnx -c Release` on the merged branch — **0 errors, 0 warnings**, with the AOT analysers now running over `Opc.Ua.Mcp.Core`. | Suite (net10.0) | Tests | |---|---| | `Opc.Ua.Core.Tests` | 4247 (86 skipped) | | `Opc.Ua.Robotics.Tests` | 540 | | `Opc.Ua.OpenUsd.Tests` | 909 | | `Opc.Ua.Tools.Tests` | 419 | | `Opc.Ua.Server.Tests` — `RuntimeNodeSet` | 94 | | `Opc.Ua.Robotics.Intent.Tests` | 22 | | `Opc.Ua.Aot.Tests` — `McpAotTests` | 3 | 0 failures. `Opc.Ua.Tools.Tests` rose from 409 to 419 with the serializer-equivalence tests from #4228 and the malformed-JSON tests from the second review pass, and `Opc.Ua.Core.Tests` covers the `DataGenerator` tests from #4227 and the import tests from #4229. `UANodeSetHelpers` also passes on **net48** (5/5). The MCP agent-usability grader scores Core 100 / Full 100. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Copilot-Session: a248a589-9a20-4372-868e-5d347e57001b Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8 Copilot-Session: a0718e8a-a158-45c5-929d-e1d6c0982431 |
||
|
|
4cbcec6e23 |
Update NuGet dependencies to latest and version ASP.NET Core per target framework (#4223)
# Description Refreshes the centrally-managed NuGet dependencies in `Directory.Packages.props` to their latest stable versions, and changes how the ASP.NET Core packages are pinned so their version follows the target framework. ## ASP.NET Core packages are now versioned per TFM `Microsoft.AspNetCore.Authentication.Certificate`, `Microsoft.AspNetCore.Authentication.JwtBearer`, `Microsoft.AspNetCore.Mvc.Testing` and `Microsoft.AspNetCore.TestHost` ship one band per .NET major, carry no `netstandard2.0` asset, and do not roll forward across majors — a `net8.0` project simply cannot consume the `10.0.x` band. A single central pin therefore had to sit on the oldest supported band and hold *every* consumer there, which is why these sat at `8.0.28` while `10.0.10` was current. They are now selected from `$(TargetFramework)`, mirroring the existing MQTTnet split: | TFM | Version | |---|---| | `net8.0` | `8.0.29` | | `net9.0` | `9.0.18` | | everything else | `10.0.10` | The last group is the fallback rather than an explicit `net10.0` match, so a TFM the file does not enumerate — a future `net11.0`, or the `net10.0` shell that legacy `netstandard2.0`/`netstandard2.1` `$(CustomTestTarget)` builds fall back to — still resolves a version. Verified against the generated `project.assets.json` that each TFM of `Opc.Ua.Bindings.Https` really does resolve its own band. ## Version changes `Crdt`/`Crdt.Transport` and the `RaftCs` family 1.1.2 · `Dekaf` 1.11.2 · `JsonSchema.Net` 9.4.0 · `Microsoft.Bcl.TimeProvider`, the `Microsoft.Extensions.*` set and `System.Composition.AttributedModel`/`System.Formats.Asn1`/`System.Text.Json`/`System.Threading.Channels`/`System.Threading.RateLimiting` 10.0.10 · `Microsoft.Extensions.Http.Resilience` and `Microsoft.Extensions.TimeProvider.Testing` 10.8.0 · `Microsoft.NET.Test.Sdk` 18.8.1 · `Microsoft.SourceLink.*` 10.0.301 · `Nerdbank.GitVersioning` 3.10.91 · `Serilog` 4.4.0 · `System.CommandLine` 2.0.10 · `Avalonia`/`Avalonia.Desktop` 12.1.1 · `MQTTnet`/`MQTTnet.Server` 5.2.0.1603 (modern band only) · `Roslynator.*` 4.16.0 · `TUnit` 1.64.6. The two `System.Collections.Immutable` `VersionOverride`s in the schema test projects move to 10.0.10 to stay ahead of what `JsonSchema.Net` requires. ## Removed: the DotNext pin `DotNext` 5.26.3 was pinned centrally but no project references it. The `docs/migrate/2.0.x/packages.md` row claiming `src/Opc.Ua.Lds.Server` consumes it was also wrong — that project references only `Makaretu.Dns.Multicast` and `Microsoft.Extensions.Hosting.Abstractions`. Both are corrected. ## BouncyCastle held at 2.6.2 — CI caught a real regression The first push of this branch took `BouncyCastle.Cryptography` to 2.7.0. CI failed `Opc.Ua.Core.Tests` on **net48 only** (`VerifyPEMSupportDirectoryStoreAsync` expecting 3 certificates and getting 0), and the cause turned out to be a genuine behavioural break rather than a flaky test. BouncyCastle is referenced **only on net472/net48**, where it backs `PEMReader`/`PEMWriter` in `Opc.Ua.Security.Certificates`. 2.7.0 tightened `Asn1.X509.TbsCertificateStructure` to throw: ``` certificate issuer is an empty distinguished name ``` `OpenSsl.PemReader.ReadObject()` therefore aborts on the *first* entry of a chain whose issuer DN is empty. `ImportPublicKeysFromPEM` has no per-object recovery, so the entire file is discarded and the store enumerates nothing. This is a **runtime** behaviour change, not a test artefact: .NET's own PEM support on net8.0+ still accepts such a certificate, so taking 2.7.0 would let the same trusted-peer store load fine on net8.0+ and come back **empty** on .NET Framework. Whether the stack *should* reject these certificates on every TFM is a fair question, but it is a security-behaviour decision that does not belong in a dependency refresh — so the pin is documented in place with that context for a follow-up. Confirmed by bisecting the package version against the net48 tests (2.6.2 passes, 2.7.0 fails) and by reading the actual PEM asset with both versions to capture the exact throw site. ## Deliberately left alone Every existing pin with a documented reason is preserved, and each was re-checked rather than assumed: - **Roslyn compat band** (`roslyn.props`) — `Microsoft.CodeAnalysis.*` stays on the pinned band and `System.Collections.Immutable`/`System.Reflection.Metadata` stay on `$(RoslynRuntimeVersion)`. Raising these is what re-breaks the analyzer `ImmutableArray<T>` identity the file warns about, and it would raise the minimum compiler that will load the shipped analyzers. `Microsoft.CodeAnalysis.Analyzers` stays at 4.14.0 for the same reason. - **`Microsoft.AspNetCore.*` 2.3.11** — last band with a `netstandard2.0` asset; already latest. - **`PacketDotNet` / `SharpPcap`** — SDL native-code pins; already latest stable. - **`System.Net.Http` 4.3.4 / `System.Text.RegularExpressions` 4.3.1 / `System.Private.Uri` 4.3.2** — transitive CVE remediation pins; already latest. - **`MQTTnet` 4.3.7.1207** for `net472`/`net48`/`netstandard2.1` — MQTTnet 5.x dropped those TFMs. - **`BouncyCastle.Cryptography` 2.6.2** — see the section above. - **`Moq`** — pinned. - **Prerelease-only upgrades skipped** — `BenchmarkDotNet` 0.16.0-preview, `NUnit` 5.0.0-beta, `NUnit.Console` 4.0.0-beta, `EmbedIO` 4.0.0-rc1, `Newtonsoft.Json` 13.0.5-beta, `PacketDotNet` 1.4.9-pre, and others. ## Overlap with #4195 — please note **OpenUSD and ModelContextProtocol are intentionally not touched here.** #4195 already moves OpenUSD to `0.6.0-alpha` together with the restored render smoke job and the `OpenUsd.Rendering.Silk.Vulkan` package that the bump actually depends on, and separately moves MCP to `2.1.0`. Rev'ing either in this PR would duplicate that work, and would risk silently downgrading MCP if the two landed in the wrong order. They are left to #4195. There is still a small expected overlap in `Directory.Packages.props`: #4195 also bumps `Microsoft.Extensions.Configuration.Abstractions`, `Microsoft.Extensions.Hosting.Abstractions` and `System.Text.Json` to 10.0.10 — the same values as here, so whichever merges second resolves to an identical line. ## The one code change Roslynator 4.16 adds **RCS1258**, which flags `TypeAttributes.Public | TypeAttributes.Class` in `ComplexTypeBuilder`. It is correct: `TypeAttributes.Class` is `0`, so the flag is a no-op, and the `DefineEnum` call directly above already passes `TypeAttributes.Public` alone. Dropping it is a verified semantic no-op (`Public | Class == Public`). ## Related Issues _No tracking issue — routine dependency maintenance, in the same spirit as the weekly Dependabot NuGet updates configured in `.github/dependabot.yml`._ ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage.<br/>_No new tests: this changes dependency versions, not behaviour. The existing suite is the proof, and the one code change is a verified no-op._ - [x] I have added all necessary documentation.<br/>_`docs/migrate/2.0.x/packages.md` — corrected version rows, removed the wrong `DotNext` row, added a section explaining the per-TFM ASP.NET Core scheme._ - [x] I have verified that my changes do not introduce (new) build or analyzer warnings.<br/>_Full `UA.slnx` build across all six TFMs (`net472`, `net48`, `netstandard2.1`, `net8.0`, `net9.0`, `net10.0`): **0 warnings, 0 errors**. `dotnet restore`: 0 warnings. `dotnet pack`: 0 warnings, 0 errors, package validation passes._ - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed.<br/>_See the verification note below._ - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. ### Update after the first CI run The initial push failed one CI job — `Opc.Ua.Core.Tests` on net48, 2 PEM tests. That was **not** flakiness: it was a real break from BouncyCastle 2.7.0, root-caused and fixed by holding that package at 2.6.2 (see the section above). `Opc.Ua.Core.Tests` on net48 now runs **4161 passed / 0 failed** locally, and the full six-TFM build is still 0 warnings / 0 errors. ### Review feedback Both review threads are addressed in `78bdd0ea3`: - The **package table's third column was stale** in more rows than the ones flagged. Every entry is now checked against the actual `PackageReference` items — `System.Threading.Channels` belongs to `Opc.Ua.Core`/`Core.Diagnostics`/`PubSub.Diagnostics` (not the LDS server), the `Microsoft.CodeAnalysis.*` rows come from the generator/analyzer variant targets under `tools/`, and `Microsoft.CodeAnalysis.Analyzers` plus `System.Reflection.Metadata` have no direct reference at all and are labelled as central pins. The column is renamed **"Referenced by"**, which is what it actually records and what makes it verifiable. - The **BouncyCastle pin now references [#4224](https://github.com/OPCFoundation/UA-.NETStandard/issues/4224)**, which carries the decision on whether the stack should reject empty-issuer certificates on every TFM, whether `ImportPublicKeysFromPEM` should recover per entry, and whether the expired `Test_chain.pem` asset should be regenerated. ## Verification note on the test run The full net10.0 run covered ~48,000 tests. 15 failed, and each was individually shown **not** to be caused by this change: - **4 in `Opc.Ua.Core.Diagnostics.Tests`** (3× `ReadChunkDecryptsStackEncryptedSymmetricChunk`, `PrivateRequestSummaryFormatsKnownRequestKinds`) — reproduced **identically at `HEAD` with the changes stashed**. Pre-existing. - **8 across `Client`, `Di`, `WotCon.Samples`, `Server`** — all pass when their project is run in isolation; they only fail under the parallelism of a full-solution run. The machine also had ~45 orphaned test processes holding sockets, which were cleared. - **3 in `Gds`/`Stress`** — the log shows socket port exhaustion (`Only one usage of each socket address...`). The `CertRotationLiveTests` pair passes when run standalone, and that project's net10.0 package closure contains **none** of the bumped packages (`BouncyCastle` is `net472`/`net48`-only, so it cannot reach a net10.0 test at all). The AOT/TUnit suite (`Opc.Ua.Aot.Tests`) was A/B tested: TUnit **1.58** produced the same intermittent 0–2 failures as 1.64, confirming pre-existing flakiness rather than a regression. On **1.64.6** it ran **129/129 twice**. `net48` was built and exercised via `CustomTestTarget=net48`; the `MSB3277`/`NU1702` warnings it surfaces in `Opc.Ua.Stress.Tests` are structural to that mode (the project builds `net8.0` while consuming `net48` outputs) and pre-date this change — the `net462` asset's assembly version simply tracks the patch number, so `10.0.0.9` became `10.0.0.10`. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2be367ab-e62d-4141-a4df-e5b2ca3cd126 |
||
|
|
0612a43fe4 |
Pluggable cryptography and hardware-held private keys (#4192)
Makes the stack's cryptographic operations replaceable - by another library, an offboard service, or hardware (TPM 2.0 / HSM / PKCS#11 token / cloud KMS) - with no performance cost in the default configuration, and extends this to certificates so a private key need never exist in process memory. Also fixes three pre-existing defects found while validating: STACKGEN001 erroring for every compilation the stack generator was merely loaded into, certificates failing to load from paths past MAX_PATH on Windows, and the MonitoredNode2 tests starving the thread pool. Fixes #4190 |
||
|
|
2b44fcad22 |
Robot intent: a task-level command API with a Part 10 lifecycle (#4165)
# Description Implement the draft **OPC UA — Robot Intent** companion model end to end: task-level motion verbs, a Part 10 lifecycle, a server hosting surface, a client, two samples and a live integration suite. It replaces the opt-in convention API merged in #4127 with an implementation generated from a companion information model. ## Why OPC 40010-1 *Robotics* describes robot **topology** in detail and defines **no motion verbs at all** — its entire actuation surface is `Start`, `Stop` and loading a named program. A conformant client can discover everything about a robot's construction and cannot ask it to move anywhere. #4127 filled that gap with ten verbs in an application-owned namespace, resolved by BrowseName, and the file said what it was: *"opt-in, explicitly non-normative industrial operation conventions"*. That contribution established a **vocabulary**. It established no **lifecycle**, and the lifecycle is the harder half. A motion takes seconds; a pick takes a minute. OPC 10000-4 §5.12.2 discards a method result when the Session ends *"independent of the task actually performed at the Server"* — so a synchronous motion method does not merely time out, it **loses the outcome of work that has already physically happened**. OPC 10000-10 §4.1 gives the OPC Foundation's own resolution: a Method performs a calculation, a **Program** runs a batch process or a machine tool part program. So `SubmitIntent` returns as soon as the intent is *admitted*, and what it returns is a NodeId — an `IntentOperationState` (a Part 10 program instance) the client subscribes to for progress and reads for the result. ## What changed **Model and shared code** — `Opc.Ua.RobotIntent.NodeSet2.xml` is source-generated in `Opc.Ua.Robotics`, so the enums, the polymorphic intent structures, `IntentOperationState : ProgramStateMachineState`, the method states and the typed clients derive from the model rather than being hand-written. Its only `RequiredModel` is the base UA namespace, so a server can adopt it without OPC 40010 or DI. `PoseMath` implements the specification's Annex C conversion between its unit quaternion and the core `ThreeDFrame` — including the `asin` clamp that stops a pole orientation becoming a domain error, and the non-negative-`w` representative that makes two servers agree on four numbers. `FrameTree` composes transforms along the frame tree. **Verbs are a DataType hierarchy, not one Method each.** A single submission and a mission step are then the same shape, and a new intent is a subtype rather than a new method — which is what `AddOperation<TRequest,TResponse>` was working around. **Server** — `IntentControllerHost` owns admission in the specification's order, the queue with PLCopen buffer modes, blending, cancellation *with the server's right to refuse*, missions with a committed base and a revisable horizon plus an IEC 61131-3 step graph, safety-aware refusals, real-time channel brokerage, and the capability declaration. All thirteen Methods are wired; command authority and channel leases are released when the holding Session closes; every client-supplied NodeId is validated against the controller being commanded (clause 11.3). `RobotIntentNodeManager`, the fluent `IIntentControllerBuilder` and `AddRobotIntent`/`ConfigureRobotIntent` make it reachable, and `RobotIntentFacetCalculator` computes which of the specification's facets an instance actually satisfies. **Client** — `RobotIntentClient` discovers `Server/RobotIntent/Controllers` and reads what a robot accepts; `IntentOperationHandle` subscribes and completes on a terminal state, recovering the result after a reconnect because clause 6.7 promises it survives; `CommandAuthorityLease` and `RealTimeChannelLease` manage the two exclusive resources; fluent builders cover every intent and mission. **Samples** — all robotics samples move under `samples/Robotics/`. `IntentEnabledRobot` is one UR5e-style offset-wrist arm on a bench, declared completely enough to show what a conformant server publishes, with a simulated safety source so the clause 10.4 refusals are demonstrable. `IntentViewerClient` turns a **click on a target prim in the OpenUSD viewport** into an intent and watches the arm execute it; it also runs headless, which is how CI exercises it. **Intents execute serially**, which satisfies every `BlockingMode` constraint by construction: the specification forbids beginning a `Single` or `Hard` intent while another executes and merely *permits* `None` and `Soft` to overlap. The convention API (`RoboticsOperationConventions`, `RoboticsOperationsBuilders`, `RoboticsOperationsClient`) is removed. ### On safety — what this deliberately does *not* claim The interface is **non-safety-rated**, and that is a property of the technology rather than a scoping choice. OPC 10000-15 carries cyclic safety data from a SafetyProvider to a SafetyConsumer; the consumer's `RequestSPDU` holds an identifier, a monitoring number and one octet of explicitly **non-safety** flags, so a caller has no channel through which to supply safety-rated arguments. Every safety fieldbus (PROFIsafe, CIP Safety, FSoE, openSAFETY) expresses a safety command as a **continuously asserted cyclic signal**, because the integrity argument rests on the fail-safe state that follows when assertion *stops* — and a Method call has no defined behaviour when it stops being called. What the host does instead is **observe and refuse**: `UpdateSafetyState` is how the application reports what the safety system is enforcing, and admission then refuses on the same values a client can read, so a refusal is explainable from the address space rather than from Server-internal state. It may never *instruct* the safety system, and no Method here commands a safe motion function, changes an operational mode or clears a stop. ## Defects this found in the specification, fixed upstream Implementing a specification is the only way to find out whether it can be implemented. Seven defects went back to [marcschier/opcua-drafts#47](https://github.com/marcschier/opcua-drafts/pull/47) rather than being worked around here: - **Refusal was unobservable.** Clause 6.2 orders six distinct refusals and clause 5.8 calls the failure set "small and diagnosable on purpose" — but `SubmitIntent` returned only `IntentId` and `Operation`, so a client could see *that* something was refused and never *which*. It now returns `Accepted`, `Failure` and `Message`, and clause 6.2 states that a refusal returns `Good` rather than substituting a Bad `StatusCode`. - **The Part 10 promotion was illegal.** `IntentOperationType.ProgramDiagnostic` was declared as a Property of `PropertyType`; OPC 10000-10 declares it a Variable of `ProgramDiagnostic2Type` reached by `HasComponent`. The declaration added a *second* member beside the inherited one instead of promoting it — which is precisely why an earlier version of this branch had deleted it. Correcting the TypeDefinition made it generate cleanly with **no change to the curated Core**. - **Clause 9 said nothing about the Method surface.** A server could declare `MissionsSupported` true and omit `SubmitMission` entirely, because the mission and channel Methods are Optional. This implementation did exactly that until an integration test caught it. A fourth honesty rule and a table now fix which Methods each declaration implies. - **Conformance was not machine-readable, and two implementations disagreed.** Clause 12 defined conformance in terms of facets and gave a Server nowhere to publish which ones it satisfied, so every client had to re-derive Table 12.2 from the address space. This repository proved why that fails: the server''s `RobotIntentFacetCalculator` and the client''s `RobotIntentRules.DeriveFacets` each implemented clause 12.2 independently and **disagreed on essentially every row** — the client''s `Blending` was `BlendingSupported` alone where the server also requires all four blending buffer modes, its `QueuedIntents` was `MaxQueueDepth > 0` where the server also requires `Buffered`. A client and server from the same release answered "does this controller conform?" differently, and both were reading the specification correctly, because several rows are behavioural and no amount of browsing settles them. `IntentCapabilitiesType` now carries **`SupportedFacets`**, exactly as OPC 10000-5 does with `ServerCapabilitiesType.ServerProfileArray`, RI-Base requires it, and clause 12.2 separates structural requirements a Server shall meet before listing a facet from attested ones governed by clause 9. - `WaitIntentDataType.Signal` was unbounded where clause 11.3 has to check it; §11.3 gained a table fixing the expected type of every NodeId-valued member; §5.7.0 gave `Ready`/`ActiveIntent`/`ActiveMission` normative meaning; §6.9 bounded `RequestedLease`; §6.5 and §6.4 now say what a server that cannot differentiate `StopMode` must do, and which stop a superseded intent gets. ## Bugs the tests and reviews caught Each of these would have been silent in the field: - **The execution engine was never wired into the public API.** `IntentControllerBuilder.RegisterAsync` built the address space and never started an `IntentControllerHost`, so through the documented `AddRobotIntent` → `ConfigureRobotIntent` path **every Method returned `BadNotImplemented`** and the registered `IIntentExecutor` was never resolved. A browsable address space is not a working robot interface, and only a live integration test showed the difference. - **NodeId scoping failed open on an empty index.** A controller advertising `CallProgramIntentDataType` with no published `ProgramType` accepted an *arbitrary* NodeId as `CallProgram.Program` — and `CallProgram` runs code the server holds. - **Blending completed the predecessor at submission time** and abandoned its motion, telling the client it succeeded and publishing an all-zero pose at the origin as a genuine `AchievedPose`. - **Mission base immutability compared only identifiers**, so a client could keep the ids and swap the *motion* of an already-committed step. - **`FinalResultData/Result` was never registered** with the node manager: Browse showed it, Read returned `BadNodeIdUnknown` — so clause 6.7's promise to a Part 10 client did not hold. - `DisposeAsync` disposed primitives out from under in-flight executor code; re-using a terminated `IntentId` destroyed the retained result and duplicated a NodeId; neither side registered the Robot Intent encodeables, so `Pose3DDataType` did not decode; host startup was hooked to a task that ran in only one hosting shape. - The result was published **after** the state went terminal; capabilities were resolved against a stale namespace table; an empty `ContentFilter` arrives as an empty element array rather than a null filter, so testing only for null made every unconditional mission transition silently untaken — a mission would run its first step and stop. ### Found by a later test-quality audit, after the above was written An audit of the test suite itself found five tests whose **names** asserted a normative requirement while their **code** asserted something weaker or nothing at all. The worst **fabricated the answer it checked**: a helper caught a decode failure and invented "5 supported intents, 6 axes, all facets true", so six capability-honesty assertions across four tests could pass while reading nothing from the server. The fallback is gone and the suite passes with honest reads. Chasing the facet gaps it reported led to the clause 12 defect above. Two further defects followed from completing that work: - **`RI-Interop-40010` was never computed**, and the code documented the omission as impossible to fix "because the calculator receives only a Robot Intent controller, not the linked OPC 40010 model". That was wrong: Annex B's structural requirement is an **inverse** `HasIntentController` reference, reachable from `IntentControllerState` — and the integration suite was already browsing exactly that. A server implementing Annex B could not claim Annex B. - **The published claim could go stale.** `SupportedFacets` was a snapshot taken during `RegisterAsync`, so a reference attached afterwards left it wrong. It is now bound through `BindRead` and recomputed on read, so the claim tracks the address space by construction rather than by discipline — which matters for anyone bolting Robot Intent onto an existing OPC 40010 node manager, where the attach ordering is often not theirs to choose. Two tests that **could not fail** were also replaced: one asserted `AccessLevel` equals `CurrentRead` to pin a read-only marking, but `BaseVariableState` initialises every variable to exactly that, so deleting the marking left it green; it is now a write expecting `BadNotWritable`, including a real OPC UA `Write` service call over a live session. Finally, a build break that a plain build hides: `RoboticsAotTests` used `out string?` in a project declaring `<Nullable>disable</Nullable>`. Building the project **without** `-f` suppresses `CS8632` through `NoWarn=nullable` and succeeds; building it **with** `-f net10.0` resolves `NoWarn` differently, `CS8632` fires, and `TreatWarningsAsErrors` makes it an error. CI builds per-TFM, so this would have failed there while passing every local build to that point. ## Testing and coverage | | | |---|---| | `dotnet build UA.slnx -t:Rebuild` | **0 errors, 2 warnings** — both pre-existing `CA1861` in `tests/Opc.Ua.Tools.Tests`, a file this PR does not touch | | **Whole `UA.slnx` test suite, net10.0** | **50 of 52 assemblies fully green** (measured at `40d9cf213`, before the `master` merge; the other two are explained below) | | `Opc.Ua.Robotics.Tests` | **467** net10.0, **447** net48 | | `Opc.Ua.Robotics.Intent.Tests` (live client <-> server) | **20** on net10.0 and net48, run repeatedly | | `Opc.Ua.OpenUsd.Tests` | **904** after merging `master` (664 before; the rest arrive with #4160 / #4162 / #4180) | | `Opc.Ua.Aot.Tests` | **121/121** | | `Opc.Ua.Di.Tests` (regression from the sample move) | **364/365**, one pre-existing skip | | **Changed-line coverage** | **90.7 %**, no file below 80 % — comfortably above the enforced band, which after #4158 applies only to patches larger than 100 changed lines | The two non-green assemblies are **not** caused by this branch. Rather than assert that, I ran the same selections at the merge-base `ae241b662` on the same machine: - `ServiceCallReassemblerTests.PrivateRequestSummaryFormatsKnownRequestKinds` expects `12.5` and gets `12,5` — a culture-sensitive assertion failing on a European locale. Fails 4/4 on **both** commits. - `CertRotationLiveTests.L2Cert2RotateCertificateDuringServerRestartRecoversSharedChannelAsync` fails on a teardown count. Fails 4/4 on **both** commits. - A third failure seen once, AOT `FindServersAsync` with `BadSecureChannelClosed`, did not reproduce: 4/4 on retest and 121/121 in the final run. Environmental. Two things are worth calling out because they would otherwise be invisible: - `tools/**` is **not** in the `coverage-thresholds.json` ignore list, and no test project referenced the OpenUSD tools assemblies — so their changed lines counted as uncovered and the real patch figure was 75 %, exactly the blocking threshold. Rather than widening the ignore list, the pure decisions in the viewport host (pointer-to-physical-pixel conversion, `RenderPickRequest` construction, stale-retry, backend discovery, pick-mode selection) were separated from the Avalonia/native shell and tested headless against a fake picking backend. - `dotnet build UA.slnx` **hides warnings on unchanged projects**; only `-t:Rebuild` surfaces them. That is how a `CA2263` introduced by a net48 portability fix survived several "zero warning" builds. The multi-target requirement earned its keep repeatedly: it caught a default interface implementation, `ArgumentNullException.ThrowIfNull`, `string.Create` with an interpolated handler, `ValueTask.FromResult`/`CompletedTask`, `Task.WaitAsync`, generic `Enum.GetValues` and generic `Enum.IsDefined`, and a non-generic `TaskCompletionSource` — none of which the net10.0 build complained about. A net10.0-**only** hang in the simulated executor and two load-sensitive flaky tests were also found and fixed at the cause. ### Merged with `master` `master` (through `e73e71184`) is merged in as `08205d81d`. Four conflicts, all where the two branches touched the same robot and OpenUSD surfaces, and none resolved by dropping a side: - #4162 added six files to `samples/MinimalRobotServer/` while this branch moved that directory to `samples/Robotics/MinimalRobotServer/`. They are kept at the moved path, and `UA.slnx` takes the union of both sides' new projects. - #4160 replaced `object` with a `UsdValue` union and rewrote `Flatten`; this branch had reformatted the same method and fixed a path in its doc comment. Git produced a hybrid that did not compile. The resolution keeps `master`'s `UsdValue` arm and `Flatten(UsdValue)` overload in this branch's block-bodied form, with the path fix retained. - `master` inserted `FindStageCamera` directly before two methods this branch had relocated, so the merge duplicated `WriteStageUsda` and `GetPrivateStateRoot`. Only `FindStageCamera` is genuinely new — `master`'s diff shows the other two as unmodified context — so the duplicates are dropped and this branch's `internal` versions kept, including the `GetPrivateStateRoot(string?)` overload the connector tests use to stay out of the real user profile. - `master` moved the OpenUSD stack to `0.4.0-alpha`; this branch pins `OpenUsd.Rendering` in the test project so the renderer-pick tests can reach the picking backend. Left at `0.1.0-alpha` that pin is below what the viewer now resolves transitively, which is `CS1705`. The pin follows `master`. CI was green on `baaf35e9` with **190 checks passing and none failing**, across every build leg (net472 / net48 / netstandard2.0 / netstandard2.1 / net8.0 / net9.0 / net10.0, Debug and Release) and the full test matrix on Windows and Linux. Since then `master` has been merged twice more and two CI-stability fixes landed (see below), so the current head is re-running. Locally, `dotnet build UA.slnx -t:Rebuild` gives **0 errors** and the 2 pre-existing `CA1861` warnings; Robotics **467** net10.0 / **447** net48, Robot Intent **20** on both, OpenUSD **904**, AOT **121/121**, source generation **3771**. ### Review round Five reviewers over disjoint areas found defects worth calling out, all fixed in `3ffec2c1`: - **A refused mission step was reported `Succeeded`.** The advance path returned the same `false` for "steps exhausted" and "step refused", so a protective stop between steps told a supervisory system the mission had completed while the robot stood halted with work unexecuted. - **Three attacker-controlled NodeIds reached the executor unvalidated** — `Pick.Tool`, `Place.Tool`, `ToolChange.DockStation`. `DockStation` drives a physical tool-exchange motion to a location the server never authorised. The replacement test enumerates NodeId members by reflection so the next one added cannot be missed. - **A lock-order inversion that loses a stop.** `CanCancel` and the mission condition evaluator ran under the host lock while the executor's own thread re-entered it via `ReportBlendBegin`. - **A non-terminal executor outcome killed the pump** from outside the `try/catch` that exists because the executor is untrusted — `Ready` stayed true and submissions kept being accepted while nothing moved. - **`Pause` published `Suspended` while the robot kept moving.** Now queue-only and honest about it; the specification gained the matching rule ([opcua-drafts `357a558`](https://github.com/marcschier/opcua-drafts/pull/47)). - **`RI-RealTimeChannel` and `RI-Safety` were false claims** — granted on an empty folder with no methods, and on the mere presence of a Mandatory node. - **The Part 10 promotions never promoted.** Namespace-1 BrowseNames declared second members beside the inherited ones; the conformance test could not see it because its lookup stripped the prefix. Fixed upstream and regenerated. - **Renderer picking never worked.** The reflective probe bound a renderer that is not a picking backend and throws off its owner thread. It now binds only `IRenderPickingBackend`, falls back immediately and loudly, and the docs say plainly that renderer picking is unavailable with this package version. - **The client lost command authority the instant it gained it** — monitored items report the current value first, and the lease treated every notification as a loss. ### CI stability fixes for master-side failures The CI split in #4158 turned on legs that had never run, and two of the failures they exposed were real bugs on `master` rather than anything this branch introduced. Both are fixed here because they were blocking every branch: - **ECC key generation was broken on macOS.** `ExportExplicitParameters` throws `PlatformNotSupportedException` there — Apple's SecurityTransforms supports named curves only — and it was called for exactly one value, the curve order `n`. The four curves OPC 10000-12 admits have published orders, so those are carried directly, with `CurveOrderTableMatchesThePlatform` checking each against the platform's own exported order wherever export works, so the constants cannot silently drift. This accounts for the two ECC failures in `test-macOS-latest-Server`. - **The certificate-leak detector raced asynchronous disposal.** It polled a fixed budget and reported whatever it saw, which cannot distinguish a genuine leak (count steady) from a suite still draining fire-and-forget disposals (count falling) — on a loaded agent that produced `Certificate leak detected: 2 instance(s)` with every test passing. It now waits for the count to **settle**, so a real leak is reported sooner than before while a slow drain gets as long as it keeps progressing. Shared by every suite that asserts no certificate leaks. The remaining macOS failures on `master` (`Gds`, `Tools`, `Redundancy.Samples`, `aot-macos`) have different root causes, are not reproducible without a macOS host, and are **not** addressed here. ### Merged with `master` again `master` through `416d619c4` (#4158, the CI matrix split and graduated coverage bands) is merged in as `08ae4528`. One conflict, in `docs/DeveloperGuide.md`, where both sides had deliberately rewritten the coverage-gate section: - `master`'s rewrite is kept in full — graduated `patch.bands`, Codecov being informational-only, and where the numbers surface per CI system. - This branch's paragraph about the `ignore` globs is kept too, re-sited ahead of those subsections. I re-checked it against the merged `coverage-thresholds.json` rather than assuming it still held: the list is still `tests/**`, `samples/**`, `obj`, `bin`, `*.g.cs`, with **`tools/**` absent** — so the warning that anything under `tools/` is measured like product code, and that an assembly no test project references contributes changed lines counted as *uncovered*, is still accurate and worth keeping. - `master`'s "reproduce a coverage failure locally" line supersedes this branch's near-identical one, so only `master`'s survives. Post-merge: `dotnet build UA.slnx -t:Rebuild` **0 errors**, 2 pre-existing `CA1861`; Robotics **467** net10.0 / **447** net48; Robot Intent **20** on both. ### Getting CI green The pipeline had never actually run on this branch until now (it was waiting on the fork-PR approval gate), so the first run surfaced four real problems, each fixed at source: - **Seven build legs failed** because `IntentEnabledRobot` pins `net10.0` for the OpenUSD payload but never opted into `RestrictForLegacyTfm`. On the legacy legs the libraries build as net472/net48, where `Opc.Ua.Types` exposes the `System.Threading.Lock` polyfill publicly, and that collides with the sample's own net10.0 BCL (`CS0433`). `Opc.Ua.OpenUsd.Connector.Viewer` was already using the opt-out for the same reason. - **A Linux-only test failure** turned out to be a real defect, not a platform quirk: `SetPose` published the *commanded* interpolated pose beside joint frames derived from the *solved* joints, so the two disagreed by the inverse-kinematics residual — about 8 mm, constant after any real motion. That is the lying-digital-twin defect one level deeper than the per-step IK fix reached. `SetPose` now derives both from the joint angles. - **Three source-generator tests** asserted the old emitted shape. The clause 6.2 Null-Variant fix was implemented in `ObjectTypeProxyGenerator` so every generated proxy benefits, which changed that shape; the tests are updated and strengthened to pin the `IsNull` branch as well. - **A CI-only timeout** in the simulated arm drain: the executor runs on a manual clock so the tests do not depend on machine speed, but the drain guarded itself with a wall-clock deadline, which put that dependence back. CI runs with coverage instrumentation, which is enough to take a per-step IK probe from ~0.6 s to over 5 s. The guard is now a simulated step budget. One failure was **not** from this PR and did not recur: a Roslyn `AccessViolationException` compiling `Opc.Ua.ISA95` on one leg, while the Debug leg of the same target passed on identical code and the Release build succeeds locally. ## Related Issues - Relates to #3827 — the request that prompted the robotics intent work. - Builds on #4127. The companion specification is drafted in the open at [marcschier/opcua-drafts#47](https://github.com/marcschier/opcua-drafts/pull/47) (`metaverse-specs/robot-intent/`), with the prior art and the reasoning behind each decision in the research document beside it. **Nothing in it is normative or endorsed by the OPC Foundation, and its NodeIds and namespace URI are provisional** — which is the main reason this is a draft. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. On the test checkbox, precisely: the whole `UA.slnx` suite now runs on net10.0 with **50 of 52 assemblies fully green**, and `Opc.Ua.Robotics.Tests` passes on **all five** target frameworks including net472, net8.0 and net9.0 which had never previously been exercised. The two remaining failures are reproduced identically at the merge-base and are detailed above, so I have ticked the box; if you would rather it stayed unticked until those two are green, say so and I will revert it. ## Points I would most like feedback on 1. **The namespace and NodeIds are provisional.** They come from a draft specification, not from the OPC Foundation. If this lands before the model is registered, the identifiers will move. 2. **The model lives in `Opc.Ua.Robotics`.** It is standalone on the base UA namespace and takes no dependency on OPC 40010, so a separate `Opc.Ua.RobotIntent` package would arguably be cleaner. Keeping it here was a deliberate call — this is where the robot-facing API already is — but it is the easiest thing to change now and the hardest later. 3. **Removing the convention API from #4127 is a breaking change** for anyone who adopted it. It was explicitly marked non-normative, so I have assumed that is acceptable — please say if a deprecation period is wanted instead. 4. **Serial execution** is a deliberate simplification. Concurrent `None`/`Soft` intents would be an optimisation, not a correction, but it is worth agreeing that reading. 5. **`samples/Robotics/` reorganisation** moves `MinimalRobotServer`. Happy to split that into its own commit if it makes review easier. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa Copilot-Session: a248a589-9a20-4372-868e-5d347e57001b |
||
|
|
81d726b7ad |
Coordinate the robot cell, and make the twin show what it is actually doing (#4162)
# Description `MinimalRobotServer` drove its two mobile manipulators through each other. The platforms *did* clear — R1 and R2 trace figure-eights 1.2 m apart at closest approach, exactly as `README.md` claimed — but each **arm reaches 1.611 m** (link offsets `0.260 + 0.680 + 0.670 + 0.158` from `robot.usda`), so the arms swept straight through one another while the chassis politely missed. Clearance has to be argued about the deployed arm, not the footprint. Three related gaps sat alongside it: motion was open-loop (two independent sine paths with no awareness of each other), only R1 had a gripper, and nothing was ever actually picked — `PartA`/`PartB` were static children of a single table and the arm replayed a canned eight-pose loop unrelated to where either the robot or the table was. This replaces all of that with a coordinated transfer cycle. ## Keeping them apart The cell is divided into reservable zones, and a robot reserves the zone *immediately ahead* before entering it, releasing the one behind once clear — block signalling rather than a global schedule: | Zone | Extent | Occupancy | | --- | --- | --- | | `EndZoneA` / `EndZoneB` | \|x\| > 1.3 m | one robot — a deployed arm needs the whole end | | `CorridorEastbound` | \|x\| ≤ 1.3 m, northern lane (y = 0.35) | one robot | | `CorridorWestbound` | \|x\| ≤ 1.3 m, southern lane (y = −1.15) | one robot | | `DockA` / `DockB` | around each charging dock (y = −2.0) | one robot | Three rules follow: - **Opposite-direction traffic passes**, being in lanes 1.5 m apart; same-direction traffic queues rather than overtaking. - **An arm may only leave its transport envelope inside an exclusively reserved end zone.** This is the rule that actually fixes the reported defect. - **A robot with no work parks on its dock**, releasing the end zone so the other robot can deliver into the station it serves. That last rule is not cosmetic. Without it the cell deadlocks permanently on the first handover: an idle robot standing beside its own table blocks the very delivery it is waiting for. A state trace showed R1 blocked at `x = 0.98` forever while R2 sat idle holding `EndZoneB`. The docks are in a southern layby rather than beside the stations, because a robot parked next to a station is close enough to foul one turning into it. ## Where the part actually is A carried part's pose is computed by forward kinematics (`RobotKinematics`) from **the same six axis values the server publishes**, not from the choreography that produced them — if the arm and the part were computed independently they would visibly drift apart. `RobotArmSolver` solves shoulder and elbow so the tool points down onto a slot, and is checked against that same forward kinematics, landing the tool on every slot of both tables to within 1e-6 m. ## Also simulated Trapezoidal accel/cruise/decel travel with the heading following the path; speed-and-separation slowdown near the other robot; an emergency stop that now halts motion rather than only blinking the beacon; battery drain with a charging detour; seeded grip-slip faults with floor recovery; and KPIs (parts moved, cycle count, last/average cycle time, utilisation, fault count). Both robots now carry a gripper. ## Two hidden couplings this surfaced - `m_r1NodeId` was keyed on *"has a tool"*, so giving R2 a gripper silently moved the dynamic-tool demo and the speed-override command target onto R2. Now keyed on R1 explicitly. - `RobotPositionsDriveLiveOpenUsdSceneAsync` sampled a fixed 1.2 s window and assumed the robot was always moving. Under a duty cycle a robot legitimately stands still while gripping, dwelling or charging, so it now polls until movement is observed — the assertion's intent is that live positions reach the scene, not that the robot never stops. ## Invariants are asserted, not assumed `CellChoreographyTests` runs ten minutes of simulated time at 50 ms and checks on **every step** that: - oriented footprints never overlap (an oriented box, not a bounding circle — the circle is so pessimistic for a 1.15 × 0.60 m platform that it demands clearances no real cell would need); - the two tool centre points never meet; - a zone is never doubly occupied; - arms only deploy inside an end zone; - parts are conserved — never duplicated, lost or carried twice; - a carried part sits exactly on the tool centre point. Plus the round trip completing, KPIs accumulating, the emergency stop halting every robot, dropped parts being recovered, and the battery draining and recharging. The README documents the zoning, traffic rules and cycle, **corrects the misleading clearance claim**, and lists ten further realism extensions that were deliberately not built (ISO 10218 / TS 15066 speed-and-separation monitoring, scanner cones, tool changer, inspection station, conveyor infeed, gate interlock, torque telemetry, maintenance counters, time-sampled pose history, traffic priority). ## Making the cycle visible Running the sample end to end against the OpenUSD connector showed the robots driving the cycle and the arms reaching the slots while **nothing was picked up or moved**. The choreographer tracked part poses and gripper opening purely in memory: the address space only ever exposed the platform poses and the twelve axis angles, so a connector had nothing to subscribe to for the parts and the blocks stayed wherever the asset authored them. A `CellTwin` folder now holds a `ThreeDCartesianCoordinates` position and a `ThreeDOrientation` heading per part, plus a jaw position per robot, bound to `/Cell/Parts/PartNN` and to the gripper jaws. The variables are registered before the motion device system is built - a binding can only carry a `SourceNodeId` that already exists - and sit outside the robotics subtree, because a workpiece is not a device and the robotics builder validates every node below its root as one it allocated itself. **The poses are published as structured vectors, not as a matrix.** A viewer composes a prim's matrix from the vector translate and rotate ops; the `matrix4d` profile is explicitly left unresolved by the reference connector, which drops a binding it cannot convert *in silence*. That is the shape the first attempt took, and it failed without a word in any log. The jaws needed the asset restructured. A carrier cube and a finger mesh were siblings under the gripper body, so driving the carrier would have slid it out from under the finger it is bolted to. Each jaw is now one `Xform` owning both, declaring the single matrix op every live-bound prim declares, with the finger points rebased onto the jaw origin. Closed puts the finger faces on the 35 mm half-width of the block, so the gripper grips it rather than passing through it. `RobotAssetContractTests` now holds **every** live-bound prim to the single-matrix contract, not just the two robot mount points. ## No part starves Collecting the lowest free slot meant a part that never landed in slot 0 was never collected at all: with two parts seeded on `WorkTableA` the second sat untouched for the life of the process while the robots shuttled the other one past it - which reads in the twin as the robots ignoring a block sitting in front of them. Parts are now stamped with the order in which they came to rest and the one that has waited longest is collected, which is what a real cell does with a buffer anyway. Verified end to end: all three parts circulate between both tables, the heading follows the carrying robot and resets on placement, and the jaws stroke between open (0.060) and closed (0.047) as parts are gripped and released. The OpenUSD packages move to 0.3.0-alpha. ## The part was not in the gripper Running it end to end showed a workpiece floating clear of the jaws that were supposed to be holding it. The platform pose and the workpiece pose leave the server as **separate bindings published by different loops**: positioning at 5 Hz (`UpdateIntervalMilliseconds = 200`), the simulation tick at 20 Hz. A carried part rides the tool centre point, so it was being drawn against a platform up to four ticks stale. Measured against the live scene with the arm stowed — where the part-to-platform distance is fixed by construction — the part sat anywhere between **0.65 m and 0.90 m** from its platform instead of at a single distance. A quarter of a metre of drift, three part widths, in front of a 96 mm jaw opening. The platform now publishes at the tick rate, and a carried part is derived from the pose that was **actually written to the address space**, recorded at the `SetFrameValue` call site rather than where the sample is produced. Each value was individually correct the whole time; only their relationship was wrong, which is why nothing caught it. `RobotOpenUsdE2eTests` now rebuilds the tool centre point from the values authored into the scene and asserts a carried part sits on it. It reproduced the defect at 0.49 m before the fix. The tolerance is the sampling straddle of reading two independently delivered bindings — one publish period is 43 mm at cruise — leaving an order of magnitude between it and the defect. Two assertions in the positioning test wanted the robots a metre apart **in X**. They pass each other in opposite corridor lanes, where X is all but identical and the separation is entirely in Y; and before the first sample arrives both platforms sit on the zero frame and coincide exactly. They now measure separation in the plane, once both have been seen to move — this was failing intermittently on net48 already. ## The opening view Framing the bounds of an enclosed scene automatically puts the eye *inside* it: the viewport opened on a close-up of whichever robot happened to be nearest, looking through the fence. The cell now authors `/Cell/OverviewCamera` — the whole floor, both work tables, the docks and the controller cabinet, from 12 m out at 28 degrees of elevation — and the connector opens on the first camera the served root layer declares. It already had the plumbing to honour a camera path; it was simply never given one. `--camera` still overrides the choice outright, and `RobotAssetContractTests` asserts the establishing shot is ordered before `/Cell/TopDownCamera`. ## The mounted gripper was never reported `DynamicToolIsComposedAsync` was failing on both TFMs, in isolation, and a longer timeout did not help. It predates this branch — I confirmed it at `58fd5e8ca` with a clean worktree. It turned out not to be a flaky test at all. **Part 5 §9.32.2** lets only a node carrying a `NodeVersion` property trigger a `ModelChangeEvent`, and `AsyncCustomNodeManager` drops entries for nodes that lack one. Neither the mounted tool nor the robot it hangs off had a `NodeVersion`, so **every addition was filtered out**. Removal kept reporting, because a deleted node is no longer in the manager's index and so takes the *"not mine, pass it through"* branch of the same filter. That asymmetry is what made it look like flakiness. A connector starting while the tool was mounted composed it and could still deactivate it later; one starting in the six-second gap was never told the tool appeared and never composed it **at all, for the life of the process**. Which happened depended only on the phase the fixture reached the test in — near-constant, hence a consistent failure rather than an intermittent one. Measured directly before the fix: model-change events arrived only on detach, exactly 18 s apart, and every resolve returned zero components: ``` 14:00:12.738 SERVER mounted <- no event 14:00:24.752 SERVER detached 14:00:24.801 model-change event <- only this ``` After enabling model-change tracking on the robot, the prim tracks the cycle — `A` = composed active (12 s mounted), `i` = inactive (6 s detached): ``` ....AAAAAAAAAAAAAAAAAAAAAAAiiiiiiiiiiiiAAAAAAAAAAAAAAAAAAAAAAAAiiiiiiiiiiii ``` **The test now asserts a transition**, not a momentary state: the prim seen both active and inactive across more than one full cycle. Unlike the old check this cannot pass on a connector that only ever hears about one direction — which is exactly what the previous pair of tests could not distinguish, since the sibling `DynamicToolIsDeactivatedWhenDetachedAsync` was satisfied by the *initial* compose and never exercised reconciliation at all. Two robustness gaps in the connector fell out of this: a rejected model-change monitored item was reported as success, leaving every `Dynamic` component frozen at whatever it resolved at start-up; and the initial compose ran *before* the subscription existed, so a change in between was seen by neither. The first now fails loudly, the second re-resolves once after subscribing. `Opc.Ua.Di.Tests` is now **402/403 on net48** (was 3 failures) and on net10.0, across three consecutive runs of the dynamic-tool tests on each. ## Related Issues - Fixes #4161 ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. Verification: full `dotnet build UA.slnx -m:1 /p:UseSharedCompilation=false` — 0 errors, no new warnings. `Opc.Ua.Di.Tests` 399/400 on **both net10.0 and net48** (1 pre-existing skip), `Opc.Ua.OpenUsd.Tests` 635, `Opc.Ua.Robotics.Tests` 103. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485 |
||
|
|
fb160a8f39 |
OpenUSD: the OPC UA to OpenUSD binding, scene materialization and connector (#4126)
**Stack 4/5** — base #4125. Part of the #4096 split; see #4096 for the overall picture. > Review after #4123, #4124 and #4125. ## What this is The generic OPC UA to OpenUSD binding, scene materialization, and the connector that drives a live stage from a running server. ## Contents **Binding (Part 1)** — `Opc.Ua.OpenUsd` and `Opc.Ua.OpenUsd.Server`. A server describes how its Variables drive USD prims, and serves the USD layers it refers to as Part 5 files with a content digest. **Scene materialization (Part 2)** — `Opc.Ua.OpenUsdScene` and `Opc.Ua.OpenUsdScene.Server`, including the USDA writer. - **Escaping fix**: string values, composition-arc references, variant set names and selections, `doc` and prim names were all emitted unescaped, so a value carrying a quote or a newline could author its own layer syntax. Every emit site now goes through the escapers. The tests assert the right property — not that the payload is absent (it legitimately survives inside the escaped literal) but that no payload becomes its own line of layer syntax. **Client** — `Opc.Ua.OpenUsd.Client` streams live values into an `IUsdSink`. `CompositeUsdSink` fans out to several sinks; the file sink interpolates between server samples so motion is continuous rather than stepped at the publishing interval. **Connector** — `tools/Opc.Ua.OpenUsd.Connector` composes a stage from a live server. The optional in-process viewport lives in a separate assembly probed at run time, so the connector still runs — and says why — when it is absent. - **Fix**: the asset cache and PKI root moved off the shared temp directory to `LocalApplicationData`. **Sample** — `PumpDeviceIntegrationServer` gains an OpenUSD representation of the pump. ## Notes for the reviewer - The viewport assembly is net10.0-only. It is what exercises the `Directory.Build.targets` change from 1/5: without it, the `net8.0` leg of the per-TFM CI build fails with `CS0433` because the `System.Threading.Lock` polyfill collides with the real type. - `UsdAssetDelivery` hashes through `SHA256.Create` rather than the static `HashData`, which does not exist on the net472/net48 targets this test project also builds for. ## Validation `dotnet build UA.slnx` — 0 errors, 0 warnings. | Suite | Result | | --- | --- | | Opc.Ua.OpenUsd.Tests | 621 / 0 | | Opc.Ua.Di.Tests | 321 / 0 | --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485 |
||
|
|
76d33cb139 |
Remove SourceGenerator.Foundations, ship analyzers under Roslyn-versioned folders for VS2022 and VS2026, and validate the packages (#4120)
# Description The two OPC UA source generators were built on `SourceGenerator.Foundations` (SGF), whose only job was getting the generator's dependency closure loaded inside the compiler. It does so with a **process-wide, first-generator-wins `AppDomain.AssemblyResolve` hook backed by a non-concurrent `Dictionary` mutated from `AssemblyLoad` callbacks**. Concurrent compilations in a shared `VBCSCompiler` race it, which is the root cause of the intermittent clean-build failure: ``` CSC : error CS8784: Generator 'StackSourceGeneratorHoist' failed to initialize. ... Exception was of type 'FileNotFoundException' with message 'Could not load file or assembly 'SourceGenerator.Foundations.Contracts, Version=2.0.14.0, ...' ``` The repository already solves that problem deterministically — #4041 made the NuGet packages ship the full closure under `analyzers/dotnet/.../cs`, and `Directory.Build.targets` registers the same closure as `Analyzer` items for in-repo `ProjectReference` consumers. SGF was therefore pure liability and is removed. ## What changed **1. SourceGenerator.Foundations removed.** `ModelSourceGenerator` and `StackSourceGenerator` are plain `[Generator(LanguageNames.CSharp)] IIncrementalGenerator` implementations again. SGF's implicit crash handling is replaced by an explicit `SourceGenerator.Guard` that reports `MODELGEN003` / `STACKGEN003` and rethrows cancellation. Its logger sink was already dead code and is dropped. `Opc.Ua.SourceGeneration.Stack.dll` shrank from **18 MB** (embedded closure) to **29 KB**. **2. Analyzers now ship under Roslyn-versioned folders.** `analyzers/dotnet/roslyn4.14/cs` and `analyzers/dotnet/roslyn5.0/cs` instead of `analyzers/dotnet/cs`, driven by `roslyn.props` at the repository root, with new `*.Pack` projects owning the package identity. The versioned folder matters: a host older than the band **skips** the analyzer instead of loading it and failing inside generator initialization. **3. Dependencies flow automatically.** Both delivery paths discover the payload by globbing the generator's output directory, so adding a package to a generator's dependency tree requires **no** `.csproj`, `.targets`, `.nuspec` or validation-script edit. Verified by temporarily adding a `PackageReference` and watching it appear in the payload with no other file touched. ## Two Roslyn bands: VS2022 and VS2026 Analyzers ship twice, once per band. The .NET SDK loads the highest band its compiler supports and **silently skips** anything above it, so a single-band package leaves every consumer below that band with no diagnostics and no generated code. | Band | Built against | Loaded by | | --- | --- | --- | | `roslyn4.14` | `Microsoft.CodeAnalysis.CSharp 4.14.0` | Visual Studio 2022 17.14+ / .NET 9 SDK | | `roslyn5.0` | `Microsoft.CodeAnalysis.CSharp 5.0.0` | Visual Studio 2026 18.0+ / .NET 10 SDK | Getting this wrong is invisible, which is most of why it took three attempts. The rule: > An analyzer must reference the **lowest** `System.Collections.Immutable` / `System.Reflection.Metadata` across all hosts it claims to support, and must never ship them. .NET binds a reference *upward* but never downward. Every failure mode is a compiler **warning**: | Diagnostic | Cause | | --- | --- | | `CS9057` | Built against a newer Roslyn than the host, so it is skipped entirely | | `CS8784` | Loaded, then `MissingMethodException` - a shipped S.C.I bound a second `ImmutableArray<T>` | | `CS8032` | Analyzer instance could not be created | This PR first shipped a `roslyn4.8` band that **never loaded** - confirmed against a real Roslyn 4.8 compiler: ``` warning CS8784: Generator 'ModelSourceGenerator' failed to initialize ... MissingMethodException: Method not found: 'IncrementalValueProvider`1<ImmutableArray`1<!!0>> IncrementalValueProviderExtensions.Collect(IncrementalValuesProvider`1<!!0>)' ``` That band was removed, and **this is not a regression**: master had the same defect, pinned to Roslyn 4.14 while referencing `System.Collections.Immutable 10.0.0.0`, so no 4.x host ever worked there either. The band that landed instead is `4.14`, and it is nearly free: `Microsoft.CodeAnalysis 4.14` depends on the **same S.C.I 9.0.0 as 5.0.0** and its `csc` runs on `net9.0`. Both bands therefore share `$(RoslynRuntimeVersion)`, so `Opc.Ua.Types` and `Opc.Ua.SourceGeneration.Core` are compiled **once** and serve both; only the five Roslyn-referencing assemblies get a second build, each from a 12-line project that glob-links the primary sources and overrides `RoslynApiVersion`. Going lower still - 4.8 wants S.C.I 7.x - would mean rebuilding that whole closure, `Opc.Ua.Types` included, and remains out of scope. (`System.Collections.Immutable 7.0.0` also does not resolve in this repository: `NU1603`, 8.0.0 substituted.) **Nothing else in the repository exercises the down-level payload**, and since every failure is a warning, a broken band would ship silently - exactly as the 4.8 one did. So `validate-source-generator-packages.ps1` now restores a real `Microsoft.Net.Compilers.Toolset 4.14.0`, runs its `csc.dll` over the packed `roslyn4.14` payload, and asserts **both** that none of `CS9057`/`CS8784`/`CS8032`/`CS8034` is emitted **and** that `/reportanalyzer` lists the generator as having executed - silence alone would also pass if the generator were never handed to the compiler. The migration analyzer, whose payload comes from a hand-written nuspec rather than `SourceGeneratorPack.targets`, gets the same check plus a band/assembly manifest assertion. To stop the trap recurring, `Microsoft.CodeAnalysis*`, `System.Collections.Immutable` and `System.Reflection.Metadata` are excluded from both the package payload and the in-repo analyzer closure, and asserted against in the validation script. `roslyn.props` and `docs/DeveloperGuide.md` document what adding a band requires. ## The shipped package set is unchanged Evaluating `PackageId`/`IsPackable` across every project in `preview-pack.slnx` gives the **same 47 package ids** on `master` and on this branch, and a full Release build plus `dotnet pack --no-build` produces exactly those 47. Only the owning project changed: | Package | master | this PR | | --- | --- | --- | | `…Opc.Ua.SourceGeneration` | `Opc.Ua.SourceGeneration.csproj` | `Opc.Ua.SourceGeneration.Pack.csproj` | | `…Opc.Ua.SourceGeneration.Stack` | `Opc.Ua.SourceGeneration.Stack.csproj` | `Opc.Ua.SourceGeneration.Stack.Pack.csproj` | `.azurepipelines/expected-packages.txt` pins the expected set and the validation script fails when the packed output drifts. ## Two new package checks **`build/<PackageId>.props` naming.** NuGet only auto-imports that exact name, and the file declares every `CompilerVisibleProperty` / `CompilerVisibleItemMetadata` the model generator relies on. It was previously packed as `build/OPCFoundation.Opc.Ua.SourceGeneration.props`, which does **not** match the package id — so **no NuGet consumer ever imported it**, and every `ModelSourceGenerator*` property and `AdditionalFiles` metadata was silently invisible. In-repo projects were unaffected because they import it by relative path. **An end-to-end consumer that actually drives the generator.** The pre-existing clean-consumer check only proved the analyzer *loads*. `Test-SourceGeneratingConsumer` builds a standalone project that feeds a real NodeSet2 in as an `AdditionalFile`, pins a `ModelSourceGeneratorPrefix` that cannot be derived from the model, and references the emitted types from hand-written code. It deliberately does **not** import the props by path, so the package's auto-import is what is under test. Both checks were verified to fail against a package rebuilt with the misnamed props. ## CI: parallelism re-enabled `buildandtest.yml` pinned three `dotnet build` invocations to `/p:UseSharedCompilation=false` and `-maxcpucount:1`. `dotnet build` defaults to all logical processors, so `-maxcpucount:1` actively disabled parallelism. Both flags were introduced silently in unrelated PRs (`f358078b1`, `208c601b0`) and suppress exactly the conditions the SGF race needed — now structurally gone. Compilation is ~72 % of what that workflow executes (233 of 323 job-minutes in run `29916768976`), so the queue-independent `Build` step durations are the baseline: | Measurement | Baseline | | --- | --- | | `Build` step, 26 `test-*` jobs | mean 5.53 min, sum 143.9 min | | `build-linux-all-tfm` | 56.3 min | | `build-windows-all-tfm` | 32.5 min | `codeql-analysis.yml` keeps the flag — the CodeQL tracer requires it. ## Incidental fixes - `Microsoft.CodeAnalysis.CSharp` declares an exact `[version]` range on `Microsoft.CodeAnalysis.Common`, so with `CentralPackageTransitivePinningEnabled` the central pins must move in lockstep or restore fails with `NU1109`. They now derive from `$(RoslynApiVersionVS2026)`. - `MigrationGeneratorTests` created its generator driver without parse options — silently fine under Roslyn 4.14, `Inconsistent language versions` under 5.0. - The migration analyzer props injected `<Analyzer>` items by hand from `analyzers/dotnet/cs`; NuGet registers analyzers itself and selects the matching folder, so the injection was obsolete and would now force the wrong assembly to load. - Exception diagnostics reported `ex.StackTrace` (null for an exception that never left its throw site, and carrying neither type nor inner chain) — now `ex.ToString()` at all three call sites. - The pack guard tested the flattened assembly list, so it only fired when *no* variant produced output; it now checks each declared variant contributed assemblies. - `GeneratePackageOnBuild` is disabled for the generators; packing happens in CI or on an explicit `dotnet pack`. ## Note for reviewers **Building this repository requires a Roslyn 5.x host** (the .NET 10 SDK or Visual Studio 2026). That is already the documented prerequisite and every CI leg installs `10.0.x` last, but it is now a hard requirement. ## Related Issues _No tracking issue: this is a follow-up to the CS8784 clean-build failure reported against #4041, which fixed the packaged delivery path but left the in-repo project-reference path relying on the SGF resolver._ - Follow-up to #4041 ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. ### Verification detail | Check | Result | | --- | --- | | `dotnet build UA.slnx` (clean, and after merging master) | 0 errors; 18 warnings, all pre-existing `CA1873` in `Opc.Ua.Client` | | `Opc.Ua.SourceGeneration.Tests` | 81/81 on net10.0 and net48 | | `Opc.Ua.SourceGeneration.Stack.Tests` | 92/92 on net10.0, 89/89 on net48 | | `Opc.Ua.SourceGeneration.Core.Tests` | 3722 passed, 8 skipped | | `Opc.Ua.MigrationAnalyzer.Tests` | 121/121 on net10.0 and net48 | | Package id set, master vs branch | identical, 47 packages | | Release build + `dotnet pack --no-build` | 47 packages, matching the manifest; both `roslyn4.14` and `roslyn5.0` present; no host-provided assemblies shipped | | `validate-source-generator-packages.ps1` | passes, including manifest, props-naming, end-to-end generator consumer and down-level host checks | | Down-level band under real csc 4.14.0 | all three analyzer packages load **and run** | | Negative tests | misnamed props → both checks fail as designed; hidden variant output → pack guard fires; 5.0 build placed in `roslyn4.14` → real `CS9057`; non-running generator name → execution assertion fires; expecting a `roslyn4.8` band → manifest check fires | --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed53262-5cfa-429a-820e-bf6146a00d8b |
||
|
|
3490d9cd05 |
Use Dekaf by default for Kafka on all target frameworks (#4038)
# Description Upgrade the Kafka transport from Dekaf 1.2.0 to 1.7.0 and use the pure-managed Dekaf adapter by default on every supported target framework. - Enable the Dekaf adapter and its guard tests on `net472`, `net48`, `netstandard2.1`, `net8.0`, `net9.0`, and `net10.0`. - Retain the `Confluent.Kafka` adapter and factory as an opt-in DI backend through `WithConfluentKafkaClient()`. - Update the central dependency pins required by Dekaf 1.7.0. - Validate NativeAOT/trimming compatibility on `net10.0` for the default Dekaf backend; the opt-in Confluent backend remains JIT-only. - Avoid per-header allocation on .NET Framework when Dekaf header memory is array-backed. Validation: - Release builds passed with zero warnings for all six library target frameworks. - Kafka adapter and DI selection tests passed on `net48` and `net10.0`. - `dotnet publish tests\Opc.Ua.Aot.Tests\Opc.Ua.Aot.Tests.csproj -c Release -r win-x64 --self-contained true` - The generated NuGet package includes Dekaf 1.7.0 and Confluent.Kafka 2.15.0 for every target framework. ## Related Issues None. ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received. |
||
|
|
b76022d13d |
Add MinimalClient - lightweight OPC UA console client with fluent API (#4006)
# Description
Adds `MinimalClient`, a compact console OPC UA application demonstrating
the unified dependency-injection surface, secure endpoint discovery,
managed sessions, fluent V2 subscriptions, and Alarms & Conditions
support.
## Changes
- Adds `ConfigureApplication(options => ...)` to build and validate one
shared `ApplicationConfiguration` for combined client/server hosts.
- Extends `OpcUaClientOptions` so the existing `AddClient(options =>
...)` callback accepts application identity, PKI, and
certificate-validation settings like `AddServer(...)`.
- Composes root `ConfigureApplication(...)` values with client defaults
in either registration order; shared values take precedence and client
values fill gaps.
- Keeps server samples on their established `AddServer(...)`
configuration pattern.
- Adds `MinimalClient` as a .NET 10 Native AOT console application using
top-level statements and `HostApplicationBuilder`.
- Discovers a `SignAndEncrypt` / `Basic256Sha256` endpoint by default;
`--insecure` and `--auto-accept` are explicit opt-ins with warnings.
- Demonstrates fluent `AddSubscription(...)`,
`TryAddMonitoredItem(...)`, and `AlarmClientFactory` usage.
- Adds targeted configuration, client, and combined client/server tests.
## Example
```csharp
builder.Services
.AddOpcUa()
.AddClient(options =>
{
options.ApplicationName = "MinimalClient";
options.ApplicationUri = "urn:localhost:OPCFoundation:MinimalClient";
options.ProductUri = "uri:opcfoundation.org:MinimalClient";
options.Session = new ManagedSessionOptions
{
SessionName = "MinimalClient",
SessionTimeout = TimeSpan.FromSeconds(60)
};
})
.AddDiscoveryAndConnect(options =>
{
options.DiscoveryUrl = discoveryUrl;
options.SecurityMode = MessageSecurityMode.SignAndEncrypt;
options.SecurityPolicyUri = SecurityPolicies.Basic256Sha256;
})
.AddSubscriptions()
.AddAlarms();
```
## Related Issues
_If there is no issue yet, open one and link it here._
## Checklist
- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce new build or
analyzer warnings.
- [ ] I ran all tests locally using the `UA.slnx` solution against at
least .NET Framework and .NET 10.
- [ ] I fixed all failing and flaky tests in the CI pipelines and all
CodeQL warnings.
- [x] I have addressed all PR feedback received.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <romanett98@gmail.com>
Co-authored-by: Marc Schier <marcschier@hotmail.com>
|
||
|
|
8538f531e7 | Update paths for the consolidated repository layout | ||
|
|
7f409657ad |
OPC UA Part 4 6.6 Redundancy (server + client) with opt-in distributed high-availability (#3918)
## Summary This branch implements **OPC 10000-4 §6.6 Redundancy** end to end — server and client, **transparent and non-transparent**, across all failover modes (`None`, `Cold`, `Warm`, `Hot`, `HotAndMirrored`, `Transparent`) — and layers an **opt-in distributed high-availability (HA)** capability on top so replicas can share address-space, session, and subscription state and expose redundancy to clients through the documented OPC UA mechanisms (for example, running a server replica set across nodes in Kubernetes). The single-instance, in-memory path stays **zero-overhead**: every distributed feature is opt-in through dependency injection, with a direct-construction fallback, and replaced APIs are kept and marked `[Obsolete]`. ## New packages | Package | Purpose | | --- | --- | | `OPCFoundation.NetStandard.Opc.Ua.Server.Redundancy` | §6.6 redundancy nodes plus opt-in distributed state: shared address space, secure session sharing, subscription/continuation-point mirroring, and leader election. | | `OPCFoundation.NetStandard.Opc.Ua.Server.Redundancy.Crdt` | Active/active, leaderless multi-writer replication via CRDTs (an extension beyond §6.6). | | `OPCFoundation.NetStandard.Opc.Ua.Server.Redundancy.K8s` | Kubernetes integration: Lease leader election, EndpointSlice peer discovery, and ServiceLevel-driven readiness. | Also adds the worked samples `Applications/RedundantServer` and `Applications/RedundantClient`, and the guides `Docs/HighAvailability.md` and `Docs/HighAvailabilityKubernetes.md`. ## Server side (§6.6) - `AddServerRedundancy(...)` populates the live `Server.ServerRedundancy` nodes (`RedundancySupport` plus `RedundantServerArray` / `ServerUriArray` / `CurrentServerId`) and drives `Server.ServiceLevel` from an `IServiceLevelProvider` (leader high, standby low). `Server.ServerRedundancy` is typed to the NonTransparent or Transparent subtype according to the configured mode. - `AddRequestServerStateChange(...)` implements the §6.6.5 manual-failover method, with `EstimatedReturnTime` and the Maintenance / NoData `ServiceLevel` sub-ranges. - Non-transparent peers resolve through `FindServers` (`ConfiguredRedundantServerSetProvider`) and advertise the `NTRS` discovery capability for GDS/NTRS registration. - Deterministic, replica-stable `EventId` synchronization for Transparent and HotAndMirrored sets (§6.6.2.2), excluding per-replica fields so de-duplicating clients do not double-process events. ## Client side (§6.6) - `DefaultServerRedundancyHandler` reads `Server.ServerRedundancy` and `Server.ServiceLevel` and fails over to the highest-ServiceLevel peer. It honors Maintenance (Part 4 Table 105 — disconnect to an available peer), keeps Warm backups disabled until failover (Table 107), and treats peers known only through `ServerUriArray` as failover candidates. - `RedundantManagedClient` implements the Cold, Warm, Hot (a), Hot (b), and HotAndMirrored client patterns: one active session, optional lightweight ServiceLevel status-check sessions to backups, and — for Hot (b) — merging of concurrent reporting streams with exact value-identity de-duplication. HotAndMirrored failover re-activates the mirrored session with the existing `AuthenticationToken` instead of recreating it. - Network redundancy: alternate communication paths via endpoint selection. ## Distributed state (opt-in; an extension beyond §6.6) - `ISharedKeyValueStore` (in-memory default) and `INodeStateStore`, with an `AddressSpaceSynchronizer` that bridges a `CustomNodeManager2`'s predefined nodes to the shared store, so node and reference additions/removals and value changes propagate to other replicas. `IDistributedValueCache` lets read/write callbacks cache the last value with a freshness bound; monitored items use the normal read pipeline and therefore participate only when the read path does. - **Secure session sharing (active/passive fast reconnect), server and client.** `DistributedSessionManager` (wired through a new additive `ISessionManagerFactory` seam on `StandardServer`) mirrors **encrypted** session state across replicas. On a failover reconnect the standby restores the session and performs the **full `ActivateSession` client-certificate signature validation** against a **single-use** `serverNonce` consumed via compare-and-swap across the replica set, enforcing the same SecurityPolicy and SecurityMode. The `AuthenticationToken` is a **lookup key only**, never an authenticator: entries are keyed by the SHA-256 digest of the token, and a cross-replica restore emits a distinct audit event. - Subscription, retransmission-queue, and continuation-point mirroring for subscription transfer and failover. - Leader election: a static single-leader provider and a shared-store **lease via compare-and-swap**, supporting active/passive and active/active (shared-read / master-write). - Fail-closed security defaults: shared records are protected at rest through `IRecordProtector`; secret-bearing mirrors require a registered protector or an explicit opt-out. ## Active/active (CRDT) `Opc.Ua.Server.Redundancy.Crdt` adds leaderless multi-writer address-space replication with CRDTs and gossip (`UseReplicatedAddressSpace`) plus CRDT-backed session metadata (`UseReplicatedSessions`). Networked gossip is **fail-closed without authenticated transport** (mutual TLS for TCP; an explicit opt-out for isolated dev/test). CRDT state is eventually consistent, so exactly-once decisions (such as the single-use nonce registry) stay on a strongly consistent store. The package is available on all stack target frameworks. ## Kubernetes `Opc.Ua.Server.Redundancy.K8s` provides Kubernetes Lease leader election, EndpointSlice-based peer discovery, and ServiceLevel-driven readiness for running an OPC UA server replica set. `Docs/HighAvailabilityKubernetes.md` covers StatefulSet/Deployment and Service manifests, RBAC, probes, time synchronization, secrets, gossip-port NetworkPolicy, and GDS/NTRS registration. ## Compatibility and validation - **Zero-overhead** single-instance default; additive, backward-compatible seams (for example `ISessionManagerFactory` and `IServerStartupTask`); replaced APIs marked `[Obsolete]` for migration. - Builds across the full stack TFM set (`net472`, `net48`, `netstandard2.0`, `netstandard2.1`, `net8.0`, `net9.0`, `net10.0`); NativeAOT-compatible where the runtime supports it. - Unit and integration tests for server, client, subscriptions, session mirroring, CRDT, and Kubernetes; the conformance suite stays green. See `Docs/HighAvailability.md` for the full OPC 10000-4 §6.6 mapping, the `Add*` (standard nodes) versus `Use*` (extension) builder convention, and the security/rotation guidance. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
44560fd3aa |
Implement Apache Kafka transport for PubSub (Part 14 Annex B.2) with HA (#3942) (#3949)
Implements **#3942** (Apache Kafka transport for PubSub, OPC UA Part 14 Annex B.2) with HA publishers & subscribers (**#3924**, Part 14 §9.1.6). ## Kafka transport (Part 14 Annex B.2) New `Opc.Ua.PubSub.Kafka` library mirroring the MQTT broker transport and the existing `IPubSubTransport`/`IPubSubTransportFactory` + injectable-adapter pattern: - Profiles `pubsub-kafka-uadp` / `pubsub-kafka-json` (library-local `KafkaProfiles`); `kafka://` / `kafkas://` addresses. - Reuses the existing `Broker*TransportDataType` types (`QueueName` = topic, `MetaDataQueueName` = metadata topic, `ResourceUri`/`AuthenticationProfileUri` = auth, `RequestedDeliveryGuarantee` = QoS) — no new DataTypes, matching the spec. - Normative `content-type` record header (`application/opcua+uadp` / `application/json`); QoS → producer acks/idempotence; SASL/TLS auth with passwords resolved via `ISecretRegistry`. - `AddKafkaTransport()` DI (JSON + UADP factories), `KafkaConnectionOptions` (bootstrap servers, GroupId, security, delivery guarantee, offset policy). ### Dual-source client by TFM (AOT + broad TFM support) Via the `IKafkaClientAdapter` seam: - **net10.0 → Dekaf** (pure-managed, no native dependency) ⇒ the library is `IsAotCompatible` and Kafka runs in the **NativeAOT-published** sample directly. - **net472 / net48 / netstandard2.1 / net8.0 / net9.0 → Confluent.Kafka** (mature, native librdkafka). **NativeAOT publish of `ConsoleReferencePubSubClient` (win-x64) was validated end-to-end**: a 26.9 MB native binary with the Kafka/Dekaf transport and **zero IL2xxx/IL3xxx trim/AOT warnings**. ## Field promotion to broker headers (builds on #3956) Now that the PubSub transcoders (#3956) are on `master`, `KafkaBrokerTransport` implements the `IPubSubHeaderTransport` capability: transcoded DataSet fields selected via `PromoteFields(...)` (fluent) or the `PromoteFields` / `PromotedFieldPrefix` route options are surfaced as **Kafka record headers** — the broker-layer analogue of the Part 14 PromotedFields concept, with *copy* semantics (the fields remain in the encoded payload). The normative `content-type` header is unaffected and values are invariant-formatted strings. This reuses the existing `KafkaMessage.Headers` channel (already emitted by both the Dekaf and Confluent adapters), so only the transport gained the capability. Covered by `KafkaHeaderTransportTests` (net10.0 Dekaf + net48 Confluent) and documented in `Docs/PubSub.md` under "Promoting fields to broker headers". ## High availability (#3924, §9.1.6) Transport-agnostic, additive, and **behaviour-neutral by default**: - `IPubSubActivationCoordinator` (per-component Active/Standby + `RoleChanged`) with the default no-op `AlwaysActiveCoordinator`. - `IPubSubLeaseStore` + `InMemoryPubSubLeaseStore` (monotonic fencing tokens) and `LeaseActivationCoordinator` (leader election with per-component renew/acquire loops). - Runtime wiring: `PubSubApplication` starts/stops the coordinator with the app; `WriterGroup`/`ReaderGroup` gate at the enable/pause boundary — Standby pauses publishing/dispatch, Active resumes. No-op under the default coordinator, so existing behaviour is unchanged. - Kafka HA: consumer `GroupId` drives Kafka-native subscriber failover; `AtLeastOnce`/`ExactlyOnce` enable idempotent producing for active-publisher handover. - DI: `WithActivationCoordinator(...)` / `WithLeaseStore(...)`. ## Samples & docs Kafka profiles (`KafkaUadp`/`KafkaJson`) added to the publisher & subscriber samples; new `Docs/PubSubKafka.md` (config, DI, auth, QoS, AOT note) linked from `Docs/README.md` + the `Docs/PubSub.md` transport table. ## Tests & validation - `Opc.Ua.PubSub.Kafka.Tests`: 82 tests, **80.28% line coverage** on the Kafka source, using a `FakeKafkaClientAdapter` over an in-memory topic bus. A **Testcontainers.Kafka** integration test runs a real produce→consume round-trip and `Assert.Ignore`-skips when Docker is unavailable. - HA failover tests (`PubSubActivationFailoverTests`): leader election + failover, fencing-token monotonicity, writer/reader standby gating (deterministic, `FakeTimeProvider`). - `Opc.Ua.PubSub.Tests`: **1134 passed / 0 failed** (1130 existing + 4 new). - Full `UA.slnx` net10 build: 0 errors; the only warnings are pre-existing CA warnings in unrelated test projects (new code is 0-warning). net48 (Confluent path) validated for the transport + test project. ### Dependencies (approved) - `Dekaf` (MIT, pure-managed, net10-only) and `Confluent.Kafka` 2.15.0 (Apache-2.0) + `librdkafka.redist` (BSD-2). - Test-only `Testcontainers.Kafka` 4.13.0 (MIT). - Dekaf's net10 package required bumping the `Microsoft.Extensions.*` / `System.*` 10.0.8 servicing family to 10.0.9. Closes #3942 |
||
|
|
ec6a5e432f |
Server session scalability: admission control, retry-after signaling, and held-Publish decoupling (#3946)
# Description Server session scalability work: the reference server degrades gracefully under connect storms, holds many thousands of steady-state long-polls on a small worker pool, and gives clients diagnostics-independent backpressure signals so they ramp instead of hammering. This combines three related efforts — and merges in #3950 — all follow-ups to the scalability analysis in #3941 (`Docs/ServerScalability.md`). Server admission control / rate limiting is **enabled by default** with conservative limits (it sheds excess load with a fast `BadServerTooBusy` rather than altering steady-state behavior), and the held-Publish decoupling is on by default; the HTTPS rate limiter and the client-side connect gate are opt-in. Nothing here changes the shipped `MaxSessionCount` (100). ## 1. Admission control / rate limiting - **TCP listener admission** — a configurable token-bucket connection gate (socket backlog default 512, raised from a hard-coded 10) sheds over-limit connections cheaply instead of accepting and starving them. - **Session-establishment admission** — `CreateSession` / `ActivateSession` acquire from a concurrency limiter; over-limit requests fault fast with **`BadServerTooBusy`** before the CPU-bound certificate validation / signing, instead of queueing behind the address-space lock. - **CreateSession RSA out of the lock** — the server-nonce signature runs outside the session semaphore, so a slow signature no longer serializes unrelated session establishment. - **HTTPS/Kestrel** — `AddHttpsRateLimiter(...)` attaches an ASP.NET Core rate limiter to the HTTPS binding via dependency injection. - Built on `System.Threading.RateLimiting`, DI-injectable (`IServerRateLimiterProvider`, `ConfigureRateLimits(...)`) with a direct-construct fallback. ## 2. Retry-after signaling (diagnostics-independent) A `BadServerTooBusy` fault's diagnostic `AdditionalInfo` only reaches the client when it requests diagnostics, and is dropped on client exception re-wrapping. This delivers the retry-after through carriers that do **not** depend on diagnostics, all feeding the client's adaptive reconnect policy: - **`ResponseHeader.additionalHeader`** (all UA transports) — a structured `RetryAfterMs` (`Int64`) in the standard `AdditionalParametersType`, attached to the fault by `ServerBusyException` + `EndpointBase.CreateFault` (`RetryAfterHeader`) and read by `ClientBase.ValidateResponse`. - **HTTP `Retry-After`** (HTTPS) — the rate limiter sets the header on its 429; the client maps a 429/503 with `Retry-After` to `BadServerTooBusy`. - **UA-TCP `Error` reason** — the client honors a `RetryAfterMs=N` token in a transient server-busy ERR message as a lower bound on channel-reconnect backoff (`RetryAfterHint`). - **Client foundation** — the hint survives the diagnostics gate and exception re-wrapping and reaches `IReconnectPolicy.TryGetNextDelay` (server-signal-aware adaptive backoff); a client-wide connect admission gate (`WithConnectRateLimiter`) ramps bulk connects. - **Load-based `Server.ServiceLevel`** — computed from session-establishment headroom (255 at low load, scaling toward a floor as sessions approach `MaxSessionCount`, with hysteresis) as a proactive capacity signal clients can read/subscribe. - Implementation summary folded into `Docs/Sessions.md` (Server retry-after backpressure section); the OPC UA spec-change proposal is filed as an OPC Foundation Mantis issue. ## 3. Held long-poll Publish decoupling A parked `Publish` (waiting for the next subscription notification) no longer occupies a `MaxRequestThreadCount` worker slot for the whole wait, so a small fixed worker pool can hold many thousands of outstanding Publishes and `MaxRequestThreadCount` no longer has to scale with session count. - **`IRequestParkSink`** + a one-shot `RequestLifetime.ParkSink`; `ServerBase.RequestQueue` awaits `Task.WhenAny(processing, ParkedTask)` and releases the active-worker slot at the **park point** rather than at completion. Only `Publish` carries a sink — every other request keeps the byte-for-byte legacy fast path (no extra allocation, no `WhenAny`). - **`DecoupleHeldPublishRequests`** (default `true`; set `false` to restore the legacy inline-await worker path). ## Docs `Docs/ServerScalability.md` (rewritten as user-facing scalability documentation), `Docs/RateLimiting.md`, `Docs/Sessions.md`. ## Tests Server admission (`SessionAdmissionRateLimitTests`); retry-after carriers (`RetryAfterHeaderTests`, `RetryAfterHintTests`, `EndpointBaseTests`, `ClientBaseTests`, `HttpsTransportChannelTests`, `AdaptiveReconnectPolicyTests`, `ConnectionStateMachineTests`); load-based ServiceLevel (`ServerServiceLevelCalculatorTests`); held-Publish decoupling (`RequestQueueTests`, `SessionPublishQueueTests`); AOT coverage (`RateLimitingAotTests`). Changed libraries build clean on **net48** and **net10.0**. |
||
|
|
f9a8343dea |
Full 1.05.06 compliant OPC UA Part 14 (PubSub) (#3892)
# Full OPC UA Part 14 (PubSub) — v1.05.06 This PR replaces the legacy PubSub stack with a modern, **OPC UA Part 14 v1.05.06**-compliant reimplementation built on current .NET: fully `async`, dependency-injection-first with a fluent builder (`services.AddOpcUa().AddPubSub(...)`), NativeAOT/trim-safe, and designed for high-throughput, highly-available, distributed deployments. The libraries — `Opc.Ua.PubSub`, `Opc.Ua.PubSub.Udp`, `Opc.Ua.PubSub.Mqtt`, `Opc.Ua.PubSub.Server`, and `Opc.Ua.PubSub.Adapter` — keep source compatibility with 1.5.378 where practical (obsolete shims) and ship a complete migration path. See the **[PubSub guide](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md)** and the **[2.0.x migration guide](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/migrate/2.0.x/pubsub.md)**. **Encodings & transports.** Both wire encodings are implemented in full — **UADP** (key/delta/event/keepalive, RawData, chunked, signed+encrypted) and **JSON** (verbose/compact, single-message, metadata; migrated from `Newtonsoft.Json` to `System.Text.Json`) — across **UDP** (uni/multi/broadcast + discovery), **MQTT** (3.1.1/5.0, retained metadata, TLS), and a from-scratch **DTLS 1.3** secured datagram transport (`opc.dtls://`) implemented on the .NET BCL crypto primitives (ECDHE, HelloRetryRequest cookies, certificate auth, AEAD record protection, HKDF key schedule), wired through `AddUdpTransport().WithDtls(...)`. Details in [Transports](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#transports) and [Encodings](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#encodings). A raw **Ethernet (Layer 2)** transport (`opc.eth://`, EtherType `0xB62C`, optional 802.1Q VLAN) carries UADP NetworkMessages directly in Ethernet II frames with no IP/UDP, via `AddEthTransport()` (`.WithPcap()` for cross-platform capture). See [Ethernet transport](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#ethernet--uadp-opceth). **Runtime schema generation.** A new schema subsystem (`Opc.Ua.Core.Schema`, `Opc.Ua.PubSub.Schema`) generates **XSD**, **OPC Binary (BSD)**, and **JSON Schema** for OPC UA data types at runtime — including PubSub NetworkMessage/DataSet message schemas — driven from the type system and source-generated metadata (no reflection; NativeAOT/trim-safe). It is consumable from the client, server, and PubSub stacks. See the **[Schema generation guide](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/SchemaGeneration.md)**. **Security, fail-closed.** Message-level security is wired end-to-end through `IPubSubSecurityWrapperResolver`: publishers are fail-closed (a secured group with no key source throws rather than emitting plaintext) and subscribers enforce per-reader inbound security. It includes AES-CTR (128/256) policies with HMAC-SHA-256, a deterministic 96-bit nonce with a messages-per-key cap, a monotonic sliding replay window, and a Security Key Service (SKS) pull client + in-memory server with SecurityGroups and key rotation. Two independent security assessments (IEC 62443-4-2 × OPC UA Part 2/4/14, plus a Microsoft SDL/SFI delta pass) drove remediation of all actionable findings — including orphaned message security, chunked-frame security bypass, unauthenticated Action invocation, pre-auth reassembly DoS, and DTLS hardening. See [Security](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#security) and [SKS](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#security-key-service-sks). **Server-side & high availability.** The runtime can optionally surface through a standard in-process OPC UA address space (`Opc.Ua.PubSub.Server`) with configuration methods, per-component diagnostics, the full PubSubState model, KeepAlive and MessageReceiveTimeout. Pluggable configuration/runtime-state stores, an id allocator, and HA runtime-state providers with failover support running publishers/subscribers in a distributed system. See [Server-side address space](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#server-side-address-space) and [High availability](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#high-availability-state-providers). **External-server adapter (bridge) with hot-reload.** A new `Opc.Ua.PubSub.Adapter` binds PubSub publishers, subscribers, and Action responders to an **external** OPC UA server over `ManagedSession` — cyclic Read or subscription-backed sources, browse-path node mapping, and metadata that resolves/retries and re-emits on model-change events. Adapter and PubSub configuration **hot-reload incrementally** (only changed writers/readers/responders are rewired; unchanged sessions are reused), driven by `IPubSubConfigurationStore.Changed` and `IOptionsMonitor`, with a pluggable change-feed source (e.g. etcd). See [Binding PubSub to an external server](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#binding-pubsub-to-an-external-opc-ua-server-client-session-adapters). **Samples, packaging & quality.** One combined sample, `ConsoleReferencePubSubClient`, demonstrates publisher, subscriber, and external-server modes on `Host.CreateApplicationBuilder` with full fluent/DI wiring; all libraries publish **NativeAOT with zero IL2026/IL3050 warnings** ([Native AOT](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#native-aot), [DI](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/DependencyInjection.md)). The change ships comprehensive NUnit/integration suites and a libFuzzer harness, maintains ≥80% coverage on the new libraries, and is documented end-to-end ([What's new in 2.0](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/WhatsNewIn2.0.md)). |
||
|
|
bab073952c |
Adding HTTPS/WSS transports + Web API binding (Part 6 §G.3 OpenAPI Mapping), shared Kestrel host, Kestrel-TCP (#3880)
# HTTPS / WSS transports + OPC UA Web API binding (Part 6 §G.3) + shared Kestrel host Combined follow-up that completes the HTTPS / WSS / Web API transport story started in #3876 and #3877. Three coordinated bodies of work land together: a **shared Kestrel host** with full WSS reverse-connect and an opt-in Kestrel-hosted `opc.tcp://` path (no separate NuGet), the **OPC UA Web API binding** (Part 6 §G.3 "OpenAPI Mapping") with a spec-compliant ASP.NET Core Minimal-API server and symmetric C# client, and a targeted **edge-security hardening** pass across every new HTTPS / WSS / JSON / Web API surface. UASC crypto / signing / sequence / nonce logic is unchanged. ## Shared Kestrel host, WSS reverse-connect, Kestrel-TCP Multiple `HttpsTransportListener` instances now share one `IHost`, so `opc.https://` and `opc.wss://` can be colocated on a single port. WSS gains full reverse-connect on **both** server and client sides via `HttpsTransportListener.CreateReverseConnection` / `TransferListenerChannelAsync`, plumbed through the same `TcpServerChannel.BeginReverseConnect(IUaSCByteTransport)` path as TCP. An opt-in `KestrelTcpTransportListener` (`AddKestrelOpcTcpTransport()`) hosts `opc.tcp://` on Kestrel via `ConnectionHandler` so consumers can run every OPC UA transport under one host; it ships **inside** `OPCFoundation.NetStandard.Opc.Ua.Bindings.Https` gated `#if NET8_0_OR_GREATER` with no new dependencies, while the raw-socket `TcpTransportListener` stays in `Opc.Ua.Core` for trimmed/AOT deployments. The key technical fix was overriding `StartReceiveLoop` in `TcpReverseConnectChannel` to do a one-shot `ReverseHello` read — `WebSocket.ReceiveAsync(CancellationToken)` aborts the socket on cancellation, which previously broke the WSS handoff. See [Docs/Transports.md](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/Transports.md) and [Docs/ReverseConnect.md](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/ReverseConnect.md). ## OPC UA Web API binding (Part 6 §G.3) All **28 services** from `opc.ua.openapi.allservices.json` are mapped at the spec routes (`/findservers`, `/getendpoints`, `/createsession`, `/read`, `/write`, `/browse`, `/call`, `/publish`, …) on ASP.NET Core Minimal-API endpoints. Both `application/json; encoding=compact` (mandatory per Part 6 §5.4.9) and `encoding=verbose` are supported via media-type negotiation. The implementation is **NativeAOT-compatible** — each route binds to a static `RequestDelegate` thunk that calls a generic instantiation of `WebApiEndpointDispatcher.HandleAsync<TRequest, TResponse>` with zero MVC reflection and no `[UnconditionalSuppressMessage]` (`<IsAotCompatible>true</IsAotCompatible>` is clean of `IL2xxx` / `IL3xxx` warnings on net10). NodeManagement (§5.8) and Query (§5.10) are deliberately omitted per the spec doc. New profile URIs `Profiles.HttpsOpenApiTransport` (profile/2338) and `Profiles.WssOpenApiTransport` (profile/2339) are emitted as discovery twins on every SM=None HTTPS / WSS endpoint. Naming follows the [OPC Foundation UA-WebApi-StarterKit](https://github.com/OPCFoundation/UA-WebApi-StarterKit). Full developer guide in [Docs/WebApi.md](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/WebApi.md). ## Authentication, WSS sub-protocol, symmetric client Four pluggable auth modes wire through `OpcUaWebApiAuthenticationBuilderExtensions`: **Anonymous**, **Bearer (OIDC/JWT)** with a built-in `JwtClaimSessionlessIdentityProvider` that projects `sub` / `scope` / `roles` onto `IUserIdentity`, **Basic**, and **Mutual TLS**. A policy scheme (`WebApiAuthSchemes.Default`) becomes the default authenticate/challenge/forbid scheme so `UseAuthentication()` populates `HttpContext.User` even with multiple handlers registered; every route carries `RequireAuthorization()` except `/findservers` and `/getendpoints` which keep `AllowAnonymous` per the spec. The **WSS `opcua+openapi` sub-protocol** (profile/2339) is accepted in two forms — bare and `opcua+openapi+<accesstoken>` (bearer rides in the sub-protocol name because browser WebSocket APIs forbid custom headers, per Part 6 §7.5.2) — and the token is validated against the registered JwtBearer scheme **before** the upgrade completes; cleartext `ws://` bearer upgrades are rejected on both server and client. A symmetric **C# client** (`Libraries/Opc.Ua.Client/WebApi`) provides `IWebApiClient` / `WebApiTransportChannel` / `WebApiWssTransportChannelFactory` so `Session`, `ManagedSession`, and the V2 `SubscriptionManager` all dispatch unchanged; fluent shortcuts `ManagedSessionBuilder.UseWebApiEndpoint(url, encoding)` and `UseWssOpenApiEndpoint(url, encoding)` are wired in. ## Edge security hardening A targeted audit against OPC UA Part 2/4/6 + Microsoft SDL drove **fail-closed** defenses across every new surface. TLS server-cert validation in both `WebApiTransportChannel` and `WebApiWssTransportChannel` delegates to the OPC UA `CertificateValidator` (with the default .NET TLS chain check as fall-back), mirroring `HttpsTransportChannel`. WSS bearer tokens are validated before upgrade; empty-password identity-token fallbacks were removed from `DefaultSessionlessIdentityProvider` and `JwtClaimSessionlessIdentityProvider`, with the upstream principal now flowing via the new `SecureChannelContext.UpstreamIdentity`. `WebApiBodyCodec.DecodeBodyAsync` enforces `MaxMessageSize` during the read (BadRequestTooLarge before allocation), and both server and client WSS receive loops reject zero-byte continuation frames. On the base-branch side, `JsonRequestMapper.ReadAllBoundedAsync` bounds JSON/binary reads, HTTPS / WSS handlers fail-closed on missing SM=None JSON endpoints (BadSecurityPolicyRejected, restrict-to-discovery), and `WebSocketByteTransport` carries the same zero-progress guard for `opcua+uacp`. ## Testing, docs, scope Approximately **300 new passing net10 tests**: 107 in `Opc.Ua.Core.Tests` (codec, media type, route table, profile constants), 153 in `Opc.Ua.Bindings.WebApi.Tests` (endpoint dispatcher, auth handlers, TLS regression, mTLS source-pin, body-size, zero-progress, identity plumbing, bearer validation), 29 reference-server integration tests in `Opc.Ua.Sessions.Tests` (HTTPS WebApi, WSS OpenAPI, WSS reverse-connect, Kestrel-TCP, shared host), and 7 `Opc.Ua.Aot.Tests` round-trips on the natively-compiled binary. Full multi-TFM `dotnet build UA.slnx` is clean. Docs updated: [Transports](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/Transports.md), [WebApi](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/WebApi.md), [ReverseConnect](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/ReverseConnect.md), [Profiles](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/Profiles.md), [MigrationGuide](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/MigrationGuide.md), [NativeAoT](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/NativeAoT.md). Out of scope (tracked as follow-ups): own-listener hosting mode for Web API, source generation of extended complex types not exercised by the 28 services, load-testing harness, `MatchEndpoints` honouring user `TransportProfileUri` on `UpdateFromServer`, rate limiting on the long-poll `/publish` path, and a richer first-class `UpstreamPrincipal` model. ## Related issues - Closes the follow-up scope of #3876 (WSS transport) - Closes the follow-up scope of #3877 (HTTPS-JSON transport) - Supersedes #3882 (folded in via fast-forward push of `openapi` → `httpwssbindingfollowups`) |
||
|
|
208c601b09 |
Fix weekly all-TFM UA.slnx build (net8/net9 + netstandard) and add Windows/Linux all-TFM CI build jobs (#3913)
The OPC UA stack builds for seven target frameworks (net472, net48, netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0), but our regular pull-request builds only compile two of them (net10.0 and net48). The complete per-framework build runs only in a weekly scheduled Azure job, so when a change breaks one of the other frameworks nobody notices for days. That is exactly what happened in build 14931, which was red on net8.0, net9.0, netstandard2.0 and netstandard2.1. This PR fixes all of those broken frameworks. The fixes are small and framework-specific: a net10.0-only test project reference is made conditional so net8.0/net9.0 can build, a set of "legacy-stripped" project shells that failed to compile as empty netstandard assemblies now build as net10.0, and a few polyfill and NuGet-package collisions in the netstandard2.1 build are resolved by building its test/app projects as net8.0 and pinning the matching MQTTnet version. After these changes, `dotnet build UA.slnx` succeeds cleanly for every framework. To stop this from regressing, the PR adds two CI jobs that run on every pull request, in parallel with the existing unit tests: one on Windows that builds the .NET Framework targets (net472, net48, netstandard2.0) and one on Linux that builds the modern .NET and netstandard2.1 targets (net8.0, net9.0, net10.0, netstandard2.1). Together they cover all seven frameworks and fail the PR if any single one won't build. The workflow's trigger list was also widened so these jobs run when only build or configuration files change (for example `*.props` or `*.targets`), not just C# source. This PR supersedes #3912 and folds in its fixes. |
||
|
|
4549e3b305 |
Encoder/decoder/session perf: JSON flush + buffer pooling + NodeId read-once + async session create + lazy loggers (#3899)
## Summary Recovers the encoder-side performance regressions surfaced by the `master` vs `master378` .NET 10 benchmark comparison. ### 1. Flush `JsonEncoder` at structural boundaries `Utf8JsonWriter` never flushes on its own, so when encoding a large or streamed payload its internal buffer grows (doubling, onto the **large object heap**) to the size of the entire message before anything reaches the underlying stream. The encoder now flushes buffered JSON at object/array boundaries once at least 16 KB has accumulated. Flushing only writes already-complete tokens and **never changes the encoded output**. Measured (net10.0, BenchmarkDotNet ShortRun): | Case (payload 1024) | Allocated | Time | gen2/op | |---|--:|--:|--:| | ArraySegmentStream | 15.7 MB -> **7.3 MB (-53%)** | 15.5 -> 13.3 ms | 1281 -> **0** | | RecyclableMemoryStream | 14.1 MB -> **6.5 MB (-54%)** | 15.1 -> 13.1 ms | 1000 -> **0** | | payload 64 | 831 KB -> **444 KB (-47%)** | 1.17 -> 0.83 ms | 82 -> **0** | ### 2. Pass `Variant`/`DataValue` by `in` `DataValue` is a `readonly struct` and `Variant` is a large `readonly struct`; the `IEncoder` write methods and array write loops now take/iterate them by `in` to avoid per-call/per-element struct copies (non-breaking per repo guideline). ### 3. Span-based `NodeId.TryFormat` New `NodeId.TryFormat(Span<char>, out int, bool)` writes the common node id forms (numeric/guid/string/opaque in the default namespace, or any non namespace-uri form) into a stack buffer; `JsonEncoder.WriteNodeId` uses it instead of allocating a string via `NodeId.Format`. **Removes ~144 B per node id written** (verified in isolation with `GC.GetAllocatedBytesForCurrentThread`). Falls back to `Format()` for the namespace-uri form. Node ids/type ids are ubiquitous in OPC UA messages. ### 4. Lazy encoder/decoder loggers `JsonEncoder`/`BinaryEncoder`/`XmlEncoder`/`BinaryDecoder` created an `ILogger` on **every** construction even though `m_logger` is only used on rare warning/error paths. The logger is now created lazily, removing a `Logger<T>` allocation from every encoder/decoder construction. ### 5. Binary decoder: single `DataValue` construction (CPU) `BinaryDecoder.ReadDataValue` read fields via a chain of up to six `With...` calls, each copying the large readonly `DataValue` struct. It now reads all fields into locals and constructs the `DataValue` once. Measured: **`BinaryDecoderMemoryStream` ~3370 us -> ~1950 us at payload 1024 (~-42%)** - recovers most of the ~2x decode regression while keeping the v2 decoder's lower allocation. ### 6. Binary encoder: cache hot struct reads (CPU) `WriteVariantValue` snapshots `Variant.TypeInfo`/`BuiltInType` into locals once instead of re-reading the readonly-struct property ~13x; `WriteDataValue` caches `WrappedValue`. Small encoder speedup (MemoryStream@1024 ~2657 -> ~2505 us). ### 7. read NodeId accessors once in the binary write path - `BinaryEncoder.WriteNodeId` / `WriteExpandedNodeId` read `IdType` / `NamespaceIndex` into locals once and pass the `IdType` plus the `NodeId` **by `in`** (no struct copy) to the private `GetNodeIdEncoding` / `WriteNodeIdBody` helpers, which read the single active identifier once. (No wrapper struct — individual values on the stack, per review.) - Fixed the misspelled internal member `NodeId.OpaqueIdentifer` -> `OpaqueIdentifier` (internal, no public-API/compat impact). ### 8. pool the in-memory JsonEncoder buffer - The default (no external stream) `JsonEncoder` no longer allocates a per-encoder `MemoryStream`; it writes through a private `ArrayPool<byte>`-backed `IBufferWriter<byte>` (`PooledBufferWriter`) and reads the result text from the pooled span. External-stream behavior is unchanged. - Buffers are returned with `clearArray: true` so encoded payloads (which may contain user tokens/secrets) are not exposed to the next pool consumer. - This supersedes the **"JSON encoder construction (~872 B/op)"** item previously listed as deferred above — it is now addressed, and composes with the structural-boundary flush from Wave 1. ### 9. Server session creation made async (removes sync-over-async) The server `Session` constructor created the session-diagnostics address-space node via `CreateSessionDiagnosticsAsync(...).AsTask().GetAwaiter().GetResult()` on every `CreateSession` — a sync-over-async block (awaits a semaphore + async node creation) that also violates the repo's no-sync-over-async rule. The diagnostics-node creation moved into `Session.InitializeAsync`, awaited by the already-async `SessionManager.CreateSessionAsync`. Before/after (ShortRun, all security policies): geomean **0.94x time / 0.94x alloc**; the heavy ECC AES-GCM / ChaCha20-Poly1305 policies are **0.71–0.89x time** (up to 29% faster) where the blocked request thread mattered most; RSA policies are at parity (crypto-bound). ### 10. Activation allocation reductions - Client `ActivateSession`: `CreateRequestHeaderForActivateSession` no longer allocates a `RequestHeader` + `AdditionalParametersType` on the common non-ECDH path (returns `null`; only allocates for ephemeral-key user-token policies). - Server activation: the duplicate `ClientNonce.ToArray()` in both the application- and user-token-signature validation paths is hoisted into a single local. ### 11. Additional optimizations - **JSON external-stream `Utf8JsonWriter` pooling** — the external-stream and external-`IBufferWriter` `JsonEncoder` ctors now rent a pooled `Utf8JsonWriter` (keyed by `Indented`, capped, returned on dispose) and re-target it via `Reset(...)` instead of allocating one per encoder. External-stream JSON allocation improves from ~1.44× to ~1.29× vs 1.5.378. Added a reuse/double-dispose round-trip test (JSON encoder tests: 1829 → green). - **Single-read `WriteDataValue`** — each `DataValue` field (`StatusCode`/timestamps/picoseconds) is read once into a local and reused for both the encoding byte and the write (previously read twice). ~5% faster on the `DataValue[]` micro-benchmark; wire output unchanged (encoder tests 3476 → green). - **Session benchmark fidelity** — `SecurityPolicyBenchmarks` now passes the cached `Endpoints` into `ConnectAsync`, so it measures pure session create/activate/close instead of re-running `GetEndpointsAsync` each iteration; added an isolated `DiscoverEndpointsAsync` benchmark. - **BinaryDecoder** reads primitives directly from the buffer span via `BinaryPrimitives` (cursor-based, bounds-checked) for the `byte[]`/`ArraySegment` constructors, and bulk-copies fixed-width numeric arrays (`Int16..UInt64`, `Float`, `Double`) in a single blit instead of element-by-element. Measured **~1.14× time / ~0.69× allocation vs 1.5.378** (near time parity, allocation win kept). - **BinaryEncoder** writes scalar primitives via `BinaryPrimitives` into an `IBufferWriter<byte>` span; added a `BinaryEncoder(IBufferWriter<byte>)` ctor; the default no-stream encoder uses a pooled `ArrayPoolBufferWriter`. Common-path (`MemoryStream`) encode time improved ~0.80× vs the prior 2.0 state. **Known tradeoff:** the external-stream constructors currently allocate more than 1.5.378 (per-primitive scratch write to the stream) — documented in `Docs/Benchmarks.md` and tracked as a follow-up to fix before that path is unambiguously better. - Removed the third-party **`Microsoft.IO.RecyclableMemoryStream`** test dependency (it was used only in benchmark/test code; `ArraySegmentStream` + `MemoryStream` baselines remain). - Wire format unchanged. Validation: `Opc.Ua.Core.Encoders.Tests` 2895 passed (post-RMS-removal count), `Opc.Ua.PubSub.Tests` UADP/binary 1200 passed, `Opc.Ua.Types` clean on all TFMs. ### 12. Certificate validator concurrency (remove validation-path lock + cache trust material) Like 1.5.378, the certificate validator was still an architectural bottleneck: the shared singleton `CertificateManager` keeps one `CertificateValidationCore` per trust-list, and that core held a single `SemaphoreSlim(1,1)` across the **slow** part of every validation — trusted-store open + full enumeration **and CRL revocation parsing** — so all peer-cert validations (every incoming secure channel on a server) serialized on it and re-parsed the trust material from disk each time. (The CPU-bound `X509Chain.Build` was already outside the lock in 2.0, so this was narrower than 1.5.378 but the same class of bottleneck.) - **Lock-free validation path.** The per-trust-list state is write-once (the manager re-creates the core on trust-list changes rather than mutating it), so it is now published once as an immutable `volatile TrustListState` record and read lock-free on the hot path. The semaphore remains only on the rare writer paths. - **Reuse the trust-list store.** The validator used to `OpenStore` + `Dispose` per validation, which discarded the store's parsed-cert cache (and, once the validator lock was gone, thrashed it under concurrency). The core now caches the opened store per identifier and disposes it on teardown; out-of-band directory changes are still picked up by the store's existing freshness check (dir last-write-time + file count). - **Cache parsed CRLs.** `DirectoryCertificateStore.IsRevokedAsync` re-read and re-parsed every CRL file on every call; it now serves a lock-free cached snapshot of parsed CRLs keyed by a per-file name/length/last-write-time signature (rebuilt only when the CRL directory changes; invalidated on AddCRL/DeleteCRL/Open/Close). - **Removed the vestigial in-memory trust list (review follow-up).** The per-trust-list in-memory `CertificateIdentifier` list (`TrustListState.TrustedList`/`IssuerList`, from `CertificateTrustList.TrustedCertificates`) was removed: in the `CertificateManager` path it was always empty (trust lists are built with a `StorePath` only) and `CertificateValidationCore` is `internal`, so it was dead weight. The validator now resolves trusted/issuer certs from the configured stores exclusively; config-supplied certs without a directory remain supported via the existing in-memory `CertificateIdentifierCollectionStore` (`InMemory:` provider). Pure internal simplification — behaviour-neutral (the list was already empty). All changes are internal — **no public API change**, NativeAOT-safe, validation results unchanged (verified against the existing `CertificateValidatorTest` suite). Concurrent validation throughput now **scales ~6× (≈43 → ≈255 validations/s from 1 to 16 parallel validators)** on an RSA-2048 trusted chain with a warm cache; per-validation cost is dominated by `X509Chain.Build` and is unchanged. Validation: full `Opc.Ua.Core.Tests` **3264 passed / 0 failed** (net10.0); certificate suite **227 passed** on net10.0, with the new concurrency / cache-invalidation tests green on **net48** too. Adds `CertificateValidationConcurrencyBenchmarks` (BenchmarkDotNet) plus concurrency-consistency and trust-store/CRL cache-invalidation regression tests. > An allocation profile (see `Docs/Benchmarks.md`) shows the residual session-establishment > allocation is dominated by per-connect endpoint discovery + string/XML materialization, > **not** the per-session object graph — so lazy-init of `NodeCache`/subscription engine > was deliberately not pursued. |
||
|
|
76bceb51e2 |
Env-var driven Pcap auto-capture + rename to Opc.Ua.Core.Diagnostics + Docs/Diagnostics.md (#3888)
# Description
This PR also renames the project itself from `Opc.Ua.Bindings.Pcap`
(assembly / NuGet / folder / root namespace) to
`Opc.Ua.Core.Diagnostics`. `Pcap`-prefixed type names (`PcapFileWriter`,
`PcapOptions`, `IPcapAuditSink`, ...) and the `AddPcap*` extension
methods are preserved — only the project shell broadens to
`Core.Diagnostics`. The new NuGet id is
`OPCFoundation.NetStandard.Opc.Ua.Core.Diagnostics`. Folders were moved
with `git mv` so blame and log are preserved.
This PR also renames the **C# namespace** from
`Opc.Ua.Core.Diagnostics.*` to `Opc.Ua.Pcap.*` (commit `efc03691c`). The
**assembly identity** stays `Opc.Ua.Core.Diagnostics` (assembly name,
NuGet id `OPCFoundation.NetStandard.Opc.Ua.Core.Diagnostics`, folder,
csproj filename, `InternalsVisibleTo`, .slnx project paths, sign-lists
are all unchanged). Result: one `Opc.Ua.Core.Diagnostics.dll` assembly
hosting types in the `Opc.Ua.Pcap.*` namespace — standard .NET pattern
(cf. `System.Net.Http.dll` hosting `System.Web.*`). Future diagnostic
features can add other `Opc.Ua.*` namespaces to the same assembly
without another rename. 149 files changed (+411/-411 net); 422/422 tests
pass on net10.0.
This PR also merges `Docs/PacketCapture.md` and `Docs/Observability.md`
into a new `Docs/Diagnostics.md`. `ITelemetryContext` leads the doc with
expanded usage and extensibility guidance (custom contexts,
OpenTelemetry exporter wiring, ActivitySource / Meter naming
conventions, testing patterns). New sections cover OPC UA server audit
events and the built-in `Server.ServerDiagnostics` info-model nodes. The
1.4 / 1.5 evolution narrative has been dropped; the still-relevant
migration content has moved into `Docs/MigrationGuide.md`. All 8 inbound
doc / slnx / XML-doc references are retargeted.
This PR also adds a new public DI extension `AddPcapFromEnvironment` to
`OPCFoundation.NetStandard.Opc.Ua.Core.Diagnostics` that activates
packet capture and/or symmetric-key logging at host start based on two
environment variables. The variables are evaluated **once** during the
`AddXXX` call (matching the existing `OPCUA_PCAP_ENABLE_DIAGNOSTICS`
opt-in semantics):
| Variable | Effect when set |
|---|---|
| `OPCUA_PCAP_FILE` | Auto-start an in-process capture session on host
start. Frames are written verbatim to the literal path; the parent
directory becomes `PcapOptions.BaseFolder` so the existing
`SessionFolder` path-traversal validation still runs. |
| `OPCUA_KEYLOGFILE` | When set without `OPCUA_PCAP_FILE`: install a
stand-alone keylog observer (SSLKEYLOGFILE-style) directly into
`IChannelCaptureRegistry`. When set together with `OPCUA_PCAP_FILE`:
keylog records go to this path instead of the conventional
`*.uakeys.json` sibling of the pcap. |
When neither variable is set the call behaves exactly like `AddPcap`
— binding registered, no auto-start. When the auto-start path is
active the host emits a `Warning`-level log line naming the env var(s)
consumed and the resolved path(s); key material is never logged.
## Public surface
The new public API is intentionally narrow:
```csharp
namespace Opc.Ua.Core.Diagnostics.DependencyInjection
{
public static class PcapEnvironmentVariableNames
{
public const string OpcuaPcapFile = "OPCUA_PCAP_FILE";
public const string OpcuaKeyLogFile = "OPCUA_KEYLOGFILE";
}
public static partial class PcapServiceCollectionExtensions
{
public static IServiceCollection AddPcapFromEnvironment(this IServiceCollection services);
public static IServiceCollection AddPcapFromEnvironment(this IServiceCollection services, Action<PcapOptions> configure);
}
}
```
`StandaloneKeyLogObserver`, `PcapEnvironmentDefaults`, and
`PcapEnvironmentAutoStartHostedService` are internal — the env-var
names and DI methods are the only public surface, which keeps it
AOT-clean.
## Behaviour-impacting code change
`InProcessCaptureSourceBase` now honors
`StartCaptureRequest.PcapFilePath` and
`StartCaptureRequest.KeyLogFilePath` for in-process capture (previously
these properties were respected only by the Replay source, despite their
generic XML docs). Behaviour is **opt-in**: callers that pass `null`
continue to get the conventional `capture.pcap` / `keys.uakeys.json` /
`keys.uakeys.txt` filenames inside `SessionFolder`. Path-traversal
validation in `CaptureSessionManager.StartAsync` is unchanged.
## Operator example
```yaml
# Kubernetes manifest snippet
env:
- name: OPCUA_PCAP_FILE
value: /var/log/opcua/cap.pcap
- name: OPCUA_KEYLOGFILE
value: /var/log/opcua/keys.uakeys.json
```
```csharp
// Program.cs
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Services.AddOpcUa().AddClient(_ => { });
builder.Services.AddPcapFromEnvironment();
await builder.Build().RunAsync();
```
For SSLKEYLOGFILE-style keylog only (no pcap):
```bash
export OPCUA_KEYLOGFILE=$HOME/opcua-keys.uakeys.json
```
## Security model
Anyone (process, container runtime, host operator, sidecar) who can set
these env vars can divert OPC UA channel keys and traffic to
attacker-controlled paths. `Docs/PacketCapture.md` gains a security note
and an operator-checklist item that ties this to the existing security
guidance: the destination path / directory must be operator-controlled
and protected with file-system permissions in addition to the binding's
own `0600`-on-Unix file mode.
|
||
|
|
f358078b1c |
[Client] Central channel manager: ref-counted shared channels, coalesced reconnect, IRetryBudget (#3288) (#3852)
# Description Introduces a central `IClientChannelManager` that owns client-side transport channels with reference counting, sharing, coalesced reconnect, and asynchronous participant notification. Sessions, discovery clients, and registration clients targeting the same `ConfiguredEndpoint` (with the same reverse-connect identity) now share a single underlying `ITransportChannel`; reconnect is transparent to callers; and the previous `AttachChannel`/`DetachChannel` + `SessionReconnectHandler` patterns are obsoleted. ## What lands ### Core types (`Stack/Opc.Ua.Core/Stack/Client/`) - `ManagedChannelKey`, `ChannelState`, `ChannelStateChange`, `ParticipantReconnectResult` - `IReconnectParticipant`, `IManagedTransportChannel`, `IClientChannelManager` - `IChannelReconnectPolicy` + `ExponentialBackoffChannelReconnectPolicy` (default mirrors historical `SessionReconnectHandler`: 500 ms → 30 s exponential) - `IRetryBudget` + `RetryBudget` — shared retry deadline so the two-level retry no longer compounds multiplicatively - `ClientChannelManager` — single non-partial sealed class with SOLID-extracted internal types under `Stack/Opc.Ua.Core/Stack/Client/Channels/Internal/`: `ChannelEntry` (behind `IChannelEntryHost`), `ManagedTransportChannelLease`, `ClientChannel`, `ClientChannelManagerMetrics` (IMeter), `ClientChannelManagerCertRotation` (auto `ReconnectAllAsync` on `CertificateManager` rotation), `ClientChannelManagerDiagnostics` (ActivitySource + EventSource). Refcount, coalesced reconnect, and transparent faulted-entry recovery via `SwapFaultedEntryAsync` (swaps a lease's underlying entry on subsequent `ReconnectAsync` when the prior cycle Faulted/Closed, preserves participant, back-off via policy `GetDelay(SwapCount)`) ### Three-state lifecycle gate `Disconnected` → `TransportConnecting` / `TransportReconnecting` → `TransportConnectedSessionReactivating` → `Ready` (or `Faulted` / `Closed`). Only `Ready` releases the service-call gate; the manager bypasses the gate internally for participant `ActivateSession` traffic during reactivation via an `AsyncLocal` scope. ### Session integration (`Libraries/Opc.Ua.Client/Session/`) - New `Session.CreateAsync(IClientChannelManager, …)` overload - `Session` implements `IReconnectParticipant`; reactivation handler distinguishes `Reactivated` / `RequiresSessionRecreate` / `TransientFailure` / `FatalForParticipant` / `FatalForChannel` - `Session.ReconnectAsync(ct)` automatically delegates to the channel manager when wired - `Session.RecreateInPlaceAsync` swaps managed-channel leases for failover-to-different-endpoint via a new atomic `IClientChannelManager.GetAsync(endpoint, participantFactory, …)` overload; same-key recreates delegate to manager reconnect; explicit-channel/connection callers retain legacy behavior - KA failure routes through `IClientChannelManager.ReconnectAsync(channel)` ### ManagedSession integration - `WithChannelManager(...)` builder + ctor param; DI-resolved automatically - Two-level retry: channel-mgr handles transparent reconnect; outer `ConnectionStateMachine` + `IReconnectPolicy` only triggers on terminal channel `Faulted`. Outer-state churn suppressed during channel-mgr cycles via `_channelReconnectInProgress` flag wired through `IManagedTransportChannel.StateChanged` - New `ManagedSession.ChannelStateChanged` event surfaces transparent reconnects - `ConnectionStateChangedEventArgs.UnderlyingChannelState` populated when outer reconnect was triggered by a channel fault - Shared `IRetryBudget` propagated so the layers enforce a single max-total-time ### Discovery / Registration / GDS Channel-manager-aware `CreateAsync` overloads on `DiscoveryClient`, `RegistrationClient`, `ServerPushConfigurationClient`, `LocalDiscoveryServerClient`, `GlobalDiscoveryServerClient`. Each registers a minimal `IReconnectParticipant`; co-located sessions and discovery probes targeting the same endpoint share a channel. ### HTTPS transport (`Stack/Opc.Ua.Bindings.Https/`) - New `IOpcUaHttpClientFactory` abstraction - HTTPS / OPC-HTTPS channels migrated to `IHttpClientFactory` + `Microsoft.Extensions.Http.Resilience` (10.6.0; AOT-compatible on net10 via source generators; legacy direct-HTTP path retained for older TFMs / no-DI consumers) ### DI `AddOpcUa().AddClient(...)` registers `IClientChannelManager` and the named HTTPS `HttpClient` + standard resilience handler automatically. `ManagedSessionBuilder` resolves the manager from DI when present. ### Obsoletions (functional, `[Obsolete]` with guidance) - `IClientBase.AttachChannel` / `DetachChannel` - `SessionReconnectHandler` - `SessionExtensions.ReconnectAsync(connection, ct)` and `(channel, ct)` legacy overloads ### Tests - Core: `ClientChannelManagerManagedTests`, `ClientChannelManagerCertRotationTests`, `RetryBudgetTests`, `OpcUaHttpClientFactoryTests` — **56 / 56 pass** - Client: `ManagedSession*Tests` updated; legacy `SessionReconnectHandlerTests` + `SessionExtensionsTests` preserved with `#pragma warning disable CS0618` — **129 / 129 pass** - Sessions integration: `ChannelManagerSharing` / `TransparentReconnect` / `SessionLifecycle` / `CertRotation` — **7 pass, 1 skipped** (`ChannelManagerExhaustionEscalatesAndRecoversWhenServerReturns` un-`[Ignore]`d and implemented in this PR via `SwapFaultedEntryAsync`; remaining skip is end-to-end `RequiresSessionRecreate` plumbing — carry-forward) - Stress (consolidated `Tests/Opc.Ua.Stress.Tests/Subscriptions/` + `Channels/`): chaos / contract / soak / lifecycle / leak-accuracy / gap / participant-result-aggregation tests merged from the previously-separate `Opc.Ua.Channels.Stress.Tests` project; nightly run via `.github/workflows/stress-test.yml` Builds clean (0 warnings / 0 errors) across all six TFMs: `net472`, `net48`, `netstandard2.1`, `net8.0`, `net9.0`, `net10.0`. ### Docs - `Docs/Sessions.md` new § 4 documents the channel manager, three-state gating, two-level retry with shared `IRetryBudget`, HTTPS resilience layering - `Docs/MigrationGuide.md` migration recipes for the four obsoleted API groups - `Docs/DependencyInjection.md` channel-manager and HTTPS factory registration ## Carry-forward (separate PRs) - End-to-end `ParticipantReconnectResult.RequiresSessionRecreate` → `Session.RecreateAsync` lifecycle wiring (currently incomplete — flagged in the 1 remaining skipped integration test) ## Related Issues - Fixes #3288 - Builds on #3282 (PR that introduced the initial `ITransportChannelManager` skeleton) ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. *(Channel-manager-specific tests + ManagedSession/client tests verified across all 6 TFMs; full UA.slnx run is pending CI.)* - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. |
||
|
|
7728b65e8d | CI: Fix pipeline cannot be cancelled regression and flaky tests (#3872) | ||
|
|
4e6c3303dd |
Add V2 subscription triggering (SetTriggering) with N:M support and recreate replay (#3854)
# Description Adds **triggered-item (SetTriggering) support to the V2 (modern) subscription engine** with a hybrid API, batched apply pipeline, and automatic replay on recreate/reconnect — closing the gap from #3834 and adding store/load round-trip for triggering relationships. ## Public API | Concern | Surface | |---|---| | Declarative | `MonitoredItemOptions.TriggeredByNames` (`IReadOnlyList<string>`) — set at item-add time | | Imperative | `ISubscription.SetTriggeringAsync(IMonitoredItem triggeringItem, IReadOnlyCollection<IMonitoredItem>? linksToAdd, IReadOnlyCollection<IMonitoredItem>? linksToRemove, CancellationToken ct)` returning `SetTriggeringResult` with per-link statuses | | Fluent helpers | name-based overloads `SetTriggeringAsync(this ISubscription, string triggeringItemName, params string[] triggeredItemNames)` (and `add`/`remove`/`ct` variant) | | N:M navigation | `IMonitoredItem.TriggeringItems` (items that trigger this) and `IMonitoredItem.TriggeredItems` (items I trigger), both on-demand projections | | Result type | `public sealed record SetTriggeringResult(IMonitoredItem TriggeringItem, IReadOnlyList<(IMonitoredItem Item, StatusCode Status)> AddResults, IReadOnlyList<(IMonitoredItem Item, StatusCode Status)> RemoveResults, StatusCode ServiceResult)` | No client handles surface on the public API. Validation in the imperative path is by **reference identity** under the manager lock — items from a different subscription with a matching name cannot sneak through, and a removed-then-re-added item with the same name does not silently re-target. ## Engine — batched apply A new manager-level operations queue + a batched apply step in `MonitoredItemManager.ApplyMonitoredItemChangesAsync`: 1. After Create / Modify / SetMonitoringMode, drain the operations queue. 2. Group by triggering item (reference identity). 3. Re-queue ops whose triggering item is not yet `Created` (bounded by `MaxTriggeringRetryCount`; permanently failed items propagate the create-failure status to the operation TCS). 4. Fold per-edge across operations targeting the same triggering item — last-intent wins; same-edge add+remove resolves to one net operation. 5. Pre-resolve: triggered items with `Created=false` resolve to `Bad_MonitoredItemIdInvalid` (adds) or `Good` no-op (removes, per Part 4 §5.13.1.6 server auto-cleanup). 6. Issue **one `SetTriggering` RPC per triggering item** carrying both `linksToAdd` and `linksToRemove` (the server processes remove before add per §5.13.5.2 — atomic swap semantics). 7. Map per-link results back to contributing operations; complete each TCS with its own `SetTriggeringResult` subset. Imperative writes mutate the per-item runtime `DesiredTriggeredByNames` synchronously so navigation (`TriggeringItems`/`TriggeredItems`) and `Snapshot()` reflect intent before the RPC fires. Per-link failures roll back the optimistic mutation so the client view matches server reality. ## State ownership `MonitoredItemOptions.TriggeredByNames` is the **initial declarative input**. The canonical runtime source of truth is a per-item `DesiredTriggeredByNames` field, initialized from options and mutated by both the imperative API and subsequent options changes. The field is preserved across `Reset` (recreate path), which is what enables automatic SetTriggering replay after recreate. ## Save / load (closes #3834) - `MonitoredItemStateSnapshot.TriggeredByNames` (`ArrayOf<string>`) replaces the old `TriggeringItemClientHandle` field (V2-only — not yet shipped, so a clean break; classic snapshot format unchanged). - `Snapshot()` reads from runtime `DesiredTriggeredByNames`; `ApplyLoadState` writes loaded names into both options and runtime state without leaking through the OnChange tracker. - **TransferSubscriptions** success path: only the local view is restored — trusting the spec (`§5.13.5`) that servers preserve triggering links across subscription transfer. No SetTriggering RPC is issued. - **Recreate-fallback** (transfer rejected), **in-session `RecreateAsync`**, and **Reconnect** paths: `Reset` preserves `DesiredTriggeredByNames` and enqueues an add-delta per preserved triggering name; the batched apply pass replays SetTriggering after items finish (re)creating. ## Error handling Per OPC UA Part 4 §5.13.5.3/§5.13.5.4 (Tables 73/74): | Condition | Engine response | |---|---| | Per-link `Bad_MonitoredItemIdInvalid` | Surfaced in `SetTriggeringResult.AddResults`/`RemoveResults`; optimistic `DesiredTriggeredByNames` mutation rolled back | | Service-level `Bad_SubscriptionIdInvalid` | Operations re-queued, not terminal — replays after the subscription state machine recreates | | Service-level `Bad_TooManyOperations` | Surfaced as `ServiceResultException` to all contributing TCSs (no auto-chunking in this version) | | Triggering item never reaches `Created` | After `MaxTriggeringRetryCount` re-queues, op completes with the triggering item's propagated error (no infinite loop) | | Communication errors | Routed through the existing retry policy with bounded retry count | ## Spec consultation Cross-checked against **OPC UA Part 4 v1.05.07 §5.13.5** (verified Table 73 / Table 74 directly via the OPC Foundation spec site) and §5.13.1.6 (deletion side-effects, monitoring-mode interactions). The only per-link spec-specific status code is `Bad_MonitoredItemIdInvalid`; `linksToRemove` is processed server-side before `linksToAdd` (driving the single-RPC-per-trigger batch shape). ## Tests - **9 new unit tests** in `MonitoredItemManagerTriggeringTests.cs` (single op, multi-op merge into one RPC, per-trig grouping, requeue-when-not-Created, per-link rollback, `Bad_SubscriptionIdInvalid` recovery, deletion races, last-intent same-edge conflict resolution). - **Updated** integration tests `SetTriggeringTrackingV2Async` and `SnapshotCapturesTriggeringForReplayAsync` to use the new API. - **2 new integration tests** in `SubscriptionTests.cs`: `DeclarativeTriggeredByNamesV2Async` (options-driven path end-to-end) and `NToMTriggeringV2Async` (one triggered item linked to two trigger items, per-link removal). - **AOT smoke test** in `SubscriptionAotTests.cs` for the `MonitoredItemStateSnapshot.TriggeredByNames` binary encoder/decoder round-trip. - All **344 V2 subscription/monitored-item unit tests** and **71 V2 subscription integration tests** pass locally (net10). ## Documentation - New: `Docs/Subscriptions.md` — *Subscriptions and Monitored Items Service Set*. Merges the previous `StreamingSubscription.md` and `SubscriptionTriggering.md` into a single guide; leads with a Classic-vs-V2 engine overview (recommending V2 for new code) and links to `Sessions.md` §4 for the full engine comparison. Covers the V2 callback API, declarative + imperative SetTriggering with N:M, save/load/restore behavior, error handling, and the `IStreamingSubscription` async-enumerable surface. - Updated: `Docs/README.md`, `Docs/AlarmsAndConditions.md`, `Docs/StateMachines.md`, `Docs/Profiles.md` — all point to the new merged doc. - Updated: `plans/26-v2-subscription-parity.md` — rows 49 (`SetTriggeringAsync`) and 111 (`TriggeringItems` plural / `TriggeredItems`). ## Related Issues - Fixes #3834 — `TransferSubscription does not automatically restore/retain links between trigger and monitored items`. The fix is two-pronged: (a) transfer preserves links server-side per spec and the client snapshot captures `TriggeredByNames` so the local view restores automatically; (b) when transfer fails and the engine falls back to recreate (or when `RecreateAsync` is invoked explicitly), `Reset` enqueues per-triggering-name add-deltas from the preserved runtime state and the batched apply pass replays `SetTriggering` after re-creation. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. |
||
|
|
2966f693b2 |
2.0 Migration Analyzer and codefixer with Agent update (#3820)
Introduces `OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer` — a single NuGet package that ships **three Roslyn components + a runtime shim** to make the 1.5.378 → 2.0 upgrade as close to "install package, run `dotnet format`, remove package" as possible: | DLL | Where | Purpose | |-----|-------|---------| | `Opc.Ua.MigrationAnalyzer.dll` | `analyzers/dotnet/cs/` | 19 Roslyn analyzers (`UA0001`–`UA0022`) covering every breaking-change pattern in [`Docs/MigrationGuide.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md). Built against the stable Roslyn 4.14 analyzer API — loads cleanly in csc.exe (no `Microsoft.CodeAnalysis.Workspaces` reference). | | `Opc.Ua.MigrationAnalyzer.CodeFixer.dll` | `analyzers/dotnet/cs/` | 14 companion `CodeFixProvider`s; loaded only by Workspaces-aware hosts (Visual Studio, `dotnet format analyzers`). | | `Opc.Ua.MigrationAnalyzer.Generator.dll` | `analyzers/dotnet/cs/` | Roslyn `IIncrementalGenerator` that emits per-consumer `internal sealed [Obsolete] class <Name>Collection : List<TElement>` shims for every `<Type>Collection` wrapper the consumer references — including **arbitrary model-compiled `<UserType>Collection` patterns**, not just the 30 well-known built-in ones. | | `Opc.Ua.MigrationAnalyzer.Core.dll` | `lib/<tfm>/` for `net472, net48, netstandard2.1, net8.0, net9.0, net10.0` | 1.5.378 → 2.0 runtime compatibility shim. Re-exposes the obsolete extension surface so 1.5.378-style call sites keep compiling against 2.0 with warnings instead of errors. | ### Migration workflow the package enables 1. **Bump every `OPCFoundation.NetStandard.Opc.Ua.*` PackageReference to `2.0.*-*`.** 2. **Add the migration package** as a build-only dependency: ```xml <PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer" Version="2.0.*-*" PrivateAssets="all" /> ``` 3. **Run `dotnet build`.** The Core shim DLL re-supplies obsolete extension members; the source generator covers `<Type>Collection` references; what remains are `[Obsolete]` warnings + `UA00xx` analyzer diagnostics + (occasionally) `MIG01` for unresolvable model-compiled element types. 4. **Apply auto-fixes** in the IDE or from the command line: ```bash dotnet format analyzers <solution>.sln \ --diagnostics UA0002 UA0003 UA0004 UA0005 UA0006 UA0007 UA0008 \ UA0009 UA0010 UA0012 UA0014 UA0019 UA0020 UA0022 \ --severity warn ``` 5. **Walk the residual** `UA0001` / `UA0011` / `UA0015` / `UA0018` / `UA0021` diagnostics manually — they cover patterns that require human judgement. 6. **Remove the migration package.** You are now on clean 2.0 with no shim dependency. ### Source generator (`Opc.Ua.MigrationAnalyzer.Generator.dll`) Per-consumer Roslyn `IIncrementalGenerator` that emits `.g.cs` files for every `<Name>Collection` reference in the consumer's compilation that fails to bind: - **Catalog override (30 entries)** — pins element types that renamed across the 1.5.378 → 2.0 boundary: `DateTime`→`DateTimeUtc`, `Guid`→`Uuid`, `byte[]`→`ByteString`, `XmlElement`→`Opc.Ua.XmlElement`, plus the primitives and built-in opc types whose element name didn't rename. - **Semantic lookup** — for model-compiled `<UserType>Collection` patterns the model compiler historically emitted for any user-defined complex type. Strips the `Collection` suffix, runs `Compilation.GetSymbolsWithName` against the unique element short name, emits the shim with the fully-qualified type reference. - **`MIG01` diagnostic** — fires when the element type can't be uniquely resolved (zero or multiple candidates). User adds the missing `using` or migrates the site manually. Each generated file emits a class shaped exactly like the 1.5.378 wrapper: ```csharp [global::System.Obsolete("'Int32Collection' was removed in 2.0. Use 'List<int>' or 'ArrayOf<int>' instead. (UA0002)")] internal sealed class Int32Collection : global::System.Collections.Generic.List<int> { public Int32Collection() { } public Int32Collection(int capacity) : base(capacity) { } public Int32Collection(global::System.Collections.Generic.IEnumerable<int> collection) : base(collection) { } public static implicit operator global::Opc.Ua.ArrayOf<int>(Int32Collection? value) => value is null ? default : value.ToArrayOf(); } ``` **Design choice — `internal` (not `public`):** the generated shim never leaks through the consumer's public API surface. Consumers with *public* methods or properties returning a `<Type>Collection` will hit `CS0050: Inconsistent accessibility` — the intended signal that the public surface must migrate to `List<T>` / `ArrayOf<T>` first; internal call sites keep compiling under the shim so iteration is incremental. ### 19-rule analyzer reference | Rule | Replaces | Default | Auto-fix | |------|----------|---------|----------| | `UA0001` | `Utils.Trace` / `Utils.LogX` | Info | No (telemetry plumbing requires manual review) | | `UA0002` | Removed `<Type>Collection` wrappers | Warning | Yes (→ `List<T>` / `ArrayOf<T>`); the source generator keeps the call site compiling in the meantime | | `UA0003` | `x == null` on now-struct built-ins | Warning | Yes (→ `x.IsNull`) | | `UA0004` | `?.` on now-struct built-ins | Warning | Yes (drop the `?`) | | `UA0005` | `byte[]` where `ByteString` is expected | Warning | Yes (append `.ToByteString()`) | | `UA0006` | `new Variant(object\|DateTime\|Guid\|byte[])` | Warning | Yes (→ `Variant.From(...)`) | | `UA0007` | `new NodeId(string)` / `new ExpandedNodeId(string)` | Warning | Yes (→ `Parse`) | | `UA0008` | `Session.Call(..., params object[])` | Warning | Yes (wrap with `Variant.From`) | | `UA0009` | `[DataContract]` / `[DataMember]` on config extensions | Warning | Yes (→ `[DataType]` / `[DataTypeField]`) | | `UA0010` | `using` / `Dispose` on `CertificateIdentifier` / `UserIdentity` / `IUserIdentityTokenHandler` | Warning | Yes (drop the disposable) | | `UA0011` | Sync `IUserIdentityTokenHandler.{Encrypt,Decrypt,Sign,Verify}` | Info | No (async promotion required) | | `UA0012` | `CertificateFactory.*` static helpers | Warning | Yes (→ instance methods) | | `UA0014` | `DataValue.IsGood(dv)` static helper | Warning | Yes (→ `dv.IsGood`) | | `UA0015` | Sync / APM members on GDS / LDS clients | Info | No (async promotion required) | | `UA0018` | `CertificateIdentifier.Certificate` getter | Info | No (→ `LoadCertificate2Async`) | | `UA0019` | `new DataValue(StatusCode[, ts])` | Warning | Yes (→ object-initializer form) | | `UA0020` | `EncodeableFactory.GlobalFactory` / `Create()` | Warning | Yes (→ `ServiceMessageContext.Factory` / `Fork()`) | | `UA0021` | `CertificateValidator` / `CertificateValidationEventArgs` (structural rename) | Info | No (see migration guide §Certificate validation) | | `UA0022` | `ApplicationConfiguration.CertificateValidator` / `ServerBase.CertificateValidator` property rename | Warning | Yes (→ `.CertificateManager`) | Plus one generator-only diagnostic: | Rule | Source | Default | Triggers | |------|--------|---------|----------| | `MIG01` | Generator | Warning | Generator can't uniquely resolve the element type for a `<Foo>Collection` reference (zero or multiple candidates). The user adds a `using` for the namespace defining `Foo`, or migrates the call site manually. | ### Runtime shim coverage (`Opc.Ua.MigrationAnalyzer.Core.dll`) C# 14 `extension` members so 1.5.378 call sites continue to compile: - **Moved obsolete extensions** the 2.0 libraries no longer carry inline: `NodeId` / `Variant` / `DataValue` null-check helpers, `Session` sync helpers, `Subscription` sync helpers, `ApplicationInstance` helpers, `ServerBase.Start` / `Stop`, `TransportChannel` APM, `ChannelBase` static factory methods, etc. - **New shims for genuinely-removed members**: `EncodeableFactory.GlobalFactory`, `CertificateIdentifier.Certificate` (throws), sync wrappers for `IUserIdentityTokenHandler.{Encrypt,Decrypt,Sign,Verify}`, sync + APM wrappers for the GDS / LDS client APIs, `GlobalDiscoverySampleServer` 1.5.378-shape ctor (in-tree `Libraries/Opc.Ua.Gds.Server.Common/`). > ⚠ The sync shims wrap their `*Async` counterparts via `Task.Run(…).GetAwaiter().GetResult()`. Intended as a **migration aid only** — port to `await` before shipping. ### Repo layout ``` Tools/Opc.Ua.MigrationAnalyzer/ # analyzer DLL Analyzers/UA00xxAnalyzer.cs # 19 DiagnosticAnalyzer impls Diagnostics/{DiagnosticIds,Descriptors,WellKnownProperties}.cs Helpers/{SymbolExtensions,UaSymbols}.cs Opc.Ua.MigrationAnalyzer.nuspec # ships all 3 analyzer DLLs + 6-TFM Core OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/ # code-fix DLL CodeFixes/UA00xxCodeFix.cs # 14 CodeFixProvider impls Tools/Opc.Ua.MigrationAnalyzer.Generator/ # source generator DLL (NEW) MigrationGenerator.cs # IIncrementalGenerator CollectionShimCatalog.cs # 30-entry element-type overrides Diagnostics/GeneratorDiagnostics.cs # MIG01 Tools/Opc.Ua.MigrationAnalyzer.Core/ # runtime shim (multi-TFM) Client/, Configuration/, Core/, Gds.Client.Common/, Types/, Marker/ Tools/Roslyn.slnx # source-gen + migration projects Tests/Opc.Ua.MigrationAnalyzer.Tests/ # 142 tests Analyzers/UA00xxTests.cs # 107 analyzer + code-fix tests Generators/MigrationGeneratorTests.cs # 35 generator tests (NEW) Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/ # 13 runtime-shim tests ``` ### Documentation - [`Docs/MigrationGuide.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md) — §3 Collection Type Migration gains a tip pointing at the generator so CS0246 is replaced with `[Obsolete]` warnings + UA0002. - [`.github/agents/opcua-v20-migration.agent.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/.github/agents/opcua-v20-migration.agent.md) — Strategy section mentions the three Roslyn components + runtime shim explicitly, plus the MIG01 diagnostic and the "internal-by-design" CS0050 hint. ### Central package pinning `Directory.Packages.props` pins `Microsoft.CodeAnalysis.*` (Common, CSharp, CSharp.Workspaces, Workspaces.Common) to **4.14.0** — the stable analyzer API surface. The earlier 5.3 pin was the *csc-internal* version, which is not safe for analyzer hosts. ### Dogfood story The full 42-sample dogfood against `OPCFoundation/UA-.NETStandard-Samples` surfaced a **critical analyzer-loader bug** in an earlier iteration: the analyzer DLL initially co-shipped its code-fix providers in one assembly, which transitively referenced `Microsoft.CodeAnalysis.Workspaces.dll`. csc.exe's analyzer host ships only `Microsoft.CodeAnalysis.dll` + `CSharp.dll` in its bincore and **silently swallows** load failures — net result: zero diagnostics across all samples. Fix landed in commit `861fa6ee1`: 1. **Split** the assembly: analyzer in `Opc.Ua.MigrationAnalyzer.dll` (Workspaces-free, csc-safe), code-fixes in `Opc.Ua.MigrationAnalyzer.CodeFixer.dll` (Workspaces-aware, IDE-only). 2. **Downgrade** to Roslyn 4.14 — the stable analyzer API. 3. `/p:ReportAnalyzer=true` confirms all 19 analyzers initialise and execute on real consumer code. This PR's new generator was smoke-tested against a minimal consumer that exercises all 30 catalog entries + a model-compiled element type + the unresolvable case: - 30 catalog patterns + model-compiled `MyThingCollection` → all emit shims, fire CS0618 with `(UA0002)` marker - Element-type renames verified: `DateTimeCollection` → `DateTimeUtc`, `ByteStringCollection` → `ByteString`, `GuidCollection` → `Uuid`, etc. - `NeverHeardOfItCollection` → `MIG01` fires with help link to the migration guide Known compatibility gaps (documented in the migration agent): - Legacy WinForms `.Net4` sample projects use pre-SDK MSBuild XML and **ignore** `Directory.Build.targets` `<PackageReference>` injection — consumers in this format must add the migration package to their csproj inline. ### Packaging design notes - Hand-authored nuspec ships 3 DLLs in `analyzers/dotnet/cs/` + 6 TFMs of Core (shim) DLLs under `lib/`. All 6 OPC UA runtime package dependencies are declared per-TFM-group. - `build/OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props` is auto-imported by NuGet (file name must match package id exactly) and injects all three analyzer DLLs as `<Analyzer>` items. - `DiagnosticIds.cs` / `DiagnosticDescriptors.cs` / `WellKnownProperties.cs` / `Helpers/*.cs` are **shared via `<Compile Link=…>`** between the analyzer and code-fixer projects to break what would otherwise be a NuGet restore cycle. The generator has no compile dependency on the analyzer; the 30-entry override table is duplicated in `CollectionShimCatalog.cs` (the analyzer table also drives diagnostic messages, so the duplication is small and intentional). - `Opc.Ua.MigrationAnalyzer.Core` is multi-TFM (`net472, net48, netstandard2.1, net8.0, net9.0, net10.0`) and **non-packable on its own**; the parent migration package's nuspec consumes each TFM's DLL by path. |
||
|
|
dbc09b8e22 |
Adopt System.TimeProvider across the stack (fixes #2458) (#3829)
# Description Adopt [`System.TimeProvider`](https://learn.microsoft.com/dotnet/api/system.timeprovider) across the stack so that every place that creates timers, runs duration math, or schedules periodic work is mockable, deterministic in tests, and immune to wall-clock changes. The migration touches Core, Core.Types, Server, Client, PubSub, Configuration, Gds (server + client), Lds, and the Quickstarts/Console reference apps. Per-component changes follow the same pattern: 1. Add a `private readonly TimeProvider m_timeProvider;` field. 2. Accept a nullable `TimeProvider? timeProvider = null` as the **last** constructor parameter (new overload; legacy constructor stays for binary compatibility and delegates with `timeProvider: null`). 3. Initialise the field with `m_timeProvider = timeProvider ??= TimeProvider.System;`. 4. Replace `new Timer(...)`, `Stopwatch`, `HiResClock.*`, `DateTime.UtcNow`-based duration math, and `Task.Delay(delay, ct)` in timing loops with the matching `m_timeProvider` API (`CreateTimer`, `GetTimestamp` / `GetElapsedTime`, `GetUtcNow().UtcDateTime`, `Task.Delay(delay, m_timeProvider, ct)`). 5. Wire the provider through DI: `AddOpcUaServerBuilder` / `AddOpcUaClientBuilder` register `TimeProvider.System` via `TryAddSingleton<TimeProvider>()` so tests can swap in `Microsoft.Extensions.Time.Testing.FakeTimeProvider`. `HiResClock` itself is kept (so the `ApplicationConfiguration.DisableHiResClock` schema flag still round-trips), but **every public member** is now `[Obsolete]` with a single shared message pointing to the new migration-guide section. The few internal call sites that still need to set/read `HiResClock` are wrapped with `#pragma warning disable CS0618`. ### Highlights per component - **Core / Stack** — `Opc.Ua.Core`, `Opc.Ua.Core.Types`, channel pipeline (`UaSCBinaryClientChannel`, `ChannelToken`, `UaSCBinaryChannel`), `ApplicationConfiguration`, certificate validators, `JwksIssuerKeyResolver`, `TimeProviderExtensions` polyfills (`GetTimestampMilliseconds`, `GetTickCount`, `CancellationTokenSource(timeout, timeProvider)`, `Task.Delay(delay, provider, ct)` polyfill for older TFMs). - **Server** — session lockout, subscription pipeline (publish/sample/keep-alive/lifetime), `MonitoredItemQueue`, alarm sampling rate, `IntervalRunner`, server diagnostics, configuration state machines, hosted services. - **Client** — `Session`, managed/V2 subscription engines, reconnect handlers, channel keep-alive, periodic publishing, complex-type discovery polling. - **PubSub** — `IntervalRunner` pipeline, message-receive timers, MQTT/UDP keep-alive, publishers/readers. - **Gds / Lds** — `LdsServer`, `LinqApplicationsDatabase` expiry housekeeping, GDS clients (push, pull), `DirectoryCertificateStore`, `ConnectionStateMachine`, classic subscription engine. - **Quickstarts / Console samples** — `SampleNodeManager`, `ReferenceNodeManager`, `AlarmNodeManager`, `BoilerState`, `MemoryBufferState`, `PublishedValuesWrites`. Samples resolve via `(Server as ITimeProviderProvider)?.TimeProvider ?? TimeProvider.System` so existing ctor signatures stay binary-compatible. - **Tests** — `Opc.Ua.PubSub.Tests` adds `IntervalRunnerTests`; existing `SubscriptionTests`, `UaPublisherTests`, and `TestDataSystem` move off `HiResClock`. Added `Microsoft.Extensions.TimeProvider.Testing` package for `FakeTimeProvider` (project-load warning surfaces on net48/net472 only — runtime is .NET 8+). ### Migration guide New section [**Time and Timer abstraction (`TimeProvider`)**](Docs/MigrationGuide.md#time-and-timer-abstraction-timeprovider) documents: - The canonical replacement table (`HiResClock.UtcNow` → `timeProvider.GetUtcNow().UtcDateTime`, `HiResClock.TickCount64` → `timeProvider.GetTimestamp()`, `new Timer` → `timeProvider.CreateTimer`, `Task.Delay(delay, ct)` → `Task.Delay(delay, timeProvider, ct)`, …). - The `TimeProvider? timeProvider = null` + `??=` constructor pattern. - DI wiring and how to swap in `FakeTimeProvider` in tests. - Before/after snippets for `HiResClock.TickCount64`, `HiResClock.UtcNow`, and `new Timer(...)`. ## Related Issues - Fixes #2458 ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. |
||
|
|
49305d9085 |
V2 subscription engine close gap for test and feature parity with classic engine (#3824)
# Description V2 subscription engine feature parity round to make the classic engine eligible for deprecation. Splits the test surface so the classic and V2 engines can evolve independently, ports the gaps the classic-engine integration tests had been exercising, and lands the public V2 API additions needed to close them. - Splits `Tests/Opc.Ua.Subscriptions.Tests` into a classic (`Opc.Ua.Subscriptions.Classic.Tests`) and a V2 (`Opc.Ua.Subscriptions.Tests`) project to shrink CI time and let the two engines diverge cleanly. - Adds the V2 public-surface gaps required by the test ports (`Subscription.ServerId` on the class, `SetSubscriptionDurableAsync`, `SendInitialValuesOnTransfer`, unified `OnSubscriptionStateChangedAsync` handler callback, per-item `ConditionRefreshAsync`, fluent `LoadSubscriptionsAsync(Stream, …)` and `Subscription.Snapshot()` / `RestoreAsync()` for save/load + transfer). - Lands handler-centric design (no `PublishingStopped` property handlers maintain derived state); always-sequential per-subscription delivery documented + tested. - **Adds short and long haul stability tests for the V2 `ManagedSession` + subscription engine. Closes #3744** (see "Issue #3744 Haul tests" below). - **Migrates SubscriptionStateSnapshot / MonitoredItemStateSnapshot to `[DataType]` source-generator codecs**. - `SubscriptionDurableV2Tests.cs` 5 V2 ports of the classic durable test patterns. - `V2FollowUpCoverageTests.cs` handler state-change callback, fluent stream-based `LoadSubscriptionsAsync`, `SendInitialValuesOnTransfer`, snapshot edge cases (empty / `DataChangeFilter` round-trip / concurrent mutation). - `SubscriptionFailoverV2Tests.cs` forced channel-break with and without `WithTransferSubscriptionsOnRecreate`. - `MonitoredItemConditionRefreshLiveV2Tests.cs` — live `ConditionRefresh` against the reference server's event source; asserts `RefreshStart` / `RefreshEnd` events flow through the handler. ## Fixes - #3744 Haul tests (V2 ManagedSession + subscription engine) `Tests/Opc.Ua.Sessions.Tests/ManagedSessionStabilityTest.cs` adds the short/long haul coverage requested by [#3744](https://github.com/OPCFoundation/UA-.NETStandard/issues/3744) plus the bonus fault-injection variant: | Test | Duration | Category | Purpose | |---|---|---|---| | `ShortHaulManagedSessionV2Async` | 2 minutes | `ManagedSessionHaul` (`[Explicit]`) | Quick local sanity check of the V2 monotonicity contract. | | `LongHaulManagedSessionV2Async` | 90 minutes (configurable via `TEST_DURATION_MINUTES`) | `ManagedSessionHaul` (`[Explicit]`) | Long-running V2 stability + monotonicity. | | `LongHaulManagedSessionWithFaultInjectionV2Async` | 90 minutes (configurable) + fault every `FAULT_INJECTION_INTERVAL_SECONDS` (default 60s) | `ManagedSessionHaul` (`[Explicit]`) | **Bonus** — force-closes the subscriber transport channel periodically; verifies V2 manager transfers/recreates the subscription and continues monotonic delivery across reconnects. | **Pattern.** A writer `ManagedSession` increments a `uint32` counter and writes it into `Scalar_Static_Mass_UInt32_UInt32_00` every 250ms. A subscriber `ManagedSession` runs a V2 subscription with a single monitored item on the same node. `MonotonicCounterHandler` asserts each received sample is *strictly greater than* the previous one. V2 `ISubscriptionNotificationHandler` per-subscription ordering guarantee. Skipped values (sampling rate < write rate) are tolerated; duplicates and reorderings are violations. |
||
|
|
eb40a59566 |
More format and diagnostic fixes (#3780)
Drive the solution to zero warnings / zero errors with analyzers on, promote the consistently-fixed rules to error, and lock in coverage on the only line of new logic. |
||
|
|
f3928db05b |
Add unified Microsoft.Extensions DI surface across OPC UA libraries (#3803)
Introduces a consistent `Microsoft.Extensions.DependencyInjection`
surface across the OPC UA libraries. Every consumer starts from a single
root entry point and chains feature-specific extension methods on a
unified builder. PubSub is intentionally excluded per the original
scope.
```csharp
services
.AddOpcUa() // Stack/Opc.Ua.Core
.AddServer(o => { /* … */ }) // Libraries/Opc.Ua.Server
.AddNodeManager<MyNodeManagerFactory>()
.Services
.AddOpcUa()
.AddClient(o => { /* … */ }); // Libraries/Opc.Ua.Client
```
Manual non-DI constructors (`new ApplicationInstance(...)`, `new
StandardServer(...)`, `new LdsServer(...)`, `new ManagedSession(...)`,
etc.) remain unchanged — DI is purely additive for users who want the
.NET Generic Host to own application lifetime, logging, and
configuration.
### What ships
| Feature library | Method on `IOpcUaBuilder` | Returns |
|----------------------------------------------|-------------------------------------------|--------------------------|
| `Opc.Ua.Core` (root) | `services.AddOpcUa()` | `IOpcUaBuilder` |
| `Opc.Ua.Configuration` | `builder.AddApplicationInstance()` |
`IOpcUaBuilder` |
| `Opc.Ua.Client` | `builder.AddClient(opt => …)` |
`IOpcUaClientBuilder` |
| `Opc.Ua.Client.ComplexTypes` | `builder.AddComplexTypes()` |
`IOpcUaBuilder` |
| `Opc.Ua.Server` | `builder.AddServer(opt => …)` |
`IOpcUaServerBuilder` |
| `Opc.Ua.Gds.Client.Common` | `builder.AddGdsClient(opt => …)` |
`IGdsClientBuilder` |
| `Opc.Ua.Gds.Server.Common` | `builder.AddGdsServer(opt => …)` |
`IGdsServerBuilder` |
| `Opc.Ua.Lds.Server` | `builder.AddLdsServer(opt => …)` |
`ILdsServerBuilder` |
| `Opc.Ua.WotCon.Server` | `builder.AddWotConServer(opt => …)` |
`IWotConServerBuilder` |
| `Opc.Ua.WotCon.Client` | `builder.AddWotConClient(opt => …)` |
`IOpcUaBuilder` |
### Design highlights
- **Binary-safe inheritance** — `IOpcUaBuilder :
IDependencyInjectionBuilder` so any existing `AddLogging(this
IDependencyInjectionBuilder)` callers keep compiling unchanged.
- **Three options overloads** per feature: `Action<TOptions>`
(AOT-safe), `IConfiguration`, `IConfigurationSection`. The two binding
overloads are annotated `[RequiresUnreferencedCode]` /
`[RequiresDynamicCode]`. Section names follow the `OpcUa:<Feature>`
convention (`OpcUa:Server`, `OpcUa:Client`, `OpcUa:Gds:Server`,
`OpcUa:Lds`, …).
- **Generic Host integration** — each server feature (regular, GDS, LDS)
registers its own `BackgroundService`. Marker singletons throw
`InvalidOperationException` on duplicate `.AddServer()` /
`.AddGdsServer()` / `.AddLdsServer()`. A regular server + GDS + LDS can
coexist in one host; each owns its own `IApplicationInstance` produced
by a shared `IApplicationInstanceFactory` so PKI roots, endpoints, and
certificates don't collide.
- **Registration isolation** — node managers registered under the
regular server via `.AddNodeManager<T>()` are wrapped in
`OpcUaServerNodeManagerRegistration` so they don't bleed into the GDS /
LDS hosted services.
- **WoT Connectivity server** composes on top of the regular server
feature: `.AddServer(...).AddWotConServer(...)` registers
`WotConnectivityNodeManagerFactory` as a server node-manager
registration.
- **Unified telemetry** — `services.AddOpcUa()` registers
`ITelemetryContext` via `TryAddSingleton` using a new
`ServiceProviderTelemetryContext` that resolves the host's
`ILoggerFactory` on first use. User-supplied registrations win.
### Removed pre-release DI surface
The pre-release `services.AddOpcUaServer(...)`,
`services.AddOpcUaClient(...)`, `services.AddOpcUaGdsClient(...)`
extensions are deleted. `IClientBuilder` was renamed to
`IOpcUaClientBuilder`. In-repo callers migrated:
- `Applications/MinimalCalcServer/Program.cs`
- `Applications/MinimalBoilerServer/Program.cs`
- `Tests/Opc.Ua.Client.Tests/ClientBuilder/OpcUaClientBuilderTests.cs`
(renamed from `OpcUaClientServiceCollectionExtensionsTests.cs`)
- Docs (`MigrationGuide.md`, `Sessions.md`,
`SourceGeneratedNodeManagers.md`, README, new
`Docs/DependencyInjection.md`)
### csproj audit
Pruned every redundant `Microsoft.Extensions.*` package reference.
`Opc.Ua.Core` already pulls in `Microsoft.Extensions.Diagnostics` and
`Microsoft.Extensions.Logging`, which transitively expose DI / Options /
Configuration.Abstractions / Options.ConfigurationExtensions to every
downstream library. Only `Microsoft.Extensions.Hosting.Abstractions`
stays explicit on the server libraries (it isn't transitively
available). Also pruned redundant `System.Formats.Asn1` (Bindings.Https)
and `System.Collections.Immutable` (Security.Certificates).
### Tests
50 new tests in eight projects:
| Project | Tests |
|--------------------------------------|-------|
| `Opc.Ua.Core.Tests` | 12 |
| `Opc.Ua.Client.Tests` | 4 |
| `Opc.Ua.Client.ComplexTypes.Tests` | 3 |
| `Opc.Ua.WotCon.Tests` | 6 |
| `Opc.Ua.Lds.Tests` | 5 |
| `Opc.Ua.Gds.Tests` | 6 |
| `Opc.Ua.Server.Tests` (integration) | 10 |
| `Opc.Ua.Aot.Tests` (smoke) | 4 |
| **Total** | **50** |
Combined-host integration tests cover the rubber-duck-driven scenarios:
telemetry-context override across legacy and new entry paths, server +
GDS + LDS coexistence with distinct ports and PKI roots,
duplicate-registration rejection, and node-manager isolation across
features.
### AOT
`dotnet publish Tests/Opc.Ua.Aot.Tests -c Release -r win-x64
--self-contained` reports zero `IL2026` / `IL3050` / `IL2104` warnings
from the new DI surface. All generic registrations carry
`[DynamicallyAccessedMembers(PublicConstructors)]`. AOT consumers stick
to the `Action<TOptions>` overload exclusively; the new
`Tests/Opc.Ua.Aot.Tests/HostingAotTests.cs` smoke fixture exercises this
end-to-end.
|
||
|
|
a193a0a278 |
Add OPC UA Server FileSystem (Part 20) and Local Discovery Server libraries (#3776)
This change introduces two independent additions to the .NET Standard
stack:
## `Libraries/Opc.Ua.Server.FileSystem` (Part 5 §C / Part 20 §4)
Server-side implementation of the OPC UA `FileType` /
`FileDirectoryType` companion model, mirroring the existing client
namespace `Libraries/Opc.Ua.Client.FileSystem`.
### Provider abstraction
- `IFileSystemProvider` — async storage abstraction
(`GetEntryAsync` / `EnumerateAsync` / `OpenRead/WriteAsync` /
`Create{Directory,File}Async` / `DeleteAsync` / `MoveAsync` /
`CopyAsync`). All paths are forward-slash provider-relative; the
empty string is the root.
- `FileSystemEntry` record + `FileWriteMode` enum.
### Default `PhysicalFileSystemProvider`
`System.IO`-based implementation that mounts a **single physical
directory** as the FileSystem root. Rejects path-traversal attempts
(`..` escape) via `Path.GetFullPath` + root-prefix check. Supports
an optional `isWritable: false` mode for read-only mounts.
### Node manager
`FileSystemNodeManager` drives every Browse / Read / Method call
through the configured provider. Each mounted provider is anchored
as a `FileDirectoryType` instance under the standard
`Server.FileSystem` object (`ObjectIds.FileSystem`, `i=16314`)
via `HasComponent`, which is what
`FileSystemClient.OpenServerFileSystem` expects to find. Mount
multiple providers by registering multiple
`FileSystemNodeManager` instances. Companion files
(`DirectoryBrowser`, `DirectoryObjectState`, `FileObjectState`,
`FileHandle`, `FileSystemNodeId`, `FileSystemNodeManagerFactory`)
are all provider-driven.
### Client-side fix
`Libraries/Opc.Ua.Client.FileSystem.FileSystemClient.EnumerateChildrenAsync`
now stops the BrowseNext loop on `continuation.Length == 0` in
addition to `continuation.IsNull`. Without this fix, an
empty-but-non-null ByteString continuation returned by the SDK after
wire round-trip would cause a spurious BrowseNext and fail with
`BadContinuationPointInvalid`.
### Reference-server hook
`Quickstarts.ReferenceServer.ReferenceServer` gains two opt-in
properties (default off):
- `EnableFileSystemNodeManager` — toggles whether the FS node
manager is registered.
- `FileSystemProvider` — caller-supplied
`IFileSystemProvider`. When `null` and the flag is on, falls
back to a `PhysicalFileSystemProvider` rooted at
`%TEMP%/OpcUaReferenceServerFs` (mount name `Temp`).
### Tests
`Tests/Opc.Ua.Client.Tests/FileSystem/FileSystemClientIntegrationTests.cs`
— 12 end-to-end tests over a real
`ServerFixture<ReferenceServer>` + `ClientFixture` (opc.tcp,
SecurityNone, temp-dir provider): round-trip write/read,
enumeration, nested `CreateDirectory` / `CreateSubdirectory`,
delete, move, copy, large-file chunked read, mount BrowseName,
missing path returns null, multi-level enumeration, text round-trip,
read-only provider rejects write.
## `Libraries/Opc.Ua.Lds.Server`
A new Local Discovery Server (LDS) library implementing OPC UA Part
12 §7:
- `LdsServer` — main server class.
- `MulticastDiscovery` — DNS-SD / mDNS bootstrap via
`Makaretu.Dns.Multicast` (version 0.27.0 added to
`Directory.Packages.props`).
- `RegisteredServerStore` — in-memory store with TTL.
- `RegistrationEntry` + `ServerOnNetworkRecord` — value types.
Comes with a sample host `Applications/ConsoleLdsServer`.
## Build / test
- `dotnet build UA.slnx -c Release` -> 0 errors on all target
frameworks (net472, net48, netstandard2.1, net8.0, net9.0, net10.0).
- `Opc.Ua.Server.Tests` -> 964 pass, 5 skip, 0 fail.
- `FileSystemClientIntegrationTests` -> 12/12 pass.
|
||
|
|
70b4498db8 |
Certificate manager and ref counted certificate type (#3688)
* New certificate and secrete abstraction with certificate infrastructure and certificate/secret stores. * leak testing for certificate resources to validate new ref counting in every test assembly * Migration guide updated and documentation added for all * Remove IDisposable from Nodestate and UserIdentity. * UserCertificateTokenHandler now just points into certificate registry/stores, no dispose required * Fix CA2000 warnings and cleanup of other diagnostics, increase warning levels in .editorconfig |
||
|
|
bd4543e961 |
ManagedSession, Pluggable Subscription engine, and fluent builder + DI (#3730)
- Add new managed session, wrapping session and providing connection management and failover handling - Add new Subscription engine that can act as alternative to current engine, but is based on channels with worker tasks - Add a fluent builder for managed session clients - Replace NodeCache with LruNodeCache, rename ILruNodeCache and LruNodeCache to INodeCache and NodeCache - Update migration guide and document new functionality. |
||
|
|
94bc03b9f6 |
fix address space issues (#3705)
* fix missing mandatory children * Source generator: fix encoding nodes, EventNotifier, and type RBAC - Emit DataType encoding nodes (Default Binary/XML/JSON) into the generated address space. CollectEncodingNodes() discovers DataTypeEncodingType instances and builds synthetic hierarchies with their HasEncoding/HasDescription references. - Auto-set EventNotifier=SubscribeToEvents on Object nodes that have forward HasEventSource or HasNotifier references. The fix works at two levels: ModelDesignValidator infers SupportsEvents on types with event references in design XML, and NodeStateGenerator checks the resolved reference set for merged instances that inherit references from their type definitions. - Wrap AccessRestrictions/RolePermissions in if(forInstance) guards for ObjectType, VariableType, DataType, and ReferenceType templates so type definition nodes remain accessible to all users. - Allow ModellingRule=None Object children on type definitions to be emitted (e.g. state machine states/transitions). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update missing nodes analysis after source generator fixes Retest with admin credentials against ConsoleReferenceServer v1.05.07. Categorize 794 truly missing nodes (after filtering optional/placeholder): - 384 encoding nodes (NodeSet2.xml-only, not in design XML) - 172 role instance children - 84 other nodes - Remaining: trust list, PubSub, diagnostics, etc. RBAC and EventNotifier fixes confirmed working. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * update * plans: detailed analysis of 292 remaining missing nodes Replaced the two previous analysis files with a single consolidated validation report. Each of the 292 nodes is classified with a reason: - 212 type-level children (ObjectType definitions not instantiated) - 48 instance-level children (diagnostics, HA, FileSystem, PubSub) - 32 orphan property templates (design-time canonical definitions) Only 1 fix recommended: Server.Auditing (i=2994) should be exposed as false per Part 5 section 6.3.3. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add GDS node manager to ConsoleReferenceServer Add Global Discovery Server functionality to the reference server: - Add Opc.Ua.Gds.Server.Common project reference - Add GdsNodeManagerFactory that creates ApplicationsNodeManager with JsonApplicationsDatabase and CertificateGroup (auto-approve) - Add GlobalDiscoveryServerConfiguration extension to config XML - Add GDS to ServerCapabilities - Registration is opt-in: only when config extension is present Also fix pre-existing source generator test failures: - Add missing CreateOrReplaceTransitionNumber stub method to StateMachineTransitionState in CompilerUtils.cs (8 tests fixed) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * plans: update missing nodes report with GDS retest results Retested after adding GDS node manager to ConsoleReferenceServer. Total missing reduced from 1325 to 1243 (82 fewer). 335 remaining 'other' nodes fully classified with per-node reasons. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * plans: Server.Auditing is present (access-restricted, not missing) Live validation confirmed i=2994 exists with value=true, visible to admin users. Returns BadUserAccessDenied for anonymous. No fixes needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CttTestRunner build errors for v1.6 API and Jint 4.8.0 - Update System.CommandLine 2.0.7 API: SetHandler->SetAction, InvokeAsync->Parse().InvokeAsync() - Remove ObjectInstance inheritance from CttAssert, CttServiceRegister, CttIdentifierConstants, CttMonitoredItemHelper; use ToJsObject() factory methods instead - Replace Jint.Native.JsValueType with .IsObject()/.IsNumber()/.IsString() etc. - Add missing using directives (Jint.Runtime, Microsoft.Extensions.Logging, etc.) - Fix NodeId.Identifier obsolete -> TryGetIdentifier<uint> - Fix DataValue.Value obsolete -> WrappedValue.AsBoxedObject() - Fix ApplicationInstance() obsolete -> ApplicationInstance(ITelemetryContext) - Fix LoadApplicationConfiguration -> LoadApplicationConfigurationAsync - Fix CheckApplicationInstanceCertificate -> CheckApplicationInstanceCertificatesAsync - Fix CoreClientUtils.SelectEndpointAsync signature (requires telemetry param) - Fix DefaultSessionFactory() obsolete -> DefaultSessionFactory(ITelemetryContext) - Fix DiscoveryClient.Create -> DiscoveryClient.CreateAsync - Fix FindServers/GetEndpoints -> FindServersAsync/GetEndpointsAsync - Fix StatusCode to uint conversions - Fix ArrayOf<T> iteration (use foreach instead of LINQ on IConvertableToArray) - Fix XmlNodeList null coalescing in CttProjectSettings - Fix JsValue.False -> JsValue.FromObject(engine, false) - Add --json option to Program.cs for machine-readable output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: emit type-level children with ModellingRule=None for all node classes The source generator's GetChildren method only included ModellingRule=None children when they were ObjectDesign instances (state machine states/transitions). This excluded Variables and Methods that are explicitly defined on types without a modelling rule, such as: - ConditionType.ConditionRefresh (Method) - ConditionType.ConditionRefresh2 (Method) - ConditionType.SupportsFilteredRetain (Variable) - ProgramStateMachineType.Creatable (Variable) - Various TrueState/FalseState display-hint properties Remove the ObjectDesign restriction so all explicitly defined type-level children with ModellingRule=None are included in the generated address space. This adds ~52 previously missing nodes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * plans: update report after ModellingRule=None generator fix Type-level children dropped from 261 to 203 (-58) after fixing the source generator to emit Variables and Methods with ModellingRule=None. Total 'other' reduced from 335 to 283. No fixes needed for any remaining nodes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * plans: all 203 type-level children confirmed present via Read Verified all 203 'type-level children' exist in the server (none return BadNodeIdUnknown). They return Good, BadUserAccessDenied, or BadSecurityModeInsufficient -- all confirming the node exists but is access-restricted for anonymous users. All 1,316 export-missing nodes are now accounted for. Zero nodes are actually missing from the server address space. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CttTestRunner session service methods for CTT JS script compatibility - Implement ServiceCreateSession with real Session.CreateAsync connection - Implement ServiceActivateSession with proper response fields - Implement ServiceCloseSession with session cleanup - Fix UaSession constructor to provide service methods via CreateServiceObject - Convert all type constructors from ClrFunction to JS functions for 'new' support (Jint 4.x ClrFunction does not support [[Construct]]) - Add UaChannel.connect/disconnect stubs for OpenSecureChannel flow - Add UaDiscovery with getEndpoints/findServers from discovery session - Add missing types: UaSubscriptionDiagnosticsDataType, UaHistoryUpdateResult, etc. - Add missing enums: IdentifierType, Constants (DateTime/numeric limits) - Add UaPkiCertificate.fromDER, UaByteString.fromStringData, PkiType, UaPkiUtility - Add UaDateTime.Now constructor, equals/msecsTo methods - Add UaStatusCode with isGood/isBad/isUncertain as JS constructor - Fix response headers with Timestamp (UaDateTime), ServiceDiagnostics, StringTable - Fix endpoint descriptions with Server, UserIdentityTokens, ServerCertificate - Add CreateResponseHeader helper for consistent response header construction - Override warnOnce.js loggers to avoid Function.caller incompatibility - Add stopTest, checkResourceError (lowercase) global functions - Add UaOpcServer constructor with all method stubs - Handle JS parse errors in include() for malformed CTT files - Test.Connect() now calls InstanciateHelpers with DiscoverySession Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add CttTestRunner: .NET replacement for CTT JavaScript host environment Implements a Jint-based JavaScript engine that can execute the OPC Foundation's Compliance Test Tool .js test scripts using the OPC UA .NET Standard stack. Features: - Parses CTT .ctt.xml project files (Qt AbstractItemModelData format) - Loads and resolves CTT include() dependency chains (~100 library files) - Registers ~250 Ua* type constructors for JavaScript - Implements session.read/write/browse/getEndpoints/findServers - Provides all OPC UA enumerations, NodeId constants, Assert methods - CLI with --list, --file, --conformance-unit, --result, --json flags - Headless operation for agent/CI integration Current status: Framework connects to OPC UA server and loads CTT test infrastructure. Session establishment flow needs additional property compatibility for CreateSession/ActivateSession request objects. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: do not apply DefaultAccessRestrictions/DefaultRolePermissions to type definitions ObjectType and VariableType nodes should never have access restrictions or role permissions from DefaultAccessRestrictions/ DefaultRolePermissions. These defaults are meant to apply to instances of the type, not to the type definition node itself. Previously, CertificateGroupType, TrustListType, SecurityGroupType and other types were incorrectly emitted with AccessRestrictions inherited from their DefaultAccessRestrictions, making them unreadable without encryption even though the spec defines no restrictions on these type definitions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * CttTestRunner: fix session establishment flow - Fix constructor patterns (new/no-new dual call support) - Fix ResponseHeader with ServiceDiagnostics/StringTable - Populate ServerNonce, ServerCertificate, ServerEndpoints in CreateSession response - Add UaVariant setter methods (setNodeId/setString/setBoolean/etc.) - Add UaCreateSessionRequest/ActivateSessionRequest with proper nested structures - Fix UaPkiUtility/UaPkiCertificate with Thumbprint/toDERFile/loadCertificateFromFile - Add missing enums: PerformUpdateType, SecurityPolicy, StructureType, etc. - Add HostInfo, Bit helper, KeyPairCollection, ExpectedAndAcceptedResults - Add _error.store(), MessageSecurityMode.toString() Status: Session creates successfully, CreateSession validation runs, hits validateCertificate in PKI chain (next to implement). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * plans: update report after DefaultAccessRestrictions type definition fix ObjectTypes no longer have incorrect access restrictions. Type children reduced from 203 to 164 (39 now visible). Total export- missing reduced from 1316 to 1146. Remaining 164 type children are individual methods/variables with spec-correct access restrictions. Two source generator fixes applied: - ModellingRule=None children for Variables/Methods - DefaultAccessRestrictions not applied to ObjectType/VariableType Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * CttTestRunner: fix PKI, cert validation, warnOnce, session response - Intercept warnOnce.js with Jint-compatible replacement (no Function.caller) - Add _log.store() global helper - Add HOSTNAMES/HostnameFromUrl helpers for cert hostname validation - Add UaPkiCertificate properties: PublicKey, Issuer, Subject, KeyUsage, Hostnames - Add UaPkiUtility.validateCertificate stub - Add UaByteString static helpers: FromByteArray, ToByteArray, fromHexString - Use JS UaNodeId constructor for proper equals() support - Fill AuthenticationToken and 32-byte ServerNonce in CreateSession response - Add clone/append to ByteString objects Session flow: CreateSession validates, ActivateSession reached, hits Channel.ServerCertificate.clone() (IsSecure check — next to fix). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * CttTestRunner: complete session establishment, crypto stubs, ByteString helpers - Add _makeByteString() helper for consistent ByteString objects with clone/append/isEmpty - Add UaCryptoProvider with asymmetricSign/Verify stubs - Add UaExtensionObject setter methods (setAnonymousIdentityToken, etc.) - Fix UaChannel with full properties (RequestedSecurityPolicyUri, IsSecure, etc.) - Fix duplicate type registrations overriding custom constructors - Fix ActivateSession response header with ServiceDiagnostics - Remove UaCreateSessionResponse/ActivateSessionRequest/CloseSessionRequest duplicates Status: Full session lifecycle works (GetEndpoints → OpenSecureChannel → CreateSession → ActivateSession). Test scripts execute and hit server capabilities initialization (MonitoredItem NodeId settings configuration). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * update * plans: add spec references for access restriction reasoning Added links to Part 3 sections 5.2.9, 5.2.11, and 6.3.3 explaining why DefaultRolePermissions are applied to instance children but not to type definitions. The NodeSet2.xml has no explicit RolePermissions on these 164 nodes -- restrictions come from model design defaults. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add GDS project references to ConsoleReferenceServer Dockerfile Add Opc.Ua.Gds.Common and Opc.Ua.Gds.Server.Common csproj COPY lines to the Docker restore layer, required since the GDS node manager was added to the ConsoleReferenceServer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * update * feat: add IsPartOfTypeHierarchy flag to bypass access restrictions on types Type hierarchy nodes (ObjectType/VariableType definitions and their children) should be universally accessible regardless of AccessRestrictions and RolePermissions. Changes: - Add IsPartOfTypeHierarchy property to NodeState - Set flag on ObjectType/VariableType in generated code (!forInstance) - Propagate flag from parent to children in AddPredefinedNode - Add flag to NodeMetadata, propagate in GetPermissionMetadata - Bypass ValidateRolePermissions and ValidateAccessRestrictions when IsPartOfTypeHierarchy is true - Revert isTypeDefinition guard: AccessRestrictions and RolePermissions are now emitted on all nodes as metadata, but enforcement is skipped for type hierarchy nodes - Fix forInstance propagation in type factories: use forInstance: forInstance for type children so deep children (TrueState/FalseState etc.) are included when !forInstance Type-level missing nodes reduced from 201 to 99. Remaining 99 are in the server but not reachable by the export tool's browse path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * plans: update report with final IsPartOfTypeHierarchy results Missing from export reduced to 998 (from 1325 original). Type children: 201 -> 99 (forInstance propagation fix). All nodes verified present via OPC UA Read. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: propagate IsPartOfTypeHierarchy in GetNodeMetadataAsync The Browse service's per-reference ValidateRolePermissionsAsync calls GetNodeMetadataAsync (not GetPermissionMetadataAsync). The IsPartOfTypeHierarchy flag was only set in GetPermissionMetadataAsync, causing type hierarchy nodes to be filtered out of Browse results. This fixes 99 ObjectType children that were previously invisible in Browse from their parent types. Type-level missing: 99 -> 0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * plans: final report -- 0 type-level children missing (694 total) Type-level missing: 99 -> 0 after fixing IsPartOfTypeHierarchy propagation in GetNodeMetadataAsync. Total export-missing: 694 (384 encoding + 236 access-restricted + 74 instance/orphan). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * update --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
94653f1aee |
Add a MCP server that enables LLM/Copilot to be a OPC UA client (#3701)
* Opc ua client Mcp Server * fix for #3699 |
||
|
|
39fa3030fa |
Add a fluent api for server, a sample and generate node managers to use with it. (#3684)
* Add source-generated NodeManagers and fluent wiring API Adds a fluent INodeManagerBuilder API for wiring per-node callbacks and a model-aware source generator that emits a partial CustomNodeManager2 plus an INodeManagerFactory for each design file. Opt-in is per class via [Opc.Ua.Server.Fluent.NodeManager] on a partial class (recommended) or project-wide via the ModelSourceGeneratorGenerateNodeManager MSBuild property (legacy). The attribute supports NamespaceUri / Design selectors for multi-design projects and a GenerateFactory toggle. Diagnostics MODELGEN010 / MODELGEN011 surface unmatched bindings and non-partial classes. Adds the ConsoleBoilerServer NativeAOT sample driving the new pipeline end-to-end with Microsoft.Extensions.Logging.Console and updated docs in Docs/SourceGeneratedNodeManagers.md. * Add BoilerServer AOT smoke test Verifies the source-generated Boiler.BoilerNodeManagerFactory loads the boiler address space, registers its namespace, and dispatches the fluent OnRead callbacks wired in BoilerNodeManager.Configure.cs. All 3 new tests pass in JIT mode and against the NativeAOT-published binary; total Aot.Tests suite: 71/71 pass. |
||
|
|
d14cd10a86 |
Model dependency annotations using source generator + flaky test fixes (#3687)
* Fix publish/dispose race that hangs Client tests
Subscription.Dispose pre-disposed m_publishTimer and m_messageWorkerCts
before calling ResetPublishTimerAndWorkerState, causing Cancel() to throw
ObjectDisposedException and leaving the publish worker stuck. Removed the
duplicate disposal so the reset method is the single owner of those resources,
and guarded Cancel/Dispose with try/catch ObjectDisposedException for defense
in depth.
Session.OnPublishComplete could run after Session.Dispose disposed
m_reconnectLock, throwing ObjectDisposedException from SemaphoreSlim.Wait()
and re-arming via the throttle path indefinitely. Added an early Disposed
guard, wrapped Wait/Release in try/catch ObjectDisposedException, and added
a Disposed guard in QueueBeginPublish so the throttle cannot reschedule
publishes after disposal.
Fixes the macOS net10.0 hang seen in Azure pipeline build 13257
(TransferSubscriptionAsync, ReconnectWithSavedSessionSecretsAsync).
* fix lockout test and use UserName token for SuccessfulAuthenticationClearsFailedAttempts test
After
|
||
|
|
09e44453a4 |
Generate type safe (method) proxies for ObjectTypes inside models (#3685)
Add interop test agent The new stack-pass ObjectMethodProxyGenerator emits proxies that derive from Opc.Ua.ObjectTypeClient and reference Session.MessageContext.NamespaceUris. Neither type exists in the in-memory test compilation, which only references Opc.Ua.Types.dll. Moving ObjectTypeClient to Opc.Ua.Types would cascade ISessionClient, IClientBase, ITransportChannel and the generated service interfaces with it, so the type stays in Opc.Ua.Core and we add a minimal stub to OpcUaCoreStubs instead. Wire Microsoft.Extensions.Options 10.0.5 + DependencyInjection.Abstractions 10.0.5 into Directory.Packages.props and the Gds.Client.Common project to enable upcoming options-driven configuration work. Modernize Opc.Ua.Gds.Client.Common (DI, options, interfaces, async dispose) |
||
|
|
36c936a361 | Update SourceGenerator dependencies resolution mechanism. (#3680) | ||
|
|
fd41681f4b |
Add support to publish OPC UA applications as Native AOT (#3633)
* make most ptojects aot compatible * Remove newtonsoft * Migrate all projects from Mono.Options to System.CommandLine 2.0.5 * Fix array clone * Replace serializers for storage using new attribute driven type geberator * Add new AOT integration tests covering all functional areas * Documentation updates |
||
|
|
e8e90ca9f4 |
Bump NUnit3TestAdapter from 6.1.0 to 6.2.0 (#3634)
--- updated-dependencies: - dependency-name: NUnit3TestAdapter dependency-version: 6.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
66f44396ff |
[Server] Improve Request Queue, fix Event handling, add Tests (#3611)
- implement RequestQueue using Channels - add event load test - add event integration test verifying event from server object is received - fix Client Reconnect Test - fix DiagnosticsNodeManager not creating RootNotifier for Server object - add Benchmarks for MonitoredItem.QueueEvent - add serveral UnitTest for DiagnosticsNodeManager Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: romanett <7413710+romanett@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> |
||
|
|
03777ecb09 |
New Json decoder/encoders, array/matrix abstractions, ByteString, and more. (#3619)
- **Collections overhaul** — new/rewritten collection types (`ArrayOf`, `Collections` commits)
- **`Variant` builder/accessor** — new APIs for constructing and accessing Variants
- **`NodeId` operators made explicit** — breaking change for implicit conversions
- **Version bumped to 1.6** with preview build support
- **NumericRange** uses Variant
- **object** type accessors and general usage removed.
### 🏗️ Infrastructure
- Build config updated; `.NET 10` targeting
- InternalsVisibleTo moved to `.csproj`
- Pipeline/CI adjustments
|