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,183 @@
|
||||
using System;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Translation helpers between the wnwrapConsumer XML payload and the
|
||||
/// proto-friendly <see cref="AlarmTransitionKind"/> wire format, plus
|
||||
/// alarm-reference composition.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These mappings stay pure and library-agnostic so they're unit
|
||||
/// testable without an AVEVA install. The COM-side I/O lives on
|
||||
/// <see cref="WnWrapAlarmConsumer"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class AlarmRecordTransitionMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Decode AVEVA's STATE string (one of <c>UNACK_ALM</c>, <c>ACK_ALM</c>,
|
||||
/// <c>UNACK_RTN</c>, <c>ACK_RTN</c>) into the worker's library-agnostic
|
||||
/// <see cref="MxAlarmStateKind"/>. Unknown values map to
|
||||
/// <see cref="MxAlarmStateKind.Unspecified"/>.
|
||||
/// </summary>
|
||||
public static MxAlarmStateKind ParseStateKind(string? stateXml)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stateXml)) return MxAlarmStateKind.Unspecified;
|
||||
return stateXml!.Trim().ToUpperInvariant() switch
|
||||
{
|
||||
"UNACK_ALM" => MxAlarmStateKind.UnackAlm,
|
||||
"ACK_ALM" => MxAlarmStateKind.AckAlm,
|
||||
"UNACK_RTN" => MxAlarmStateKind.UnackRtn,
|
||||
"ACK_RTN" => MxAlarmStateKind.AckRtn,
|
||||
_ => MxAlarmStateKind.Unspecified,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decide which proto transition kind a state change represents.
|
||||
/// The decision table:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>previous=Unspecified</c> + <c>current=*Alm</c> → Raise (new alarm).</description></item>
|
||||
/// <item><description><c>previous=Unspecified</c> + <c>current=*Rtn</c> → Clear (alarm appeared in cleared state — rare; missed the raise).</description></item>
|
||||
/// <item><description><c>previous=Unack*</c> + <c>current=Ack*</c> → Acknowledge.</description></item>
|
||||
/// <item><description><c>previous=*Alm</c> + <c>current=*Rtn</c> → Clear.</description></item>
|
||||
/// <item><description><c>previous=*Rtn</c> + <c>current=*Alm</c> → Raise (re-trigger after clear).</description></item>
|
||||
/// <item><description>Anything else → Unspecified (no proto kind to emit).</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static AlarmTransitionKind MapTransition(
|
||||
MxAlarmStateKind previous,
|
||||
MxAlarmStateKind current)
|
||||
{
|
||||
if (current == MxAlarmStateKind.Unspecified) return AlarmTransitionKind.Unspecified;
|
||||
|
||||
bool currentIsAlm = current is MxAlarmStateKind.UnackAlm or MxAlarmStateKind.AckAlm;
|
||||
bool currentIsRtn = current is MxAlarmStateKind.UnackRtn or MxAlarmStateKind.AckRtn;
|
||||
bool currentIsAcked = current is MxAlarmStateKind.AckAlm or MxAlarmStateKind.AckRtn;
|
||||
|
||||
if (previous == MxAlarmStateKind.Unspecified)
|
||||
{
|
||||
return currentIsAlm ? AlarmTransitionKind.Raise : AlarmTransitionKind.Clear;
|
||||
}
|
||||
|
||||
bool previousIsAlm = previous is MxAlarmStateKind.UnackAlm or MxAlarmStateKind.AckAlm;
|
||||
bool previousIsRtn = previous is MxAlarmStateKind.UnackRtn or MxAlarmStateKind.AckRtn;
|
||||
bool previousIsAcked = previous is MxAlarmStateKind.AckAlm or MxAlarmStateKind.AckRtn;
|
||||
|
||||
if (previousIsAlm && currentIsRtn) return AlarmTransitionKind.Clear;
|
||||
if (previousIsRtn && currentIsAlm) return AlarmTransitionKind.Raise;
|
||||
if (!previousIsAcked && currentIsAcked) return AlarmTransitionKind.Acknowledge;
|
||||
|
||||
return AlarmTransitionKind.Unspecified;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compose <c>alarm_full_reference</c> as <c>Provider!Group.AlarmName</c>.
|
||||
/// The format mirrors AVEVA's standard alarm-reference syntax so
|
||||
/// downstream consumers that already speak it (e.g. the gateway's
|
||||
/// AcknowledgeAlarm RPC echoing a reference back as a GUID lookup)
|
||||
/// don't need translation.
|
||||
/// </summary>
|
||||
public static string ComposeFullReference(string? providerName, string? groupName, string? alarmName)
|
||||
{
|
||||
string provider = providerName ?? string.Empty;
|
||||
string group = groupName ?? string.Empty;
|
||||
string name = alarmName ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(provider))
|
||||
{
|
||||
return string.IsNullOrEmpty(group) ? name : $"{group}.{name}";
|
||||
}
|
||||
return string.IsNullOrEmpty(group)
|
||||
? $"{provider}!{name}"
|
||||
: $"{provider}!{group}.{name}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reassemble a UTC <see cref="DateTime"/> from the wnwrap XML's
|
||||
/// <c>DATE</c> + <c>TIME</c> + <c>GMTOFFSET</c> + <c>DSTADJUST</c>
|
||||
/// fields. Returns <see cref="DateTime.MinValue"/> when DATE / TIME
|
||||
/// can't be parsed (best-effort — failure is non-fatal; the proto
|
||||
/// will carry the epoch and the EventQueue's fault counter records
|
||||
/// the parse miss).
|
||||
/// </summary>
|
||||
/// <param name="xmlDate">e.g. <c>"2026/5/1"</c> (no zero-padding).</param>
|
||||
/// <param name="xmlTime">e.g. <c>"13:26:14.709"</c>.</param>
|
||||
/// <param name="gmtOffsetMinutes">Offset of the producer's local time vs UTC, in minutes.</param>
|
||||
/// <param name="dstAdjustMinutes">DST adjustment already applied to local time, in minutes.</param>
|
||||
public static DateTime ParseTransitionTimestampUtc(
|
||||
string? xmlDate,
|
||||
string? xmlTime,
|
||||
int gmtOffsetMinutes,
|
||||
int dstAdjustMinutes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(xmlDate) || string.IsNullOrWhiteSpace(xmlTime))
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
|
||||
// Parse DATE: yyyy/M/d (no zero padding observed). Use ParseExact with
|
||||
// multiple format candidates — AVEVA's locale may format differently
|
||||
// on non-en-US hosts.
|
||||
string[] dateFormats =
|
||||
{
|
||||
"yyyy/M/d", "yyyy/MM/dd", "M/d/yyyy", "MM/dd/yyyy",
|
||||
"d/M/yyyy", "dd/MM/yyyy",
|
||||
};
|
||||
string dateTrim = xmlDate!.Trim();
|
||||
if (!DateTime.TryParseExact(
|
||||
dateTrim,
|
||||
dateFormats,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.None,
|
||||
out DateTime date))
|
||||
{
|
||||
if (!DateTime.TryParse(
|
||||
dateTrim,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.None,
|
||||
out date))
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse TIME: H:m:s.fff (variable precision).
|
||||
string[] timeFormats =
|
||||
{
|
||||
"H:m:s.fff", "H:m:s.ff", "H:m:s.f", "H:m:s",
|
||||
"HH:mm:ss.fff", "HH:mm:ss.ff", "HH:mm:ss.f", "HH:mm:ss",
|
||||
};
|
||||
string timeTrim = xmlTime!.Trim();
|
||||
if (!DateTime.TryParseExact(
|
||||
timeTrim,
|
||||
timeFormats,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.None,
|
||||
out DateTime time))
|
||||
{
|
||||
if (!DateTime.TryParse(
|
||||
timeTrim,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.None,
|
||||
out time))
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
|
||||
DateTime localProducerTime = new DateTime(
|
||||
date.Year, date.Month, date.Day,
|
||||
time.Hour, time.Minute, time.Second, time.Millisecond,
|
||||
DateTimeKind.Unspecified);
|
||||
|
||||
// GMTOFFSET = minutes east of UTC (or behind, depending on convention).
|
||||
// The wnwrap convention observed: GMTOFFSET=240, DSTADJUST=0 for
|
||||
// EDT (UTC-4) — so the field is "minutes from local to UTC". To get
|
||||
// UTC, ADD the offset.
|
||||
DateTime utc = localProducerTime.AddMinutes(gmtOffsetMinutes - dstAdjustMinutes);
|
||||
return DateTime.SpecifyKind(utc, DateTimeKind.Utc);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user