mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 06:31:35 +08:00
55bd944609
# Description Adds the pure Types-side WoT Thing Description / Thing Model to NodeSet conversion: resolution, context, declarations, types, values, localization, URI/format handling, bounded hashing, plus supporting native XML codec, NodeSet alias/value helpers, RelativePathFormatter and a JsonElement equality polyfill. - Carrier-witness fix: a missing native event is accepted only when the owning preserved type declares it through a `GeneratesEvent` reference (directly or via its type/supertype chain) AND a bounded, validated selected EventType carrier identity equals the normalized authored native target. Native-present events get a full compare; an ambiguous native key and a missing map stay strict; a readable document is never archive authority. - Support-only dependencies: additive per-instance, non-serialized `WotDocument.IsSupportOnly(kind, name)` (ArgumentNullException guard). The resolver marks support-only after closure; explicit selection wins; the View builder excludes only true support-only members, so unit/actsOn dependencies stay in the JSON. Nested controls cover inner value-only projection under an outer value-only selection (unit kept, support-only) versus an outer explicit SelectAll (explicit, not support-only). - Documentation/XML comments and member-order only in commit 5c368d3a; no logic change. - Timeout repair on current head6957dd46: invocation-scoped `ArchiveDerivations` reuse removes repeated document-set derivation work without a global cache (functional files `WotNodeSetConverter.{Archive,ArchiveDerivations,Partitions,WoT}.cs`, plus `WotArchiveDerivationsTests` and `WotDocumentSetSharedValidationTests`). The original local full29 reproduction changed from 27 passed / 2 Refresh timeouts to 29 passed with unchanged limits; official CI on this head FAILED at three per-project time ceilings (see the evidence ledger). Temporary RF1 diagnostics were fully removed; the final tree differs from0f7b9a09only in the six functional files. - Test-only net48 compile repair in commitb838907(earlier head): official CI on5c368d3awas RED on the net48 build with CS1061 (see ledger); the fix adds `using System.Threading;` and an unqualified `CancellationTokenSource` in one test file. No production change. Defaults: DI default document budget 256 versus the sample's explicit 1024. Corpus: canonical 540-JSON inventory; the actual manifest is 535 entries = 394 TD + 141 TM (the roles of the other 5 are not claimed). Native Pumps assertions (9624 nodes / 339 partitions) are unchanged.
246 lines
10 KiB
C#
246 lines
10 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.Globalization;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Client;
|
|
using Opc.Ua.WotCon.Bindings;
|
|
using Opc.Ua.WotCon.Bindings.OpcUa;
|
|
using Opc.Ua.WotCon.Server;
|
|
|
|
namespace AggregationServer
|
|
{
|
|
/// <summary>
|
|
/// Builds and runs the reusable generic aggregation host.
|
|
/// </summary>
|
|
public static class AggregationServerHost
|
|
{
|
|
/// <summary>
|
|
/// Builds a host from explicit options.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static IHost Build(AggregationServerOptions options)
|
|
{
|
|
if (options is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(options));
|
|
}
|
|
|
|
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
|
|
Configure(builder, options);
|
|
return builder.Build();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds and runs a host from explicit options.
|
|
/// </summary>
|
|
public static async Task RunAsync(
|
|
AggregationServerOptions options,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
using IHost host = Build(options);
|
|
await host.RunAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds and runs a host from command-line configuration.
|
|
/// </summary>
|
|
public static async Task RunAsync(
|
|
string[] args,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
|
var options = new AggregationServerOptions
|
|
{
|
|
EndpointUrl = builder.Configuration["endpoint"],
|
|
Host = builder.Configuration["host"] ?? "localhost",
|
|
Port = ReadPort(builder.Configuration),
|
|
ApplicationName =
|
|
builder.Configuration["applicationName"] ?? "AggregationServer",
|
|
PkiRoot = builder.Configuration["pkiRoot"],
|
|
MaximumDocumentBytes = ReadMaximumDocumentBytes(builder.Configuration)
|
|
};
|
|
Configure(builder, options);
|
|
using IHost host = builder.Build();
|
|
await host.RunAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
private static void Configure(
|
|
HostApplicationBuilder builder,
|
|
AggregationServerOptions options)
|
|
{
|
|
Validate(options);
|
|
builder.Logging.ClearProviders();
|
|
builder.Logging.AddConsole();
|
|
builder.Services.AddSingleton(options);
|
|
|
|
string endpoint = options.EndpointUrl ??
|
|
$"opc.tcp://{options.Host}:{options.Port}/AggregationServer";
|
|
IOpcUaBuilder opcUa = builder.Services.AddOpcUa();
|
|
opcUa.AddOpcTcpTransport();
|
|
opcUa.AddServer(server =>
|
|
{
|
|
server.ApplicationName = options.ApplicationName;
|
|
server.ApplicationUri =
|
|
$"urn:localhost:OPCFoundation:{options.ApplicationName}";
|
|
server.ProductUri = "uri:opcfoundation.org:AggregationServer";
|
|
if (!string.IsNullOrWhiteSpace(options.PkiRoot))
|
|
{
|
|
server.PkiRoot = options.PkiRoot;
|
|
}
|
|
server.AutoAcceptUntrustedCertificates = true;
|
|
server.IncludeUnsecurePolicyNone = true;
|
|
server.EndpointUrls.Add(endpoint);
|
|
});
|
|
opcUa.AddWotRegistryServer(registry =>
|
|
{
|
|
registry.AutoRefresh = false;
|
|
registry.Bounds.MaxDocumentBytes = options.MaximumDocumentBytes;
|
|
registry.ManagementAccess = new WotManagementAccessPolicy
|
|
{
|
|
MinimumSecurityMode = MessageSecurityMode.None,
|
|
AllowAnonymous = true,
|
|
RequiredRoleId = ObjectIds.WellKnownRole_Anonymous
|
|
};
|
|
});
|
|
|
|
// The bundled companion registry holds 540 partitioned documents (31 MiB), more than
|
|
// the default 256-document resolver budget, so the sample states a larger budget
|
|
// explicitly. The byte budgets stay at their defaults.
|
|
builder.Services.AddSingleton(new Opc.Ua.Wot.WotNodeSetConverterOptions
|
|
{
|
|
MaxJsonDocumentSize = options.MaximumDocumentBytes,
|
|
MaxResolverDocumentBytes = options.MaximumDocumentBytes,
|
|
MaxResolverDocuments = 1024
|
|
});
|
|
|
|
opcUa.AddClient(client =>
|
|
{
|
|
client.ApplicationName = $"{options.ApplicationName}.Upstream";
|
|
client.ApplicationUri =
|
|
$"urn:localhost:OPCFoundation:{options.ApplicationName}.Upstream";
|
|
client.ProductUri = "uri:opcfoundation.org:AggregationServer";
|
|
if (!string.IsNullOrWhiteSpace(options.PkiRoot))
|
|
{
|
|
client.PkiRoot = Path.Combine(options.PkiRoot, "upstream");
|
|
}
|
|
client.AutoAcceptUntrustedCertificates = true;
|
|
client.Session = new ManagedSessionOptions
|
|
{
|
|
SessionName = $"{options.ApplicationName}.Upstream",
|
|
SessionTimeout = TimeSpan.FromSeconds(60)
|
|
};
|
|
}).AddManagedClientPool();
|
|
|
|
opcUa.AddHttpWotBinding();
|
|
opcUa.AddModbusWotBinding();
|
|
|
|
// The aggregation topology deliberately federates source servers that run on the
|
|
// same host as this server, so the loopback gate the default policy applies must be
|
|
// opened explicitly. Keep every other check (scheme, blocked hosts, private ranges)
|
|
// at its secure default.
|
|
opcUa.AddWotEndpointPolicy(new WotEndpointPolicy { AllowLoopback = true });
|
|
|
|
builder.Services.AddSingleton<IWotBindingExecutor>(serviceProvider =>
|
|
{
|
|
IManagedSessionPool pool =
|
|
serviceProvider.GetRequiredService<IManagedSessionPool>();
|
|
return new OpcUaWotBindingExecutor(new OpcUaWotBindingOptions
|
|
{
|
|
DisposeSession = false,
|
|
SessionFactory = async (url, ct) =>
|
|
{
|
|
var endpointDescription = new EndpointDescription
|
|
{
|
|
EndpointUrl = url,
|
|
SecurityMode = MessageSecurityMode.None,
|
|
SecurityPolicyUri = SecurityPolicies.None
|
|
};
|
|
var configuredEndpoint = new ConfiguredEndpoint(
|
|
null,
|
|
endpointDescription,
|
|
null);
|
|
return await pool.GetOrConnectAsync(url, configuredEndpoint, ct)
|
|
.ConfigureAwait(false);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
private static int ReadPort(ConfigurationManager configuration)
|
|
{
|
|
return int.TryParse(
|
|
configuration["port"],
|
|
NumberStyles.Integer,
|
|
CultureInfo.InvariantCulture,
|
|
out int port)
|
|
? port
|
|
: 62550;
|
|
}
|
|
|
|
private static int ReadMaximumDocumentBytes(ConfigurationManager configuration)
|
|
{
|
|
return int.TryParse(
|
|
configuration["maximumDocumentBytes"],
|
|
NumberStyles.Integer,
|
|
CultureInfo.InvariantCulture,
|
|
out int maximumDocumentBytes)
|
|
? maximumDocumentBytes
|
|
: 32 * 1024 * 1024;
|
|
}
|
|
|
|
private static void Validate(AggregationServerOptions options)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(options.ApplicationName))
|
|
{
|
|
throw new ArgumentException("ApplicationName is required.", nameof(options));
|
|
}
|
|
if (options.MaximumDocumentBytes <= 0)
|
|
{
|
|
throw new ArgumentException(
|
|
"MaximumDocumentBytes must be positive.",
|
|
nameof(options));
|
|
}
|
|
if (options.EndpointUrl is null &&
|
|
(string.IsNullOrWhiteSpace(options.Host) || options.Port is < 1 or > 65535))
|
|
{
|
|
throw new ArgumentException("A valid host and port are required.", nameof(options));
|
|
}
|
|
}
|
|
}
|
|
}
|