rename: prefix gateway projects/namespaces with ZB.MOM.WW + sln→slnx

Apply the ZB.MOM.WW. prefix to all gateway-side projects, folders,
.csproj/.sln contents, C# namespaces, using directives, generated proto
C# (csharp_namespace + checked-in generated files), InternalsVisibleTo
attributes, project-name string literals (LoadProject, .sln lookups,
worker exe paths, staticwebassets manifest), and the install/script/doc
references that point at any of the above. Migrate the solution from
.sln to .slnx via `dotnet sln migrate` and delete the old file.

External-runtime identifiers are intentionally NOT prefixed so external
configuration keeps working:
- GatewayMetrics.cs MeterName ("MxGateway.Server")
- DashboardAuthenticationDefaults Scheme/Policy ("MxGateway.Dashboard")
- GatewayRequestLoggingMiddleware logger category ("MxGateway.Request")
- StaRuntime thread name ("MxGateway.Worker.STA")
- appsettings.json root section "MxGateway" + env-var prefix
  MxGateway__... and secret-name MxGateway:ApiKeyPepper
- C:\ProgramData\MxGateway\ data dir paths

Also fixes two tests that were not rename-related but became visible
while validating the rename:

- WorkerLiveMxAccessSmokeTests.ShutDownAsync: cancellation that the
  gateway service correctly maps to RpcException(Cancelled) per gRPC
  convention was being misclassified as a stream fault. Added a sibling
  catch on RpcException with StatusCode.Cancelled.

- IntegrationTestEnvironment.ResolveRepositoryRoot: extracted IsRepositoryRoot
  and made it accept either a .git marker OR a .sln/.slnx next to src/
  so the worker-exe walker works in non-git working copies.

clients/proto/proto-inputs.json's protoRoot updated to point at
src/ZB.MOM.WW.MxGateway.Contracts/Protos.

Verified by `dotnet build` and a full `dotnet test` of the .slnx with
MXGATEWAY_RUN_LIVE_{MXACCESS,LDAP,GALAXY}_TESTS=1:
  Tests: 472/472 pass
  Worker.Tests: 280/280 pass (4 dev-rig [Fact(Skip=...)] skipped)
  IntegrationTests: 18/18 pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-23 16:22:23 -04:00
