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,48 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
/// <summary>
/// Configuration for the gateway's always-on central alarm monitor
/// (<see cref="Alarms.GatewayAlarmMonitor"/>). When <see cref="Enabled"/>
/// is true the gateway opens one gateway-owned worker session dedicated to
/// alarms, caches the active-alarm set, and fans it out to every client
/// through the <c>StreamAlarms</c> RPC — no client opens its own session
/// to see alarms.
/// </summary>
/// <remarks>
/// Defaults preserve current behaviour (alarm monitoring disabled).
/// Operators opt in by setting <c>MxGateway:Alarms:Enabled = true</c> and
/// supplying a canonical <c>\\&lt;machine&gt;\Galaxy!&lt;area&gt;</c>
/// subscription expression. The literal "Galaxy" provider is correct
/// regardless of the configured Galaxy database name (the wnwrap consumer
/// does not accept the database name as the provider).
/// </remarks>
public sealed class AlarmsOptions
{
/// <summary>Gate the gateway's always-on central alarm monitor. Default false.</summary>
public bool Enabled { get; init; }
/// <summary>
/// AVEVA alarm-subscription expression the monitor subscribes on
/// startup. When empty and <see cref="Enabled"/> is true, the gateway
/// falls back to <c>\\$(MachineName)\Galaxy!$(DefaultArea)</c> if
/// <see cref="DefaultArea"/> is set; otherwise the monitor faults with
/// a configuration diagnostic.
/// </summary>
public string SubscriptionExpression { get; init; } = string.Empty;
/// <summary>
/// Optional area name used to compose a default subscription when
/// <see cref="SubscriptionExpression"/> is empty. Combined with
/// <c>Environment.MachineName</c> as
/// <c>\\&lt;MachineName&gt;\Galaxy!&lt;DefaultArea&gt;</c>.
/// </summary>
public string DefaultArea { get; init; } = string.Empty;
/// <summary>
/// How often the monitor reconciles its in-process alarm cache against
/// the worker's authoritative active-alarm snapshot, catching any
/// transitions the live poll-and-diff feed missed. Default 30 seconds;
/// the monitor floors it at 5 seconds.
/// </summary>
public int ReconcileIntervalSeconds { get; init; } = 30;
}
@@ -0,0 +1,7 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public enum AuthenticationMode
{
ApiKey,
Disabled
}
@@ -0,0 +1,16 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class AuthenticationOptions
{
/// <summary>Gets the authentication mode.</summary>
public AuthenticationMode Mode { get; init; } = AuthenticationMode.ApiKey;
/// <summary>Gets the SQLite database path for authentication credentials.</summary>
public string SqlitePath { get; init; } = @"C:\ProgramData\MxGateway\gateway-auth.db";
/// <summary>Gets the secret manager name for API key pepper.</summary>
public string PepperSecretName { get; init; } = "MxGateway:ApiKeyPepper";
/// <summary>Gets whether database migrations should run on startup.</summary>
public bool RunMigrationsOnStartup { get; init; } = true;
}
@@ -0,0 +1,28 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class DashboardOptions
{
/// <summary>Gets whether the dashboard is enabled.</summary>
public bool Enabled { get; init; } = true;
/// <summary>Gets the dashboard URL path base.</summary>
public string PathBase { get; init; } = "/dashboard";
/// <summary>Gets whether dashboard access requires admin scope.</summary>
public bool RequireAdminScope { get; init; } = true;
/// <summary>Gets whether anonymous localhost access to dashboard is allowed.</summary>
public bool AllowAnonymousLocalhost { get; init; } = true;
/// <summary>Gets the dashboard snapshot update interval in milliseconds.</summary>
public int SnapshotIntervalMilliseconds { get; init; } = 1_000;
/// <summary>Gets the maximum number of recent faults to display.</summary>
public int RecentFaultLimit { get; init; } = 100;
/// <summary>Gets the maximum number of recent sessions to display.</summary>
public int RecentSessionLimit { get; init; } = 200;
/// <summary>Gets whether to show full tag values in the dashboard.</summary>
public bool ShowTagValues { get; init; }
}
@@ -0,0 +1,7 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveAuthenticationConfiguration(
string Mode,
string SqlitePath,
string PepperSecretName,
bool RunMigrationsOnStartup);
@@ -0,0 +1,11 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveDashboardConfiguration(
bool Enabled,
string PathBase,
bool RequireAdminScope,
bool AllowAnonymousLocalhost,
int SnapshotIntervalMilliseconds,
int RecentFaultLimit,
int RecentSessionLimit,
bool ShowTagValues);
@@ -0,0 +1,5 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveEventConfiguration(
int QueueCapacity,
string BackpressurePolicy);
@@ -0,0 +1,10 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveGatewayConfiguration(
EffectiveAuthenticationConfiguration Authentication,
EffectiveLdapConfiguration Ldap,
EffectiveWorkerConfiguration Worker,
EffectiveSessionConfiguration Sessions,
EffectiveEventConfiguration Events,
EffectiveDashboardConfiguration Dashboard,
EffectiveProtocolConfiguration Protocol);
@@ -0,0 +1,15 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveLdapConfiguration(
bool Enabled,
string Server,
int Port,
bool UseTls,
bool AllowInsecureLdap,
string SearchBase,
string ServiceAccountDn,
string ServiceAccountPassword,
string UserNameAttribute,
string DisplayNameAttribute,
string GroupAttribute,
string RequiredGroup);
@@ -0,0 +1,5 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveProtocolConfiguration(
uint WorkerProtocolVersion,
int MaxGrpcMessageBytes);
@@ -0,0 +1,9 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveSessionConfiguration(
int DefaultCommandTimeoutSeconds,
int MaxSessions,
int MaxPendingCommandsPerSession,
int DefaultLeaseSeconds,
int LeaseSweepIntervalSeconds,
bool AllowMultipleEventSubscribers);
@@ -0,0 +1,11 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveWorkerConfiguration(
string ExecutablePath,
string? WorkingDirectory,
string RequiredArchitecture,
int StartupTimeoutSeconds,
int ShutdownTimeoutSeconds,
int HeartbeatIntervalSeconds,
int HeartbeatGraceSeconds,
int MaxMessageBytes);
@@ -0,0 +1,8 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public enum EventBackpressurePolicy
{
FailFast,
DisconnectSubscriber
}
@@ -0,0 +1,14 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class EventOptions
{
/// <summary>
/// Gets the event queue capacity.
/// </summary>
public int QueueCapacity { get; init; } = 10_000;
/// <summary>
/// Gets the backpressure policy for event queue overflow.
/// </summary>
public EventBackpressurePolicy BackpressurePolicy { get; init; } = EventBackpressurePolicy.FailFast;
}
@@ -0,0 +1,67 @@
using Microsoft.Extensions.Options;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
/// <summary>Provides the effective gateway configuration with sensitive values redacted.</summary>
public sealed class GatewayConfigurationProvider(IOptions<GatewayOptions> options) : IGatewayConfigurationProvider
{
/// <summary>Marker string for redacted sensitive configuration values.</summary>
public const string RedactedValue = "[redacted]";
/// <inheritdoc />
public EffectiveGatewayConfiguration GetEffectiveConfiguration()
{
GatewayOptions value = options.Value;
return new EffectiveGatewayConfiguration(
Authentication: new EffectiveAuthenticationConfiguration(
Mode: value.Authentication.Mode.ToString(),
SqlitePath: value.Authentication.SqlitePath,
PepperSecretName: RedactedValue,
RunMigrationsOnStartup: value.Authentication.RunMigrationsOnStartup),
Ldap: new EffectiveLdapConfiguration(
Enabled: value.Ldap.Enabled,
Server: value.Ldap.Server,
Port: value.Ldap.Port,
UseTls: value.Ldap.UseTls,
AllowInsecureLdap: value.Ldap.AllowInsecureLdap,
SearchBase: value.Ldap.SearchBase,
ServiceAccountDn: value.Ldap.ServiceAccountDn,
ServiceAccountPassword: RedactedValue,
UserNameAttribute: value.Ldap.UserNameAttribute,
DisplayNameAttribute: value.Ldap.DisplayNameAttribute,
GroupAttribute: value.Ldap.GroupAttribute,
RequiredGroup: value.Ldap.RequiredGroup),
Worker: new EffectiveWorkerConfiguration(
ExecutablePath: value.Worker.ExecutablePath,
WorkingDirectory: value.Worker.WorkingDirectory,
RequiredArchitecture: value.Worker.RequiredArchitecture.ToString(),
StartupTimeoutSeconds: value.Worker.StartupTimeoutSeconds,
ShutdownTimeoutSeconds: value.Worker.ShutdownTimeoutSeconds,
HeartbeatIntervalSeconds: value.Worker.HeartbeatIntervalSeconds,
HeartbeatGraceSeconds: value.Worker.HeartbeatGraceSeconds,
MaxMessageBytes: value.Worker.MaxMessageBytes),
Sessions: new EffectiveSessionConfiguration(
DefaultCommandTimeoutSeconds: value.Sessions.DefaultCommandTimeoutSeconds,
MaxSessions: value.Sessions.MaxSessions,
MaxPendingCommandsPerSession: value.Sessions.MaxPendingCommandsPerSession,
DefaultLeaseSeconds: value.Sessions.DefaultLeaseSeconds,
LeaseSweepIntervalSeconds: value.Sessions.LeaseSweepIntervalSeconds,
AllowMultipleEventSubscribers: value.Sessions.AllowMultipleEventSubscribers),
Events: new EffectiveEventConfiguration(
QueueCapacity: value.Events.QueueCapacity,
BackpressurePolicy: value.Events.BackpressurePolicy.ToString()),
Dashboard: new EffectiveDashboardConfiguration(
Enabled: value.Dashboard.Enabled,
PathBase: value.Dashboard.PathBase,
RequireAdminScope: value.Dashboard.RequireAdminScope,
AllowAnonymousLocalhost: value.Dashboard.AllowAnonymousLocalhost,
SnapshotIntervalMilliseconds: value.Dashboard.SnapshotIntervalMilliseconds,
RecentFaultLimit: value.Dashboard.RecentFaultLimit,
RecentSessionLimit: value.Dashboard.RecentSessionLimit,
ShowTagValues: value.Dashboard.ShowTagValues),
Protocol: new EffectiveProtocolConfiguration(
value.Protocol.WorkerProtocolVersion,
value.Protocol.MaxGrpcMessageBytes));
}
}
@@ -0,0 +1,22 @@
using Microsoft.Extensions.Options;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public static class GatewayConfigurationServiceCollectionExtensions
{
/// <summary>Registers gateway configuration services in the dependency injection container.</summary>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddGatewayConfiguration(this IServiceCollection services)
{
services
.AddOptions<GatewayOptions>()
.BindConfiguration(GatewayOptions.SectionName)
.ValidateOnStart();
services.AddSingleton<IValidateOptions<GatewayOptions>, GatewayOptionsValidator>();
services.AddSingleton<IGatewayConfigurationProvider, GatewayConfigurationProvider>();
return services;
}
}
@@ -0,0 +1,45 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class GatewayOptions
{
public const string SectionName = "MxGateway";
/// <summary>
/// Gets authentication configuration options.
/// </summary>
public AuthenticationOptions Authentication { get; init; } = new();
public LdapOptions Ldap { get; init; } = new();
/// <summary>
/// Gets worker process configuration options.
/// </summary>
public WorkerOptions Worker { get; init; } = new();
/// <summary>
/// Gets session management configuration options.
/// </summary>
public SessionOptions Sessions { get; init; } = new();
/// <summary>
/// Gets event stream configuration options.
/// </summary>
public EventOptions Events { get; init; } = new();
/// <summary>
/// Gets dashboard configuration options.
/// </summary>
public DashboardOptions Dashboard { get; init; } = new();
/// <summary>
/// Gets protocol configuration options.
/// </summary>
public ProtocolOptions Protocol { get; init; } = new();
/// <summary>
/// Gets alarm-subsystem configuration options. Drives the gateway's
/// auto-subscribe-on-session-open hook; default values preserve legacy
/// behaviour (alarms disabled).
/// </summary>
public AlarmsOptions Alarms { get; init; } = new();
}
@@ -0,0 +1,321 @@
using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Contracts;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class GatewayOptionsValidator : IValidateOptions<GatewayOptions>
{
private const int MinimumMaxMessageBytes = 1024;
private const int MaximumMaxMessageBytes = 256 * 1024 * 1024;
/// <summary>
/// Validates gateway configuration options.
/// </summary>
/// <param name="name">Options name.</param>
/// <param name="options">Gateway options to validate.</param>
/// <returns>Validation result.</returns>
public ValidateOptionsResult Validate(string? name, GatewayOptions options)
{
List<string> failures = [];
ValidateAuthentication(options.Authentication, failures);
ValidateLdap(options.Ldap, failures);
ValidateWorker(options.Worker, failures);
ValidateSessions(options.Sessions, failures);
ValidateEvents(options.Events, failures);
ValidateDashboard(options.Dashboard, failures);
ValidateProtocol(options.Protocol, failures);
ValidateAlarms(options.Alarms, failures);
return failures.Count == 0
? ValidateOptionsResult.Success
: ValidateOptionsResult.Fail(failures);
}
private static void ValidateAuthentication(AuthenticationOptions options, List<string> failures)
{
if (!Enum.IsDefined(options.Mode))
{
failures.Add("MxGateway:Authentication:Mode must be a supported authentication mode.");
return;
}
if (options.Mode == AuthenticationMode.ApiKey)
{
AddIfBlank(
options.SqlitePath,
"MxGateway:Authentication:SqlitePath is required when API-key authentication is enabled.",
failures);
AddIfInvalidPath(
options.SqlitePath,
"MxGateway:Authentication:SqlitePath must be a valid filesystem path.",
failures);
AddIfBlank(
options.PepperSecretName,
"MxGateway:Authentication:PepperSecretName is required when API-key authentication is enabled.",
failures);
}
}
private static void ValidateLdap(LdapOptions options, List<string> failures)
{
if (!options.Enabled)
{
return;
}
AddIfBlank(options.Server, "MxGateway:Ldap:Server is required when LDAP login is enabled.", failures);
AddIfBlank(options.SearchBase, "MxGateway:Ldap:SearchBase is required when LDAP login is enabled.", failures);
AddIfBlank(
options.ServiceAccountDn,
"MxGateway:Ldap:ServiceAccountDn is required when LDAP login is enabled.",
failures);
AddIfBlank(
options.ServiceAccountPassword,
"MxGateway:Ldap:ServiceAccountPassword is required when LDAP login is enabled.",
failures);
AddIfBlank(
options.UserNameAttribute,
"MxGateway:Ldap:UserNameAttribute is required when LDAP login is enabled.",
failures);
AddIfBlank(
options.DisplayNameAttribute,
"MxGateway:Ldap:DisplayNameAttribute is required when LDAP login is enabled.",
failures);
AddIfBlank(
options.GroupAttribute,
"MxGateway:Ldap:GroupAttribute is required when LDAP login is enabled.",
failures);
AddIfBlank(
options.RequiredGroup,
"MxGateway:Ldap:RequiredGroup is required when LDAP login is enabled.",
failures);
AddIfNotPositive(options.Port, "MxGateway:Ldap:Port must be greater than zero.", failures);
if (!options.UseTls && !options.AllowInsecureLdap)
{
failures.Add("MxGateway:Ldap:AllowInsecureLdap must be true when UseTls is false.");
}
}
private static void ValidateWorker(WorkerOptions options, List<string> failures)
{
AddIfBlank(options.ExecutablePath, "MxGateway:Worker:ExecutablePath is required.", failures);
AddIfInvalidPath(
options.ExecutablePath,
"MxGateway:Worker:ExecutablePath must be a valid filesystem path.",
failures);
if (!string.IsNullOrWhiteSpace(options.ExecutablePath)
&& !string.Equals(Path.GetExtension(options.ExecutablePath), ".exe", StringComparison.OrdinalIgnoreCase))
{
failures.Add("MxGateway:Worker:ExecutablePath must point to a .exe file.");
}
if (!string.IsNullOrWhiteSpace(options.WorkingDirectory))
{
AddIfInvalidPath(
options.WorkingDirectory,
"MxGateway:Worker:WorkingDirectory must be a valid filesystem path.",
failures);
}
if (!Enum.IsDefined(options.RequiredArchitecture))
{
failures.Add("MxGateway:Worker:RequiredArchitecture must be a supported worker architecture.");
}
AddIfNotPositive(
options.StartupTimeoutSeconds,
"MxGateway:Worker:StartupTimeoutSeconds must be greater than zero.",
failures);
AddIfNotPositive(
options.StartupProbeRetryAttempts,
"MxGateway:Worker:StartupProbeRetryAttempts must be greater than zero.",
failures);
AddIfNotPositive(
options.StartupProbeRetryDelayMilliseconds,
"MxGateway:Worker:StartupProbeRetryDelayMilliseconds must be greater than zero.",
failures);
AddIfNotPositive(
options.PipeConnectAttemptTimeoutMilliseconds,
"MxGateway:Worker:PipeConnectAttemptTimeoutMilliseconds must be greater than zero.",
failures);
AddIfNotPositive(
options.ShutdownTimeoutSeconds,
"MxGateway:Worker:ShutdownTimeoutSeconds must be greater than zero.",
failures);
AddIfNotPositive(
options.HeartbeatIntervalSeconds,
"MxGateway:Worker:HeartbeatIntervalSeconds must be greater than zero.",
failures);
AddIfNotPositive(
options.HeartbeatGraceSeconds,
"MxGateway:Worker:HeartbeatGraceSeconds must be greater than zero.",
failures);
if (options.HeartbeatGraceSeconds < options.HeartbeatIntervalSeconds)
{
failures.Add(
"MxGateway:Worker:HeartbeatGraceSeconds must be greater than or equal to HeartbeatIntervalSeconds.");
}
if (options.MaxMessageBytes is < MinimumMaxMessageBytes or > MaximumMaxMessageBytes)
{
failures.Add(
$"MxGateway:Worker:MaxMessageBytes must be between {MinimumMaxMessageBytes} and {MaximumMaxMessageBytes}.");
}
}
private static void ValidateSessions(SessionOptions options, List<string> failures)
{
AddIfNotPositive(
options.DefaultCommandTimeoutSeconds,
"MxGateway:Sessions:DefaultCommandTimeoutSeconds must be greater than zero.",
failures);
AddIfNotPositive(options.MaxSessions, "MxGateway:Sessions:MaxSessions must be greater than zero.", failures);
AddIfNotPositive(
options.MaxPendingCommandsPerSession,
"MxGateway:Sessions:MaxPendingCommandsPerSession must be greater than zero.",
failures);
AddIfNotPositive(
options.DefaultLeaseSeconds,
"MxGateway:Sessions:DefaultLeaseSeconds must be greater than zero.",
failures);
AddIfNotPositive(
options.LeaseSweepIntervalSeconds,
"MxGateway:Sessions:LeaseSweepIntervalSeconds must be greater than zero.",
failures);
if (options.AllowMultipleEventSubscribers)
{
failures.Add(
"MxGateway:Sessions:AllowMultipleEventSubscribers is not supported until event fan-out is implemented.");
}
}
private static void ValidateEvents(EventOptions options, List<string> failures)
{
AddIfNotPositive(options.QueueCapacity, "MxGateway:Events:QueueCapacity must be greater than zero.", failures);
if (!Enum.IsDefined(options.BackpressurePolicy))
{
failures.Add("MxGateway:Events:BackpressurePolicy must be a supported backpressure policy.");
}
}
private static void ValidateDashboard(DashboardOptions options, List<string> failures)
{
if (options.Enabled)
{
AddIfBlank(options.PathBase, "MxGateway:Dashboard:PathBase is required when the dashboard is enabled.", failures);
if (!string.IsNullOrWhiteSpace(options.PathBase) && !options.PathBase.StartsWith('/'))
{
failures.Add("MxGateway:Dashboard:PathBase must start with '/'.");
}
}
AddIfNotPositive(
options.SnapshotIntervalMilliseconds,
"MxGateway:Dashboard:SnapshotIntervalMilliseconds must be greater than zero.",
failures);
AddIfNegative(
options.RecentFaultLimit,
"MxGateway:Dashboard:RecentFaultLimit must be greater than or equal to zero.",
failures);
AddIfNegative(
options.RecentSessionLimit,
"MxGateway:Dashboard:RecentSessionLimit must be greater than or equal to zero.",
failures);
}
private static void ValidateAlarms(AlarmsOptions options, List<string> failures)
{
if (!options.Enabled)
{
return;
}
// When the central alarm monitor is enabled, it needs either a canonical
// SubscriptionExpression or a DefaultArea to compose one from. Validating
// it at startup makes the misconfiguration fail-fast at boot, in line
// with every other section.
if (string.IsNullOrWhiteSpace(options.SubscriptionExpression)
&& string.IsNullOrWhiteSpace(options.DefaultArea))
{
failures.Add(
"MxGateway:Alarms requires either a non-blank SubscriptionExpression or a non-blank DefaultArea when Enabled is true.");
}
if (!string.IsNullOrWhiteSpace(options.SubscriptionExpression)
&& !options.SubscriptionExpression.StartsWith(@"\\", StringComparison.Ordinal))
{
failures.Add(
@"MxGateway:Alarms:SubscriptionExpression must start with '\\' (canonical \\<host>\Galaxy!<area> shape).");
}
}
private static void ValidateProtocol(ProtocolOptions options, List<string> failures)
{
if (options.WorkerProtocolVersion != GatewayContractInfo.WorkerProtocolVersion)
{
failures.Add(
$"MxGateway:Protocol:WorkerProtocolVersion must be {GatewayContractInfo.WorkerProtocolVersion}.");
}
if (options.MaxGrpcMessageBytes is < MinimumMaxMessageBytes or > MaximumMaxMessageBytes)
{
failures.Add(
$"MxGateway:Protocol:MaxGrpcMessageBytes must be between {MinimumMaxMessageBytes} and {MaximumMaxMessageBytes}.");
}
}
private static void AddIfBlank(string? value, string message, List<string> failures)
{
if (string.IsNullOrWhiteSpace(value))
{
failures.Add(message);
}
}
private static void AddIfNotPositive(int value, string message, List<string> failures)
{
if (value <= 0)
{
failures.Add(message);
}
}
private static void AddIfNegative(int value, string message, List<string> failures)
{
if (value < 0)
{
failures.Add(message);
}
}
private static void AddIfInvalidPath(string? value, string message, List<string> failures)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
try
{
_ = Path.GetFullPath(value);
}
catch (ArgumentException)
{
failures.Add(message);
}
catch (NotSupportedException)
{
failures.Add(message);
}
catch (PathTooLongException)
{
failures.Add(message);
}
}
}
@@ -0,0 +1,12 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
/// <summary>
/// Provides the effective gateway configuration, applying defaults and validations.
/// </summary>
public interface IGatewayConfigurationProvider
{
/// <summary>
/// Returns the validated and effective gateway configuration.
/// </summary>
EffectiveGatewayConfiguration GetEffectiveConfiguration();
}
@@ -0,0 +1,28 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class LdapOptions
{
public bool Enabled { get; init; } = true;
public string Server { get; init; } = "localhost";
public int Port { get; init; } = 3893;
public bool UseTls { get; init; }
public bool AllowInsecureLdap { get; init; } = true;
public string SearchBase { get; init; } = "dc=lmxopcua,dc=local";
public string ServiceAccountDn { get; init; } = "cn=serviceaccount,dc=lmxopcua,dc=local";
public string ServiceAccountPassword { get; init; } = "serviceaccount123";
public string UserNameAttribute { get; init; } = "cn";
public string DisplayNameAttribute { get; init; } = "cn";
public string GroupAttribute { get; init; } = "memberOf";
public string RequiredGroup { get; init; } = "GwAdmin";
}
@@ -0,0 +1,16 @@
using ZB.MOM.WW.MxGateway.Contracts;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
/// <summary>
/// Configuration options for the worker protocol version.
/// </summary>
public sealed class ProtocolOptions
{
/// <summary>
/// Gets or sets the worker protocol version.
/// </summary>
public uint WorkerProtocolVersion { get; init; } = GatewayContractInfo.WorkerProtocolVersion;
public int MaxGrpcMessageBytes { get; init; } = 16 * 1024 * 1024;
}
@@ -0,0 +1,28 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class SessionOptions
{
/// <summary>
/// Gets the default command timeout in seconds.
/// </summary>
public int DefaultCommandTimeoutSeconds { get; init; } = 30;
/// <summary>
/// Gets the maximum number of concurrent sessions.
/// </summary>
public int MaxSessions { get; init; } = 64;
/// <summary>
/// Gets the maximum number of pending commands per session.
/// </summary>
public int MaxPendingCommandsPerSession { get; init; } = 128;
public int DefaultLeaseSeconds { get; init; } = 1800;
public int LeaseSweepIntervalSeconds { get; init; } = 30;
/// <summary>
/// Gets a value indicating whether multiple event subscribers are allowed per session.
/// </summary>
public bool AllowMultipleEventSubscribers { get; init; }
}
@@ -0,0 +1,7 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public enum WorkerArchitecture
{
X86,
X64
}
@@ -0,0 +1,38 @@
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class WorkerOptions
{
/// <summary>The path to the worker executable.</summary>
public string ExecutablePath { get; init; } =
@"src\ZB.MOM.WW.MxGateway.Worker\bin\x86\Release\ZB.MOM.WW.MxGateway.Worker.exe";
/// <summary>The working directory for the worker process, or null to inherit.</summary>
public string? WorkingDirectory { get; init; }
/// <summary>The required processor architecture for the worker.</summary>
public WorkerArchitecture RequiredArchitecture { get; init; } = WorkerArchitecture.X86;
/// <summary>The maximum time in seconds for the worker to start.</summary>
public int StartupTimeoutSeconds { get; init; } = 30;
/// <summary>The number of retry attempts for the startup probe.</summary>
public int StartupProbeRetryAttempts { get; init; } = 3;
/// <summary>The delay in milliseconds between startup probe retries.</summary>
public int StartupProbeRetryDelayMilliseconds { get; init; } = 250;
/// <summary>The timeout in milliseconds for connecting to the worker pipe.</summary>
public int PipeConnectAttemptTimeoutMilliseconds { get; init; } = 2000;
/// <summary>The maximum time in seconds for graceful shutdown.</summary>
public int ShutdownTimeoutSeconds { get; init; } = 10;
/// <summary>The interval in seconds for worker heartbeats.</summary>
public int HeartbeatIntervalSeconds { get; init; } = 5;
/// <summary>The grace period in seconds after a heartbeat before considering the worker unresponsive.</summary>
public int HeartbeatGraceSeconds { get; init; } = 15;
/// <summary>The maximum message size in bytes for IPC communication.</summary>
public int MaxMessageBytes { get; init; } = 16 * 1024 * 1024;
}