- Server-004: pass the role-derived display name to UserIdentity's base ctor (the SDK's DisplayName has no public setter) and drop the dead Display property; make RoleBasedIdentity internal sealed. - Server-006: derive a bounded CancellationToken from the SDK's OperationContext.OperationDeadline in OnReadValue / OnWriteValue so a stalled driver call can no longer pin the request thread. - Server-008: mark handled slots via CallMethodRequest.Processed = true in RouteScriptedAlarmMethodCalls (the SDK skips on Processed, not on a Good error slot). - Server-012: PeerHttpProbeLoop.ProbeAsync stops mutating client.Timeout per call; uses a per-request CancellationTokenSource linked to the shutdown token instead. - Server-014: wire SealedBootstrap into Program.cs via AddSealedBootstrap + OpcUaServerService so the generation-sealed cache + stale-config flag + resilient reader actually run; /healthz now reflects cache-fallback state. - Server-015: replace the stale 'PR 16 / PR 17 minimum-viable scope' class summaries on OtOpcUaServer and OpcUaServerOptions with the shipped LDAP + anonymous-role + configurable security-profile prose. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
58 lines
2.9 KiB
C#
58 lines
2.9 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Server;
|
|
|
|
/// <summary>
|
|
/// DI registration helpers consumed by <c>Program.cs</c>. Extracted so tests can assert
|
|
/// the production wire-up actually composes without spinning up the full <c>Host</c>.
|
|
/// </summary>
|
|
public static class ServerWiring
|
|
{
|
|
/// <summary>
|
|
/// Server-014 — registers the Phase 6.1 Stream D generation-sealed bootstrap chain:
|
|
/// <see cref="GenerationSealedCache"/>, <see cref="StaleConfigFlag"/>,
|
|
/// <see cref="ResilientConfigReader"/>, and <see cref="SealedBootstrap"/>. Without these
|
|
/// registrations <c>OpcUaServerService</c> cannot consume the sealed bootstrap and the
|
|
/// <see cref="StaleConfigFlag"/> stays inert — <c>/healthz</c>'s <c>usingStaleConfig</c>
|
|
/// never flips on a DB outage with a warm cache.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The cache root is sourced from <see cref="NodeOptions.LocalCachePath"/> — same path
|
|
/// the legacy <see cref="NodeBootstrap"/> uses for its LiteDB cache, so both bootstrap
|
|
/// paths persist alongside each other while the migration completes.
|
|
/// </remarks>
|
|
public static IServiceCollection AddSealedBootstrap(this IServiceCollection services, NodeOptions options)
|
|
{
|
|
// Use a sibling directory off LocalCachePath so the LiteDB file and the
|
|
// GenerationSealedCache snapshots don't clash. The cache root is a directory;
|
|
// LocalCachePath is canonically the LiteDB file path.
|
|
var cacheRoot = ResolveCacheRoot(options.LocalCachePath);
|
|
// Register NodeOptions only if the caller hasn't already done so — Program.cs
|
|
// registers it earlier in its DI chain, but the wiring helper supports standalone
|
|
// unit tests that want to compose just the SealedBootstrap chain.
|
|
if (!services.Any(d => d.ServiceType == typeof(NodeOptions)))
|
|
services.AddSingleton(options);
|
|
services.AddSingleton(new GenerationSealedCache(cacheRoot));
|
|
services.AddSingleton<StaleConfigFlag>();
|
|
services.AddSingleton<ResilientConfigReader>();
|
|
services.AddSingleton<SealedBootstrap>();
|
|
return services;
|
|
}
|
|
|
|
private static string ResolveCacheRoot(string localCachePath)
|
|
{
|
|
// LocalCachePath is the LiteDB file (e.g. "config_cache.db"); the sealed cache is a
|
|
// directory. Pick a sibling folder so the two don't share a path.
|
|
if (string.IsNullOrWhiteSpace(localCachePath))
|
|
return Path.Combine(Path.GetTempPath(), "otopcua-sealed-cache");
|
|
|
|
var dir = Path.GetDirectoryName(localCachePath);
|
|
var name = Path.GetFileNameWithoutExtension(localCachePath);
|
|
var root = string.IsNullOrEmpty(dir)
|
|
? $"{name}.sealed"
|
|
: Path.Combine(dir, $"{name}.sealed");
|
|
return root;
|
|
}
|
|
}
|