mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 00:31:32 +08:00
0e600c0f0b
# Description Complete the remaining hosted-server DI hooks so applications can compose the regular server without subclassing `StandardServer` solely for these extension points. - Add `ConfigureServerProperties` and ordered, per-server `ConfigureResources` callbacks. BuildInfo defaults come from the effective application configuration; explicit property registrations take precedence without mutating shared options. - Add authenticator instance/validator-aware factory overloads and generic/async-delegate startup tasks. Startup tasks are awaited in registration order, and failures or cancellation clean up the server. Host shutdown waits for execution cleanup before disposing injected dependencies, including when its shutdown deadline expires. - Add asynchronous and legacy node-manager factory instances plus deferred, configuration-aware `ArrayOf<IAsyncNodeManagerFactory>` registrations. Resolve factories after configuration and certificate setup while preserving ownership and feature isolation. - Register alias stores before address-space creation and add opt-in standard `TagVariables`/`Topics` materialization. Existing custom-category behavior is retained; browse nodes are startup snapshots while queries and supported LastChange values remain live. - Derive the default DI server from `ReverseConnectServer`, retaining DI roles/session hooks and authoritative XML/stream configuration. Ignore queued callbacks from stopped or superseded timers to prevent reconnection or rearming after shutdown. - Fix resource translation lookup/formatting exposed by the new callbacks, retain existing constructor signatures and direct/custom-server paths, and document the optional hosting APIs. Adopting these hooks is optional; existing construction and subclass-based customization remain supported. The migration guide is unchanged. No changes to UA-.NETStandard-Samples and no new dependencies. ## Related Issues - Fixes #4410 ## Validation - Focused server hosting, aliases, reverse-connect, configuration, and resource suites: **571 passed on net10.0 and 571 passed on net48**. - Existing affected real-session alias integration fixtures: **11 passed on each framework**. - Real reverse-connect discovery, session creation, and BuildInfo reads verified for options, XML-file, and stream configuration. - Published and executed the Windows NativeAOT test binary: **7 HostingAotTests passed**. - Final server-library build passed across all configured target frameworks with **0 warnings and 0 errors**; no new diagnostics in changed test files. - Focused changed-line coverage: **336/338 executable production lines (99.41%)**. - Deterministic timer regressions failed before the shutdown fix and pass afterward. - CI shutdown follow-up: the unchanged `UnavailableUpstreamEndpointFailsMappedReadAsync` scenario passes locally, and **245 hosting tests pass on each of net10.0 and net48**, including regressions for an expired host shutdown deadline and already-canceled startup. The local net48 run uses a short NUnit work directory to avoid Windows path-length limits. ## Azure build 18136 follow-up - Await WoT browse reconciliation before upload, default-version, and enable/disable handlers return. Three barrier-controlled regressions reproduce the exact missing version metadata before the fix. - Make the reconciliation test observe the actual corrective state frame rather than require a new acknowledgement after a no-op reseed. The corrected tests also verify value, status, timestamp, and quiescence; production gossip behavior is unchanged. - Drain redirected stdout/stderr when sample processes exit, replacing a fixed grace delay that could lose the final success line. A controlled real-process regression fails before the fix. - Fix CA1508, CA1823, CA1850, CA2012, and CA1861 at their sources, and correct unsupported legacy AOT declarations and the existing stress-placeholder target. Modern test targets and AOT analysis remain enabled. Follow-up validation: - Full **Release `UA.slnx` builds pass on net10.0 and net48 with 0 warnings and 0 errors**. - Complete previously failing WoT net48 job: **1496 passed**. The identical binary was run from a short local assembly path to avoid the nested repair worktree exceeding Windows path-length limits. - Complete previously failing Redundancy.Server net10.0 job: **783 passed**. - All short-haul HA/process cases: **12 passed**, including both original identity-failover topologies. - Affected WoT lifecycle selection: **113 passed on Release net10.0**; controlled replication selection: **15 passed on each framework**. - No test assertions, timeouts, coverage thresholds, collector settings, or CI failure gates were weakened. Existing legacy empty-project result notices and auto-property coverage exclusions remain unchanged. These are full solution builds plus the reported test scopes, not a full local `UA.slnx` test run. Remote validation at `77c1055f1adb1557c5bf99b70e76f4a0cd95ee94`: **all 265 PR checks passed**, including Azure build **18172**, GitHub Build and Test, CodeQL, and Docker image checks. ## 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. - [x] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received. --------- Co-authored-by: romanett <romanett98@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
135 lines
5.6 KiB
C#
135 lines
5.6 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.Server.Tests
|
|
{
|
|
/// <summary>
|
|
/// Exercises callbacks already queued when their reverse-connect timer is disposed.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("ReverseConnect")]
|
|
[Parallelizable]
|
|
public sealed class ReverseConnectServerTimerTests
|
|
{
|
|
[TestCase(false)]
|
|
[TestCase(true)]
|
|
public async Task QueuedCallbackAfterShutdownCannotConnectOrRearmAsync(bool dispose)
|
|
{
|
|
var scheduler = new QueuedTimerScheduler();
|
|
using var server = new ReverseConnectServer(NUnitTelemetryContext.Create(), scheduler.Clock.Object);
|
|
server.AddReverseConnection(s_clientUrl);
|
|
Action queuedCallback = scheduler.Callbacks[0];
|
|
|
|
if (dispose)
|
|
{
|
|
server.Dispose();
|
|
}
|
|
else
|
|
{
|
|
await server.StopAsync(CancellationToken.None).ConfigureAwait(false);
|
|
}
|
|
queuedCallback();
|
|
|
|
Assert.That(scheduler.Timers, Has.Count.EqualTo(1));
|
|
scheduler.Timers[0].Verify(timer => timer.Dispose(), Times.Once);
|
|
Assert.That(server.GetReverseConnections()[s_clientUrl].LastState, Is.EqualTo(ReverseConnectState.Closed));
|
|
}
|
|
|
|
[Test]
|
|
public void QueuedCallbackFromRemovedTimerDoesNotReplaceCurrentTimer()
|
|
{
|
|
var scheduler = new QueuedTimerScheduler();
|
|
using var server = new ReverseConnectServer(NUnitTelemetryContext.Create(), scheduler.Clock.Object);
|
|
server.AddReverseConnection(s_clientUrl, enabled: false);
|
|
Action queuedCallback = scheduler.Callbacks[0];
|
|
|
|
Assert.That(server.RemoveReverseConnection(s_clientUrl), Is.True);
|
|
server.AddReverseConnection(s_clientUrl, enabled: false);
|
|
queuedCallback();
|
|
|
|
Assert.That(scheduler.Timers, Has.Count.EqualTo(2));
|
|
scheduler.Timers[0].Verify(timer => timer.Dispose(), Times.Once);
|
|
scheduler.Timers[1].Verify(timer => timer.Dispose(), Times.Never);
|
|
}
|
|
|
|
[Test]
|
|
public void ActiveCallbackSchedulesTheNextAttempt()
|
|
{
|
|
var scheduler = new QueuedTimerScheduler();
|
|
using var server = new ReverseConnectServer(NUnitTelemetryContext.Create(), scheduler.Clock.Object);
|
|
server.AddReverseConnection(s_clientUrl, enabled: false);
|
|
|
|
scheduler.Callbacks[0]();
|
|
|
|
Assert.That(scheduler.Timers, Has.Count.EqualTo(2));
|
|
scheduler.Timers[0].Verify(timer => timer.Dispose(), Times.Once);
|
|
scheduler.Timers[1].Verify(timer => timer.Dispose(), Times.Never);
|
|
Assert.That(scheduler.DueTimes[0], Is.EqualTo(TimeSpan.FromSeconds(1)));
|
|
Assert.That(scheduler.DueTimes[1],
|
|
Is.EqualTo(TimeSpan.FromMilliseconds(ReverseConnectServer.DefaultReverseConnectInterval)));
|
|
}
|
|
|
|
private sealed class QueuedTimerScheduler
|
|
{
|
|
public QueuedTimerScheduler()
|
|
{
|
|
Clock.Setup(clock => clock.CreateTimer(
|
|
It.IsAny<TimerCallback>(),
|
|
It.IsAny<object>(),
|
|
It.IsAny<TimeSpan>(),
|
|
It.IsAny<TimeSpan>()))
|
|
.Returns((TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) =>
|
|
{
|
|
Assert.That(period, Is.EqualTo(Timeout.InfiniteTimeSpan));
|
|
var timer = new Mock<ITimer>();
|
|
Timers.Add(timer);
|
|
Callbacks.Add(() => callback(state));
|
|
DueTimes.Add(dueTime);
|
|
return timer.Object;
|
|
});
|
|
}
|
|
|
|
public Mock<TimeProvider> Clock { get; } = new();
|
|
public List<Mock<ITimer>> Timers { get; } = [];
|
|
public List<Action> Callbacks { get; } = [];
|
|
public List<TimeSpan> DueTimes { get; } = [];
|
|
}
|
|
|
|
private static readonly Uri s_clientUrl = new("opc.tcp://localhost:4840/queued-timer");
|
|
}
|
|
}
|