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:
@@ -0,0 +1,225 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Per-session owner of the worker's alarm-side state. Lazy-creates an
|
||||
/// <see cref="AlarmDispatcher"/> (with a wnwrap-backed
|
||||
/// <see cref="WnWrapAlarmConsumer"/> by default) on the first
|
||||
/// <see cref="Subscribe"/> call, then routes
|
||||
/// <see cref="Acknowledge"/> / <see cref="QueryActive"/> /
|
||||
/// <see cref="Unsubscribe"/> through the same instance for the
|
||||
/// session's lifetime.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Construction is dependency-injectable: the consumer factory
|
||||
/// (default <c>() => new WnWrapAlarmConsumer()</c>) lets tests
|
||||
/// substitute a fake without touching AVEVA COM. The event queue
|
||||
/// is supplied by the owning <see cref="MxAccessStaSession"/> so
|
||||
/// the alarm-side proto events land on the same queue the worker
|
||||
/// already drains for IPC dispatch.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Threading: invoked from <see cref="MxAccessCommandExecutor"/>
|
||||
/// which runs on the STA. The wnwrap consumer owns no internal
|
||||
/// timer — the worker's STA drives <see cref="PollOnce"/> via
|
||||
/// <c>StaRuntime.InvokeAsync</c>, so the consumer's transition
|
||||
/// events fire on the same STA. The
|
||||
/// <see cref="AlarmDispatcher"/>'s event handler hands transitions
|
||||
/// into the thread-safe <see cref="MxAccessEventQueue"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class AlarmCommandHandler : IAlarmCommandHandler
|
||||
{
|
||||
private readonly MxAccessEventQueue eventQueue;
|
||||
private readonly Func<IMxAccessAlarmConsumer> consumerFactory;
|
||||
private readonly Action? threadAffinityCheck;
|
||||
private readonly object syncRoot = new object();
|
||||
private AlarmDispatcher? dispatcher;
|
||||
private bool disposed;
|
||||
|
||||
public AlarmCommandHandler(MxAccessEventQueue eventQueue)
|
||||
: this(eventQueue, () => new WnWrapAlarmConsumer(), threadAffinityCheck: null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Test seam — inject a custom consumer factory.</summary>
|
||||
public AlarmCommandHandler(
|
||||
MxAccessEventQueue eventQueue,
|
||||
Func<IMxAccessAlarmConsumer> consumerFactory)
|
||||
: this(eventQueue, consumerFactory, threadAffinityCheck: null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker-024: production constructor that also injects an
|
||||
/// STA-affinity guard. <paramref name="threadAffinityCheck"/> is
|
||||
/// invoked at the entry of every method that touches the underlying
|
||||
/// <see cref="IMxAccessAlarmConsumer"/> (or the wnwrap COM object
|
||||
/// through it) — <see cref="Subscribe"/>, <see cref="Unsubscribe"/>,
|
||||
/// <see cref="Acknowledge"/>, <see cref="AcknowledgeByName"/>,
|
||||
/// <see cref="QueryActive"/>, <see cref="PollOnce"/> — so an
|
||||
/// off-STA call raises a programming-error diagnostic instead of
|
||||
/// deadlocking on cross-apartment marshaling to the
|
||||
/// <c>ThreadingModel=Apartment</c> wnwrap CLSID. The guard is
|
||||
/// optional: tests that already drive the handler on a single
|
||||
/// thread can pass <c>null</c>.
|
||||
/// </summary>
|
||||
public AlarmCommandHandler(
|
||||
MxAccessEventQueue eventQueue,
|
||||
Func<IMxAccessAlarmConsumer> consumerFactory,
|
||||
Action? threadAffinityCheck)
|
||||
{
|
||||
this.eventQueue = eventQueue ?? throw new ArgumentNullException(nameof(eventQueue));
|
||||
this.consumerFactory = consumerFactory ?? throw new ArgumentNullException(nameof(consumerFactory));
|
||||
this.threadAffinityCheck = threadAffinityCheck;
|
||||
}
|
||||
|
||||
public bool IsSubscribed
|
||||
{
|
||||
get { lock (syncRoot) return dispatcher is not null; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Subscribe(string subscription, string sessionId)
|
||||
{
|
||||
if (disposed) throw new ObjectDisposedException(nameof(AlarmCommandHandler));
|
||||
if (subscription is null) throw new ArgumentNullException(nameof(subscription));
|
||||
threadAffinityCheck?.Invoke();
|
||||
|
||||
lock (syncRoot)
|
||||
{
|
||||
if (dispatcher is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"AlarmCommandHandler already has an active subscription; " +
|
||||
"call Unsubscribe before issuing another SubscribeAlarms command.");
|
||||
}
|
||||
IMxAccessAlarmConsumer consumer = consumerFactory()
|
||||
?? throw new InvalidOperationException("Alarm consumer factory returned null.");
|
||||
MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(
|
||||
eventQueue, new MxAccessEventMapper());
|
||||
dispatcher = new AlarmDispatcher(consumer, sink, sessionId ?? string.Empty);
|
||||
try
|
||||
{
|
||||
dispatcher.Subscribe(subscription);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try { dispatcher.Dispose(); } catch { /* swallow */ }
|
||||
dispatcher = null;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Unsubscribe()
|
||||
{
|
||||
threadAffinityCheck?.Invoke();
|
||||
AlarmDispatcher? toDispose;
|
||||
lock (syncRoot)
|
||||
{
|
||||
toDispose = dispatcher;
|
||||
dispatcher = null;
|
||||
}
|
||||
toDispose?.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Acknowledge(
|
||||
Guid alarmGuid,
|
||||
string comment,
|
||||
string operatorUser,
|
||||
string operatorNode,
|
||||
string operatorDomain,
|
||||
string operatorFullName)
|
||||
{
|
||||
threadAffinityCheck?.Invoke();
|
||||
AlarmDispatcher? d = GetDispatcherOrThrow();
|
||||
return d.Acknowledge(
|
||||
alarmGuid,
|
||||
comment ?? string.Empty,
|
||||
operatorUser ?? string.Empty,
|
||||
operatorNode ?? string.Empty,
|
||||
operatorDomain ?? string.Empty,
|
||||
operatorFullName ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int AcknowledgeByName(
|
||||
string alarmName,
|
||||
string providerName,
|
||||
string groupName,
|
||||
string comment,
|
||||
string operatorUser,
|
||||
string operatorNode,
|
||||
string operatorDomain,
|
||||
string operatorFullName)
|
||||
{
|
||||
threadAffinityCheck?.Invoke();
|
||||
AlarmDispatcher? d = GetDispatcherOrThrow();
|
||||
return d.AcknowledgeByName(
|
||||
alarmName ?? string.Empty,
|
||||
providerName ?? string.Empty,
|
||||
groupName ?? string.Empty,
|
||||
comment ?? string.Empty,
|
||||
operatorUser ?? string.Empty,
|
||||
operatorNode ?? string.Empty,
|
||||
operatorDomain ?? string.Empty,
|
||||
operatorFullName ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix)
|
||||
{
|
||||
threadAffinityCheck?.Invoke();
|
||||
AlarmDispatcher? d = GetDispatcherOrThrow();
|
||||
IReadOnlyList<ActiveAlarmSnapshot> all = d.SnapshotActiveAlarms();
|
||||
if (string.IsNullOrEmpty(alarmFilterPrefix)) return all;
|
||||
List<ActiveAlarmSnapshot> filtered = new List<ActiveAlarmSnapshot>(all.Count);
|
||||
foreach (ActiveAlarmSnapshot snap in all)
|
||||
{
|
||||
if (snap.AlarmFullReference.StartsWith(alarmFilterPrefix!, StringComparison.Ordinal))
|
||||
{
|
||||
filtered.Add(snap);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PollOnce()
|
||||
{
|
||||
threadAffinityCheck?.Invoke();
|
||||
AlarmDispatcher? d;
|
||||
lock (syncRoot) d = dispatcher;
|
||||
// No-op when not yet subscribed or already disposed.
|
||||
d?.PollOnce();
|
||||
}
|
||||
|
||||
private AlarmDispatcher GetDispatcherOrThrow()
|
||||
{
|
||||
if (disposed) throw new ObjectDisposedException(nameof(AlarmCommandHandler));
|
||||
AlarmDispatcher? d;
|
||||
lock (syncRoot) d = dispatcher;
|
||||
if (d is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"AlarmCommandHandler has no active subscription; " +
|
||||
"call SubscribeAlarms before issuing alarm-related commands.");
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
Unsubscribe();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user