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,287 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using WNWRAPCONSUMERLib;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.Tests.Probes;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime probe — instantiate AVEVA's standalone wnwrapConsumer COM
|
||||
/// class (CLSID 7AB52E5F-36B2-4A30-AE46-952A746F667C, registered at
|
||||
/// C:\Program Files (x86)\Common Files\ArchestrA\wnwrapConsumer.dll),
|
||||
/// subscribe to the dev rig's `\\<machine>\Galaxy!DEV` provider, and
|
||||
/// poll <c>GetXmlCurrentAlarms2</c> while a System Platform script flips
|
||||
/// <c>TestMachine_001.TestAlarm001</c> every 10s. The XML payload bypasses
|
||||
/// the FILETIME→DateTime auto-marshaling that crashes
|
||||
/// <c>aaAlarmManagedClient.AlarmClient.GetHighPriAlarm</c>.
|
||||
///
|
||||
/// Skip-gated; flip Skip=null to run on the dev rig.
|
||||
/// </summary>
|
||||
public sealed class WnWrapConsumerProbeTests
|
||||
{
|
||||
private static readonly string MachineName = Environment.MachineName;
|
||||
private static readonly string SubscriptionExpression =
|
||||
$@"\\{MachineName}\Galaxy!DEV";
|
||||
|
||||
// XML query form — per WIN-911 / ArchestrA reference. NODE is the
|
||||
// machine, PROVIDER is the literal "Galaxy", GROUP is the area.
|
||||
private static readonly string XmlAlarmQuery =
|
||||
"<QUERIES FROM_PRIORITY=\"1\" TO_PRIORITY=\"999\" ALARM_STATE=\"ALL\" DISPLAY_MODE=\"Summary\">" +
|
||||
"<QUERY>" +
|
||||
$"<NODE>{Environment.MachineName}</NODE>" +
|
||||
"<PROVIDER>Galaxy</PROVIDER>" +
|
||||
"<GROUP>DEV</GROUP>" +
|
||||
"</QUERY>" +
|
||||
"</QUERIES>";
|
||||
|
||||
private const int MaxAlarmsPerFetch = 100;
|
||||
private static readonly TimeSpan PumpDuration = TimeSpan.FromSeconds(30);
|
||||
private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
private readonly ITestOutputHelper output;
|
||||
private readonly ConcurrentQueue<string> log = new ConcurrentQueue<string>();
|
||||
private readonly Stopwatch elapsed = Stopwatch.StartNew();
|
||||
|
||||
public WnWrapConsumerProbeTests(ITestOutputHelper output)
|
||||
{
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
[Fact(Skip = "Runtime probe — flip Skip=null on the dev rig (AVEVA installed) to capture wnwrapConsumer XML alarm output. Verified working 2026-05-01.")]
|
||||
public void ProbeWnWrapConsumer_OnDevRig_LogsXmlAlarmStream()
|
||||
{
|
||||
Exception? threadException = null;
|
||||
var done = new ManualResetEventSlim(false);
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try { RunProbe(); }
|
||||
catch (Exception ex) { threadException = ex; }
|
||||
finally { done.Set(); }
|
||||
});
|
||||
thread.IsBackground = false;
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
done.Wait();
|
||||
thread.Join();
|
||||
|
||||
output.WriteLine($"Captured {log.Count} log line(s):");
|
||||
while (log.TryDequeue(out string? line))
|
||||
{
|
||||
output.WriteLine(line);
|
||||
}
|
||||
|
||||
if (threadException != null)
|
||||
{
|
||||
throw threadException;
|
||||
}
|
||||
}
|
||||
|
||||
private void RunProbe()
|
||||
{
|
||||
wwAlarmConsumerClass? client = null;
|
||||
try
|
||||
{
|
||||
Log("Creating wwAlarmConsumerClass via CoCreateInstance...");
|
||||
client = new wwAlarmConsumerClass();
|
||||
Log($"Instantiated. RuntimeType={client.GetType().FullName}");
|
||||
|
||||
// Lifecycle: per AlarmClientDiscovery.md finding, InitializeConsumer
|
||||
// MUST precede RegisterConsumer for the alarm provider to become
|
||||
// visible. The wnwrap surface mirrors that requirement.
|
||||
try
|
||||
{
|
||||
int init = client.InitializeConsumer("MxGatewayProbe.WnWrap");
|
||||
Log($"InitializeConsumer -> {init}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"InitializeConsumer threw: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// hWnd=0 — XML pull-based; no message pump needed.
|
||||
int reg = client.RegisterConsumer(
|
||||
hWnd: 0,
|
||||
szProductName: "MxGatewayProbe",
|
||||
szApplicationName: "MxGatewayProbe.WnWrap",
|
||||
szVersion: "1.0");
|
||||
Log($"RegisterConsumer(hWnd=0) -> {reg}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"RegisterConsumer threw: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
// Try both subscription mechanisms: classic Subscribe (canonical
|
||||
// scope from prior aaAlarmManagedClient probe), and
|
||||
// SetXmlAlarmQuery (the wnwrap-native filter format).
|
||||
try
|
||||
{
|
||||
int sub = client.Subscribe(
|
||||
szSubscription: SubscriptionExpression,
|
||||
wFromPri: 1,
|
||||
wToPri: 999,
|
||||
QueryType: eQueryType.qtSummary,
|
||||
SortFlags: eSortFlags.sfReturnNewestFirst,
|
||||
FilterMask: eAlarmFilterState.asAlarmActiveNow,
|
||||
FilterSpecification: eAlarmFilterState.asAlarmActiveNow);
|
||||
Log($"Subscribe('{SubscriptionExpression}') -> {sub}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"Subscribe threw: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Log($"SetXmlAlarmQuery payload: {XmlAlarmQuery}");
|
||||
client.SetXmlAlarmQuery(XmlAlarmQuery);
|
||||
Log("SetXmlAlarmQuery -> ok");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"SetXmlAlarmQuery threw: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
// Echo the query back so we can confirm what the consumer is
|
||||
// actually filtering on (provider may rewrite or reject some
|
||||
// attributes silently).
|
||||
try
|
||||
{
|
||||
object echo = string.Empty;
|
||||
client.GetXmlAlarmQuery(out echo);
|
||||
Log($"GetXmlAlarmQuery (round-trip) -> {Truncate(echo?.ToString() ?? "<null>", 600)}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"GetXmlAlarmQuery threw: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
// Pump phase: poll GetXmlCurrentAlarms2 every PollInterval; log on
|
||||
// every change in payload. Run for PumpDuration. The user's flip
|
||||
// script writes TestMachine_001.TestAlarm001 every 10s; expect at
|
||||
// least 2-3 transitions over a 30s window.
|
||||
Log($"Polling GetXmlCurrentAlarms2 every {PollInterval.TotalMilliseconds:F0}ms for {PumpDuration.TotalSeconds:F0}s.");
|
||||
DateTime deadline = DateTime.UtcNow + PumpDuration;
|
||||
DateTime nextPoll = DateTime.UtcNow;
|
||||
int pollCount = 0;
|
||||
string lastV2 = string.Empty;
|
||||
string lastV1 = string.Empty;
|
||||
int v2Ok = 0, v2Throw = 0, v1Ok = 0, v1Throw = 0;
|
||||
int statsOk = 0, statsThrow = 0;
|
||||
string lastStats = string.Empty;
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
if (DateTime.UtcNow >= nextPoll)
|
||||
{
|
||||
pollCount++;
|
||||
|
||||
// V2 channel.
|
||||
try
|
||||
{
|
||||
object xml2 = string.Empty;
|
||||
client.GetXmlCurrentAlarms2(MaxAlarmsPerFetch, out xml2);
|
||||
v2Ok++;
|
||||
string s = xml2?.ToString() ?? "<null>";
|
||||
if (s != lastV2)
|
||||
{
|
||||
Log($"GetXmlCurrentAlarms2 #{pollCount} (CHANGED, len={s.Length}): {Truncate(s, 1200)}");
|
||||
lastV2 = s;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
v2Throw++;
|
||||
string es = $"{ex.GetType().Name}: {ex.Message}";
|
||||
if (es != lastV2)
|
||||
{
|
||||
Log($"GetXmlCurrentAlarms2 #{pollCount} threw: {es}");
|
||||
lastV2 = es;
|
||||
}
|
||||
}
|
||||
|
||||
// V1 channel — different vtable slot; either may be the
|
||||
// populated one in this AVEVA build.
|
||||
try
|
||||
{
|
||||
object xml1 = string.Empty;
|
||||
client.GetXmlCurrentAlarms(MaxAlarmsPerFetch, out xml1);
|
||||
v1Ok++;
|
||||
string s = xml1?.ToString() ?? "<null>";
|
||||
if (s != lastV1)
|
||||
{
|
||||
Log($"GetXmlCurrentAlarms #{pollCount} (CHANGED, len={s.Length}): {Truncate(s, 1200)}");
|
||||
lastV1 = s;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
v1Throw++;
|
||||
string es = $"{ex.GetType().Name}: {ex.Message}";
|
||||
if (es != lastV1)
|
||||
{
|
||||
Log($"GetXmlCurrentAlarms #{pollCount} threw: {es}");
|
||||
lastV1 = es;
|
||||
}
|
||||
}
|
||||
|
||||
// Stats channel — heartbeat + active-count even if the XML
|
||||
// calls are dry, this surfaces whether wnwrap sees any
|
||||
// alarms in the subscribed scope at all.
|
||||
try
|
||||
{
|
||||
int pct, total, active, newAlms, changes;
|
||||
client.GetStatistics(
|
||||
out pct, out total, out active, out newAlms, out changes,
|
||||
IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
||||
statsOk++;
|
||||
string statsSummary = $"pct={pct} total={total} active={active} new={newAlms} changes={changes}";
|
||||
if (statsSummary != lastStats)
|
||||
{
|
||||
Log($"GetStatistics #{pollCount} (CHANGED): {statsSummary}");
|
||||
lastStats = statsSummary;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
statsThrow++;
|
||||
Log($"GetStatistics #{pollCount} threw: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
nextPoll = DateTime.UtcNow + PollInterval;
|
||||
}
|
||||
Thread.Sleep(20);
|
||||
}
|
||||
Log($"Pump done. Tally: v2 ok={v2Ok} threw={v2Throw}, v1 ok={v1Ok} threw={v1Throw}, stats ok={statsOk} threw={statsThrow}");
|
||||
|
||||
try { int dereg = client.DeregisterConsumer(); Log($"DeregisterConsumer -> {dereg}"); }
|
||||
catch (Exception ex) { Log($"DeregisterConsumer threw: {ex.GetType().Name}: {ex.Message}"); }
|
||||
|
||||
try { int uninit = client.UninitializeConsumer(); Log($"UninitializeConsumer -> {uninit}"); }
|
||||
catch (Exception ex) { Log($"UninitializeConsumer threw: {ex.GetType().Name}: {ex.Message}"); }
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (client != null && Marshal.IsComObject(client))
|
||||
{
|
||||
try { Marshal.FinalReleaseComObject(client); } catch { /* swallow */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Log(string line)
|
||||
{
|
||||
log.Enqueue($"[t={elapsed.Elapsed.TotalSeconds:F3}s] {line}");
|
||||
}
|
||||
|
||||
private static string Truncate(string s, int max)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s) || s.Length <= max) return s ?? string.Empty;
|
||||
return s.Substring(0, max) + $"…[+{s.Length - max} chars]";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user