fix(server): resolve Low code-review findings (Server-004,006,008,012,014,015)

- 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>
This commit is contained in:
Joseph Doherty
2026-05-23 07:24:20 -04:00
parent 2b33b64a58
commit 6134050ceb
14 changed files with 698 additions and 40 deletions

View File

@@ -0,0 +1,57 @@
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;
}
}