parent 867bf18116
commit dc9c0c950c
491 changed files with 32854 additions and 8414 deletions
@@ -0,0 +1,213 @@
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Alarms;
using ZB.MOM.WW.MxGateway.Server.Sessions;
namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
/// <summary>
/// Default <see cref="IDashboardLiveDataService"/>. Owns one shared gateway
/// session for the whole dashboard: it is opened lazily on first use and
/// re-opened transparently whenever it faults, is closed, or its lease
/// expires. All access is serialised through <see cref="_gate"/> so the
/// single backing worker only ever sees one in-flight command.
/// </summary>
public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsyncDisposable
{
private const string BackendName = "Galaxy";
private const string ClientName = "mxgateway-dashboard";
private static readonly TimeSpan ReadTimeout = TimeSpan.FromSeconds(5);
private readonly ISessionManager _sessionManager;
private readonly IGatewayAlarmService _alarmService;
private readonly ILogger<DashboardLiveDataService> _logger;
private readonly SemaphoreSlim _gate = new(1, 1);
private readonly HashSet<string> _subscribed = new(StringComparer.OrdinalIgnoreCase);
private GatewaySession? _session;
private int _serverHandle;
private bool _disposed;
/// <summary>Initializes the live-data service.</summary>
/// <param name="sessionManager">Gateway session manager.</param>
/// <param name="alarmService">Gateway central alarm service.</param>
/// <param name="logger">Diagnostic logger.</param>
public DashboardLiveDataService(
ISessionManager sessionManager,
IGatewayAlarmService alarmService,
ILogger<DashboardLiveDataService> logger)
{
_sessionManager = sessionManager ?? throw new ArgumentNullException(nameof(sessionManager));
_alarmService = alarmService ?? throw new ArgumentNullException(nameof(alarmService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task<DashboardLiveReadResult> ReadAsync(
IReadOnlyCollection<string> tagAddresses,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(tagAddresses);
if (tagAddresses.Count == 0)
{
return DashboardLiveReadResult.Empty;
}
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
(GatewaySession session, int serverHandle) = await EnsureReadyAsync(cancellationToken)
.ConfigureAwait(false);
string[] toSubscribe = tagAddresses.Where(tag => !_subscribed.Contains(tag)).ToArray();
if (toSubscribe.Length > 0)
{
await session.SubscribeBulkAsync(serverHandle, toSubscribe, cancellationToken)
.ConfigureAwait(false);
foreach (string tag in toSubscribe)
{
_subscribed.Add(tag);
}
}
IReadOnlyList<BulkReadResult> results = await session
.ReadBulkAsync(serverHandle, tagAddresses.ToArray(), ReadTimeout, cancellationToken)
.ConfigureAwait(false);
DashboardTagValue[] values = results
.Select(DashboardTagValue.FromBulkReadResult)
.ToArray();
return new DashboardLiveReadResult(values, null, session.SessionId, session.WorkerProcessId);
}
catch (Exception exception) when (exception is not OperationCanceledException)
{
InvalidateSession();
_logger.LogWarning(exception, "Dashboard live read failed; the dashboard session will be re-opened.");
return new DashboardLiveReadResult([], exception.Message, null, null);
}
finally
{
_gate.Release();
}
}
/// <inheritdoc />
public Task<DashboardAlarmQueryResult> QueryAlarmsAsync(CancellationToken cancellationToken)
{
// Alarms come from the gateway's always-on central monitor; the
// dashboard reads its in-process cache directly — no session needed.
DashboardActiveAlarm[] alarms = _alarmService.CurrentAlarms
.Select(DashboardActiveAlarm.FromSnapshot)
.ToArray();
string? error = _alarmService.State is GatewayAlarmMonitorState.Monitoring
or GatewayAlarmMonitorState.Disabled
? null
: _alarmService.LastError ?? $"Alarm monitor is {_alarmService.State}.";
return Task.FromResult(new DashboardAlarmQueryResult(alarms, error, _alarmService.WorkerProcessId));
}
// Returns a Ready session + its Register server handle, opening a fresh
// session when none exists or the current one is no longer usable. Callers
// must hold _gate.
private async Task<(GatewaySession Session, int ServerHandle)> EnsureReadyAsync(
CancellationToken cancellationToken)
{
ObjectDisposedException.ThrowIf(_disposed, this);
GatewaySession? existing = _session;
if (existing is not null
&& existing.State == SessionState.Ready
&& _sessionManager.TryGetSession(existing.SessionId, out _))
{
return (existing, _serverHandle);
}
if (existing is not null)
{
_logger.LogInformation(
"Dashboard session {SessionId} is no longer usable (state {State}); re-opening.",
existing.SessionId,
existing.State);
await CloseQuietlyAsync(existing.SessionId).ConfigureAwait(false);
}
_subscribed.Clear();
_session = null;
GatewaySession session = await _sessionManager.OpenSessionAsync(
new SessionOpenRequest(BackendName, ClientName, Guid.NewGuid().ToString("N"), CommandTimeout: null),
ClientName,
cancellationToken)
.ConfigureAwait(false);
WorkerCommandReply reply = await session.InvokeAsync(
new WorkerCommand
{
Command = new MxCommand
{
Kind = MxCommandKind.Register,
Register = new RegisterCommand { ClientName = ClientName },
},
},
cancellationToken)
.ConfigureAwait(false);
int? serverHandle = reply.Reply?.Register?.ServerHandle;
if (serverHandle is null)
{
string diagnostic = reply.Reply?.ProtocolStatus?.Message
?? reply.Reply?.DiagnosticMessage
?? "Worker did not return a server handle for Register.";
await CloseQuietlyAsync(session.SessionId).ConfigureAwait(false);
throw new InvalidOperationException($"Dashboard session registration failed: {diagnostic}");
}
_session = session;
_serverHandle = serverHandle.Value;
_logger.LogInformation(
"Dashboard session {SessionId} opened (worker pid {WorkerPid}).",
session.SessionId,
session.WorkerProcessId);
return (session, _serverHandle);
}
// Drops the cached session so the next call re-opens. Callers must hold _gate.
private void InvalidateSession()
{
_session = null;
_serverHandle = 0;
_subscribed.Clear();
}
private async Task CloseQuietlyAsync(string sessionId)
{
try
{
await _sessionManager.CloseSessionAsync(sessionId, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception exception)
{
_logger.LogDebug(exception, "Closing stale dashboard session {SessionId} failed.", sessionId);
}
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
_disposed = true;
GatewaySession? session = _session;
_session = null;
if (session is not null)
{
await CloseQuietlyAsync(session.SessionId).ConfigureAwait(false);
}
_gate.Dispose();
}
}