mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-17 05:11:13 +08:00
master
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
15d0ce7bd5 |
Implement OPC UA Part 10 Programs (#4789)
# Description Implement OPC UA Part 10 Programs across `Opc.Ua.Core`/`Core.Types`, `Opc.Ua.Server`, and `Opc.Ua.Client`, reusing the existing state-machine, source-generation, node-management, auditing, and streaming-subscription infrastructure. - Add asynchronous provider-backed execution, standard controls, internal/autonomous transitions, generation checks, and nested-state activation/resumption. - Add checked creation/adoption, vendor Create adapters, deletion, recycling and instance limits, auto-delete, DI, and direct-construction paths. - Maintain ProgramDiagnostic2 and retained final results; publish captured intermediate results and concrete transition/audit events with access controls. - Add generated SDK models/proxies, typed client discovery and reads, event streams, and subscribe-before-call readiness handling. - Add unit, connected client/server, generated Annex A-style model, and NativeAOT coverage. Document usage and migration in `docs/Programs.md` and related guides. ### Compatibility and design decisions The linked Part 10 document is v1.05.00; namespace-zero definitions remain aligned with 1.05.07. Documentation distinguishes normative requirements from conflicting specification text and SDK conventions. - Correct state numbers to Halted=11, Ready=12, Running=13, Suspended=14. Generated subtypes retain inherited namespace-zero state/transition identifiers. - Managed Reset is Halted-to-Ready only. Preserve the existing low-level Reset-from-Suspended behavior for compatibility. - Preserve UInt32 limit types; use `UInt32.MaxValue` as a documented SDK unlimited convention. RecycleCount overflow reports BadOutOfRange without stopping unlimited execution. - In-memory default with injectable providers. Durable recovery, distributed execution/failover, and a production Annex A download engine are excluded. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] I have addressed **all** PR feedback received. Tests and focused coverage evidence are above; the combined tests/coverage checkbox remains open because a whole-repository coverage increase has not been established. CLA status, CI/CodeQL results, tracking-issue approval, and PR feedback are not asserted. |
||
|
|
c4e0721a29 |
Add a documentation learning path and fix issues found while verifying it (#4555)
## Summary
### Documentation
- Stage the documentation index as a learning path: a first application,
independent client, server, and PubSub paths, then data and services,
companion models, production, advanced integration, and draft or
experimental capabilities.
- Add Getting started (repository samples or NuGet packages), OPC UA
concepts, Your first information model, and a production readiness
checklist.
- Add Native AOT application publishing, a first PubSub publisher and
subscriber exchange, and the UDP transport options.
- Add or expand contents navigation throughout `docs/`, and keep
contributor documentation separate.
- Correct outdated SDK examples and capability guidance, consolidate
repeated explanations into canonical guides, and repair local links and
heading structure.
- Rewrite What's New in 2.0 as a concise feature overview with short
linked details; keep migration instructions in the Migration Guide.
- Expand the feature catalogue with specification IDs, GDS, LDS, the OPC
10000-21 onboarding registrar, ISA-95, xRegistry, OpenUSD, and
sample-local models; distinguish draft status, explain crypto
pluggability, and bring performance guidance forward.
- Address review feedback on wording, active monitored items during
shadow reload, and the `Samples.md` filename and references.
- Regenerate the `opcua-v20-migration` plugin's bundled copy of the
migration guides so it matches the edited `docs/migrate/2.0.x` guides.
- Merge the latest `master`, preserving upstream resource-isolation
guidance within the task-based documentation index.
- Retain deferred Lingity findings where appropriate; this PR does not
claim that every document meets the Lingity score thresholds.
### Fixes found while verifying the guides
Each fix has regression tests that fail without it.
- **Client:** V2 subscriptions compared their lifetime with the
operation timeout instead of the session timeout, and logged a spurious
warning. `ISubscriptionEngineContext`, which is new in 2.0, gains a
`SessionTimeout` property; implementations must add it.
- **PubSub:** a subscriber now registers the `DataSetMetaData`
configured on its readers (OPC 10000-14 6.2.9.4), and the UADP decoder
decodes DataSetMessages that carry no ConfigurationVersion. Before, a
secured subscriber of the reference sample received no DataSets.
- **Source generator:** a ModelDesign method that declares its arguments
inline, without a method type, produced code that did not compile; it
now uses `MethodState`. The generated code of existing models is
unchanged.
- **MinimalClient sample:** waits for its first data-change notification
before it disconnects.
### Known issues, not addressed here
- UDP multicast: the publisher does not send on the interface that the
subscriber joins, and `MulticastLoopback` defaults to false. The PubSub
guide uses unicast for the exchange on one computer.
- The UDP metadata announcement is sent unsecured on secured writer
groups; a secured subscriber drops it with a warning.
- A server does not start in globalization-invariant mode, because
`ResourceManager` creates `CultureInfo("en-US")`. The Native AOT guide
cautions against the mode.
## Validation
- `git diff --check` and
`./.azurepipelines/validate-migration-plugin.ps1` (15 bundled migration
docs validated, plugin manifests synchronized).
- Local Markdown links, anchors, file-link casing, and contents lists
checked across all 87 documents under `docs/`. The index links every
top-level guide and every target of the previous index.
- Both Getting started routes, Your first information model, the PubSub
exchange, and the Native AOT publish of the Getting started client and
server were run as documented. The NuGet route and the tutorial use the
public `2.0.0-preview.5` packages.
- The changed libraries build for all target frameworks in Release
without warnings.
- Tests on .NET 10: Opc.Ua.SourceGeneration.Tests 199 passed;
Opc.Ua.SourceGeneration.Core.Tests 3,977 passed, 8 skipped;
Opc.Ua.Server.Tests `Fluent` category 621 passed; Opc.Ua.Client.Tests
subscription-engine tests 70 passed; Opc.Ua.PubSub.Tests 1,374 passed, 2
skipped; PubSub fuzz replay 145 passed. The new and affected tests also
pass on .NET Framework 4.8.
- Earlier merge validation: 174 resource-isolation tests passed on .NET
10.
- The full solution suite was not run locally.
Review and validation artifacts are maintained outside the repository.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
46fa7d364a |
Harden recovery, concurrency, and resource lifetimes across the OPC UA stack (#4516)
# Description Bound managed-session recovery, preserve Publish workers and recovery ownership, and address the client, server, transport, and certificate audit findings and subsequent concurrency review. Also reduce the memory retained by exhaustive session-test diagnostics. ## Recovery and Publish ownership - Add `ManagedSessionOptions.ChannelReconnectTimeout` through direct construction, the fluent builder, and DI. The automatic duration is `max(3 * KeepAliveInterval, revised SessionTimeout, OperationTimeout)`, sampled at cycle start. Positive overrides are supported; `Timeout.InfiniteTimeSpan` explicitly opts out. Raw sessions retain their unlimited default. - Add `CreateReconnectBudget(TimeProvider)` as a required `IReconnectParticipant` member, as agreed in review. Custom participants implement it and may return null for no participant-specific bound. - Enforce the earliest participant/caller deadline through transport reconnect, activation/recreation, and subscription restoration. Late callers can shorten an active cycle but cannot extend it. - Cancel and unwind recovery, fence late responses, and release channel recovery ownership before the existing `Faulted` event hands control to the outer reconnect/failover policy. Preserve session recreate admission through final Publish cleanup and keepalive suppression until shared recovery is complete. - Treat participant budget creation/evaluation failures, including exceptions at the exact deadline, as terminal shared-recovery failures. Notify participants before exposing `Faulted`, preserve the original diagnostic, and never silently replace a finite budget with unlimited recovery. - Keep caller-supplied replacement channels, managed bindings, wrapper event subscriptions, and recovery-owner identities consistent across success, failure, and cancellation. Reject replacement during active recovery and fence retired callbacks so they cannot clear newer ownership or suppress later keepalive recovery. - Retain eligible Publish workers through temporary subscription-identifier clearing without retaining workers for never-created or removed subscriptions. Preserve cancellation, acknowledgement rollback, and drain-before-replacement ordering. - Coordinate automatic subscription updates with session recovery: cancel admitted updates, keep them paused across a failed-restoration handoff, and retry only after explicit restoration completes. This prevents competing CreateSubscription requests and updates against a retiring session, while preserving manual recreation, transfer fallback, and pending option changes. - Exercise the real ManagedSession, V2 subscription engine, and channel ready gate with scripted transports and an injected clock. Cover transfer fallback, deadline handoff, late completions, and resumed notification delivery. ## Audit and review corrections - Correct certificate/trust-list transactions, change-pump lifetimes, validation, peer revalidation, and reset/resource ownership. - Harden node admission, attribute/range/dimension validation, monitored-item sampling and lifecycle transitions, event fanout, and server shutdown. Run lifecycle readiness and compensation outside shared monitored-item admission, with stale-completion ownership checks. - Close synchronous node-manager admission before teardown and retain resources until admitted service, lifecycle, and asynchronous method work returns. Dispose the monitored-item manager outside both locks, reject late calls before they can reach it, and forward asynchronous disposal through the adapter so the master awaits the same completion. Preserve reentrant shutdown initiation, cancellation/failure unwinding, and shared cleanup-failure reporting. - Bound WSS pending-request retention using connection-local wire handles while preserving caller-visible request handles and translating Cancel targets. Safely ignore retired replies and drain admitted send/close users before resource disposal. - Clarify that the shared `ClientBase` caller-handle sequence is distinct from the non-wrapping WSS wire allocator. Existing exhaustion/reopen, late-reply, and Cancel-translation regressions verify the connection-local contract; no allocator behavior was changed by this clarification. - Preserve virtual-node validation and resolver service errors instead of converting them to cached misses. A mismatched NodeId reports `BadNodeIdInvalid`; callback type mismatches and resolver service errors keep their exact status. Per-item boundaries preserve unaffected reads, writes, history operations, method calls, browse paths, and monitored-item creation/restoration, including permission-validation paths. Caller cancellation still propagates; stored-item validation failures are logged without aborting later restores. - Correct client file/role/state-machine/streaming APIs, node-cache behavior, redundancy/failover ownership, and session configuration/identity handling. - Cancel each owned peer-discovery scope before disposing it, including when the bounded wait wins a cancellation-delivery race. Keep late results out of the endpoint cache and preserve single-flight lookup ownership. - Keep materialized alias Browse results synchronized with the live store by default, preserving explicit startup-only opt-out. Apply HistoryUpdate service results per value while retaining explicit all-or-nothing provider APIs. - Update the directly related feature documentation and add focused positive, negative, concurrency, and ownership regressions. ## Latest review-feedback corrections - Fence stale failed lease observations with a successful-confirmation generation distinct from the revocation epoch. Snapshot it before each store read, including failed-CAS revalidation, so an old failure cannot revoke or reschedule a newer lease while fresh ownership loss still revokes leadership. Preserve independent overlapping acquisitions, expiry, disposal, and late-success protections. - Isolate attributable WSS response-decoding errors to their request, including retired handles. Keep admitted socket sends connection-owned so caller timeout/cancellation cannot abort sibling requests; shutdown still cancels and drains those sends. - Restart keepalive and classic Publish replenishment after successful session reuse. Retire V2 worker demand after intentional deletion/disable without confusing that state with temporary recovery resets. - Browse virtual directories lazily across continuation pages without the 1,024-entry snapshot cap. Preserve active-page cancellation, bounded metadata retention, pushback, and exactly-once asynchronous cursor cleanup. - Apply certificate/CRL staging in last-operation order, preflight required issuer storage before writes, select actual policy-compatible RSA keys for None endpoints, frame certificate chains independently of signature OIDs, and preserve admitted validation results during rejected-writer shutdown. - Preserve base asynchronous disposal when a legacy override omits its base call; avoid double derived disposal in the adapter. Let genuinely admitted nested work finish while rejecting stale captured contexts and returning null from closed lookup APIs. - Re-park recovered monitored items if AddNodes rolls back. Give event compensation a fresh finite deadline, and preserve healthy Browse/Translate siblings at every reference/path boundary while retaining starting-node errors and caller cancellation. - Give final reconnect notifications a live, bounded shutdown-linked lifetime. Synchronize child state before parent lifecycle handlers outside the parent transition lock. - Document the accepted existing policies: direct reverse connections are single-use without a ReverseConnectManager, and multi-node stream capacity is shared rather than reserved per node. The expanded readiness controls also exposed a real generation-handoff race: an old source could finish draining after activation was deferred but before readiness tasks were collected, causing the wanted replacement to be omitted from the wait set. A deterministic test gates that exact interval; the fix keeps the replacement in the readiness wait. The original race passed 200 repetitions after the fix. A separate control now awaits both producer readiness and iterator entry under the same original deadline, rather than treating those independent signals as equivalent. Temporary logging and repetition are removed. ## CI integration contracts The History and Server CI failures exposed integration tests that still asserted the behavior intentionally changed by the audit fixes. A Windows WoT negative test raced its caller deadline against upstream retries. A subsequent Ubuntu Client failure exposed a real automatic-update versus explicit-restoration race: - History insertion collisions now verify exact per-value statuses and all persisted timestamps/values, including preservation of the existing conflicting entry. Explicit atomic-provider rollback controls remain. - Alias materialization tests wait for actual publication and verify the added node plus forward/inverse `AliasFor` references, instead of racing a startup-only snapshot expectation. Explicit refresh opt-out controls remain. - The unavailable-upstream WoT test uses an explicit finite retry policy through existing DI configuration, preserving its original four-minute guard and requiring a mapped-value service error before caller cancellation. Production retry defaults remain unchanged. - The Client fix retains the exact one-create and late-response assertions. Three deterministic real-subscription/real-manager cases cover paused creation, disposal while paused, and cancellation of an admitted create followed by explicit restoration and a pending option update. Both composed restoration-deadline cases also prove automatic updates stay blocked at the outer handoff and resume after restoration. - The merged-head net48 correction prevents lost peer-resolver cancellation and distinguishes the harness's delayed initial `Connected` event from actual outer recovery completion. A deterministic timer-order test covers both cooperative and cancellation-ignoring resolvers, cancellation before return, rejection of late cached results, and a fresh subsequent lookup. - The remaining CodeQL alert was a false content flow through `ArrayOf.Count` into an integer-only trust-list summary. Projecting the same count through `Span.Length` keeps cardinality distinct from serialized contents without allocating, dropping diagnostics, suppressing the rule, or dismissing the alert. Five log-contract cases cover null/empty/nonempty results and mixed chain-capable/fallback listeners, asserting the exact count, message, and structured properties. - Classic transfer acknowledges messages selected for asynchronous Republish only after their responses arrive. The existing locked replay-selection decision returns the immediate-acknowledgement subset to Session, preserving the public transfer API, advertised availability, and immediate acknowledgement of older gapped messages not selected for replay. A callback changing `RepublishAfterTransfer` cannot change the already-selected acknowledgement ownership. - Transfer integration tests explicitly control acknowledged versus retained origin messages. Exact assertions distinguish retained replay from requested fresh initial values by sequence number, client handles, values, status, and source timestamp. Atomic counters, observed delivery/keepalive progress, and failure-safe session/file cleanup replace timing assumptions and racy resets. - Server lifecycle tests verify the documented independent five-second compensation deadline rather than requiring a non-cancellable token. They cancel the actual request, prove compensation and unrelated monitored-item creation still proceed one tick before the deadline, preserve the original failure and ownership assertions, and verify the completed deadline is retired. - Synchronous and asynchronous directory-browser tests require the exact provider exception instead of an empty-directory success, then verify the failed cursor is not enumerated again. These correct expectations left behind by the intentional, documented provider-error propagation change; production behavior is unchanged. No tests were disabled, assertions loosened, coverage thresholds lowered, or CI checks made optional. Existing platform exclusions and Explicit debug variants remain. ## Related Issues - Fixes #4514 - Fixes #4515 - Builds on merged #4503 and retains the #4508 publishing-drain behavior. ## Validation **Lease-election review correction (`a13afa641182c70983e16bbcfebf507e666a45a5`):** The correction is based on the existing upstream merge `43079e2b851fe338a4082066f8d14abfdafb6e32`. Three deterministic stale-verdict cases fail on the original source and pass with separate confirmation fencing. Six new cases cover the initial foreign-owner verdict, failed-CAS revalidation in both invocation orders, and fresh ownership loss through both read paths, including revalidation begun after another successful confirmation. | Target and configuration | Passed | Failed | Skipped | | --- | ---: | ---: | ---: | | net10.0 Debug | 120 | 0 | 0 | | net48 Debug | 120 | 0 | 0 | The selection is the full `Opc.Ua.Core.Tests.Redundancy` namespace, including all 38 `SharedStoreLeaseElectionTests` cases and the existing expiry, clock rollback, disposal, and overlapping-success regressions. New witnesses are `StaleForeignLeaseReadDoesNotRevokeNewerAcquisitionAsync`, `StaleFailedRevalidationDoesNotRevokeNewerAcquisitionAsync`, and `FreshForeignLeaseObservationRevokesLeadershipAsync`. They assert resulting leadership, exact transitions, independent progress, and the unchanged expiry boundary. Retaining the initial generation for the fallback read independently makes the fresh-loss-after-confirmation control fail; that mutation was removed before the final rebuilt runs. The successful scoped command is `dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -c Debug --no-restore --filter "FullyQualifiedName~Opc.Ua.Core.Tests.Redundancy"`, with `CustomTestTarget` and `-f` matching each row and that framework's assets restored. Both changed C# files pass formatting and analyzers through informational severity on both frameworks. These are scoped results, not a full-solution test or measured coverage-delta claim. CI monitoring is paused for this feedback-only operation; no current-head CI success is claimed. **Latest CI contract correction (`1cd1f2047bb5b50c9bf300b1ff710b0b2e935a29`):** All four failing Server shard 23 jobs in [run 36120946740](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/36120946740) reported the same six cases. Two local replays reproduced all six failures. The source changes and documentation from `cd43b831a` establish both intentional contracts: independent bounded event compensation and surfaced provider errors. Only the two test files are corrected; production code and CI policy are unchanged. | Target and configuration | Passed | Failed | Skipped | | --- | ---: | ---: | ---: | | net48 Debug | 113 | 0 | 0 | | net10.0 Release, CI Coverlet settings | 113 | 0 | 0 | The selection contains `MonitoredItemManagerLifecycleTests`, `DirectoryObjectStateTests`, `DirectoryBrowserBoundedEnumerationTests`, and `EventCompensationReadinessHasFiniteDeadlineWithInfiniteSourceTimeoutAsync`. Both changed files pass formatting and analyzers through informational severity on both frameworks. Independent mutations restoring a non-cancellable compensation token and swallowing provider errors fail all four lifecycle cases and both browser cases, respectively. Both mutations were removed before the final passing runs and commit. The scoped command is `dotnet test tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj` with `CustomTestTarget` and `-f` set to the selected framework, `--no-restore`, and `--filter "FullyQualifiedName~MonitoredItemManagerLifecycleTests|FullyQualifiedName~DirectoryObjectStateTests|FullyQualifiedName~DirectoryBrowserBoundedEnumerationTests|FullyQualifiedName~EventCompensationReadinessHasFiniteDeadlineWithInfiniteSourceTimeoutAsync"`. The modern run additionally uses `-c Release /p:CollectCoverage=true --collect "XPlat Code Coverage" --settings tests\coverlet.runsettings.xml`. These are scoped results, not a full-solution test or measured coverage-delta claim. Hosted checks for that commit were pending at publication. The fail-fast CI watch was stopped when the requested operation switched to review feedback; no current-head CI success is claimed. **Earlier master merge (`8b15ea6ef9fc2214ac8ec886f373bb3846dd6d6f`, base `b4136d8e9bab0df680f41d9ef985f519160534b1`):** The `TcpListenerChannel` conflict preserves both lifetime responsibilities: disposal clears the retained peer-certificate chain and unregisters the quota-scoped message-assembly scheduler. Both fields and all upstream deadline handling remain intact. The only additional manual source adjustment adds the parentheses required by the existing analyzer around the session-count fallback. Master advanced during validation. The initial transport merge `80303c434ce813cf75aa7fe54a6a32fe2aeafc30` was retained, and the newer base was merged normally in `8b15ea6ef9fc2214ac8ec886f373bb3846dd6d6f`, without rewriting history. The additional recovery, fluent-pipeline, and test-framework overlaps were inspected; the branch's cancellation, validation-error, and acknowledgement ownership fixes remain alongside the upstream changes. Full `UA.slnx` builds passed on net10.0 and net48 with **0 warnings and 0 errors**. The final scoped tests rebuilt against the committed tree `2074f8efcb61d8d9c6106e7d6f76ee941940e358`: | Target | Passed | Failed | Skipped | | --- | ---: | ---: | ---: | | net10.0 | 2,653 | 0 | 0 | | net48 | 2,529 | 0 | 7 | The net48 skips are existing platform exclusions: six RSA_DH_AesGcm nonce cases and one ClientWebSocket certificate-validation case requiring .NET 7 or later. The selection covers transport cleanup, reassembly budgets/deadlines/admission, peer-chain ownership, server session binding, fluent lifecycles and source generation, reconnect and keepalive, transfer acknowledgements, Publish queues, and JSON encoding. Modern-.NET WSS transport and real-server integration cases also pass. The resolved file passed solution-relative formatting and analyzer verification through informational severity on both frameworks. These are scoped test results, not unfiltered full-suite or measured-coverage claims. Hosted CI was not monitored or repaired as part of this merge-only operation. Selected local results below are overlapping validation runs, not distinct-test totals. **Earlier review-feedback batch (`cd43b831a8f11de3f7d65c3681712a61e3c669a5`):** The final net10.0 scoped selection passed **471 tests, with no failures or skips**. The final net48 solution-root selection passed **439 tests, with no failures and one unchanged platform skip** (`ValidateServerCertificateFallsBackToTlsPolicyErrors`, which requires newer ClientWebSocket certificate-validation support). Both full nonincremental solution builds completed with **0 warnings and 0 errors**, and all **40 changed C# files** passed informational-level formatting/analyzer verification. Both frameworks validated the identical final tracked diff. The focused matrix covers every one of the 20 current review threads, including real mid-frame WSS cancellation, stale admission contexts, directory continuation ownership, multi-element Browse/Translate failures, and the readiness drain race. The net10.0 solution-root test command has a separate pre-existing runner conflict: three unrelated AOT projects reject the VSTest target under Microsoft.Testing.Platform 2.4.0 and SDK 10. Per-project net10.0 results are recorded separately; no runner migration, test exclusion, assertion relaxation, or CI-policy change is included. These are bounded selections, not unfiltered full-suite or measured coverage claims. At publication of `cd43b831a`, hosted CI was not green: the `OPCFoundation.UA-.NETStandard` check reported `ACTION_REQUIRED`, AppVeyor reported `FAILURE`, and the two signed-build jobs were in progress. No CI jobs were rerun, cancelled, approved, or repaired as part of that feedback-only operation. **Classic transfer correction (`f425376fca045794122e19aaf9cafebc4a717d91`):** The preceding head's [macOS net10.0 shard 25/30](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/35980326299/job/107570512458) failed `TransferSubscriptionTest(DisconnectedRepublish).TransferSubscriptionAsync(True,False)` at the static-node count assertion: expected 10, received 20. The trace contains a successful, explicitly requested republish of retained sequence 1 and a fresh initial-value batch at sequence 3, not duplicate dispatch of one response. Controlled lost origin acknowledgements reproduced that incorrect test expectation on both the preceding PR head and exact master base `a2539cb53dd1666cef856f33812d7a8830d68909`. The maintainer explicitly authorized correcting this existing issue in the PR. Retained replay and requested initial values are independent, as specified by [OPC UA Part 4, TransferSubscriptions](https://reference.opcfoundation.org/specs/OPC-10000-4/5.14.7). The reason that the original hosted run retained its particular origin message remains unproved. The expanded full-suite run then exposed a **real production race**, not just a count assertion: Session queued acknowledgements for all transferred available messages before the asynchronous Republish responses arrived. Publish could delete those messages first, causing `BadMessageNotAvailable`. A gated real-Session/classic-engine regression failed both replay-enabled cases on the original implementation while both no-replay controls passed. The ownership fix passes all 24 combinations of empty/consecutive/unsorted-gapped queues, replay preference, sequential delivery, and a reentrant preference change. All 40 normal real-server transfer cases pass on both frameworks. Independent mutations suppressing actual replay and duplicating observed sequence records each fail the intended assertion; both mutations were removed. The full classic suite passes on both frameworks. Hosted CI was pending when that correction was published; the earlier failed hosted job was not rerun for a lucky pass. **Local net48 caveat:** the full Client run timed out in the pre-existing fake-clock test `ReconnectAsyncSwapsFaultedLeaseEntryAsync`, which does not exercise subscription transfer. Its unchanged isolated replay passed, but that does not resolve the full-suite timeout. No unrelated code or assertions were changed, and a clean full Client net48 run is not claimed. | Scope | net10.0 | net48 | | --- | --- | --- | | Full Client.Tests after transfer acknowledgement correction | 2,549 passed | 2,552 passed, 1 timeout failure noted above, 1 existing platform skip | | Full classic-subscription project after transfer correction | 57 passed, including all 40 normal transfer cases | 57 passed, including all 40 normal transfer cases | | Gated transferred-message acknowledgement ownership | 24 passed, included above | 24 passed, included above | | Saved-session consumer of the shared subscription helper | 40 passed | 16 passed, 24 existing unsupported-security-policy skips | | Audit regression matrix | 2,339 passed | 1,934 passed, 34 existing skips | | Full Client.Tests after review follow-ups | 2,520 passed | Focused selections only | | Full Client.Tests after CI recovery correction | 2,523 passed, CI coverage collector | 2,527 passed, 1 existing platform skip | | Latest merged-head recovery/redundancy selection | 139 passed | 139 passed | | Full Client.Tests after merged-head net48 correction | Focused selection above | 2,529 passed, 1 existing platform skip | | Lifecycle and actual-dimension regressions | 146 passed | 146 passed | | Focused recovery follow-ups | 7 passed | 7 passed | | WSS-related fixtures | 93 passed | Not applicable | | CI History integration corrections | 28 passed | 28 passed | | CI alias, dispatcher, and explicit-atomic controls | 63 passed | 63 passed | | Full WoT sample project with CI coverage collector | 29 passed | Not applicable | | Trust-list effects and count-only summary contract | 22 passed | 22 passed | | Virtual-node feedback, node-manager, dispatcher, and lifecycle selections | 661 passed, 63 existing not-executed cases; all 48 virtual-node cases passed | 661 passed, 63 existing not-executed cases; all 48 virtual-node cases passed | | WSS wire-handle exhaustion, late replies, and Cancel translation | 9 passed | Not applicable | | Synchronous node-manager disposal and related service/lifecycle suites | 1,037 passed, 64 not-executed cases from existing fixture assumptions; all 93 sampling/disposal cases passed | 1,037 passed, 64 not-executed cases from existing fixture assumptions; all 93 sampling/disposal cases passed | - Full solution builds passed on net10.0 and net48 during the audit work. The subsequent full non-incremental net10.0 build also completed with zero warnings/errors. - Modified source/test formatting and analyzer checks passed through informational severity. - Original composed recovery regressions passed repeated runs on both frameworks. Independent mutations removing Publish cancellation, bypassing the recreate drain call, or ignoring ready-gate cancellation each failed the intended assertion; all mutations were restored before passing runs. - The latest recovery correction passed 100 executions of each original restoration-deadline scenario with unchanged assertions. Restoring premature update admission independently made both strengthened handoff cases fail; the correct retention behavior was restored afterward. - Startup-event and cancellation-delivery timing probes reproduced the subsequent net48 failures, then passed 100 executions of each of the three scenarios with the fixes. All timing probes and repetition were removed. The permanent timeout-order regression also went RED on the original cancellation cleanup without sleeps or scheduling amplification. - Final post-format recovery/subscription/engine checks passed all 151 selected cases on net10.0. The first local full net48 attempt aborted in `clr.dll` with native access violation `0xc0000005`; an unchanged-assembly full diagnostic rerun passed as recorded above. That native host crash has not been attributed or claimed fixed. - A minimal replay with the same CodeQL CLI and query-pack versions reproduced the custom-count alert and confirmed that the standard span-length projection is clean while direct sensitive-value and raw-identifier logging controls remain flagged. The source was already logging only a count; no identifier or certificate disclosure was established at the reported location. - The virtual-node feedback regressions first reproduced five error-preservation/cache failures; expanded service-dispatch cases exposed two additional whole-batch failures before the operation-boundary fixes. The final selections pass on both frameworks. Genuine misses remain cached, successful results remain reusable, failed validation allows same-cache retry, and request cancellation still stops the batch. - Three deterministic synchronous-manager disposal regressions reproduced early resource release and lifecycle calls reaching a manager during disposal. The fix passes direct and adapted calls, a sampling-worker join after the last operation exits, reentrant shutdown, cancellation and failure paths, repeated disposal, and exact rejection across 39 service/lifecycle entry points both during drain and after cleanup. The original outside-node-lock disposal regression remains and now requires the worker to observe closed admission. - Prior-head CI on `c985f8bded77a60449d9cd381c8a004d8590ccf6` completed with **168 passing checks, one expected release-package-publication skip, and no failures or pending checks**. All test shards, signed builds, container builds, the coverage check, and the CodeQL PR check passed. Hosted CodeQL analysis `1829262912` completed successfully and marks count-flow alert 385 fixed. This is evidence for that head, not a claim that subsequent review follow-ups have completed hosted CI. ### Session-test diagnostic memory Complete browse listings are streamed to unique NUnit attachments rather than retained in inline NUnit/VSTest output. Detached regression-test results are cleaned up without deleting runner-collected attachments. - Two covered session-suite runs each completed with 782 passed and 379 existing skips. Peak working set fell from 4.86 GiB to 2.44-2.78 GiB. - TRX output shrank from 303.1 MiB to approximately 40.7 MiB, retaining all 46 complete browse attachments. - Seven output regressions cover exact content, empty/single/large listings, disabled/enabled output, bounded inline summaries, and unique filenames. The separate Azure build-18452 VM deprovisioning cause remains unproved without provider logs. The memory measurements do not establish its cause. Unfiltered local `UA.slnx` tests, including the long stress/soak suites, have not been run on both frameworks. A local coverage delta was not collected; builds and focused tests are not substitutes for those claims. ## 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. - [ ] All checks on the latest review-follow-up head have completed successfully; prior-head CI passed as recorded above. - [x] I have addressed **all** PR feedback received. Tests have been added and pass as recorded above; the hosted coverage check passed on the prior head identified above. The combined tests/coverage-increase checkbox remains open because no local coverage delta was measured. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 68f26810-22b8-476f-abcd-0a8c82d6e208 |
||
|
|
fa8d45f2cf |
Fix server/core validation, lifecycle and protocol regressions (#4477)
# Description **Server/core audit fixes with regression tests. Latest review-fix validation, merge validation and earlier acceptance results are distinguished below.** This addresses a 95-finding server/core report. Every finding was reviewed with the OPC UA specification knowledge base or, where the standard does not prescribe a C# implementation, the corresponding ownership/configuration contract. Incorrect or withdrawn subclaims remain qualified rather than being presented as additional fixes. The original checkpoint covers session identity/certificate validation and revocation; certificate/key ownership and transactional updates; TCP reconnect, receive/admission and buffer lifetime; HTTP quotas; subscription admission, durability and pooled payloads; file-operation lifecycle; historian aggregates; fluent/state-machine behavior; and related utilities. It includes targeted regressions and directly related documentation. The approved acceptance cleanup is separate in the commit history: | Cleanup | Verified result | | --- | --- | | Invalid identity-policy and KeyCredential fixture inputs | Fixtures explicitly advertise Username and use its generated policy ID. Credential persistence uses valid clear mode inside the existing encrypted admin context. Original permission, lockout and negative encryption assertions remain. | | Native GDS lifetime and concurrent connection failures | Retain ApplicationInstance until client teardown. A deterministic native reproducer proved the test-only channel cap of 10 was exhausted; removing that override restores the existing bounded default of 1000. No global serialization or production quota change. | | Stress certificate ownership and close accounting | Balance two helper references and the fake transport's server-certificate ownership. Replace the invalid event-count bound with exactly-once physical disposal, opens = closes, zero active entries and one logical entry lifetime. | | Participant deadlines | Use the injected TimeProvider with the existing cross-framework WaitAsync overload. Tests pin one tick before, exact expiry, terminal notification, recovery and timely completion. | | Reproducibility and platform assertions | Preserve canonical WoT bytes/hashes, explicit display-culture contracts and exact numeric-vector assertions. Accept Apple's CryptographicException subtype without weakening disposal balance. | | Missing user-signature proof | Reject null/empty proofs before OS crypto. Return BadUserSignatureInvalid per Part 4, 5.7.3.3. None-policy and valid-signature controls remain. A CI-only test nullable-context error was corrected without changing production behavior. | | Durable sample restore and shared test storage | Dequeue waits for actual batch residency instead of treating failed restore completion as success, and restored files close before deletion. Five deterministic regressions failed before the fix. The two fixtures that clear the shared batch directory are isolated; all original 3,000-item assertions remain unchanged. | ## Latest review follow-up Commit `828856850219518970e8e2f580b8f94b3c1fc748` fixes the deferred robotics disposal reported in the latest review. `RobotIntentNodeManager.DisposeAsync` now joins a shared controller-host drain and then explicitly awaits base node-manager disposal. A controller's bounded shutdown wait may defer its cleanup, but no longer lets the node-manager override finish early. Concurrent callers, including an asynchronous call after synchronous `Dispose`, cannot bypass the drain after the host list has been detached. Synchronous disposal remains nonblocking for deferred work. Deferred initialization failures are logged and retained, and base cleanup failures propagate to asynchronous callers. The existing controller-level bounded disposal contract is unchanged; the node-manager contract is documented in `docs/Robotics.md`. | Regression requirement | Evidence | | --- | --- | | Wait for deferred hosts; preserve synchronous compatibility and concurrent joins | `NodeManagerAsyncDisposalWaitsForDeferredHostsAsync`: four cases covering both entry-point orders and zero/positive host shutdown waits. All four failed before the fix. | | Wait through base cleanup and observe failures | `NodeManagerDisposalWaitsForBaseCleanupAndReportsItsFailureAsync`: four cases hold the actual base-resource semaphore, verify pending callers after the host finishes, then check success or the same propagated exception and exactly-once cleanup. | The focused `IntentHostingCoverageTests` and `IntentControllerHostTests.Dispose` selection passed **30/30 on net10.0 and 30/30 on net48**, with no failures or skips. The final net10 solution build passed with zero errors and only the four previously documented CA2249 warnings in unchanged fuzz-test code. No new warnings, analyzer suppressions, dependencies, or weakened assertions were added. The review thread received a commit-specific reply and was explicitly resolved. This verification is scoped to the robotics feedback; the earlier full Subscriptions-run limitation remains recorded in the merge section below. ## Documentation review follow-up (2026-09-14) Commit `f9a9686fcc5d3cfd19cbbc8c2d826e20f5c7e72f` addresses the six follow-up threads on documentation and implementation rationale. Each has an in-thread reply citing the commit and an explicit resolution. Multiline XML documentation now covers the declarations introduced or modified throughout the entire PR, including internal contracts, samples, tests, and public members of related private helpers. A syntax-aware inventory checked **2,031 declarations across all 256 PR C# paths with zero documentation gaps**. A supplementary check also covers **75 previously unchanged public members** of private helpers involved in the changed code. Token comparison across the conditional-compilation branches proves this follow-up changes **no executable C# or test assertions**. Project-file changes are rationale comments only. The implementation questions are documented at their source: | Question | Resolution | | --- | --- | | Certificate assembly friendship | Server Push configuration reuses the internal explicit-subject CSR helper with the existing key; no public crypto API was added. | | Core.Types assembly friendship | Server builders use internal transition callbacks/revision guards; Server.Tests checks the revision deterministically without creating a public compatibility contract. | | Always-single-reader notifications | One dequeuer preserves event order; optional concurrency fans out within one event and joins before the next event. | | Asynchronous disposal completion | `DisposeAsync` already joins the completion published after admitted operations and all cleanup attempts. The compatibility `Dispose` remains nonblocking to avoid deadlocking an admitted callback against its own drain. | The requested detailed review disposition follows. It distinguishes actual fixes from confirmed policies and incorrect subclaims rather than counting every comment as a new runtime change. ### Detailed disposition of review 5191795284 The [SDK review](https://github.com/OPCFoundation/UA-.NETStandard/pull/4477#pullrequestreview-5191795284) was submitted against `1c4a9df6c`, before the subsequent merge and feedback fixes. Its **15 inline threads** have each received an action-specific reply and an explicit resolution. A fresh GraphQL query verified those replies and resolution states; the fixes in `b8a59990f` remain ancestors of the current branch. | Review concern | Action and disposition | Concrete regression evidence | | --- | --- | --- | | Injected certificate-store providers bypassed | **Fixed.** Registration, configuration snapshots, and validation use the injected providers. Explicit custom types remain authoritative; the built-in Directory catch-all is deferred so native X509 paths and later custom providers are not captured. Failed store opens dispose the created store. | `InjectedProviderResolvesRegisteredAndConfiguredTrustStores` covers default-provider ordering and both registration paths; provider validation/open-failure cases and the unchanged `TrustedCertificateInjectedViaIndependentX509StoreIsObservedAsync` cover real use and independent-store visibility. | | Unguarded timer-thread leadership callbacks | **Fixed.** Each subscriber's exception is logged independently and cannot suppress subsequent subscribers or fail lease operations. `IsLeader` reads the deadline without invoking application callbacks. | `ThrowingLeadershipSubscriberDoesNotFailAcquireOrDisposeAsync`, `ExpiryNotifiesOtherSubscribersWhenOneThrowsAsync`, and `IsLeaderDoesNotDispatchExpiryNotificationsAsync` were all demonstrated failing before the fix. | | Legacy pending-key provider accepts an unfinishable regenerated CSR | **Fixed by explicit capability validation.** A provider implementing only `IPendingCertificateKeyStore` receives `BadNotSupported` before key generation or persistence when regeneration is requested. Existing-key CSR/update remains supported. Regeneration requires matching-only consumption; no unsafe destructive probing was introduced. | `LegacyPendingProviderRejectsRegenerationBeforeCreatingOrSavingAKeyAsync` checks no key or store mutation; `LegacyPendingProviderCompletesExistingKeySigningAndUpdateAsync` proves the supported flow completes. | | Shutdown waits behind user deactivation | **Fixed.** Accepted deactivations drain before the master's lifecycle gate is taken. Configuration retains its binding until the shared drain completes, including concurrent shutdown callers and direct master disposal. | Four cases of `ShutdownDrainsDeactivationOutsideTheSessionClosingGateAsync` exercise real configuration/binding/ServerInternalData/master reentry, retained address space, exact session/subscription closure, and no production cancellation shortcut. | | Subclass cleanup runs under the operation-lifetime lock | **Fixed.** Cleanup starts outside that lock. A pre-published completion joins all disposers, and only the active asynchronous teardown context may use guarded helpers. Captured contexts lose that privilege when the callback exits. | `SynchronousOwnedCleanupLetsSamplingWorkerReachClosedAdmissionAsync`, `DisposeCoreCanUseGuardedMembersWithoutReopeningCallerAdmissionAsync`, captured-context rejection, and exact cleanup-failure/ownership assertions cover both synchronous and suspended cleanup. | | Discovery requestId-zero chunk read after pool return | **Fixed by the earlier merge.** Discovery validation precedes `SaveIntermediateChunk`; no read occurs after that path can return the buffer. The ordering is retained. | `DiscoveryChannelReadsTheFirstChunkBeforeItIsReturnedAsync` uses a poisoning pool and the requestId-zero path. | | T1.8 OPN cleanup misses post-decryption throws | **Fixed by the earlier merge and pinned with new regressions.** Both sequence rejection and certificate mismatch release the decrypted body and input ownership. | `OpenSecureChannelRejectionAfterDecryptionReturnsEveryRentalAsync` checks the exact audit status and pool conservation. Omitting either corresponding release in an isolated snapshot made its case fail; no mutation remains. | | T1.2 mount-root protection depends on the provider | **Fixed at the host boundary.** Separator-only aliases resolve to the protected root. Traversal, empty/whitespace segments, backslashes, and drive/stream qualifiers are rejected before arbitrary providers are invoked. Valid nested paths and root destinations remain supported. | A deliberately permissive provider is used by `NonCanonicalObjectPathsCannotDeleteMoveOrCopyAsync` and invalid-target cases; exact zero-call checks are paired with legitimate nested-path/root-destination controls. | | Inline trusted/issuer entries now face CRL checks | **Intentional strict behavior, documented and tested.** A configured backing store with strict unknown-status rejection requires current revocation information. Explicit-only lists without a store retain their no-store behavior. Actual revocation cannot be suppressed; a matching store entry's policy takes precedence. | `InlineIssuerWithStoreHonorsStrictUnknownRevocationPolicyAsync`, `InlineIssuerRevocationIsNeverSuppressedAsync`, store-precedence cases, and explicit-only controls preserve the missing-information versus actual-revocation distinction in Part 4 6.1.3. | | RSA minimum applied to unspecified ECC identifiers | **Fixed at the application-certificate check.** The RSA minimum is compared only with an actual RSA public key. An unspecified ECC identity is not replaced simply because its key size is below an RSA threshold. | `SilentCertificateCheckPreservesUnspecifiedEccIdentityAsync` checks DER, thumbprint, store contents, and private-key retention; explicit RSA and HTTPS minimum controls remain. | | Failed directory refresh wedges corrective mutations | **Fixed.** Recoverable automatic pre-refresh failures are logged and leave reconciliation pending, but do not block corrective provider mutations. Explicit refresh errors, capacity admission, caller cancellation, and actual provider denials remain enforced. | `CorrectiveDeleteRecoversAfterPersistentOverLimitRefreshFailureAsync` converges an externally over-limit directory back to its exact allowed contents. Capacity, cancellation/token, and six refresh-error controls check negative paths. | | Timed-transition rejection logs at 10 Hz | **Fixed.** Repeated `(state, revision, status)` warnings are deduplicated without changing the retry cadence. A different status or state entry permits another warning. | `RepeatedTimedRejectionLogsOnceAndRecoversAsync` checks one warning over eleven rejections and success on the next attempt. Status-change and same/different-state reentry controls pin reset behavior. | | Bad unsubscribe strands retired-generation bookkeeping | **Fixed.** Retired all-event bookkeeping completes even when unsubscribe reports Bad; the first Bad is still returned to the caller. | Good and BadCommunicationError cases of `RetiredAllEventsDeletionCompletesBookkeepingAndReleasesGenerationAsync` check event/snapshot removal, generation release, and exactly one unsubscribe without finalization replay. | | CSR subject parsing and fallback | **Fixed for existing and regenerated keys.** Omitted subjects fall back to the actual stored certificate. Regeneration resolves the public certificate's subject before creating a key. Malformed or unresolvable subjects return `BadInvalidArgument` before pending-key mutation. | Store-only fallback, real-generator null/empty cases, and missing-subject/malformed-DN controls verify the signed CSR, matching fresh pending key, unchanged active identity, and no destructive side effects. | | Committed RemoveUser change reported as failed | **Fixed for RemoveUser and ModifyUser.** Accepted cleanup is still awaited and close failures remain logged, but cannot replace the committed mutation result. Precommit failures retain their original result and side effects. | `CommittedUserChangeKeepsResultAndPublishedUsersWhenCloseFailsAsync` checks published Users, Good result, logging, and continued closes. `FailedUserChangeKeepsPublishedUsersAndDoesNotCloseSessionsAsync` checks BadNotFound with no publication or close. | | Alleged pooled decoded-request aliasing | **No runtime redesign needed; reviewer rebuttal confirmed.** The decoder copies the relevant input data. Misleading ownership comments were corrected rather than introducing a new dispatch model. | `DecodedRequestRetainsValuesAfterInputBuffersAreReturnedAsync` checks the exact decoded NodeId and ByteString after returned arrays are poisoned. | The current documentation-only follow-up rechecked **104 Core, 473 Server, and 4 Configuration cases** covering these concerns: **581 passed, zero failed or skipped**. The final net10 solution build also passed with **zero warnings and errors**. The complete earlier Core/Server/Configuration and net48 results remain listed separately rather than being presented as rerun current-head acceptance. ## Earlier review follow-up Commit `b8a59990fe2e8e967a12ffd5d9158beed7831143` addresses all 19 review threads. Each thread has an in-thread reply naming this commit and was explicitly resolved after verification; the final paginated query found 19 resolved and zero unresolved threads. The follow-up fixes injected certificate-store selection (including default provider ordering and native X509 paths), actual-RSA minimum enforcement, and CSR capability/subject validation before key mutation. Node-manager teardown uses simpler scoped ownership and drains accepted user revocation outside the master gate; committed user results and retired-event cleanup remain correct when cleanup reports failure. File mutations can recover after failed refresh without bypassing capacity or cancellation, provider paths protect mount roots, and repeated timed rejections no longer flood logs. Leadership subscriber failures are isolated without callbacks from `IsLeader`. Regressions also pin the already-merged OPN/discovery buffer fixes and prove decoded request values do not borrow returned input buffers. The strict inline issuer revocation policy is documented and tested, not weakened. Existing tests were changed only for the reviewed behavior: the incorrect committed-user result was corrected, and inline-issuer/root-alias controls were expanded. ### Review-fix validation | Suite | Scope | Framework | Passed | Existing TRX NotExecuted | | --- | --- | --- | ---: | ---: | | Core | Complete project | net10.0 | 4,970 | 88 | | Server | Complete project | net10.0 | 6,414 | 69 | | Configuration | Complete project | net10.0 | 231 | 0 | | Core | Certificate, lease and buffer compatibility slice | net48 | 153 | 0 | | Server | Reviewed lifecycle, user, file, timer and CSR compatibility slice | net48 | 692 | 0 | | Configuration | Complete project | net48 | 231 | 0 | | CSR provider regressions | Final portable-mock follow-up | net10.0 | 11 | 0 | All runs exited successfully with normal concurrency and leak detection. The Server console reports 5 skips; its TRX additionally records existing assumption/explicit cases, hence the 69-record count above. The Framework build caught two modern-only `ValueTask.CompletedTask` usages in new mock setup; only those expressions changed to the equivalent portable `default(ValueTask)`. The complete modern suites precede that test-only correction; all production files remained byte-identical, and the corrected fixture passed on both targets. The final net10 solution build succeeded with 0 errors and the same 4 CA2249 warnings in unchanged fuzz-test code. Diff checks and changed-line whitespace checks are clean; whole-file formatting still identifies 10 unchanged baseline lines, which were not reformatted outside the feedback scope. The committed 35-file tree was hash-verified against the validation inputs. This is the review-feedback verification scope, not a rerun or sign-off of the historical complete platform/native/soak acceptance campaign below. ## Latest upstream merge Merge `9d9bf679f317164c5b2b90924cd3cdc27a86d451` integrates `master` through `162fc5ebb` (#4486), including the AddNodes child-name index, shared certificate-error reporting, CTT sample-data corrections, and the accompanying regressions and documentation. The CreateSession conflict preserves this branch's certificate ownership, required certificate, application-URI, and channel-certificate checks. The validation result now retains nested errors through `ThrowIfInvalid`, allowing the upstream shared reporting helper to mask trust, policy, and revocation failures consistently without losing audit detail. The new indexed AddNodes regression initially attempted to reuse a deterministic NodeId after renaming its existing node. Renaming frees the BrowseName, not that NodeId. `IndexedBrowseNameReusePreservesExistingNodeIdentityAsync` now proves both contracts: the derived-ID collision leaves the original node intact, then a fresh explicitly requested ID can reuse the old name while the new name remains protected against duplicates. No production identity rule was relaxed. The incoming seeded random-index test uses the existing `UnsecureRandom` wrapper with the same seed and sequence; no new analyzer suppression was added. | Local merge validation | Framework | Passed | Existing exclusions | | --- | --- | ---: | ---: | | Core, complete project | net10.0 | 5,081 | 88 | | NodeState / BaseInstanceState | net10.0 | 579 | 0 | | Server node management, certificate and session-security selection | net10.0 | 112 | 0 | | InformationModel node-management services | net10.0 | 29 | 1 | | NodeState / BaseInstanceState | net48 | 578 | 0 | | Core certificate validation, error reporting and TCP buffers | net48 | 111 | 0 | | Server node management, certificate and session-security selection | net48 | 112 | 0 | All selected runs exited successfully at normal concurrency: **6,602 passing test executions**. Modified test assemblies were explicitly rebuilt, and the combined name/identity regression was verified by name in both net10 and net48 results. No skips or weakened assertions were introduced. The final `dotnet build UA.slnx -c Release --no-restore -m:1` with `CustomTestTarget=net10.0` succeeded with **zero warnings and zero errors**. The resolved source and adjusted tests pass whitespace verification, and their source hashes match the validation inputs. This is merge-specific verification. Earlier review results and the previously reported incomplete full Subscriptions runs below remain scoped to their own commits; they were not reopened or relabeled as passing by this operation. ## Previous upstream merge (2026-09-15 morning) Merge `c4694e20ab089c555af233642042133ff3a03ac8` integrates `master` through `484d6c887`, retaining #4481's shared Like parser/GDS query fixes, #4488's RequestHandle recovery in ServiceFaults, and #4487's expired-session and durable-transfer corrections. Three conflict resolutions preserve both sides: - Alias searches now use the shared `LikePattern` implementation, including upstream malformed-list validation. A timeout overload retains this branch's 100 ms per-alias deadline and `BadTimeout` reporting without keeping a second wildcard translator. The shared matcher's original unlimited overload remains unchanged for existing GDS/ContentFilter callers. New regressions cover bounded prefix/list scans, reuse after timeout, and the alias service result. - Expired activation closes outside the session-manager gate using upstream's once-only timeout claim, audit, and diagnostic accounting. This branch's original-channel and security-policy checks remain in place. - TCP buffer tests retain this branch's ownership, poisoning-pool and post-decryption checks alongside the upstream RequestHandle/fault overload tests; neither set was discarded. The upstream HTTPS binary behavior is intentionally retained: a decoder `ServiceResultException` produces HTTP 200 with an OPC UA ServiceFault carrying the decoder status, response timestamp, and recoverable RequestHandle, rather than the previous HTTP 500 text response. Other exceptions retain their existing HTTP error handling. | Local validation | Framework | Passed | TRX NotExecuted | | --- | --- | ---: | ---: | | Core, complete project | net10.0 | 5,054 | 88 | | Sessions, complete project | net10.0 | 780 | 610 | | Server aliases, expiry/security and subscriptions | net10.0 | 288 | 0 | | GDS database, paging and live directory calls | net10.0 | 263 | 0 | | Shared Like patterns and bounded evaluation | net10.0 | 109 | 0 | | Subscriptions transfer/durability/service selection | net10.0 | 23 | 1 | | HTTPS WebApi, complete project | net10.0 | 346 | 0 | | InformationModel aliases | net10.0 | 54 | 5 | | Shared Like patterns and bounded evaluation | net48 | 109 | 0 | | Core fault handles, TCP/HTTPS and filters | net48 | 268 | 0 | | Server aliases, expiry/security and subscriptions | net48 | 288 | 0 | These selected runs exited successfully with normal test concurrency: **7,582 passing test executions**. Sessions' console reports 379 skips; the TRX also contains conditional/assumption exclusions, so the table uses the actual 610 NotExecuted records. No skips or weakened assertions were added. The new `EvaluationTimeoutBoundsWorkAndPreservesPatternReuse` and `SharedPatternTimeoutRemainsAnAliasServiceError` tests passed on both targets. **Validation limitation:** two unfiltered Subscriptions runs did not produce a final result and were stopped, including a retry with a hang guard. They are not counted as passing. The isolated failover fixture passed 2/2, and the transfer/durability/service selection above passed. The stall's cause was not established; no unrelated code or test was changed to conceal it. The final `dotnet build UA.slnx -c Release --no-restore -m:1` with `CustomTestTarget=net10.0` succeeded with **0 warnings and 0 errors**. Whitespace verification passed for all hand-reconciled files, and staged source hashes match the validation inputs. These are merge-specific checks, not a new full platform/native/soak acceptance sign-off. Earlier review and acceptance evidence below remains dated to its own commit. ## Previous upstream merge (2026-09-14) Merge `c3e09553e4d1509502d4ba751960d58491bea2cf` integrates `master` through `6313e38a0` (#4478), including the CTT-driven Part 13 aggregate corrections and FindAlias argument validation. The upstream tests and CTT documentation are retained. The single conflict in `AliasNameWildcardMatcher` is reconciled through the branch's shared regex compiler: upstream malformed-pattern rejection and `IsValidPattern` coexist with OPC UA character escaping, whole-string/newline matching, the 100 ms timeout, and service-level error reporting. The upstream ReferenceTypeFilter checks and chronological aggregate behavior are preserved. Local merge verification: | Suite / selection | Framework | Passed | Existing skips | | --- | --- | ---: | ---: | | Server aliases, aggregates and historian | net10.0 | 1,011 | 0 | | Server aliases, aggregates and historian | net48 | 1,011 | 0 | | InformationModel aliases | net10.0 | 54 | 5 | | Features aliases | net10.0 | 11 | 0 | | Client aliases | net10.0 | 28 | 0 | | History aggregate/historical-access consumers | net10.0 | 265 | 0 | All selected tests passed at normal concurrency. The final net10 solution build passed with 0 errors and 0 warnings. The resolved file's whitespace check and the staged diff check passed. These are merge-specific checks, not a new full platform/native/soak acceptance claim. ## Previous upstream merges Integrates `master` through `8e874d073`, including the Client, Types, Core and source-generator audit fixes in #4471, #4473, #4474 and #4475, plus the event queue-size revision from #4480. The merge commits are `150ea0439` (26 conflict resolutions) and `a0e17c07c` (the base update that arrived during validation). The overlapping fixes are reconciled rather than replacing either branch: - **Rejected-certificate retention follows this branch, as explicitly requested:** zero means unlimited history; negative values disable new storage while preserving existing backend trimming behavior (Directory clears prior history; shared key/value leaves it intact). The processor and manager policy are not redefined, and documentation/tests describe the distinction. - Preserve owned certificate snapshots and borrowed validation-core lifetimes alongside upstream cache invalidation, store-first CRL checks and `BadCertificateRevocationUnknown` for unsupported revocation lookup. - Combine exactly-once chunk ownership, receive-loop cancellation, stale-epoch rejection and detach-before-adoption with upstream signature-before-padding verification and secret cleanup. - Preserve pooled `HttpClient` per-request configuration together with bounded response reads and deadlines that remain active through body consumption. Regression cases exercise already-used clients and both timeout precedence cases. - Retain the binary-compatible channel-key constructor and normalize omitted profiles; preserve exact profile filtering and upstream binary-first HTTPS/WSS endpoint ordering. - Preserve monotonic lease expiry and stale-confirmation fencing while adding ordered, non-reentrant leadership notifications outside the state lock. ## Validation Local validation of the conflict-resolution merge: | Suite | Framework | Passed | Existing skips | | --- | --- | ---: | ---: | | Core | net10.0 | 4,940 | 88 | | Client | net10.0 | 2,278 | 0 | | Server | net10.0 | 6,088 | 5 | | Types | net10.0 | 10,394 | 1 | | SourceGeneration.Core | net10.0 | 3,940 | 8 | | SourceGeneration | net10.0 | 185 | 0 | | Core | net48 | 4,855 | 104 | After the subsequent four-file #4480 update, the related Server slice passed 811 tests with 5 existing skips, the real durable queue-size cases passed 6/6, and EventManager's 21 cases passed again after analyzer-only expected-array cleanup. The final `dotnet build UA.slnx -c Release` with `CustomTestTarget=net10.0` succeeded: 0 errors and 4 pre-existing CA2249 warnings in unchanged fuzz-test code. Whitespace verification passed for the resolved files and the follow-up. Merge-specific proof includes `RequestTimeoutRemainsActiveWhileReadingTheOpenApiBodyAsync` on already-used shared clients with either the request or HttpClient timeout shorter, the existing ownership/expiry regressions from both branches, and both branches' retention tests adjusted only to the explicitly chosen zero/negative contract. Earlier acceptance at `1c4a9df6c` passed hosted Linux/macOS CI, CodeQL, image builds and the canonical coverage gate (87.15% line, 77.27% branch, 92.14% patch). Both Framework project matrices, all three Windows native suites (183 cases), and the unfiltered net10 stress project (230 cases, including full 60-minute chaos and 30-minute memory soaks) also passed. Those are **pre-merge results**, not a claim that the full framework/platform/native/soak matrix has been rerun on this merge. The previous acceptance workflow remains separate from this review-feedback operation. Security checks, meaningful assertions, leak detection, default native concurrency, full soak durations and coverage thresholds remain intact. The user-set ready-for-review state is unchanged. No architectural or complete merged-head acceptance sign-off is claimed. ## Related Issues - Fixes #4476 ## 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. |
||
|
|
ececea15c8 |
Reimplement the GDS ApplicationsNodeManager as a generated fluent node manager (#4434)
# Description Builds on #4428, which is now merged; this branch is rebased onto master. The GDS `ApplicationsNodeManager` becomes a source-generated `[NodeManager]`: the companion model, its loader and the fluent plumbing are emitted, and what stays hand-written is the behaviour. Getting there needed three things from the shared surface, which are the first two commits. ## Commits **1. Add the fluent hooks a hand-written manager needs at startup** - `OnReadRolePermissions` / `OnReadUserRolePermissions` on the node builder. Every other `NodeState` `On*` hook was already fluent; these were not, and a server that grants access on something the static model cannot express has to compute the permission set per request. - `Node<TState>(TState)` returns a builder over a node the caller already holds, for nodes materialised after `Configure` resolved its graph. - `OnAddressSpaceReadyAsync` on `FluentNodeManagerBase` — the one seam between the address space existing and the wiring being applied, for managers whose wiring depends on asynchronous setup. `Configure` is a `partial void` and cannot await. `DiNodeManager` had already invented the hook privately, so its declaration becomes an `override`. **2. Let a `[NodeManager]` bind to a model a referenced assembly supplies** A model belongs to the assembly that emits its types, which leaves a node manager for it homeless whenever that assembly cannot reference `Opc.Ua.Server` — a model-only package shared with clients, for instance. Binding was rejected twice over: the design was skipped before the binding was matched (`MODELGEN010`), and fluent-accessors-only mode refused to run alongside manager generation (`MODELGEN014`). The binding is now matched before the skip decision, and a matched binding puts that design into accessors-only mode. A design nothing binds to is still skipped, so this cannot start duplicating types by accident. The emitted manager also gains what a real manager needs: `DefaultNamespaceUris()` plus a protected constructor taking a replacement set; `GenerateDefaultConstructor=false` to suppress the two-argument form; its own logger category; and the call to `OnAddressSpaceReadyAsync`. Generated fluent accessors also stop re-enabling nullable warnings — a bare `#nullable enable` undid the shared header's `disable warnings`, so any model with a structure-typed method argument reported CS8600 on every generated `out T` in a project that builds warnings as errors. **3. Reimplement the GDS `ApplicationsNodeManager` as a generated node manager** The design stays owned by `Opc.Ua.Gds.Common`; the server assembly only binds a manager to it. **NodeIds do not move.** `NamespaceIndexes[0]` must stay the application record namespace — it is the one the application database, the certificate request store and the base allocator mint ids in — so the constructor passes the namespace order explicitly rather than adopting the generated model-first default. A test pins both ends against a live server. Four overrides go, leaving `OnAddressSpaceReadyAsync` and `Dispose(bool)` — no address-space overrides at all: | Removed | Why it was unnecessary | | --- | --- | | `New` | The base allocator already mints into `NamespaceIndexes[0]`, with a thread-safe counter. The override existed only because `Create(assignNodeIds: true)` asks the factory with the declaration id still set; `AssignInstanceNodeId` nulls it and re-asks, documented as being there for exactly this allocator. | | `AddBehaviourToPredefinedNodeAsync` | Its `AuthorizationServiceType` branch re-wired a node that was already wired, and its `KeyCredentialServiceType` branch had nothing to wire — the model ships that folder empty. | | `GetManagerHandleAsync`, `ValidateNodeAsync` | Verbatim reimplementations of the base that also blocked the fluent base's virtual-node support. | | `DeleteAddressSpaceAsync` | A `// TBD` passthrough. | Everything else — all sixteen directory methods with their self-administration permissions, the certificate groups' concrete types and trust-list flags, and the authorization service (created through this PR's `Add` surface and wired in the same place) — is one `Configure` pass. **4. Bind the certificate groups from the `Configure` pass** Acquiring a certificate group and binding it to its nodes are different kinds of work: the first is I/O (`InitAsync` opens certificate stores and creates CA certificates), the second only touches the address space. Each now runs in the phase it belongs to, which puts the whole of the wiring in one place. Ownership no longer rides on the binding. A group joins `m_ownedCertificateGroups` the moment it is constructed, before `InitAsync` can throw, so a group that fails half way through is still disposed — which the previous code could not manage, keying ownership off a dictionary it filled only after initialization succeeded. > Commit 3's message says this move is impossible and blames `PushTest`. **That was a wrong diagnosis, and this commit supersedes it.** Two real defects were hiding behind those failures: - **`Create(..., NodeId.Null, ..., assignNodeIds: false)` does not null the NodeId.** `NodeState.Create` overrides it only when handed a non-null one, so a custom group node kept the namespace-0 id of its type declaration. `AssignInstanceNodeId` hid this by re-asking the factory with the id nulled; the builder validates instead, and rejected it. The node is now created with an id in the manager's own namespace, as the `Default` authorization service already was. - **The custom certificate group node was not browseable.** It answered `GetCertificateGroups` and `ReadNode`, but a type-filtered recursive browse from the Objects folder never returned it — which OPC 10000-12 §7.8.2 requires of a certificate group. Staging the node through the builder gives it the reference it was missing. ### Two `PushTest` methods change — please review these closely The second defect had been masking what those tests actually did. Selecting the group positionally, `groups[3]` resolved to the server's **own `ServerConfiguration` group** while the custom one stayed invisible, so `UpdateTrustListOfCustomGroupAsync` and `AddRemoveCertOfCustomGroupAsync` were exercising a *transactional* trust list under a custom-group name. Browse results, before and after: | # | Before (baseline) | After | | --- | --- | --- | | 0-2 | GDS Directory groups (`ns=3`) | GDS Directory groups (`ns=3`) | | 3 | **`DefaultApplicationGroup` `i=14156`** — the server's own | **`MyCustomGroup` `ns=2`** | | 4 | — | `DefaultApplicationGroup` `i=14156` | They now resolve the group by browse name — which asserts it is browseable at all — and expect the immediate-apply semantics the GDS actually gives the groups of its own `Directory`: `CloseAndUpdate` writes the stores at once and reports `applyChangesRequired == false`, so the `ApplyChanges` calls go away (they would return `Bad_NothingToDo`). The read-back assertions are what prove the writes landed, and they are unchanged. This is a genuine loss of the *transactional* trust-list coverage those two methods were accidentally providing — the server's own group is still covered by the other `PushTest` methods, which target it deliberately. **5. Select `GetCertificates` results by type instead of by position** `GetCertificatesAsync` checked entry zero of a parallel-array result. Which entry that is depends on the order the server reports its certificate types in, an empty result throws `IndexOutOfRange` rather than failing an assertion, and a second type going missing or unparseable would not be noticed. Every returned pair is now checked, and a failure names the certificate type. **6. Drop the `.Common` suffix from the three GDS projects** Every other companion specification ships as `<Family>` / `<Family>.Client` / `<Family>.Server` — Di, ISA95, Positioning, Robotics, Vision, WotCon, XRegistry. GDS was the exception, with a `.Common` suffix that named nothing. | Before | After | | --- | --- | | `src/Opc.Ua.Gds.Common` | `src/Opc.Ua.Gds` | | `src/Opc.Ua.Gds.Client.Common` | `src/Opc.Ua.Gds.Client` | | `src/Opc.Ua.Gds.Server.Common` | `src/Opc.Ua.Gds.Server` | **No source changes were needed.** `RootNamespace` was already the final name in all three projects — the namespaces have always been `Opc.Ua.Gds`, `Opc.Ua.Gds.Client` and `Opc.Ua.Gds.Server`, and only the assembly and package ids disagreed. Nothing compiling against these assemblies changes a `using`. The package ids do change, which is consumer-visible, so `docs/migrate/2.0.x/packages.md` gains a renamed-packages section — it is the document that undertakes to cover NuGet renames — and the migration skill's package table gains the two rows a 1.5.378 consumer needs. Build infrastructure that names the projects or their output moves with them: both solution files, `expected-packages.txt` (re-sorted — it is maintained alphabetically), both signing manifests, the reference-server Dockerfile, the metapackage nuspecs, and the analyzer's package dependencies. `preview-pack.slnx` is derived from `UA.slnx` and needs no edit. The migration analyzer's shim tree mirrors the source layout by its own documented convention, so `Gds.Client.Common/` becomes `Gds.Client/`. Prose naming past work is left alone — plan 23 still records that its deferral happened during the "GDS Client.Common modernization", which is the name that effort had. ## Validation `net10.0`, on master + these four commits, from a clean GDS PKI: - `UA.slnx`: 0 errors, 0 warnings in changed files (the solution's 30 pre-existing warning lines are CA1861/CA1850/CA1307 in `XRegistry`, `Types.Tests` and `WotCon.Tests` — same rules and projects as the base). - `Opc.Ua.Gds.Tests`: 1,117 passed, 0 failed, 54 skipped — identical to the base branch. - `Opc.Ua.Server.Tests`: 4,961 passed, 0 failed, 5 skipped (measured before commit 4, which touches only the GDS assembly and its tests). - `Opc.Ua.SourceGeneration.Tests`: 169 passed. - `Opc.Ua.SourceGeneration.Core.Tests`, NodeManager/Generators/Fluent: 84 passed; `GenerateCode`: 21 passed, 4 skipped. - `Opc.Ua.MigrationAnalyzer.Core.Tests`: 10 passed, 3 skipped. - After the rename: assemblies build as `Opc.Ua.Gds.dll` / `.Client.dll` / `.Server.dll`, with no `*.Common.dll` produced anywhere. - Commits 1-3 build standalone. - `Opc.Ua.Gds.Server.Common` also builds for `net48` and `netstandard2.1` (checked before the rebase onto #4428). Not yet run: the full `Opc.Ua.SourceGeneration.Core.Tests` suite (~3,800), and the older target frameworks since the rebase. ## Checklist - [x] Existing tests pass - [x] New tests cover the new surface (role permissions, resolved-node builder, referenced-model binding, constructor/namespace control, GDS namespace invariant, custom-group node namespace) - [ ] Full Core.Tests sweep and net48/netstandard2.1 re-run after the rebase 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
3543d7cf6c |
Report Executable/UserExecutable for state machine causes (#4368)
# Description A Part 16 cause is a Method, and whether it may fire depends on the state the machine is in. Part 3 defines `Executable` / `UserExecutable` as exactly that question, and Part 4 has `Call` refuse a non-executable Method with `Bad_NotExecutable`. `FiniteStateMachineState` already knows the answer — `IsCausePermitted(context, causeId, checkUserAccessRights)` — but nothing outside alarm shelving connected the two. Across the library, `OnReadExecutable` was assigned in exactly three places, all in `AlarmConditionState`. Everywhere else a cause method kept the `true` its node was constructed with, so a client could not tell which causes currently apply and found out only by calling one and being refused with `Bad_NotSupported` from `DoCause`. ## What changed **`StateMachineBuilder.WithCause`** now answers both attributes from `IsCausePermitted` alongside installing the call handler — on the unified builder (via the dispatcher's `InstallCauseMethod`) and on the `Opc.Ua.Server.Fluent` surface reached through `INodeBuilder.AsStateMachine()`, which had the identical gap. `Executable` ignores user rights; `UserExecutable` also runs the machine's `OnCheckUserPermission` callback. Per the issue's note about servers that deliberately keep methods executable and answer at call time, both overloads take `reportExecutable: false` to opt out. **`ProgramStateMachineState`** wires its own `Start` / `Suspend` / `Resume` / `Halt` / `Reset` in `OnAfterCreate`, so a Program server no longer repeats the boilerplate the class already shipped handlers for. The Boiler quickstart's copy of it is deleted rather than left as an example. **Generator-emitted `*StateMachineState` subclasses** are left alone. The generator emits no Part 16 `StateTable` / `TransitionTable` / `CauseMappings` overrides — the same gap that makes the DI software-update dispatcher write `CurrentState` directly instead of going through `SetState` (see `docs/SoftwareUpdate.md`) — so there is nothing to derive executability from. A vendor machine from a NodeSet gets its causes through `StateMachineBuilder.For(...).WithCause(...)`, which the first change covers. Worth revisiting if the generator gains table emission. ## Notes for reviewers Two details behind the `ProgramStateMachineState` change: - The five methods are `OptionalPlaceholder` on `ProgramStateMachineType`, so the generated class has no typed properties for them. They are matched on browse name **without** the namespace index, because a subtype that redeclares them puts them in its own namespace — the Boiler model uses `1:Start`, not `0:Start`. - `NodeState.Create` rebuilds the child list from the type template, so a method added by hand before it is gone by the time `OnAfterCreate` runs. `WireCauseMethods(ISystemContext)` is public for that case. **Behaviour change worth flagging:** a `WithCause` against a machine whose `CauseMappings` lack that cause now reports the method non-executable where it previously reported executable. Such a call was already failing with `Bad_NotSupported` inside `DoCause`, so it fails earlier with the code Part 4 specifies rather than newly failing — but it is visible to clients that read the attribute. ## Related Issues - Fixes #4363 ## 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. ### Tests New: `StateMachineBuilderCauseExecutableTests` (definition mode, the opt-out, `OnCheckUserPermission` affecting `UserExecutable` only, suspended machines) and `ProgramStateMachineStateTests`, plus two cases added to the existing fluent-builder fixture. All read through `MethodState.ReadAttribute`, so a handler that is installed but never consulted fails the test. Run locally on **net10.0**: `Opc.Ua.Server.Tests` (4797), `Opc.Ua.Core.Tests` (4374), `Opc.Ua.Di.Tests`, `Opc.Ua.Features.Tests`, `Opc.Ua.Robotics.Intent.Tests` and the Boiler AOT tests all pass; `UA.slnx` builds with no new warnings. The .NET Framework legs were not run locally — leaving that checklist box to CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e61ad4b306 |
Fully support sub-state machines in the fluent state-machine builder (#4322)
# Description Part 16 §B.3 places `HasSubStateMachine` on the parent **state** node, but the fluent builder attached it to the state-machine **root** because `FluentFiniteStateMachineState` never materialized state nodes at all. Investigating #3821 showed the gap was wider than the reference placement: 1. **No state nodes existed.** The NodeId written into `CurrentState/Id` — `new NodeId(stateId, ElementNamespaceIndex)` — was **dangling**: no node with that id was in the address space. 2. **`AvailableStates` was never populated**, so `GetAvailableStatesAsync` returned empty against a fluent-built server. 3. **The whole client discovery chain was therefore dead.** `ObserveEffectiveStateAsync` enumerates `AvailableStates` and browses `HasSubStateMachine` from each state node — it found nothing. The client side was already spec-correct; only the server deviated. 4. **`HasSubStateMachine` is non-hierarchical** (a subtype of `NonHierarchicalReferences`, `i=32`). Using it as the sub-SM's `BaseInstanceState.ReferenceTypeId` left the sub-SM unreachable from a hierarchical browse of the machine. ## What changed **Element nodes are materialized.** Definition-mode machines now create one `StateType` node per declared state and one `TransitionType` node per declared transition — each a `HasComponent` child carrying `StateNumber` / `TransitionNumber`, with the §B.4 `FromState` / `ToState` / `HasEffect` references and `HasCause` added by `WithCause`. `AvailableStates`, `AvailableTransitions` and `LastTransition` are populated, so `CurrentState/Id` and `LastTransition/Id` resolve to real, browsable nodes. NodeIds are per-instance, so several machines built from one definition coexist. **`HasSubStateMachine` moved to the state node**, with the inverse `SubStateMachineOf`, and the sub-SM is attached as a `HasComponent` child (fixing 4). **Core gains element-id mapping hooks.** `GetCurrentStateId` / `UpdateStateVariable` / `UpdateTransitionVariable` hard-coded the numeric form and were non-virtual, so a subclass could not redirect them. They now route through `public virtual` `GetStateNodeId` / `GetStateId` / `GetTransitionNodeId` / `GetTransitionId`, whose defaults reproduce the previous `(elementId, ElementNamespaceIndex)` convention **exactly** — every stack-shipped and generator-emitted machine is unaffected, and there is a regression test asserting that. **Generated machines report their model namespace.** The generator now emits an `ElementNamespaceUri` override on `FiniteStateMachineType` subtypes, taken from the nearest type that actually *declares* state children — a subtype that only adds behaviour inherits its elements, and their namespace, from its base. **Ordering and validation fixes.** `WithSubStateMachine` and `WithInitialState` are now order-independent (previously, attaching a sub-SM before setting the initial state left the child suspended until the parent left and re-entered the state — silently). `WithSubStateMachine` validates `parentStateId`, and the builder rejects duplicate element browse names, since element NodeIds are now derived from them. ## A defect this surfaced Code that assembles nodes with the generated `CreateInstanceOf<Type>` factories never calls `NodeState.CreateAsPredefinedNode`, so `OnBeforeCreate` / `OnAfterCreate` are skipped for the entire subtree — and `OnAfterCreate` is the only place a state machine resolves its element namespace. It is also where `ConditionState` binds its `Enable` / `Disable` / `AddComment` handlers and `AlarmConditionState` binds the shelving methods, so the impact is broader than state machines. This PR fixes only the two call sites it depends on — DI's `SoftwareUpdateFacetWiring` and Robotics' `RoboticsOperationBuilder`. **The repository-wide audit is deliberately left out of scope and tracked in #4320** (24 files call these factories; none completed the lifecycle). The Robotics regression had no test coverage at all, so an assertion on `CurrentState/Id` was added and verified to fail without the fix. ## Breaking changes - Sub-state machines are **no longer browsable from the state-machine root**. Callers passing a machine's `ObjectId` to `GetSubStateMachineAsync` must pass a state NodeId instead — from `GetAvailableStatesAsync` or a snapshot's `CurrentStateId`. - `CurrentState/Id` for fluent machines changes from the numeric form to the materialized node's id. Code reading a numeric id out of it should call `FiniteStateMachineState.GetStateId` rather than parsing the identifier. - Generated machines qualify `CurrentState/Id` / `LastTransition/Id` with their companion-spec namespace instead of the OPC UA one. Documented in `docs/WhatsNewIn2.0.md` and `docs/StateMachines.md`. ## Notes for reviewers - Adding `virtual` to the `FiniteStateMachineState` members is a binary (not source) breaking change — assumed acceptable on the 2.0 line, but worth confirming against the compatibility policy. - Address-space cost is one node plus one property per declared state and transition, per machine instance; called out in the docs for servers instantiating many machines. - `AvailableTransitions` is modelled as a `HasComponent` `BaseDataVariable` (matching the standard NodeSet and what the client's browse path expects), not a Property. ## Related Issues - Fixes #3821 - Surfaced #4320 (node create lifecycle audit — not addressed 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**, 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. ### Test runs (net10.0) | Suite | Result | |---|---| | `Opc.Ua.Server.Tests` (full) | 4734 passed, 5 skipped | | `Opc.Ua.SourceGeneration.Core.Tests` | 3780 passed | | `Opc.Ua.SourceGeneration.Tests` | 132 passed | | `Opc.Ua.SourceGeneration.Stack.Tests` | 94 passed | | `Opc.Ua.Types.Tests` | 8763 passed | | `Opc.Ua.Robotics.Tests` | 540 passed | | `Opc.Ua.Robotics.Intent.Tests` | 22 passed | | `Opc.Ua.Di.Tests` | 424 passed | | `Opc.Ua.ISA95.Tests` | 136 passed | | `Opc.Ua.Aas.Tests` | 670 passed | | `Opc.Ua.Positioning.Tests` | 69 passed | | `Opc.Ua.Client.Tests` (StateMachines) | 36 passed | Only net10.0 was run locally; the .NET Framework leg is left to CI, hence the unchecked box above. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com> |
||
|
|
8a10139b8c |
Fix OPC UA address-space compliance in the device integration stack (#4117)
# Description
A live OPC UA compliance audit of `samples/PumpDeviceIntegrationServer`
against the **OPC 40223 Pumps** and **OPC 10000-100 (DI)** companion
specifications found **13 address-space violations** plus a
serialization bug in the MCP tool. Every fix lands at its **root cause
in the shared stack** rather than as a workaround in the sample, so all
servers benefit.
The sample now exposes **N fully simulated pumps** (`--pumps N`) and
advertises the DI conformance facets it actually satisfies.
> This branch has been merged with `master` including **#4116** ("Add
simulation to PumpDeviceIntegrationServer and Event modelling"), which
independently reworked the same pump simulation and the same fluent
alarm builders. See [Relationship to #4116](#relationship-to-4116) below
for how the two were reconciled.
## Findings and fixes
| # | Finding | Root cause | Fix |
|---|---------|-----------|-----|
| 1 | `OverTempAlarm` created by the fluent API did not exist in the
address space | `AttachAlarm` attached the alarm with `AddChild` but
never indexed it into the node manager | Register the node; the same gap
existed in `AddObject`, `CreateInstance` and the state-machine creators
|
| 2 | Alarms were unsubscribable — `EventNotifier = 0` everywhere, no
`HasNotifier` from `Server` | No event-source wiring at all | Set
`HasCondition`, initialise
`SourceNode`/`SourceName`/`ConditionName`/`InputNode`, promote
`EventNotifier` up the ancestor chain, publish `HasNotifier` from the
`Server` object |
| 3 | Every instance NodeId was minted in the **DI standard namespace**
(`ns=4;s=5001_Pump #1`) | `DiNodeManager.New` inherited the parent's
namespace index | New `DiNodeManager.InstanceNamespaceIndex`; instances
move to the server's application namespace |
| 4 | `Server/Namespaces` described only 2 of 6 namespaces | The
NodeSet2 importer drops nodes parented to namespace 0, so companion-spec
metadata objects never reached the address space — and no route covered
the server's own namespace | New `NamespaceMetadataPublisher` walks the
`NamespaceArray` and fills version/publication date from
`ModelDependencyAttribute` |
| 5 | Vendor BrowseNames (`Diagnostics`, `LastError`, …) sat in the DI
namespace | Builders defaulted to the parent's namespace | Default to
the server namespace; **spec-defined BrowseNames are unchanged** |
| 6 | `AccessLevel="5"` from the Pumps NodeSet was reported as `1` | The
ModelDesign `AccessLevel` enum is not `[Flags]` and cannot represent
`CurrentRead \| HistoryRead` | Carry the verbatim NodeSet2 bitmask and
emit named `AccessLevels` constants |
| 7 | `Historizing = true` while `HistoryRead` returned
`BadHistoryOperationUnsupported` | Nothing reconciled the declared
history surface with the wired historians | Startup clears the
advertisement (including masking the attribute read callbacks) when no
provider resolves; the sample wires a real historian |
| 8 | Mandatory `ProductInstanceUri` unset | Sample omission | Populated
on every pump |
| 9 | Mandatory `TrueState`/`FalseState` empty on every supervision
boolean | Absent in the official NodeSet; a server must still populate
them | Populated on every pump |
| 10 | Machinery `Machines` folder empty (OPC 40001-1 §9.2 says
**shall**) | `TryAddToMachinesFolder` used `AddChild`, which
*re-parents* the device away from `DeviceSet` | Add the `Organizes`
reference instead |
| 11 | BrowseName contained a space and `#` | Sample naming | `Pump_1`
with the readable label in `DisplayName` |
| 12 | `Maintenance` group materialised but empty | Sample omission |
Populated |
| 13 | Uninitialised values reported `Good` | No initial-value status |
`BadWaitingForInitialData` until the first simulation tick |
| — | `ServerProfileArray` advertised only `StandardUA2017` |
`DiNodeManager` did not implement the existing `IConformanceContributor`
| Contribute DI 1.05 server facets computed at runtime from what is
actually wired, merged with `StandardUA2017` |
| — | MCP tool serialised boolean `false` and numeric `0` as JSON `null`
| `Variant.Null` is `default`, so `Variant.Equals` adopts the non-null
type and compares against the default value | Dispatch on `BuiltInType`
and use typed `TryGetValue` accessors instead of the prohibited
`AsBoxedObject` |
## Relationship to #4116
#4116 landed on `master` while this branch was in flight and targeted
overlapping ground. The merge is a **union of both intents**, not a
fast-forward:
- **`AlarmBuilderExtensions`** — kept both sides. #4116 contributes the
parent-must-be-an-Object guard, `SetEnableState`, and the
`HasEventSource` references; this branch contributes the `HasCondition`
reference, condition source initialisation, **registration of the alarm
with the node manager so it is browsable**, notifier promotion up the
whole ancestor chain, and root-notifier registration.
- **`SupervisionBuilderExtensions`** — took #4116's `SetAlarmActive`. It
is more spec-correct than this branch's version: it honours
`EnabledState`, resets Acked/Confirmed on activation, and computes
`Retain` per OPC 10000-9 so an unacknowledged Condition stays retained
after going inactive. This branch's null guard was kept so a malformed
alarm cannot dereference a missing `EnabledState`.
- **`PumpNodeManager.Configure.cs`** — took #4116's **push-based**
simulation (`RegisterPumpSimulation`, `CreatePumpSimulation`,
`IValueUpdater<T>`, `Initialize`/`Advance`/`Publish`, deterministic
per-pump phase offsets) as the structure, then folded in this branch's
compliance work: configurable pump count, Maintenance group,
`TrueState`/`FalseState`, historian wiring, per-pump mandatory
Identification values, and `BadWaitingForInitialData` until the first
tick.
- **README** — union, keeping #4116's address-space validation workflow
section.
Two test adjustments were needed, both because the merge legitimately
changed behaviour the tests pinned:
- `PumpHostedReferenceTests` now resolves pump NodeIds from the browse
result instead of hard-coding the DI namespace, and waits for the first
published value. `PumpCreatedAfterStartupJoinsTheLiveSimulationAsync`
was **added** to restore #4116's coverage that a pump created after
startup through `ConfigureDevicesFor` joins the live simulation.
- `FluentAlarmRegistrationIntegrationTests` no longer asserts that
`Retain` clears when an alarm goes inactive — that pinned this branch's
weaker semantics, and #4116's Part 9 behaviour is correct.
## Breaking change
Runtime-created DI device instance NodeIds move from the companion-spec
namespace to the server's application namespace, e.g.
`ns=<DI>;s=5001_Pump #1` becomes `ns=<application>;s=5001_Pump #1`. This
invalidates NodeIds cached by clients from earlier 2.0 previews. It is
**not** a model change — standard DI/Machinery/Pumps type NodeIds and
spec-defined BrowseNames are untouched.
Device integration hosting is new in 2.0, so there is nothing to migrate
from 1.5.378 and this is deliberately *not* in `docs/MigrationGuide.md`.
Anyone tracking 2.0 previews should rediscover devices by browsing
`DeviceSet` rather than persisting NodeIds, and resolve namespace
indexes from `NamespaceArray` per connection.
## Verification
Compliance is pinned by regression tests that assert **through the
server's service surface** (`BrowseAsync` / `ReadAsync` /
`HistoryReadAsync`), not by inspecting `NodeState` objects in memory.
That distinction mattered: three defects survived in-memory assertions
and were only caught through the read path — most subtly, `Historizing`
was cleared while `AccessLevel` still advertised `HistoryRead`, because
attribute read callbacks re-applied the NodeSet bits.
Post-merge on net10.0:
| Suite | Result |
|---|---|
| `Opc.Ua.Di.Tests` | 326 passed |
| `Opc.Ua.Server.Tests` | 3846 passed, 5 skipped |
| `Opc.Ua.Tools.Tests` | 288 passed |
| `Opc.Ua.History.Tests` | 506 passed, 27 skipped |
| `Opc.Ua.SourceGeneration.Core.Tests` | 3722 passed, 8 skipped |
The full solution was also built and tested on **net48** before the
merge.
Review feedback from `copilot-pull-request-reviewer` has been addressed
in commit `badad31dc`: the shared `ServerObjectState` lock was removed
(the forward `HasNotifier` edge is published through the owning node
manager instead), MCP array serialization now preserves element types,
and the pump option guards report the specific rejected option. The only
failure is
`ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync`, a
pre-existing certificate-store issue that **passes when run in
isolation** and is unrelated to this change.
## Documentation
Updated `docs/DeviceIntegration.md`, `docs/MigrationGuide.md`,
`docs/HistoricalAccess.md`, `docs/DeveloperGuide.md`,
`docs/McpServer.md`, `docs/README.md`,
`samples/PumpDeviceIntegrationServer/README.md` and
`tools/Opc.Ua.Mcp/README.md`, and consolidated the four node-manager
documents into a single [`docs/NodeManagers.md`](docs/NodeManagers.md) —
overview, built-in managers (including `MasterNodeManager`), core vs
custom, registration, server address-space metadata, and source
generation — with a TOC and all inbound links repointed.
## Related Issues
No tracking issue exists for this work yet — it originated from an
ad-hoc compliance audit of the pump sample. Given the size of the change
and the runtime-breaking NodeId move, please open (or link) a tracking
issue so the design is recorded as an ADR before merging.
- Fixes #
## Checklist
_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._
- [ ] 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.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0d50c3fc-58b1-4c99-85a4-1e32c88666d9
|
||
|
|
8538f531e7 | Update paths for the consolidated repository layout | ||
|
|
7cec2d614d | Move repository files to the consolidated layout |