chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)

Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.

- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
  the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
  mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
  integration, install).

Build green (0 errors); unit tests pass. Docs left for a separate pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-17 01:55:28 -04:00
parent 69f02fed7f
commit a25593a9c6
1044 changed files with 365 additions and 343 deletions

View File

@@ -0,0 +1,122 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
using ZB.MOM.WW.OtOpcUa.Server.Phase7;
using ZB.MOM.WW.OtOpcUa.Server.Security;
namespace ZB.MOM.WW.OtOpcUa.Server;
/// <summary>
/// BackgroundService that owns the OPC UA server lifecycle (decision #30, replacing TopShelf).
/// Bootstraps config, starts the <see cref="DriverHost"/>, starts the OPC UA server via
/// <see cref="OpcUaApplicationHost"/>, drives each driver's discovery into the address space,
/// runs until stopped.
/// </summary>
public sealed class OpcUaServerService(
NodeBootstrap bootstrap,
DriverHost driverHost,
OpcUaApplicationHost applicationHost,
DriverEquipmentContentRegistry equipmentContentRegistry,
DriverInstanceBootstrapper driverBootstrapper,
Phase7Composer phase7Composer,
AuthorizationBootstrap authorizationBootstrap,
IServiceScopeFactory scopeFactory,
ILogger<OpcUaServerService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("OtOpcUa.Server starting");
var result = await bootstrap.LoadCurrentGenerationAsync(stoppingToken);
logger.LogInformation("Bootstrap complete: source={Source} generation={Gen}", result.Source, result.GenerationId);
// ADR-001 Option A — populate per-driver Equipment namespace snapshots into the
// registry before StartAsync walks the address space. The walker on the OPC UA side
// reads synchronously from the registry; pre-loading here means the hot path stays
// non-blocking + each driver pays at most one Config-DB query at bootstrap time.
// Skipped when no generation is Published yet — the fleet boots into a UNS-less
// address space until the first publish, then the registry fills on next restart.
if (result.GenerationId is { } gen)
{
// Task #248 — register IDriver instances from the published DriverInstance
// rows BEFORE the equipment-content load + Phase 7 compose, so the rest of
// the pipeline sees a populated DriverHost. Without this step Phase 7's
// CachedTagUpstreamSource has no upstream feed + virtual-tag scripts read
// BadNodeIdUnknown for every tag path (gap surfaced by task #240 smoke).
await driverBootstrapper.RegisterDriversFromGenerationAsync(gen, stoppingToken);
await PopulateEquipmentContentAsync(gen, stoppingToken);
// Phase 7 follow-up #246 — load Script + VirtualTag + ScriptedAlarm rows,
// compose VirtualTagEngine + ScriptedAlarmEngine, start the driver-bridge
// feed. SetPhase7Sources MUST run before applicationHost.StartAsync because
// OtOpcUaServer + DriverNodeManager construction captures the field values
// — late binding after server start is rejected with InvalidOperationException.
// No-op when the generation has no virtual tags or scripted alarms.
var phase7 = await phase7Composer.PrepareAsync(gen, stoppingToken);
applicationHost.SetPhase7Sources(phase7.VirtualReadable, phase7.ScriptedAlarmReadable);
// Phase 6.2 Stream C wiring — build the AuthorizationGate + NodeScopeResolver
// from the published generation's NodeAcl rows and the populated equipment
// registry. No-op when Node:Authorization:Enabled=false. Must run before
// StartAsync: OtOpcUaServer + DriverNodeManager construction captures the
// field values on the application host.
var (authzGate, scopeResolver) = await authorizationBootstrap
.BuildAsync(gen, stoppingToken).ConfigureAwait(false);
applicationHost.SetAuthorization(authzGate, scopeResolver);
}
await applicationHost.StartAsync(stoppingToken);
logger.LogInformation("OtOpcUa.Server running. Hosted drivers: {Count}", driverHost.RegisteredDriverIds.Count);
try
{
await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken);
}
catch (OperationCanceledException)
{
logger.LogInformation("OtOpcUa.Server stopping");
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
await base.StopAsync(cancellationToken);
// Dispose Phase 7 first so the bridge stops feeding the cache + the engines
// stop firing alarm/historian events before the OPC UA server tears down its
// node managers. Otherwise an in-flight cascade could try to push through a
// disposed source and surface as a noisy shutdown warning.
await phase7Composer.DisposeAsync();
await applicationHost.DisposeAsync();
await driverHost.DisposeAsync();
}
/// <summary>
/// Pre-load an <c>EquipmentNamespaceContent</c> snapshot for each registered driver at
/// the bootstrapped generation. Null results (driver has no Equipment rows —
/// Modbus/AB CIP/TwinCAT/FOCAS today per decisions #116#121) are skipped: the walker
/// wire-in sees Get(driverId) return null + falls back to DiscoverAsync-owns-it.
/// Opens one scope so the scoped <c>OtOpcUaConfigDbContext</c> is shared across all
/// per-driver queries rather than paying scope-setup overhead per driver.
/// </summary>
private async Task PopulateEquipmentContentAsync(long generationId, CancellationToken ct)
{
using var scope = scopeFactory.CreateScope();
var loader = scope.ServiceProvider.GetRequiredService<EquipmentNamespaceContentLoader>();
var loaded = 0;
foreach (var driverId in driverHost.RegisteredDriverIds)
{
var content = await loader.LoadAsync(driverId, generationId, ct).ConfigureAwait(false);
if (content is null) continue;
equipmentContentRegistry.Set(driverId, content);
loaded++;
}
logger.LogInformation(
"Equipment namespace snapshots loaded for {Count}/{Total} driver(s) at generation {Gen}",
loaded, driverHost.RegisteredDriverIds.Count, generationId);
}
}