mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 14:43:08 +08:00
7d5f7a7076
# Description Extract shared stack, regression-test, sample and documentation changes from draft #4462 into a separate prerequisite PR. The PR now contains **19 shared paths**: the original eighteen extracted files plus a focused user-management regression file added during review. It does not include UaLens source, desktop dependencies, solution registrations or application documentation. ## Changes - Correct the V2 ConditionRefresh target and response validation. Bound disposal-time remote subscription deletion with an independent five-second TimeProvider-backed cancellation deadline, allowing local cleanup after a server outage. This is a per-delete limit, not a global application shutdown deadline. - Preserve configured PubSub writer message/header settings, canonical numeric JSON publisher matching, and flattened DataValue quality/timestamps. - Add asynchronous user-management client wrappers over the generated standard proxy, and set file/directory method declaration IDs needed by typed client workflows. - Enable AI client NativeAOT compatibility and add focused AI client regressions. - Enable the ConsoleReferenceServer file-system node manager and configure 1 ms publishing/sampling demonstrations. These sample defaults do not promise hard real-time delivery. - Document the subscription and PubSub behavior. Make the new subscription/PubSub regression helpers portable to .NET Framework without weakening their assertions. ## Review follow-up Commit `812297ce1` aligns both public `ListUsersAsync` signatures with `ValueTask<ArrayOf<UserManagementUser>>`. The implementation constructs its result directly from an owned snapshot array, without an intermediate list. Five focused cases pass on **net10.0 and net48**: `ListUsersReturnsArrayOfSnapshotsAsync` checks both interface and concrete entry points and preserves snapshot order, values and flags; `ListUsersReturnsEmptyArrayAsync` covers typed-empty and missing values; `ListUsersPreservesBadReadStatusAsync` checks the exact server failure status. All **eight single-line XML summaries** were converted to multi-line form. A complete scan of the fourteen changed C# files, including the new test, finds zero remaining single-line summaries. Scoped whitespace/line-ending verification passes. The sole UaLens caller was adapted separately in draft #4462 (`4caf69f33`). A fresh combined dependency-graph build and all **924 UaLens net10.0 tests** pass with both changes. Each of the three review threads received an action-taken reply and was resolved only after the fixes were pushed and verified. ## Split boundary The branch originated at `a45105375`; the subsequent master merge `f209f3804` incorporates upstream `4633aa7a3`. The original shared changes and review regression remain separate from the app. Draft #4462 retains `tools\Opc.Ua.Lens`, `tests\Opc.Ua.Lens.Tests`, and its six solution/package/documentation integration files. **Merge this prerequisite before the UaLens draft.** The draft consumes the extracted APIs. Published history is preserved; neither PR has been merged automatically. ## Initial split validation The following results describe the initial split validation at `fa7199afa`; current review-specific validation is recorded above. | Scope | net10.0 | net48 | |---|---:|---:| | Client subscription/file-system tests | 168 passed | 168 passed | | Server file-system/user-management tests | 244 passed | 244 passed | | PubSub writer/reader regressions | 34 passed | 34 passed | | ConsoleReferenceServer build | Passed | Passed | The corrected portable regression helpers were rechecked on net10.0 after the net48 fixes. The combined UaLens application also passed its 924-case net10.0 suite before extraction. **Focused NativeAOT: 16 passed, zero failed/skipped.** A session-local TUnit runner linked the exact committed `AIClientAotTests.cs` and referenced the real AI client project. It published without warnings or suppressions and executed as a genuine Windows native binary with no CLR header. The canonical `Opc.Ua.Aot.Tests` project was blocked by pre-existing CS8632 nullable-annotation errors in unchanged upstream `SchemaAotTests.cs` (lines 82, 127, 129 and 132). Its existing nullable configuration and all upstream tests were preserved; the focused result is not a claim that the full AOT test project builds. The net48 hosting selection uses a short NUnit WorkDirectory: the existing fixture builds long PKI paths that otherwise exceed the platform's path limit. The original full selection passed without test exclusions, source changes or increased timeouts. Scoped builds reported inherited warnings in unchanged upstream files: CA1508 in `ReferenceBuilderExtensions.cs`, CA1861 in `NodeSetImportIntegrationTests.cs`, and net48 CA1823 in `DefaultNodeIdFactory.cs`. They were not suppressed or folded into this split. Full-solution, coverage and CI/CodeQL gates remain unasserted. ## Related Issues - Tracking/design review: #4461. - Prerequisite extracted from draft #4462. ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [ ] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received. Tests and scoped results are documented above; measured coverage, inherited-warning qualification, CLA status, full-solution results and CI completion are not asserted by these checkboxes. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
331 lines
16 KiB
C#
331 lines
16 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 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.CommandLine;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Gds.Server;
|
|
using Opc.Ua.Server;
|
|
|
|
namespace Quickstarts.ReferenceServer
|
|
{
|
|
/// <summary>
|
|
/// The program.
|
|
/// </summary>
|
|
public static class Program
|
|
{
|
|
public static Task<int> Main(string[] args)
|
|
{
|
|
Console.WriteLine("{0} OPC UA Reference Server", Utils.IsRunningOnMono() ? "Mono" : ".NET Core");
|
|
|
|
Console.WriteLine(
|
|
"OPC UA library: {0} @ {1} -- {2}",
|
|
Utils.GetAssemblyBuildNumber(),
|
|
Utils.GetAssemblyTimestamp().ToString("G", CultureInfo.InvariantCulture),
|
|
Utils.GetAssemblySoftwareVersion()
|
|
);
|
|
|
|
// The application name and config file names
|
|
string applicationName = Utils.IsRunningOnMono() ? "MonoReferenceServer" : "ConsoleReferenceServer";
|
|
string configSectionName = Utils.IsRunningOnMono()
|
|
? "Quickstarts.MonoReferenceServer"
|
|
: "Quickstarts.ReferenceServer";
|
|
|
|
// command line options
|
|
var autoAcceptOption = new Option<bool>("--autoaccept", "-a") { Description = "auto accept certificates (for testing only)" };
|
|
var consoleOption = new Option<bool>("--console", "-c") { Description = "log to console" };
|
|
var logOption = new Option<bool>("--log", "-l") { Description = "log app output" };
|
|
var fileOption = new Option<bool>("--file", "-f") { Description = "log to file" };
|
|
var passwordOption = new Option<string>("--password", "-p") { Description = "optional password for private key" };
|
|
var renewOption = new Option<bool>("--renew", "-r") { Description = "renew application certificate" };
|
|
var timeoutOption = new Option<int>("--timeout", "-t")
|
|
{
|
|
Description = "timeout in seconds to exit application",
|
|
DefaultValueFactory = _ => -1
|
|
};
|
|
var shadowConfigOption = new Option<bool>("--shadowconfig", "-s") { Description = "create configuration in pki root" };
|
|
var samplingGroupsOption = new Option<bool>("--samplinggroups", "--sg")
|
|
{
|
|
Description = "use the sampling group mechanism in the Reference Node Manager"
|
|
};
|
|
var cttOption = new Option<bool>("--ctt") { Description = "CTT mode, use to preset alarms for CTT testing." };
|
|
var provisionOption = new Option<bool>("--provision")
|
|
{
|
|
Description = "start server in provisioning mode with limited namespace for certificate provisioning"
|
|
};
|
|
var reverseConnectOption = new Option<string>("--reverseconnect", "--rc")
|
|
{
|
|
Description = "Connect to the specified client endpoint for reverse connect. (e.g. --rc opc.tcp://localhost:65300)"
|
|
};
|
|
|
|
var rootCommand = new RootCommand(
|
|
Utils.IsRunningOnMono()
|
|
? $"Usage: mono {applicationName}.exe [OPTIONS]"
|
|
: $"Usage: dotnet {applicationName}.dll [OPTIONS]")
|
|
{
|
|
autoAcceptOption,
|
|
consoleOption,
|
|
logOption,
|
|
fileOption,
|
|
passwordOption,
|
|
renewOption,
|
|
timeoutOption,
|
|
shadowConfigOption,
|
|
samplingGroupsOption,
|
|
cttOption,
|
|
provisionOption,
|
|
reverseConnectOption
|
|
};
|
|
|
|
rootCommand.SetAction(async (parseResult, cancellationToken) =>
|
|
{
|
|
bool autoAccept = parseResult.GetValue(autoAcceptOption);
|
|
bool logConsole = parseResult.GetValue(consoleOption);
|
|
bool appLog = parseResult.GetValue(logOption);
|
|
bool fileLog = parseResult.GetValue(fileOption);
|
|
string? passwordStr = parseResult.GetValue(passwordOption);
|
|
char[]? password = passwordStr?.ToCharArray();
|
|
bool renewCertificate = parseResult.GetValue(renewOption);
|
|
int timeoutSec = parseResult.GetValue(timeoutOption);
|
|
int timeout = timeoutSec >= 0 ? timeoutSec * 1000 : -1;
|
|
bool shadowConfig = parseResult.GetValue(shadowConfigOption);
|
|
bool samplingGroups = parseResult.GetValue(samplingGroupsOption);
|
|
bool cttMode = parseResult.GetValue(cttOption);
|
|
bool provisioningMode = parseResult.GetValue(provisionOption);
|
|
string? reverseConnectUrlString = parseResult.GetValue(reverseConnectOption);
|
|
|
|
// Use CTT-specific config when CTT mode is enabled.
|
|
// Mono uses a separate server (MonoReferenceServer) with its own config, so CTT config is not applicable there.
|
|
if (cttMode && !Utils.IsRunningOnMono())
|
|
{
|
|
configSectionName = "Ctt.ReferenceServer";
|
|
}
|
|
|
|
using var telemetry = new ConsoleTelemetry();
|
|
ILogger logger = LoggerUtils.Null.Logger;
|
|
try
|
|
{
|
|
// log console output to logger
|
|
if (logConsole && appLog)
|
|
{
|
|
logger = telemetry.CreateLogger("Main");
|
|
}
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
// create the UA server
|
|
var server = new UAServer<ReferenceServer>(
|
|
telemetry,
|
|
// The console reference server is the canonical
|
|
// host that exposes the full address space — turn
|
|
// FileSystem (Part 20) on by default so clients
|
|
// can browse Server.FileSystem without extra
|
|
// configuration. Unit-test fixtures construct
|
|
// ReferenceServer directly and keep the property
|
|
// at its default false.
|
|
t => new ReferenceServer(t) { EnableFileSystemNodeManager = true })
|
|
{
|
|
AutoAccept = autoAccept,
|
|
Password = password
|
|
};
|
|
|
|
// load the server configuration, validate certificates
|
|
Console.WriteLine($"Loading configuration from {configSectionName}.");
|
|
await server.LoadAsync(applicationName, configSectionName).ConfigureAwait(false);
|
|
|
|
// use the shadow config to map the config to an externally accessible location
|
|
if (shadowConfig)
|
|
{
|
|
Console.WriteLine("Using shadow configuration.");
|
|
// --shadowconfig requires TraceConfiguration.OutputFilePath and SourceFilePath to be set;
|
|
// null values would have thrown NullReferenceException historically — preserve that behavior.
|
|
string shadowPath = Directory
|
|
.GetParent(
|
|
Path.GetDirectoryName(
|
|
Utils.ReplaceSpecialFolderNames(server.Configuration.TraceConfiguration!.OutputFilePath))!)!
|
|
.FullName;
|
|
string shadowFilePath = Path.Combine(
|
|
shadowPath,
|
|
Path.GetFileName(server.Configuration.SourceFilePath)!
|
|
);
|
|
if (!File.Exists(shadowFilePath))
|
|
{
|
|
Console.WriteLine("Create a copy of the config in the shadow location.");
|
|
File.Copy(server.Configuration.SourceFilePath!, shadowFilePath, true);
|
|
}
|
|
Console.WriteLine($"Reloading configuration from shadow location {shadowFilePath}.");
|
|
await server
|
|
.LoadAsync(applicationName, Path.Combine(shadowPath, configSectionName))
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
// setup the logging
|
|
telemetry.ConfigureLogging(
|
|
server.Configuration,
|
|
applicationName,
|
|
logConsole,
|
|
fileLog,
|
|
appLog,
|
|
LogLevel.Information);
|
|
|
|
// check or renew the certificate
|
|
Console.WriteLine("Check the certificate.");
|
|
await server.CheckCertificateAsync(renewCertificate).ConfigureAwait(false);
|
|
|
|
// Create and add the node managers
|
|
server.Create(Servers.Utils.NodeManagerFactories);
|
|
|
|
// Add async node managers
|
|
foreach (IAsyncNodeManagerFactory asyncFactory in Servers.Utils.AsyncNodeManagerFactories)
|
|
{
|
|
server.Server!.AddNodeManager(asyncFactory);
|
|
}
|
|
|
|
// Add GDS node manager if configured
|
|
GlobalDiscoveryServerConfiguration? gdsConfig = server.Configuration
|
|
.ParseExtension<GlobalDiscoveryServerConfiguration>();
|
|
if (gdsConfig != null)
|
|
{
|
|
Console.WriteLine("GDS configuration found. Adding GDS node manager.");
|
|
server.Server!.AddNodeManager(new GdsNodeManagerFactory(gdsConfig));
|
|
}
|
|
|
|
// enable provisioning mode if requested
|
|
if (provisioningMode)
|
|
{
|
|
Console.WriteLine("Enabling provisioning mode.");
|
|
Servers.Utils.EnableProvisioningMode(server.Server!);
|
|
// Auto-accept is required in provisioning mode
|
|
if (!autoAccept)
|
|
{
|
|
Console.WriteLine("Auto-accept enabled for provisioning mode.");
|
|
autoAccept = true;
|
|
server.AutoAccept = autoAccept;
|
|
}
|
|
}
|
|
|
|
// enable the sampling groups if requested
|
|
if (samplingGroups)
|
|
{
|
|
Servers.Utils.UseSamplingGroupsInReferenceNodeManager(server.Server!);
|
|
}
|
|
|
|
// start the server
|
|
Console.WriteLine("Start the server.");
|
|
await server.StartAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
// setup reverse connect if specified
|
|
if (!string.IsNullOrEmpty(reverseConnectUrlString))
|
|
{
|
|
try
|
|
{
|
|
Console.WriteLine($"Adding reverse connection to {reverseConnectUrlString}.");
|
|
var reverseConnectUrl = new Uri(reverseConnectUrlString);
|
|
server.Server!.AddReverseConnection(reverseConnectUrl);
|
|
}
|
|
catch (UriFormatException ex)
|
|
{
|
|
logger.InvalidReverseConnectUrl(ex, reverseConnectUrlString);
|
|
throw new ErrorExitException(
|
|
$"Invalid reverse connect URL: {reverseConnectUrlString}",
|
|
ExitCode.ErrorInvalidCommandLine);
|
|
}
|
|
}
|
|
|
|
// Apply custom settings for CTT testing
|
|
if (cttMode)
|
|
{
|
|
Console.WriteLine("Apply settings for CTT.");
|
|
// start Alarms and other settings for CTT test
|
|
await Servers.Utils.ApplyCTTModeAsync(Console.Out, server.Server!)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
Console.WriteLine($"Server started ({sw.ElapsedMilliseconds} ms). Press Ctrl-C to exit...");
|
|
|
|
// wait for timeout or Ctrl-C (cancellationToken is cancelled on Ctrl-C by System.CommandLine)
|
|
if (timeout >= 0)
|
|
{
|
|
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
timeoutCts.CancelAfter(timeout);
|
|
try
|
|
{
|
|
await Task.Delay(Timeout.Infinite, timeoutCts.Token).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// expected — timeout or Ctrl-C
|
|
}
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
await Task.Delay(Timeout.Infinite, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// expected — Ctrl-C
|
|
}
|
|
}
|
|
|
|
// stop server. May have to wait for clients to disconnect.
|
|
Console.WriteLine("Server stopped. Waiting for exit...");
|
|
await server.StopAsync(default).ConfigureAwait(false);
|
|
|
|
return (int)ExitCode.Ok;
|
|
}
|
|
catch (ErrorExitException eee)
|
|
{
|
|
Console.WriteLine($"The application exits with error: {eee.Message}");
|
|
return (int)eee.ExitCode;
|
|
}
|
|
});
|
|
|
|
args = ConsoleUtils.MergeEnvironmentArgs(args, "REFSERVER", rootCommand);
|
|
ParseResult parseResult = rootCommand.Parse(args);
|
|
return parseResult
|
|
.InvokeAsync(new InvocationConfiguration(), CancellationToken.None);
|
|
}
|
|
}
|
|
|
|
internal static partial class ProgramLog
|
|
{
|
|
[LoggerMessage(EventId = ConsoleReferenceServerEventIds.Program + 0, Level = LogLevel.Error,
|
|
Message = "Invalid reverse connect URL: {Url}")]
|
|
public static partial void InvalidReverseConnectUrl(this ILogger logger, UriFormatException ex, string url);
|
|
}
|
|
|
|
}
|