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
@@ -1,14 +0,0 @@
namespace MxGateway.Contracts;
/// <summary>
/// Exposes version metadata shared by gateway components before generated
/// protobuf contracts are introduced.
/// </summary>
public static class GatewayContractInfo
{
public const uint GatewayProtocolVersion = 3;
public const uint WorkerProtocolVersion = 1;
public const string DefaultBackendName = "mxaccess-worker";
}
@@ -1,338 +0,0 @@
// <auto-generated>
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: mxaccess_gateway.proto
// </auto-generated>
#pragma warning disable 0414, 1591, 8981, 0612
#region Designer generated code
using grpc = global::Grpc.Core;
namespace MxGateway.Contracts.Proto {
/// <summary>
/// Public client API for MXAccess sessions hosted by the gateway.
/// </summary>
public static partial class MxAccessGateway
{
static readonly string __ServiceName = "mxaccess_gateway.v1.MxAccessGateway";
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static void __Helper_SerializeMessage(global::Google.Protobuf.IMessage message, grpc::SerializationContext context)
{
#if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION
if (message is global::Google.Protobuf.IBufferMessage)
{
context.SetPayloadLength(message.CalculateSize());
global::Google.Protobuf.MessageExtensions.WriteTo(message, context.GetBufferWriter());
context.Complete();
return;
}
#endif
context.Complete(global::Google.Protobuf.MessageExtensions.ToByteArray(message));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static class __Helper_MessageCache<T>
{
public static readonly bool IsBufferMessage = global::System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(global::Google.Protobuf.IBufferMessage)).IsAssignableFrom(typeof(T));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static T __Helper_DeserializeMessage<T>(grpc::DeserializationContext context, global::Google.Protobuf.MessageParser<T> parser) where T : global::Google.Protobuf.IMessage<T>
{
#if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION
if (__Helper_MessageCache<T>.IsBufferMessage)
{
return parser.ParseFrom(context.PayloadAsReadOnlySequence());
}
#endif
return parser.ParseFrom(context.PayloadAsNewBuffer());
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.OpenSessionRequest> __Marshaller_mxaccess_gateway_v1_OpenSessionRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.OpenSessionRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.OpenSessionReply> __Marshaller_mxaccess_gateway_v1_OpenSessionReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.OpenSessionReply.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.CloseSessionRequest> __Marshaller_mxaccess_gateway_v1_CloseSessionRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.CloseSessionRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.CloseSessionReply> __Marshaller_mxaccess_gateway_v1_CloseSessionReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.CloseSessionReply.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.MxCommandRequest> __Marshaller_mxaccess_gateway_v1_MxCommandRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.MxCommandRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.MxCommandReply> __Marshaller_mxaccess_gateway_v1_MxCommandReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.MxCommandReply.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.StreamEventsRequest> __Marshaller_mxaccess_gateway_v1_StreamEventsRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.StreamEventsRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.MxEvent> __Marshaller_mxaccess_gateway_v1_MxEvent = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.MxEvent.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest> __Marshaller_mxaccess_gateway_v1_AcknowledgeAlarmRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply> __Marshaller_mxaccess_gateway_v1_AcknowledgeAlarmReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest> __Marshaller_mxaccess_gateway_v1_QueryActiveAlarmsRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot> __Marshaller_mxaccess_gateway_v1_ActiveAlarmSnapshot = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::MxGateway.Contracts.Proto.OpenSessionRequest, global::MxGateway.Contracts.Proto.OpenSessionReply> __Method_OpenSession = new grpc::Method<global::MxGateway.Contracts.Proto.OpenSessionRequest, global::MxGateway.Contracts.Proto.OpenSessionReply>(
grpc::MethodType.Unary,
__ServiceName,
"OpenSession",
__Marshaller_mxaccess_gateway_v1_OpenSessionRequest,
__Marshaller_mxaccess_gateway_v1_OpenSessionReply);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::MxGateway.Contracts.Proto.CloseSessionRequest, global::MxGateway.Contracts.Proto.CloseSessionReply> __Method_CloseSession = new grpc::Method<global::MxGateway.Contracts.Proto.CloseSessionRequest, global::MxGateway.Contracts.Proto.CloseSessionReply>(
grpc::MethodType.Unary,
__ServiceName,
"CloseSession",
__Marshaller_mxaccess_gateway_v1_CloseSessionRequest,
__Marshaller_mxaccess_gateway_v1_CloseSessionReply);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::MxGateway.Contracts.Proto.MxCommandRequest, global::MxGateway.Contracts.Proto.MxCommandReply> __Method_Invoke = new grpc::Method<global::MxGateway.Contracts.Proto.MxCommandRequest, global::MxGateway.Contracts.Proto.MxCommandReply>(
grpc::MethodType.Unary,
__ServiceName,
"Invoke",
__Marshaller_mxaccess_gateway_v1_MxCommandRequest,
__Marshaller_mxaccess_gateway_v1_MxCommandReply);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::MxGateway.Contracts.Proto.StreamEventsRequest, global::MxGateway.Contracts.Proto.MxEvent> __Method_StreamEvents = new grpc::Method<global::MxGateway.Contracts.Proto.StreamEventsRequest, global::MxGateway.Contracts.Proto.MxEvent>(
grpc::MethodType.ServerStreaming,
__ServiceName,
"StreamEvents",
__Marshaller_mxaccess_gateway_v1_StreamEventsRequest,
__Marshaller_mxaccess_gateway_v1_MxEvent);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest, global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply> __Method_AcknowledgeAlarm = new grpc::Method<global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest, global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply>(
grpc::MethodType.Unary,
__ServiceName,
"AcknowledgeAlarm",
__Marshaller_mxaccess_gateway_v1_AcknowledgeAlarmRequest,
__Marshaller_mxaccess_gateway_v1_AcknowledgeAlarmReply);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest, global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot> __Method_QueryActiveAlarms = new grpc::Method<global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest, global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot>(
grpc::MethodType.ServerStreaming,
__ServiceName,
"QueryActiveAlarms",
__Marshaller_mxaccess_gateway_v1_QueryActiveAlarmsRequest,
__Marshaller_mxaccess_gateway_v1_ActiveAlarmSnapshot);
/// <summary>Service descriptor</summary>
public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor
{
get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.Services[0]; }
}
/// <summary>Base class for server-side implementations of MxAccessGateway</summary>
[grpc::BindServiceMethod(typeof(MxAccessGateway), "BindService")]
public abstract partial class MxAccessGatewayBase
{
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task<global::MxGateway.Contracts.Proto.OpenSessionReply> OpenSession(global::MxGateway.Contracts.Proto.OpenSessionRequest request, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task<global::MxGateway.Contracts.Proto.CloseSessionReply> CloseSession(global::MxGateway.Contracts.Proto.CloseSessionRequest request, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task<global::MxGateway.Contracts.Proto.MxCommandReply> Invoke(global::MxGateway.Contracts.Proto.MxCommandRequest request, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task StreamEvents(global::MxGateway.Contracts.Proto.StreamEventsRequest request, grpc::IServerStreamWriter<global::MxGateway.Contracts.Proto.MxEvent> responseStream, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task<global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply> AcknowledgeAlarm(global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest request, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task QueryActiveAlarms(global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest request, grpc::IServerStreamWriter<global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot> responseStream, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
}
/// <summary>Client for MxAccessGateway</summary>
public partial class MxAccessGatewayClient : grpc::ClientBase<MxAccessGatewayClient>
{
/// <summary>Creates a new client for MxAccessGateway</summary>
/// <param name="channel">The channel to use to make remote calls.</param>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public MxAccessGatewayClient(grpc::ChannelBase channel) : base(channel)
{
}
/// <summary>Creates a new client for MxAccessGateway that uses a custom <c>CallInvoker</c>.</summary>
/// <param name="callInvoker">The callInvoker to use to make remote calls.</param>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public MxAccessGatewayClient(grpc::CallInvoker callInvoker) : base(callInvoker)
{
}
/// <summary>Protected parameterless constructor to allow creation of test doubles.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
protected MxAccessGatewayClient() : base()
{
}
/// <summary>Protected constructor to allow creation of configured clients.</summary>
/// <param name="configuration">The client configuration.</param>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
protected MxAccessGatewayClient(ClientBaseConfiguration configuration) : base(configuration)
{
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.OpenSessionReply OpenSession(global::MxGateway.Contracts.Proto.OpenSessionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return OpenSession(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.OpenSessionReply OpenSession(global::MxGateway.Contracts.Proto.OpenSessionRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_OpenSession, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.OpenSessionReply> OpenSessionAsync(global::MxGateway.Contracts.Proto.OpenSessionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return OpenSessionAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.OpenSessionReply> OpenSessionAsync(global::MxGateway.Contracts.Proto.OpenSessionRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_OpenSession, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.CloseSessionReply CloseSession(global::MxGateway.Contracts.Proto.CloseSessionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return CloseSession(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.CloseSessionReply CloseSession(global::MxGateway.Contracts.Proto.CloseSessionRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_CloseSession, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.CloseSessionReply> CloseSessionAsync(global::MxGateway.Contracts.Proto.CloseSessionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return CloseSessionAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.CloseSessionReply> CloseSessionAsync(global::MxGateway.Contracts.Proto.CloseSessionRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_CloseSession, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.MxCommandReply Invoke(global::MxGateway.Contracts.Proto.MxCommandRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return Invoke(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.MxCommandReply Invoke(global::MxGateway.Contracts.Proto.MxCommandRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_Invoke, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.MxCommandReply> InvokeAsync(global::MxGateway.Contracts.Proto.MxCommandRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return InvokeAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.MxCommandReply> InvokeAsync(global::MxGateway.Contracts.Proto.MxCommandRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_Invoke, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncServerStreamingCall<global::MxGateway.Contracts.Proto.MxEvent> StreamEvents(global::MxGateway.Contracts.Proto.StreamEventsRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return StreamEvents(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncServerStreamingCall<global::MxGateway.Contracts.Proto.MxEvent> StreamEvents(global::MxGateway.Contracts.Proto.StreamEventsRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncServerStreamingCall(__Method_StreamEvents, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply AcknowledgeAlarm(global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return AcknowledgeAlarm(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply AcknowledgeAlarm(global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_AcknowledgeAlarm, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply> AcknowledgeAlarmAsync(global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return AcknowledgeAlarmAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply> AcknowledgeAlarmAsync(global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_AcknowledgeAlarm, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncServerStreamingCall<global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot> QueryActiveAlarms(global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return QueryActiveAlarms(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncServerStreamingCall<global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot> QueryActiveAlarms(global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncServerStreamingCall(__Method_QueryActiveAlarms, null, options, request);
}
/// <summary>Creates a new instance of client from given <c>ClientBaseConfiguration</c>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
protected override MxAccessGatewayClient NewInstance(ClientBaseConfiguration configuration)
{
return new MxAccessGatewayClient(configuration);
}
}
/// <summary>Creates service definition that can be registered with a server</summary>
/// <param name="serviceImpl">An object implementing the server-side handling logic.</param>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public static grpc::ServerServiceDefinition BindService(MxAccessGatewayBase serviceImpl)
{
return grpc::ServerServiceDefinition.CreateBuilder()
.AddMethod(__Method_OpenSession, serviceImpl.OpenSession)
.AddMethod(__Method_CloseSession, serviceImpl.CloseSession)
.AddMethod(__Method_Invoke, serviceImpl.Invoke)
.AddMethod(__Method_StreamEvents, serviceImpl.StreamEvents)
.AddMethod(__Method_AcknowledgeAlarm, serviceImpl.AcknowledgeAlarm)
.AddMethod(__Method_QueryActiveAlarms, serviceImpl.QueryActiveAlarms).Build();
}
/// <summary>Register service method with a service binder with or without implementation. Useful when customizing the service binding logic.
/// Note: this method is part of an experimental API that can change or be removed without any prior notice.</summary>
/// <param name="serviceBinder">Service methods will be bound by calling <c>AddMethod</c> on this object.</param>
/// <param name="serviceImpl">An object implementing the server-side handling logic.</param>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public static void BindService(grpc::ServiceBinderBase serviceBinder, MxAccessGatewayBase serviceImpl)
{
serviceBinder.AddMethod(__Method_OpenSession, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::MxGateway.Contracts.Proto.OpenSessionRequest, global::MxGateway.Contracts.Proto.OpenSessionReply>(serviceImpl.OpenSession));
serviceBinder.AddMethod(__Method_CloseSession, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::MxGateway.Contracts.Proto.CloseSessionRequest, global::MxGateway.Contracts.Proto.CloseSessionReply>(serviceImpl.CloseSession));
serviceBinder.AddMethod(__Method_Invoke, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::MxGateway.Contracts.Proto.MxCommandRequest, global::MxGateway.Contracts.Proto.MxCommandReply>(serviceImpl.Invoke));
serviceBinder.AddMethod(__Method_StreamEvents, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod<global::MxGateway.Contracts.Proto.StreamEventsRequest, global::MxGateway.Contracts.Proto.MxEvent>(serviceImpl.StreamEvents));
serviceBinder.AddMethod(__Method_AcknowledgeAlarm, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest, global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply>(serviceImpl.AcknowledgeAlarm));
serviceBinder.AddMethod(__Method_QueryActiveAlarms, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod<global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest, global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot>(serviceImpl.QueryActiveAlarms));
}
}
}
#endregion
@@ -1,626 +0,0 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MxGateway.Contracts;
using MxGateway.Contracts.Proto;
using MxGateway.Server.Configuration;
using MxGateway.Server.Grpc;
using MxGateway.Server.Metrics;
using MxGateway.Server.Security.Authentication;
using MxGateway.Server.Security.Authorization;
using MxGateway.Server.Sessions;
using MxGateway.Server.Workers;
using Xunit.Abstractions;
namespace MxGateway.IntegrationTests;
public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
{
private static readonly TimeSpan CommandTimeout = TimeSpan.FromSeconds(15);
private static readonly TimeSpan StreamShutdownTimeout = TimeSpan.FromSeconds(10);
/// <summary>
/// Verifies that a gateway session can register, add item, advise, and stream events from live MXAccess.
/// </summary>
[LiveMxAccessFact]
[Trait("Category", "LiveMxAccess")]
public async Task GatewaySession_WithLiveWorker_RegistersAdvisesStreamsDataAndCloses()
{
string workerExecutablePath = IntegrationTestEnvironment.ResolveLiveMxAccessWorkerExecutablePath();
Assert.True(
File.Exists(workerExecutablePath),
$"Live MXAccess worker executable was not found at {workerExecutablePath}. Build the worker or set {IntegrationTestEnvironment.LiveMxAccessWorkerExecutableVariableName}.");
TestWorkerProcessFactory processFactory = new(output);
await using GatewayServiceFixture fixture = new(workerExecutablePath, processFactory, output);
string? sessionId = null;
RecordingServerStreamWriter<MxEvent>? eventWriter = null;
Task? streamTask = null;
try
{
OpenSessionReply openReply = await fixture.Service.OpenSession(
new OpenSessionRequest
{
ClientSessionName = "live-mxaccess-smoke",
ClientCorrelationId = "live-open",
CommandTimeout = Duration.FromTimeSpan(CommandTimeout),
},
new TestServerCallContext()).ConfigureAwait(false);
sessionId = openReply.SessionId;
output.WriteLine($"OpenSession status={openReply.ProtocolStatus.Code} session={sessionId} worker_pid={openReply.WorkerProcessId}");
Assert.Equal(ProtocolStatusCode.Ok, openReply.ProtocolStatus.Code);
Assert.True(openReply.WorkerProcessId > 0);
eventWriter = new RecordingServerStreamWriter<MxEvent>();
streamTask = fixture.Service.StreamEvents(
new StreamEventsRequest { SessionId = sessionId },
eventWriter,
new TestServerCallContext());
MxCommandReply registerReply = await fixture.Service.Invoke(
CreateRegisterRequest(sessionId),
new TestServerCallContext()).ConfigureAwait(false);
LogReply("Register", registerReply);
Assert.Equal(ProtocolStatusCode.Ok, registerReply.ProtocolStatus.Code);
Assert.True(registerReply.Register.ServerHandle > 0);
MxCommandReply addItemReply = await fixture.Service.Invoke(
CreateAddItemRequest(sessionId, registerReply.Register.ServerHandle),
new TestServerCallContext()).ConfigureAwait(false);
LogReply("AddItem", addItemReply);
Assert.Equal(ProtocolStatusCode.Ok, addItemReply.ProtocolStatus.Code);
Assert.True(addItemReply.AddItem.ItemHandle > 0);
MxCommandReply adviseReply = await fixture.Service.Invoke(
CreateAdviseRequest(
sessionId,
registerReply.Register.ServerHandle,
addItemReply.AddItem.ItemHandle),
new TestServerCallContext()).ConfigureAwait(false);
LogReply("Advise", adviseReply);
Assert.Equal(ProtocolStatusCode.Ok, adviseReply.ProtocolStatus.Code);
MxEvent dataChange = await eventWriter
.WaitForFirstMessageAsync(IntegrationTestEnvironment.LiveMxAccessEventTimeout)
.ConfigureAwait(false);
LogEvent(dataChange);
Assert.Equal(MxEventFamily.OnDataChange, dataChange.Family);
Assert.Equal(sessionId, dataChange.SessionId);
Assert.Equal(registerReply.Register.ServerHandle, dataChange.ServerHandle);
Assert.Equal(addItemReply.AddItem.ItemHandle, dataChange.ItemHandle);
}
finally
{
try
{
if (!string.IsNullOrWhiteSpace(sessionId))
{
await CloseSessionAsync(fixture, sessionId).ConfigureAwait(false);
}
if (streamTask is not null)
{
await streamTask.WaitAsync(StreamShutdownTimeout).ConfigureAwait(false);
}
}
finally
{
await processFactory.WaitForProcessesAsync(StreamShutdownTimeout).ConfigureAwait(false);
}
}
}
private static MxCommandRequest CreateRegisterRequest(string sessionId)
{
return new MxCommandRequest
{
SessionId = sessionId,
ClientCorrelationId = "live-register",
Command = new MxCommand
{
Kind = MxCommandKind.Register,
Register = new RegisterCommand
{
ClientName = IntegrationTestEnvironment.LiveMxAccessClientName,
},
},
};
}
private static MxCommandRequest CreateAddItemRequest(
string sessionId,
int serverHandle)
{
return new MxCommandRequest
{
SessionId = sessionId,
ClientCorrelationId = "live-add-item",
Command = new MxCommand
{
Kind = MxCommandKind.AddItem,
AddItem = new AddItemCommand
{
ServerHandle = serverHandle,
ItemDefinition = IntegrationTestEnvironment.LiveMxAccessItem,
},
},
};
}
private static MxCommandRequest CreateAdviseRequest(
string sessionId,
int serverHandle,
int itemHandle)
{
return new MxCommandRequest
{
SessionId = sessionId,
ClientCorrelationId = "live-advise",
Command = new MxCommand
{
Kind = MxCommandKind.Advise,
Advise = new AdviseCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
},
};
}
private async Task CloseSessionAsync(
GatewayServiceFixture fixture,
string sessionId)
{
CloseSessionReply closeReply = await fixture.Service.CloseSession(
new CloseSessionRequest
{
SessionId = sessionId,
ClientCorrelationId = "live-close",
},
new TestServerCallContext()).ConfigureAwait(false);
output.WriteLine($"CloseSession status={closeReply.ProtocolStatus.Code} final_state={closeReply.FinalState}");
}
private void LogReply(
string method,
MxCommandReply reply)
{
output.WriteLine(
$"{method} status={reply.ProtocolStatus.Code} hresult={reply.Hresult} diagnostic={reply.DiagnosticMessage}");
foreach (MxStatusProxy status in reply.Statuses)
{
output.WriteLine(
$"{method} mxstatus success={status.Success} category={status.Category} detail={status.Detail} text={status.DiagnosticText}");
}
}
private void LogEvent(MxEvent dataChange)
{
output.WriteLine(
$"Event family={dataChange.Family} worker_sequence={dataChange.WorkerSequence} server_handle={dataChange.ServerHandle} item_handle={dataChange.ItemHandle} quality={dataChange.Quality}");
output.WriteLine(
$"Event value_type={dataChange.Value?.DataType} raw_status={dataChange.RawStatus}");
}
/// <summary>
/// Test fixture that assembles the gateway service with a worker process factory for live MXAccess testing.
/// </summary>
private sealed class GatewayServiceFixture : IAsyncDisposable
{
private readonly GatewayMetrics _metrics = new();
private readonly SessionRegistry _registry = new();
private readonly ILoggerFactory _loggerFactory;
/// <summary>
/// Initializes the fixture with worker executable path, factory, and test output helper.
/// </summary>
/// <param name="workerExecutablePath">Path to the worker process executable.</param>
/// <param name="processFactory">Factory for creating worker processes.</param>
/// <param name="output">Test output helper for logging.</param>
public GatewayServiceFixture(
string workerExecutablePath,
IWorkerProcessFactory processFactory,
ITestOutputHelper output)
{
IOptions<GatewayOptions> options = Options.Create(CreateOptions(workerExecutablePath));
_loggerFactory = LoggerFactory.Create(builder => builder.AddProvider(new TestOutputLoggerProvider(output)));
WorkerProcessLauncher launcher = new(
options,
processFactory,
new WorkerProcessStartedProbe(),
_metrics);
SessionWorkerClientFactory workerClientFactory = new(
launcher,
options,
_metrics,
_loggerFactory);
SessionManager sessionManager = new(
_registry,
workerClientFactory,
options,
_metrics,
logger: _loggerFactory.CreateLogger<SessionManager>());
MxAccessGrpcMapper mapper = new();
EventStreamService eventStreamService = new(
sessionManager,
options,
mapper,
_metrics,
_loggerFactory.CreateLogger<EventStreamService>());
Service = new MxAccessGatewayService(
sessionManager,
new GatewayRequestIdentityAccessor(),
new AllowAllConstraintEnforcer(),
new MxAccessGrpcRequestValidator(),
mapper,
eventStreamService,
_metrics,
_loggerFactory.CreateLogger<MxAccessGatewayService>());
}
/// <summary>
/// The assembled gateway service instance.
/// </summary>
public MxAccessGatewayService Service { get; }
/// <summary>
/// Disposes the fixture resources and closes all sessions.
/// </summary>
public async ValueTask DisposeAsync()
{
foreach (GatewaySession session in _registry.Snapshot())
{
await session.DisposeAsync().ConfigureAwait(false);
}
_loggerFactory.Dispose();
_metrics.Dispose();
}
private static GatewayOptions CreateOptions(string workerExecutablePath)
{
return new GatewayOptions
{
Worker = new WorkerOptions
{
ExecutablePath = workerExecutablePath,
StartupTimeoutSeconds = 30,
ShutdownTimeoutSeconds = 15,
HeartbeatIntervalSeconds = 5,
HeartbeatGraceSeconds = 15,
MaxMessageBytes = WorkerFrameProtocolOptions.DefaultMaxMessageBytes,
RequiredArchitecture = WorkerArchitecture.X86,
},
Sessions = new SessionOptions
{
DefaultCommandTimeoutSeconds = 15,
MaxSessions = 1,
},
Events = new EventOptions
{
QueueCapacity = 32,
},
};
}
}
/// <summary>
/// Gathers messages written to a server stream for test inspection.
/// </summary>
private sealed class RecordingServerStreamWriter<T> : IServerStreamWriter<T>
{
private readonly object syncRoot = new();
private readonly TaskCompletionSource<T> firstMessage = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly List<T> messages = [];
/// <summary>
/// All messages that have been written to the stream.
/// </summary>
public IReadOnlyList<T> Messages
{
get
{
lock (syncRoot)
{
return messages.ToArray();
}
}
}
/// <summary>
/// Inherited write options.
/// </summary>
public WriteOptions? WriteOptions { get; set; }
/// <summary>
/// Records the message and completes the first-message task.
/// </summary>
/// <param name="message">The message to write.</param>
public Task WriteAsync(T message)
{
lock (syncRoot)
{
messages.Add(message);
}
firstMessage.TrySetResult(message);
return Task.CompletedTask;
}
/// <summary>
/// Waits for the first message up to the specified timeout.
/// </summary>
/// <param name="timeout">The maximum time to wait.</param>
/// <returns>The first message written to the stream.</returns>
public async Task<T> WaitForFirstMessageAsync(TimeSpan timeout)
{
return await firstMessage.Task.WaitAsync(timeout).ConfigureAwait(false);
}
}
/// <summary>
/// Mock server call context for testing gRPC calls.
/// </summary>
private sealed class TestServerCallContext(CancellationToken cancellationToken = default) : ServerCallContext
{
private readonly Metadata requestHeaders = [];
private readonly Metadata responseTrailers = [];
private readonly Dictionary<object, object> userState = [];
private Status status;
private WriteOptions? writeOptions;
/// <inheritdoc />
protected override string MethodCore => "/mxaccess_gateway.v1.MxAccessGateway/Test";
/// <inheritdoc />
protected override string HostCore => "localhost";
/// <inheritdoc />
protected override string PeerCore => "ipv4:127.0.0.1:5000";
/// <inheritdoc />
protected override DateTime DeadlineCore => DateTime.UtcNow.AddMinutes(1);
/// <inheritdoc />
protected override Metadata RequestHeadersCore => requestHeaders;
/// <inheritdoc />
protected override CancellationToken CancellationTokenCore => cancellationToken;
/// <inheritdoc />
protected override Metadata ResponseTrailersCore => responseTrailers;
/// <inheritdoc />
protected override Status StatusCore
{
get => status;
set => status = value;
}
/// <inheritdoc />
protected override WriteOptions? WriteOptionsCore
{
get => writeOptions;
set => writeOptions = value;
}
/// <inheritdoc />
protected override AuthContext AuthContextCore { get; } = new(
string.Empty,
new Dictionary<string, List<AuthProperty>>(StringComparer.Ordinal));
/// <inheritdoc />
protected override IDictionary<object, object> UserStateCore => userState;
/// <inheritdoc />
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)
{
return Task.CompletedTask;
}
/// <inheritdoc />
protected override ContextPropagationToken CreatePropagationTokenCore(
ContextPropagationOptions? options)
{
throw new NotSupportedException();
}
}
/// <summary>
/// Factory that launches worker processes and records their outputs for testing.
/// </summary>
private sealed class TestWorkerProcessFactory(ITestOutputHelper output) : IWorkerProcessFactory
{
private readonly ConcurrentBag<TestWorkerProcess> processes = [];
/// <inheritdoc />
public IWorkerProcess Start(ProcessStartInfo startInfo)
{
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
Process process = new()
{
StartInfo = startInfo,
EnableRaisingEvents = true,
};
process.OutputDataReceived += (_, args) => WriteWorkerOutput("stdout", args.Data);
process.ErrorDataReceived += (_, args) => WriteWorkerOutput("stderr", args.Data);
if (!process.Start())
{
process.Dispose();
throw new InvalidOperationException("Worker process failed to start.");
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();
TestWorkerProcess workerProcess = new(process);
processes.Add(workerProcess);
output.WriteLine($"WorkerProcess started pid={workerProcess.Id} path={startInfo.FileName}");
return workerProcess;
}
/// <inheritdoc />
public async Task WaitForProcessesAsync(TimeSpan timeout)
{
foreach (TestWorkerProcess process in processes)
{
if (process.HasExited)
{
output.WriteLine($"WorkerProcess exited pid={process.Id} exit_code={process.ExitCode}");
continue;
}
using CancellationTokenSource timeoutCancellation = new(timeout);
await process.WaitForExitAsync(timeoutCancellation.Token).ConfigureAwait(false);
output.WriteLine($"WorkerProcess exited pid={process.Id} exit_code={process.ExitCode}");
}
}
private void WriteWorkerOutput(
string streamName,
string? line)
{
if (!string.IsNullOrWhiteSpace(line))
{
output.WriteLine($"worker_{streamName}: {line}");
}
}
}
/// <summary>
/// Adapter wrapping a System.Diagnostics.Process as IWorkerProcess for testing.
/// </summary>
private sealed class TestWorkerProcess(Process process) : IWorkerProcess
{
/// <inheritdoc />
public int Id => process.Id;
/// <inheritdoc />
public bool HasExited => process.HasExited;
/// <inheritdoc />
public int? ExitCode => process.HasExited ? process.ExitCode : null;
/// <inheritdoc />
public async ValueTask WaitForExitAsync(CancellationToken cancellationToken)
{
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public void Kill(bool entireProcessTree)
{
process.Kill(entireProcessTree);
}
/// <inheritdoc />
public void Dispose()
{
process.Dispose();
}
}
/// <summary>
/// Logger provider that writes all output to the test output helper.
/// </summary>
private sealed class TestOutputLoggerProvider(ITestOutputHelper output) : ILoggerProvider
{
/// <inheritdoc />
public ILogger CreateLogger(string categoryName)
{
return new TestOutputLogger(output, categoryName);
}
/// <inheritdoc />
public void Dispose()
{
}
}
/// <summary>
/// Logger that writes messages to the test output helper.
/// </summary>
private sealed class TestOutputLogger(
ITestOutputHelper output,
string categoryName) : ILogger
{
/// <inheritdoc />
public IDisposable? BeginScope<TState>(TState state)
where TState : notnull
{
return null;
}
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel)
{
return logLevel >= LogLevel.Information;
}
/// <inheritdoc />
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
output.WriteLine($"{logLevel} {categoryName}: {formatter(state, exception)}");
if (exception is not null)
{
output.WriteLine(exception.ToString());
}
}
}
private sealed class AllowAllConstraintEnforcer : IConstraintEnforcer
{
public Task<ConstraintFailure?> CheckReadTagAsync(
ApiKeyIdentity? identity,
string tagAddress,
CancellationToken cancellationToken) => Task.FromResult<ConstraintFailure?>(null);
public Task<ConstraintFailure?> CheckReadHandleAsync(
ApiKeyIdentity? identity,
GatewaySession session,
int serverHandle,
int itemHandle,
CancellationToken cancellationToken) => Task.FromResult<ConstraintFailure?>(null);
public Task<ConstraintFailure?> CheckWriteHandleAsync(
ApiKeyIdentity? identity,
GatewaySession session,
int serverHandle,
int itemHandle,
CancellationToken cancellationToken) => Task.FromResult<ConstraintFailure?>(null);
public Task RecordDenialAsync(
ApiKeyIdentity? identity,
string commandKind,
string target,
ConstraintFailure failure,
CancellationToken cancellationToken) => Task.CompletedTask;
}
}
@@ -1,48 +0,0 @@
namespace MxGateway.Server.Configuration;
/// <summary>
/// Per-gateway alarm-subsystem configuration. Drives the auto-subscribe
/// hook in <see cref="Sessions.SessionManager"/>: when
/// <see cref="Enabled"/> is true and a session reaches Ready, the
/// manager issues a <c>SubscribeAlarmsCommand</c> to the worker with
/// the configured <see cref="SubscriptionExpression"/>.
/// </summary>
/// <remarks>
/// Defaults preserve current behaviour (alarms 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 doesn't
/// accept the database name as the provider).
/// </remarks>
public sealed class AlarmsOptions
{
/// <summary>Gate the auto-subscribe hook on session open. Default false.</summary>
public bool Enabled { get; init; }
/// <summary>
/// AVEVA alarm-subscription expression. 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 session open
/// fails 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>
/// If true, an auto-subscribe failure faults the session. If false
/// (default), the failure is logged and the session remains Ready —
/// alarm-side commands return "not subscribed" but data subscriptions
/// work normally.
/// </summary>
public bool RequireSubscribeOnOpen { get; init; }
}
@@ -1,69 +0,0 @@
@inherits LayoutComponentBase
@inject IOptions<GatewayOptions> GatewayOptions
<div class="dashboard-shell">
<nav class="navbar navbar-expand-lg bg-body border-bottom dashboard-navbar">
<div class="container-fluid">
<a class="navbar-brand" href="">MXAccess Gateway</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#dashboardNav"
aria-controls="dashboardNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="dashboardNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">Overview</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="sessions">Sessions</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="workers">Workers</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="events">Events</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="galaxy">Galaxy</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="apikeys">API Keys</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="settings">Settings</NavLink>
</li>
</ul>
<AuthorizeView>
<Authorized Context="authState">
<div class="d-flex align-items-center gap-2">
<span class="navbar-text">@authState.User.Identity?.Name</span>
<form method="post" action="@DashboardPath("/logout")">
<AntiforgeryToken />
<button class="btn btn-outline-secondary btn-sm" type="submit">Sign out</button>
</form>
</div>
</Authorized>
<NotAuthorized>
<a class="btn btn-outline-secondary btn-sm" href="@DashboardPath("/login")">Sign in</a>
</NotAuthorized>
</AuthorizeView>
</div>
</div>
</nav>
<main class="container-fluid dashboard-content">
@Body
</main>
</div>
@code {
private string DashboardPath(string relativePath)
{
string pathBase = GatewayOptions.Value.Dashboard.PathBase.TrimEnd('/');
if (string.IsNullOrWhiteSpace(pathBase))
{
pathBase = "/dashboard";
}
return $"{pathBase}{relativePath}";
}
}
@@ -1,17 +0,0 @@
<span class="badge @CssClass">@Text</span>
@code {
[Parameter]
public string? Text { get; set; }
private string CssClass => Text switch
{
"Ready" or "Healthy" => "text-bg-success",
"Creating" or "StartingWorker" or "WaitingForPipe" or "InitializingWorker" or "Closing" => "text-bg-info",
"Closed" => "text-bg-secondary",
"Stale" => "text-bg-warning",
"Faulted" or "Unavailable" => "text-bg-danger",
"Unknown" => "text-bg-light text-dark border",
_ => "text-bg-light text-dark border"
};
}
@@ -1,13 +0,0 @@
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.Extensions.Options
@using MxGateway.Contracts.Proto
@using MxGateway.Server.Configuration
@using MxGateway.Server.Dashboard
@using MxGateway.Server.Dashboard.Components.Layout
@using MxGateway.Server.Dashboard.Components.Shared
@using MxGateway.Server.Security.Authorization
@using MxGateway.Server.Workers
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@@ -1,23 +0,0 @@
namespace MxGateway.Server.Galaxy;
/// <summary>
/// Connection settings for the AVEVA System Platform Galaxy Repository (ZB) database.
/// Bound to the <c>MxGateway:Galaxy</c> configuration section.
/// </summary>
public sealed class GalaxyRepositoryOptions
{
public const string SectionName = "MxGateway:Galaxy";
/// <summary>The SQL Server connection string for the Galaxy Repository database.</summary>
public string ConnectionString { get; init; } =
"Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;";
/// <summary>The timeout in seconds for SQL commands executed against the Galaxy Repository.</summary>
public int CommandTimeoutSeconds { get; init; } = 60;
/// <summary>
/// Interval (seconds) between background refreshes of the dashboard Galaxy summary
/// cache. SQL is hit at most once per interval regardless of dashboard render rate.
/// </summary>
public int DashboardRefreshIntervalSeconds { get; init; } = 30;
}
@@ -1,33 +0,0 @@
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Options;
using MxGateway.Server.Configuration;
namespace MxGateway.Server.Security.Authentication;
/// <summary>
/// Factory for creating SQLite connections to the authentication store.
/// </summary>
public sealed class AuthSqliteConnectionFactory(IOptions<GatewayOptions> options)
{
/// <summary>
/// Creates and configures a SQLite connection to the auth database.
/// </summary>
public SqliteConnection CreateConnection()
{
string sqlitePath = options.Value.Authentication.SqlitePath;
string? directory = Path.GetDirectoryName(sqlitePath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
SqliteConnectionStringBuilder builder = new()
{
DataSource = sqlitePath,
Mode = SqliteOpenMode.ReadWriteCreate
};
return new SqliteConnection(builder.ToString());
}
}
@@ -1,13 +0,0 @@
namespace MxGateway.Server.Security.Authorization;
public static class GatewayScopes
{
public const string SessionOpen = "session:open";
public const string SessionClose = "session:close";
public const string InvokeRead = "invoke:read";
public const string InvokeWrite = "invoke:write";
public const string InvokeSecure = "invoke:secure";
public const string EventsRead = "events:read";
public const string MetadataRead = "metadata:read";
public const string Admin = "admin";
}
@@ -1,41 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MxGateway.Contracts.Proto;
namespace MxGateway.Server.Sessions;
/// <summary>
/// PR A.6 / A.7 — gateway-side dispatcher for the alarm-RPC surface.
/// Bridges the public <c>AcknowledgeAlarm</c> + <c>QueryActiveAlarms</c>
/// gRPC handlers to the worker process that hosts
/// <c>IMxAccessAlarmConsumer</c>.
/// </summary>
/// <remarks>
/// <para>
/// Production implementations live in <c>WorkerAlarmRpcDispatcher</c>
/// (this PR ships a not-yet-wired default that returns a clear
/// worker-pending diagnostic) and route through the existing
/// worker-pipe IPC. Tests inject a fake to exercise the gateway
/// handler shape without spinning up a worker process.
/// </para>
/// <para>
/// The dispatcher is session-scoped: every call resolves the
/// session and forwards to that session's worker. The handler
/// constructs the <see cref="AcknowledgeAlarmReply"/> /
/// <see cref="ActiveAlarmSnapshot"/> stream from the dispatcher's
/// output without further translation.
/// </para>
/// </remarks>
public interface IAlarmRpcDispatcher
{
/// <summary>Forward an Acknowledge to the worker that owns the session.</summary>
Task<AcknowledgeAlarmReply> AcknowledgeAsync(
AcknowledgeAlarmRequest request,
CancellationToken cancellationToken);
/// <summary>Walk active alarms on the worker that owns the session.</summary>
IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync(
QueryActiveAlarmsRequest request,
CancellationToken cancellationToken);
}
@@ -1,52 +0,0 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using MxGateway.Contracts.Proto;
using MxGateway.Server.Grpc;
namespace MxGateway.Server.Sessions;
/// <summary>
/// PR A.6 / A.7 — default <see cref="IAlarmRpcDispatcher"/> shipped while
/// the worker-side AlarmClient event subscription is gated on dev-rig
/// validation. Acknowledges with a structured "worker-pending"
/// diagnostic and yields an empty active-alarm stream.
/// </summary>
/// <remarks>
/// <para>
/// Replaces the inline diagnostic strings in
/// <c>MxAccessGatewayService.AcknowledgeAlarm</c> /
/// <c>QueryActiveAlarms</c> from PR A.3 with an injectable seam.
/// When the worker dispatcher (PR A.6/A.7 dev-rig follow-up) lands,
/// <c>WorkerAlarmRpcDispatcher</c> replaces this implementation in
/// the DI container and the same handler shape comes alive without
/// further changes to the public RPC surface.
/// </para>
/// </remarks>
public sealed class NotWiredAlarmRpcDispatcher : IAlarmRpcDispatcher
{
/// <inheritdoc />
public Task<AcknowledgeAlarmReply> AcknowledgeAsync(
AcknowledgeAlarmRequest request,
CancellationToken cancellationToken)
{
return Task.FromResult(new AcknowledgeAlarmReply
{
SessionId = request.SessionId,
CorrelationId = request.ClientCorrelationId,
ProtocolStatus = MxAccessGrpcMapper.Ok("AcknowledgeAlarm accepted; worker dispatch pending dev-rig wiring."),
DiagnosticMessage = "Gateway-side AcknowledgeAlarm accepted; the worker-side AlarmClient consumer (PR A.5) is in place but the dispatcher hookup is gated on validating the AVEVA alarm-provider event subscription on the dev rig.",
});
}
/// <inheritdoc />
#pragma warning disable CS1998 // Async method lacks 'await' operators — empty stream is intentional.
public async IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync(
QueryActiveAlarmsRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
yield break;
}
#pragma warning restore CS1998
}
@@ -1,231 +0,0 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Google.Protobuf.WellKnownTypes;
using MxGateway.Contracts.Proto;
using MxGateway.Server.Grpc;
namespace MxGateway.Server.Sessions;
/// <summary>
/// Production <see cref="IAlarmRpcDispatcher"/> that routes the public
/// <c>AcknowledgeAlarm</c> + <c>QueryActiveAlarms</c> RPCs through the
/// worker pipe IPC. Replaces <see cref="NotWiredAlarmRpcDispatcher"/>
/// once the worker AlarmCommandHandler is wired in.
/// </summary>
/// <remarks>
/// <para>
/// <c>QueryActiveAlarms</c> is fully wired: issues a
/// <see cref="QueryActiveAlarmsCommand"/> over the pipe and yields
/// each <see cref="ActiveAlarmSnapshot"/> from the
/// <see cref="QueryActiveAlarmsReplyPayload"/>.
/// </para>
/// <para>
/// <c>AcknowledgeAlarm</c> is partially wired: the public RPC's
/// <see cref="AcknowledgeAlarmRequest.AlarmFullReference"/> is a
/// <c>Provider!Group.Tag</c> string, but the worker's wnwrap consumer
/// acks by GUID. When the supplied reference parses as a GUID
/// directly, the dispatcher forwards it as-is. Otherwise it
/// returns an <c>Unimplemented</c> diagnostic. Resolving
/// reference→GUID requires an additional worker IPC command
/// (e.g. <c>AlarmAckByName</c> wrapping
/// <c>wwAlarmConsumerClass.AlarmAckByName</c>) and is tracked as
/// a follow-up.
/// </para>
/// </remarks>
public sealed class WorkerAlarmRpcDispatcher : IAlarmRpcDispatcher
{
private readonly ISessionRegistry sessionRegistry;
private readonly TimeProvider timeProvider;
public WorkerAlarmRpcDispatcher(ISessionRegistry sessionRegistry, TimeProvider? timeProvider = null)
{
this.sessionRegistry = sessionRegistry ?? throw new System.ArgumentNullException(nameof(sessionRegistry));
this.timeProvider = timeProvider ?? TimeProvider.System;
}
/// <summary>
/// Parse a full alarm reference of the form <c>Provider!Group.Tag</c>
/// into its components. Convention: the first <c>!</c> separates
/// provider from <c>Group.Tag</c>; the first <c>.</c> after the
/// <c>!</c> separates group from tag (the tag itself may contain
/// more dots — e.g. <c>TestMachine_001.TestAlarm001</c>).
/// </summary>
/// <returns>true on a well-formed reference; false otherwise.</returns>
public static bool TryParseAlarmReference(
string? reference,
out string providerName,
out string groupName,
out string alarmName)
{
providerName = string.Empty;
groupName = string.Empty;
alarmName = string.Empty;
if (string.IsNullOrWhiteSpace(reference)) return false;
int bang = reference!.IndexOf('!');
if (bang <= 0 || bang == reference.Length - 1) return false;
string left = reference[..bang];
string right = reference[(bang + 1)..];
int dot = right.IndexOf('.');
if (dot <= 0 || dot == right.Length - 1) return false;
providerName = left;
groupName = right[..dot];
alarmName = right[(dot + 1)..];
return true;
}
/// <inheritdoc />
public async Task<AcknowledgeAlarmReply> AcknowledgeAsync(
AcknowledgeAlarmRequest request,
CancellationToken cancellationToken)
{
if (request is null) throw new System.ArgumentNullException(nameof(request));
if (!sessionRegistry.TryGet(request.SessionId, out GatewaySession session))
{
return new AcknowledgeAlarmReply
{
SessionId = request.SessionId,
CorrelationId = request.ClientCorrelationId,
ProtocolStatus = MxAccessGrpcMapper.SessionNotFound(
$"Session '{request.SessionId}' not found."),
DiagnosticMessage = "AcknowledgeAlarm: session not found.",
};
}
WorkerCommand workerCommand;
if (System.Guid.TryParse(request.AlarmFullReference, out System.Guid guid))
{
workerCommand = new WorkerCommand
{
Command = new MxCommand
{
Kind = MxCommandKind.AcknowledgeAlarm,
AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand
{
AlarmGuid = guid.ToString(),
Comment = request.Comment ?? string.Empty,
OperatorUser = request.OperatorUser ?? string.Empty,
// Operator node/domain/full-name are not on the public
// RPC surface today; pass empty strings so the worker
// honours the existing AcknowledgeAlarmCommand schema.
OperatorNode = string.Empty,
OperatorDomain = string.Empty,
OperatorFullName = string.Empty,
},
},
EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()),
};
}
else if (TryParseAlarmReference(
request.AlarmFullReference,
out string providerName,
out string groupName,
out string alarmName))
{
workerCommand = new WorkerCommand
{
Command = new MxCommand
{
Kind = MxCommandKind.AcknowledgeAlarmByName,
AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand
{
AlarmName = alarmName,
ProviderName = providerName,
GroupName = groupName,
Comment = request.Comment ?? string.Empty,
OperatorUser = request.OperatorUser ?? string.Empty,
OperatorNode = string.Empty,
OperatorDomain = string.Empty,
OperatorFullName = string.Empty,
},
},
EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()),
};
}
else
{
return new AcknowledgeAlarmReply
{
SessionId = request.SessionId,
CorrelationId = request.ClientCorrelationId,
ProtocolStatus = new ProtocolStatus
{
Code = ProtocolStatusCode.InvalidRequest,
Message = "AlarmFullReference must be a canonical GUID or 'Provider!Group.Tag' format.",
},
DiagnosticMessage = $"AcknowledgeAlarm received unrecognized reference '{request.AlarmFullReference}'.",
};
}
WorkerCommandReply workerReply = await session.InvokeAsync(workerCommand, cancellationToken)
.ConfigureAwait(false);
MxCommandReply mxReply = workerReply.Reply ?? new MxCommandReply
{
ProtocolStatus = new ProtocolStatus
{
Code = ProtocolStatusCode.ProtocolViolation,
Message = "Worker reply did not include an MxCommandReply.",
},
};
AcknowledgeAlarmReply reply = new AcknowledgeAlarmReply
{
SessionId = request.SessionId,
CorrelationId = request.ClientCorrelationId,
ProtocolStatus = mxReply.ProtocolStatus ?? MxAccessGrpcMapper.Ok(),
DiagnosticMessage = mxReply.DiagnosticMessage ?? string.Empty,
};
if (mxReply.HasHresult)
{
reply.Hresult = mxReply.Hresult;
}
return reply;
}
/// <inheritdoc />
public async IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync(
QueryActiveAlarmsRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
if (request is null) throw new System.ArgumentNullException(nameof(request));
if (!sessionRegistry.TryGet(request.SessionId, out GatewaySession session))
{
yield break;
}
WorkerCommand workerCommand = new WorkerCommand
{
Command = new MxCommand
{
Kind = MxCommandKind.QueryActiveAlarms,
QueryActiveAlarmsCommand = new QueryActiveAlarmsCommand
{
AlarmFilterPrefix = request.AlarmFilterPrefix ?? string.Empty,
},
},
EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()),
};
WorkerCommandReply workerReply = await session.InvokeAsync(workerCommand, cancellationToken)
.ConfigureAwait(false);
MxCommandReply? mxReply = workerReply.Reply;
if (mxReply?.ProtocolStatus?.Code != ProtocolStatusCode.Ok) yield break;
QueryActiveAlarmsReplyPayload? payload = mxReply.QueryActiveAlarms;
if (payload is null) yield break;
foreach (ActiveAlarmSnapshot snapshot in payload.Snapshots)
{
cancellationToken.ThrowIfCancellationRequested();
yield return snapshot;
}
}
}
@@ -1,39 +0,0 @@
namespace MxGateway.Server.Workers;
/// <summary>Configurable options for worker client behavior.</summary>
public sealed class WorkerClientOptions
{
/// <summary>Default maximum age of a heartbeat before the client enters faulted state.</summary>
public static readonly TimeSpan DefaultHeartbeatGrace = TimeSpan.FromSeconds(15);
/// <summary>Default interval for checking heartbeat staleness.</summary>
public static readonly TimeSpan DefaultHeartbeatCheckInterval = TimeSpan.FromSeconds(1);
/// <summary>Default timeout when the event queue is full.</summary>
public static readonly TimeSpan DefaultEventChannelFullModeTimeout = TimeSpan.FromSeconds(5);
/// <summary>Initializes options with default values.</summary>
public WorkerClientOptions()
{
HeartbeatGrace = DefaultHeartbeatGrace;
HeartbeatCheckInterval = DefaultHeartbeatCheckInterval;
EventChannelCapacity = 1_024;
EventChannelFullModeTimeout = DefaultEventChannelFullModeTimeout;
MaxPendingCommands = 128;
}
/// <summary>Maximum allowed age of the last heartbeat before faulting the client.</summary>
public TimeSpan HeartbeatGrace { get; init; }
/// <summary>Interval at which to check for heartbeat expiration.</summary>
public TimeSpan HeartbeatCheckInterval { get; init; }
/// <summary>Maximum number of events buffered before backpressure is applied.</summary>
public int EventChannelCapacity { get; init; }
/// <summary>Time to wait for the event queue to drain before faulting.</summary>
public TimeSpan EventChannelFullModeTimeout { get; init; }
/// <summary>Maximum number of concurrent pending commands.</summary>
public int MaxPendingCommands { get; init; }
}
@@ -1,165 +0,0 @@
:root {
--mxgw-surface: #f7f8fa;
--mxgw-border: #d8dee6;
--mxgw-ink-muted: #667085;
--mxgw-accent: #146c64;
}
.dashboard-body {
background: var(--mxgw-surface);
color: #1f2933;
}
.dashboard-navbar {
min-height: 3.5rem;
}
.dashboard-content {
padding: 1.25rem;
}
.dashboard-page-header {
align-items: center;
display: flex;
gap: 1rem;
justify-content: space-between;
margin-bottom: 1rem;
}
.dashboard-page-header h1,
.section-heading h2 {
font-size: 1.35rem;
font-weight: 650;
letter-spacing: 0;
margin: 0;
}
.section-heading {
margin-bottom: .75rem;
}
.dashboard-section {
background: #fff;
border-top: 1px solid var(--mxgw-border);
margin-top: 1rem;
padding: 1rem 0 0;
}
.metric-grid {
display: grid;
gap: .75rem;
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
}
.metric-grid.compact {
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
}
.metric-card {
border-color: var(--mxgw-border);
border-radius: .375rem;
box-shadow: none;
}
.metric-label {
color: var(--mxgw-ink-muted);
font-size: .78rem;
font-weight: 650;
letter-spacing: 0;
text-transform: uppercase;
}
.metric-value {
color: var(--mxgw-accent);
font-size: 1.7rem;
font-weight: 700;
letter-spacing: 0;
line-height: 1.25;
overflow-wrap: anywhere;
}
.metric-detail {
color: var(--mxgw-ink-muted);
font-size: .85rem;
overflow-wrap: anywhere;
}
.dashboard-table {
--bs-table-bg: #fff;
border-color: var(--mxgw-border);
margin-bottom: 0;
}
.dashboard-table th {
color: #344054;
font-weight: 650;
white-space: nowrap;
}
.dashboard-table td {
max-width: 24rem;
overflow-wrap: anywhere;
}
.details-table th {
width: 14rem;
}
.empty-state {
background: #fff;
border: 1px dashed var(--mxgw-border);
border-radius: .375rem;
color: var(--mxgw-ink-muted);
padding: 1rem;
}
.dashboard-login {
max-width: 28rem;
}
.login-card {
border-color: var(--mxgw-border);
border-radius: .375rem;
}
.api-key-management-grid {
display: grid;
gap: .75rem;
grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
}
.scope-grid {
display: grid;
gap: .35rem .75rem;
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
}
.one-time-secret {
display: block;
overflow-wrap: anywhere;
white-space: normal;
}
.api-key-create-modal {
display: block;
}
.api-key-create-modal .modal-body {
max-height: min(70vh, 44rem);
overflow-y: auto;
}
@media (max-width: 700px) {
.dashboard-content {
padding: .75rem;
}
.dashboard-page-header {
align-items: flex-start;
flex-direction: column;
}
.details-table th {
width: 9rem;
}
}
@@ -1,27 +0,0 @@
using MxGateway.Contracts;
namespace MxGateway.Tests.Contracts;
public sealed class GatewayContractInfoTests
{
/// <summary>Verifies that the default backend name is "mxaccess-worker".</summary>
[Fact]
public void DefaultBackendName_IsMxAccessWorker()
{
Assert.Equal("mxaccess-worker", GatewayContractInfo.DefaultBackendName);
}
/// <summary>Verifies that the gateway protocol version is bumped to three after the alarm proto extension.</summary>
[Fact]
public void GatewayProtocolVersion_IsVersionThree()
{
Assert.Equal(3u, GatewayContractInfo.GatewayProtocolVersion);
}
/// <summary>Verifies that the worker protocol version starts at version one.</summary>
[Fact]
public void WorkerProtocolVersion_StartsAtVersionOne()
{
Assert.Equal(1u, GatewayContractInfo.WorkerProtocolVersion);
}
}
@@ -1,392 +0,0 @@
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using MxGateway.Contracts;
using MxGateway.Contracts.Proto;
namespace MxGateway.Tests.Contracts;
public sealed class ProtobufContractRoundTripTests
{
/// <summary>Verifies that gateway descriptor contains expected public service methods.</summary>
[Fact]
public void GatewayDescriptor_ContainsInitialPublicServiceMethods()
{
var service = Assert.Single(
MxaccessGatewayReflection.Descriptor.Services,
descriptor => descriptor.Name == "MxAccessGateway");
Assert.Contains(service.Methods, method => method.Name == "OpenSession");
Assert.Contains(service.Methods, method => method.Name == "CloseSession");
Assert.Contains(service.Methods, method => method.Name == "Invoke");
Assert.Contains(service.Methods, method => method.Name == "StreamEvents");
Assert.Contains(service.Methods, method => method.Name == "AcknowledgeAlarm");
Assert.Contains(service.Methods, method => method.Name == "QueryActiveAlarms");
}
/// <summary>Verifies that worker envelope descriptor contains required correlation fields.</summary>
[Fact]
public void WorkerEnvelopeDescriptor_ContainsRequiredCorrelationFields()
{
var fields = WorkerEnvelope.Descriptor.Fields.InDeclarationOrder();
Assert.Contains(fields, field => field.Name == "protocol_version");
Assert.Contains(fields, field => field.Name == "session_id");
Assert.Contains(fields, field => field.Name == "sequence");
Assert.Contains(fields, field => field.Name == "correlation_id");
}
/// <summary>Verifies that command request round-trips through serialization.</summary>
[Fact]
public void CommandRequest_RoundTripsMethodSpecificPayload()
{
var original = new MxCommandRequest
{
SessionId = "session-1",
ClientCorrelationId = "client-correlation-1",
Command = new MxCommand
{
Kind = MxCommandKind.Register,
Register = new RegisterCommand
{
ClientName = "mxaccessgw-test-client",
},
},
};
var parsed = MxCommandRequest.Parser.ParseFrom(original.ToByteArray());
Assert.Equal(original, parsed);
Assert.Equal(MxCommand.PayloadOneofCase.Register, parsed.Command.PayloadCase);
}
/// <summary>Verifies that command reply round-trips with return values and statuses.</summary>
[Fact]
public void CommandReply_RoundTripsHResultReturnValueOutParamsAndStatuses()
{
var original = new MxCommandReply
{
SessionId = "session-1",
CorrelationId = "gateway-correlation-1",
Kind = MxCommandKind.AddItem,
ProtocolStatus = new ProtocolStatus
{
Code = ProtocolStatusCode.Ok,
},
Hresult = 0,
ReturnValue = new MxValue
{
DataType = MxDataType.Integer,
Int32Value = 1234,
VariantType = "VT_I4",
},
AddItem = new AddItemReply
{
ItemHandle = 1234,
},
};
original.Statuses.Add(new MxStatusProxy
{
Success = 1,
Category = MxStatusCategory.Ok,
DetectedBy = MxStatusSource.RespondingLmx,
Detail = 0,
});
var parsed = MxCommandReply.Parser.ParseFrom(original.ToByteArray());
Assert.Equal(original, parsed);
Assert.True(parsed.HasHresult);
Assert.Equal(MxCommandReply.PayloadOneofCase.AddItem, parsed.PayloadCase);
Assert.Single(parsed.Statuses);
}
/// <summary>Verifies that event round-trips with value, status, and sequence.</summary>
[Fact]
public void Event_RoundTripsValueStatusSequenceAndBufferedBody()
{
var timestamp = Timestamp.FromDateTime(new DateTime(2026, 4, 26, 20, 0, 0, DateTimeKind.Utc));
var original = new MxEvent
{
Family = MxEventFamily.OnBufferedDataChange,
SessionId = "session-1",
ServerHandle = 10,
ItemHandle = 20,
Value = new MxValue
{
DataType = MxDataType.Float,
ArrayValue = new MxArray
{
ElementDataType = MxDataType.Float,
FloatValues = new FloatArray
{
Values = { 1.5f, 2.5f },
},
Dimensions = { 2 },
VariantType = "VT_ARRAY|VT_R4",
},
},
Quality = 192,
SourceTimestamp = timestamp,
WorkerSequence = 42,
WorkerTimestamp = timestamp,
GatewayReceiveTimestamp = timestamp,
OnBufferedDataChange = new OnBufferedDataChangeEvent
{
DataType = MxDataType.Float,
QualityValues = new MxArray
{
ElementDataType = MxDataType.Integer,
Int32Values = new Int32Array
{
Values = { 192, 192 },
},
Dimensions = { 2 },
},
TimestampValues = new MxArray
{
ElementDataType = MxDataType.Time,
TimestampValues = new TimestampArray
{
Values = { timestamp, timestamp },
},
Dimensions = { 2 },
},
},
};
original.Statuses.Add(new MxStatusProxy
{
Success = 1,
Category = MxStatusCategory.Ok,
DetectedBy = MxStatusSource.RespondingNmx,
Detail = 0,
});
var parsed = MxEvent.Parser.ParseFrom(original.ToByteArray());
Assert.Equal(original, parsed);
Assert.Equal(MxEvent.BodyOneofCase.OnBufferedDataChange, parsed.BodyCase);
Assert.Single(parsed.Statuses);
}
/// <summary>Verifies that worker envelope round-trips through serialization preserving protocol and command fields.</summary>
[Fact]
public void WorkerEnvelope_RoundTripsProtocolFieldsAndCommandBody()
{
var original = new WorkerEnvelope
{
ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
SessionId = "session-1",
Sequence = 7,
CorrelationId = "gateway-correlation-1",
WorkerCommand = new WorkerCommand
{
EnqueueTimestamp = Timestamp.FromDateTime(
new DateTime(2026, 4, 26, 20, 5, 0, DateTimeKind.Utc)),
Command = new MxCommand
{
Kind = MxCommandKind.Advise,
Advise = new AdviseCommand
{
ServerHandle = 10,
ItemHandle = 20,
},
},
},
};
var parsed = WorkerEnvelope.Parser.ParseFrom(original.ToByteArray());
Assert.Equal(original, parsed);
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerCommand, parsed.BodyCase);
Assert.Equal(MxCommand.PayloadOneofCase.Advise, parsed.WorkerCommand.Command.PayloadCase);
}
/// <summary>Verifies that an OnAlarmTransition event round-trips with full payload.</summary>
[Fact]
public void Event_RoundTripsOnAlarmTransitionWithFullPayload()
{
var raise = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc));
var ack = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 30, DateTimeKind.Utc));
var original = new MxEvent
{
Family = MxEventFamily.OnAlarmTransition,
SessionId = "session-1",
WorkerSequence = 99,
WorkerTimestamp = ack,
GatewayReceiveTimestamp = ack,
OnAlarmTransition = new OnAlarmTransitionEvent
{
AlarmFullReference = "Tank01.Level.HiHi",
SourceObjectReference = "Tank01",
AlarmTypeName = "AnalogLimitAlarm.HiHi",
TransitionKind = AlarmTransitionKind.Acknowledge,
Severity = 750,
OriginalRaiseTimestamp = raise,
TransitionTimestamp = ack,
OperatorUser = "operator1",
OperatorComment = "investigating",
Category = "Process",
Description = "Tank 01 high-high level",
CurrentValue = new MxValue
{
DataType = MxDataType.Float,
FloatValue = 95.4f,
VariantType = "VT_R4",
},
LimitValue = new MxValue
{
DataType = MxDataType.Float,
FloatValue = 90.0f,
VariantType = "VT_R4",
},
},
};
var parsed = MxEvent.Parser.ParseFrom(original.ToByteArray());
Assert.Equal(original, parsed);
Assert.Equal(MxEvent.BodyOneofCase.OnAlarmTransition, parsed.BodyCase);
Assert.Equal(AlarmTransitionKind.Acknowledge, parsed.OnAlarmTransition.TransitionKind);
Assert.Equal(raise, parsed.OnAlarmTransition.OriginalRaiseTimestamp);
Assert.Equal("operator1", parsed.OnAlarmTransition.OperatorUser);
}
/// <summary>Verifies that an OnAlarmTransition event round-trips with only the required fields populated.</summary>
[Fact]
public void Event_RoundTripsOnAlarmTransitionWithOptionalFieldsEmpty()
{
var raise = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc));
var original = new MxEvent
{
Family = MxEventFamily.OnAlarmTransition,
SessionId = "session-1",
WorkerSequence = 100,
OnAlarmTransition = new OnAlarmTransitionEvent
{
AlarmFullReference = "Tank01.Level.HiHi",
AlarmTypeName = "AnalogLimitAlarm.HiHi",
TransitionKind = AlarmTransitionKind.Raise,
Severity = 750,
TransitionTimestamp = raise,
},
};
var parsed = MxEvent.Parser.ParseFrom(original.ToByteArray());
Assert.Equal(original, parsed);
Assert.Equal(string.Empty, parsed.OnAlarmTransition.OperatorUser);
Assert.Equal(string.Empty, parsed.OnAlarmTransition.OperatorComment);
Assert.Null(parsed.OnAlarmTransition.OriginalRaiseTimestamp);
Assert.Null(parsed.OnAlarmTransition.CurrentValue);
}
/// <summary>Verifies that an MxEvent body oneof rejects multiple bodies — last write wins per proto3 semantics.</summary>
[Fact]
public void Event_OneofGuard_LastBodyWins()
{
var ev = new MxEvent
{
Family = MxEventFamily.OnAlarmTransition,
OnDataChange = new OnDataChangeEvent(),
OnAlarmTransition = new OnAlarmTransitionEvent
{
AlarmFullReference = "X",
TransitionKind = AlarmTransitionKind.Raise,
},
};
Assert.Equal(MxEvent.BodyOneofCase.OnAlarmTransition, ev.BodyCase);
Assert.Null(ev.OnDataChange);
}
/// <summary>Verifies that AcknowledgeAlarmRequest round-trips through serialization.</summary>
[Fact]
public void AcknowledgeAlarmRequest_RoundTripsAllFields()
{
var original = new AcknowledgeAlarmRequest
{
SessionId = "session-1",
ClientCorrelationId = "client-correlation-7",
AlarmFullReference = "Tank01.Level.HiHi",
Comment = "shift handover",
OperatorUser = "operator2",
};
var parsed = AcknowledgeAlarmRequest.Parser.ParseFrom(original.ToByteArray());
Assert.Equal(original, parsed);
}
/// <summary>Verifies that AcknowledgeAlarmReply round-trips with status, hresult, and diagnostics.</summary>
[Fact]
public void AcknowledgeAlarmReply_RoundTripsStatusAndHresult()
{
var original = new AcknowledgeAlarmReply
{
SessionId = "session-1",
CorrelationId = "gateway-correlation-7",
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
Hresult = 0,
Status = new MxStatusProxy
{
Success = 1,
Category = MxStatusCategory.Ok,
DetectedBy = MxStatusSource.RespondingLmx,
},
DiagnosticMessage = "ack accepted",
};
var parsed = AcknowledgeAlarmReply.Parser.ParseFrom(original.ToByteArray());
Assert.Equal(original, parsed);
Assert.True(parsed.HasHresult);
}
/// <summary>Verifies that ActiveAlarmSnapshot round-trips with current state and operator metadata.</summary>
[Fact]
public void ActiveAlarmSnapshot_RoundTripsAllFields()
{
var raise = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc));
var ack = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 30, DateTimeKind.Utc));
var original = new ActiveAlarmSnapshot
{
AlarmFullReference = "Tank01.Level.HiHi",
SourceObjectReference = "Tank01",
AlarmTypeName = "AnalogLimitAlarm.HiHi",
Severity = 750,
OriginalRaiseTimestamp = raise,
CurrentState = AlarmConditionState.ActiveAcked,
Category = "Process",
Description = "Tank 01 high-high level",
LastTransitionTimestamp = ack,
OperatorUser = "operator2",
OperatorComment = "investigating",
};
var parsed = ActiveAlarmSnapshot.Parser.ParseFrom(original.ToByteArray());
Assert.Equal(original, parsed);
Assert.Equal(AlarmConditionState.ActiveAcked, parsed.CurrentState);
}
/// <summary>Verifies that QueryActiveAlarmsRequest round-trips empty filter prefix.</summary>
[Fact]
public void QueryActiveAlarmsRequest_RoundTripsWithAndWithoutFilter()
{
var withoutFilter = new QueryActiveAlarmsRequest
{
SessionId = "session-1",
ClientCorrelationId = "client-correlation-8",
};
var withFilter = new QueryActiveAlarmsRequest
{
SessionId = "session-1",
ClientCorrelationId = "client-correlation-9",
AlarmFilterPrefix = "Tank01.",
};
Assert.Equal(withoutFilter, QueryActiveAlarmsRequest.Parser.ParseFrom(withoutFilter.ToByteArray()));
Assert.Equal(withFilter, QueryActiveAlarmsRequest.Parser.ParseFrom(withFilter.ToByteArray()));
}
}
@@ -1,139 +0,0 @@
using MxGateway.Server.Galaxy;
using MxGateway.Contracts.Proto.Galaxy;
namespace MxGateway.Tests.Galaxy;
public sealed class GalaxyHierarchyCacheTests
{
/// <summary>
/// Verifies cache returns empty entry before any refresh occurs.
/// </summary>
[Fact]
public void Current_BeforeAnyRefresh_ReturnsEmpty()
{
GalaxyDeployNotifier notifier = new();
GalaxyHierarchyCache cache = CreateCache(notifier, new ManualTimeProvider());
GalaxyHierarchyCacheEntry entry = cache.Current;
Assert.Equal(GalaxyCacheStatus.Unknown, entry.Status);
Assert.False(entry.HasData);
Assert.Equal(0, entry.ObjectCount);
Assert.Empty(entry.Objects);
}
/// <summary>
/// Verifies cache marks unavailable and does not publish when SQL is unreachable.
/// </summary>
[Fact]
public async Task RefreshAsync_WhenSqlIsUnreachable_MarksUnavailableAndDoesNotPublish()
{
GalaxyDeployNotifier notifier = new();
ManualTimeProvider clock = new(DateTimeOffset.Parse("2026-04-28T12:00:00Z"));
GalaxyHierarchyCache cache = CreateCache(notifier, clock);
await cache.RefreshAsync(CancellationToken.None);
Assert.Equal(GalaxyCacheStatus.Unavailable, cache.Current.Status);
Assert.False(string.IsNullOrWhiteSpace(cache.Current.LastError));
Assert.Null(notifier.Latest);
Assert.True(cache.WaitForFirstLoadAsync(CancellationToken.None).IsCompletedSuccessfully);
}
/// <summary>
/// Verifies HasData returns true for healthy cache entries.
/// </summary>
[Fact]
public void HasData_OnHealthyEntry_IsTrue()
{
GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with
{
Status = GalaxyCacheStatus.Healthy,
LastSuccessAt = DateTimeOffset.UtcNow,
ObjectCount = 1,
};
Assert.True(entry.HasData);
}
/// <summary>
/// Verifies HasData returns false for unknown cache entries.
/// </summary>
[Fact]
public void HasData_OnUnknownEntry_IsFalse()
{
Assert.False(GalaxyHierarchyCacheEntry.Empty.HasData);
}
[Fact]
public void GalaxyHierarchyIndex_BuildsPathsAndTagLookupsWithoutThrowingOnBadMetadata()
{
GalaxyObject root = new()
{
GobjectId = 1,
TagName = "Area1",
ContainedName = "Area1",
};
GalaxyObject duplicate = new()
{
GobjectId = 1,
TagName = "DuplicateArea",
ContainedName = "DuplicateArea",
};
GalaxyObject child = new()
{
GobjectId = 2,
ParentGobjectId = 1,
TagName = "Pump_001",
ContainedName = "Pump",
Attributes =
{
new GalaxyAttribute
{
FullTagReference = "Pump_001.PV",
IsHistorized = true,
},
},
};
GalaxyObject orphan = new()
{
GobjectId = 3,
ParentGobjectId = 99,
TagName = "Orphan_001",
ContainedName = "Orphan",
};
GalaxyHierarchyIndex index = GalaxyHierarchyIndex.Build([root, duplicate, child, orphan]);
Assert.Equal("Area1/Pump", index.ObjectViewsById[2].ContainedPath);
Assert.Equal("Orphan", index.ObjectViewsById[3].ContainedPath);
Assert.Same(child, index.TagsByAddress["Pump_001.PV"].Object);
Assert.NotNull(index.TagsByAddress["Pump_001.PV"].Attribute);
Assert.Same(root, index.ObjectViewsById[1].Object);
}
private static GalaxyHierarchyCache CreateCache(GalaxyDeployNotifier notifier, TimeProvider clock)
{
GalaxyRepositoryOptions options = new()
{
ConnectionString = "Server=127.0.0.1,65500;Database=ZB;Connection Timeout=1;Encrypt=False;",
CommandTimeoutSeconds = 1,
};
MxGateway.Server.Galaxy.GalaxyRepository repository = new(options);
return new GalaxyHierarchyCache(repository, notifier, clock);
}
private sealed class ManualTimeProvider(DateTimeOffset start = default) : TimeProvider
{
private DateTimeOffset _now = start == default ? DateTimeOffset.UtcNow : start;
/// <inheritdoc />
public override DateTimeOffset GetUtcNow() => _now;
/// <summary>
/// Advances the current time by the specified duration.
/// </summary>
/// <param name="duration">Time duration to advance.</param>
public void Advance(TimeSpan duration) => _now += duration;
}
}
@@ -1,138 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using MxGateway.Server;
using MxGateway.Server.Metrics;
namespace MxGateway.Tests.Gateway;
public sealed class GatewayApplicationTests
{
/// <summary>Verifies that Build maps the live health check endpoint.</summary>
[Fact]
public void Build_MapsLiveHealthEndpoint()
{
WebApplication app = GatewayApplication.Build([]);
RouteEndpoint endpoint = Assert.Single(
((IEndpointRouteBuilder)app).DataSources
.SelectMany(dataSource => dataSource.Endpoints)
.OfType<RouteEndpoint>(),
candidate => candidate.RoutePattern.RawText == "/health/live");
Assert.Equal("LiveHealth", endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName);
}
/// <summary>Verifies that Build registers the gateway metrics service.</summary>
[Fact]
public void Build_RegistersGatewayMetrics()
{
WebApplication app = GatewayApplication.Build([]);
GatewayMetrics metrics = app.Services.GetRequiredService<GatewayMetrics>();
Assert.NotNull(metrics);
}
/// <summary>Verifies that Build maps dashboard and authentication endpoints when the dashboard is enabled.</summary>
[Fact]
public void Build_WhenDashboardEnabled_MapsBlazorDashboardAndAuthEndpoints()
{
WebApplication app = GatewayApplication.Build([]);
IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app);
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/dashboard/");
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/dashboard/sessions");
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/dashboard/workers");
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/dashboard/events");
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/dashboard/settings");
Assert.Contains(endpoints, endpoint =>
endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == "DashboardLogin");
Assert.Contains(endpoints, endpoint =>
endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == "DashboardLogout");
}
/// <summary>Verifies that Build does not map dashboard routes when the dashboard is disabled.</summary>
[Fact]
public void Build_WhenDashboardEnabled_DashboardRoutesAllowAnonymousAccess()
{
WebApplication app = GatewayApplication.Build([]);
IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app)
.Where(endpoint => endpoint.RoutePattern.RawText?.StartsWith(
"/dashboard",
StringComparison.Ordinal) == true)
.ToArray();
Assert.NotEmpty(endpoints);
Assert.DoesNotContain(endpoints, endpoint => endpoint.Metadata.GetMetadata<IAuthorizeData>() is not null);
}
[Fact]
public void Build_WhenDashboardDisabled_DoesNotMapDashboardRoutes()
{
WebApplication app = GatewayApplication.Build(["--MxGateway:Dashboard:Enabled=false"]);
IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app);
Assert.DoesNotContain(endpoints, endpoint =>
endpoint.RoutePattern.RawText?.StartsWith("/dashboard", StringComparison.Ordinal) == true);
Assert.DoesNotContain(endpoints, endpoint =>
endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName?.StartsWith(
"Dashboard",
StringComparison.Ordinal) == true);
}
/// <summary>Verifies that StartAsync fails when gateway configuration is invalid.</summary>
/// <param name="key">Configuration key to override.</param>
/// <param name="value">Invalid configuration value.</param>
/// <param name="expectedFailure">Expected validation error message.</param>
[Theory]
[InlineData(
"MxGateway:Worker:ExecutablePath",
"worker.dll",
"MxGateway:Worker:ExecutablePath must point to a .exe file.")]
[InlineData(
"MxGateway:Events:QueueCapacity",
"0",
"MxGateway:Events:QueueCapacity must be greater than zero.")]
[InlineData(
"MxGateway:Authentication:PepperSecretName",
"",
"MxGateway:Authentication:PepperSecretName is required")]
[InlineData(
"MxGateway:Dashboard:PathBase",
"dashboard",
"MxGateway:Dashboard:PathBase must start with '/'.")]
[InlineData(
"MxGateway:Ldap:RequiredGroup",
"",
"MxGateway:Ldap:RequiredGroup is required when LDAP login is enabled.")]
[InlineData(
"MxGateway:Ldap:AllowInsecureLdap",
"false",
"MxGateway:Ldap:AllowInsecureLdap must be true when UseTls is false.")]
public async Task StartAsync_InvalidGatewayConfiguration_FailsStartup(
string key,
string value,
string expectedFailure)
{
await using WebApplication app = GatewayApplication.Build(
[$"--{key}={value}", "--urls=http://127.0.0.1:0"]);
OptionsValidationException exception = await Assert.ThrowsAsync<OptionsValidationException>(
() => app.StartAsync());
Assert.Contains(
exception.Failures,
failure => failure.Contains(expectedFailure, StringComparison.Ordinal));
}
private static IReadOnlyList<RouteEndpoint> GetRouteEndpoints(WebApplication app)
{
return ((IEndpointRouteBuilder)app).DataSources
.SelectMany(dataSource => dataSource.Endpoints)
.OfType<RouteEndpoint>()
.ToArray();
}
}
@@ -1,54 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using MxGateway.Contracts.Proto;
using MxGateway.Server.Sessions;
namespace MxGateway.Tests.Gateway.Sessions;
/// <summary>
/// PR A.6 / A.7 — pins the not-yet-wired dispatcher's behaviour:
/// AcknowledgeAsync returns OK with a worker-pending diagnostic and
/// QueryActiveAlarmsAsync yields an empty stream. Production
/// <c>WorkerAlarmRpcDispatcher</c> (dev-rig follow-up) replaces this
/// impl in DI without changing the gateway handler shape.
/// </summary>
public sealed class NotWiredAlarmRpcDispatcherTests
{
[Fact]
public async Task AcknowledgeAsync_returns_ok_with_worker_pending_diagnostic()
{
IAlarmRpcDispatcher dispatcher = new NotWiredAlarmRpcDispatcher();
AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
new AcknowledgeAlarmRequest
{
SessionId = "session-1",
ClientCorrelationId = "corr-1",
AlarmFullReference = "Tank01.Level.HiHi",
Comment = "investigating",
OperatorUser = "alice",
},
CancellationToken.None);
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.Equal("session-1", reply.SessionId);
Assert.Equal("corr-1", reply.CorrelationId);
Assert.Contains("worker", reply.DiagnosticMessage, System.StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task QueryActiveAlarmsAsync_yields_no_snapshots()
{
IAlarmRpcDispatcher dispatcher = new NotWiredAlarmRpcDispatcher();
int count = 0;
await foreach (ActiveAlarmSnapshot _ in dispatcher.QueryActiveAlarmsAsync(
new QueryActiveAlarmsRequest { SessionId = "session-1" },
CancellationToken.None))
{
count++;
}
Assert.Equal(0, count);
}
}
@@ -1,266 +0,0 @@
using System.Runtime.CompilerServices;
using Google.Protobuf.WellKnownTypes;
using Microsoft.Extensions.Options;
using MxGateway.Contracts.Proto;
using MxGateway.Server.Configuration;
using MxGateway.Server.Metrics;
using MxGateway.Server.Sessions;
using MxGateway.Server.Workers;
namespace MxGateway.Tests.Gateway.Sessions;
/// <summary>
/// Pins the alarm auto-subscribe hook on session open. Runs in
/// its own file because the cases are orthogonal to
/// <see cref="SessionManagerTests"/> (alarms-disabled vs.
/// alarms-enabled lanes), and the fake worker client below verifies
/// the issued <c>SubscribeAlarms</c> command shape directly.
/// </summary>
public sealed class SessionManagerAlarmAutoSubscribeTests
{
[Fact]
public async Task OpenSessionAsync_DoesNotAutoSubscribe_WhenAlarmsDisabled()
{
AlarmAutoSubscribeWorkerClient worker = new();
SessionManager manager = NewManager(worker, alarms: new AlarmsOptions { Enabled = false });
await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", CancellationToken.None);
Assert.Equal(0, worker.SubscribeAlarmsInvokeCount);
}
[Fact]
public async Task OpenSessionAsync_AutoSubscribes_WhenEnabledWithExpression()
{
AlarmAutoSubscribeWorkerClient worker = new();
SessionManager manager = NewManager(worker, alarms: new AlarmsOptions
{
Enabled = true,
SubscriptionExpression = @"\\HOST\Galaxy!Area1",
});
GatewaySession session = await manager.OpenSessionAsync(
CreateOpenRequest(), "client-1", CancellationToken.None);
Assert.Equal(SessionState.Ready, session.State);
Assert.Equal(1, worker.SubscribeAlarmsInvokeCount);
Assert.Equal(@"\\HOST\Galaxy!Area1",
worker.LastSubscribeAlarmsCommand!.SubscriptionExpression);
}
[Fact]
public async Task OpenSessionAsync_FallsBackToDefaultArea_WhenExpressionEmpty()
{
AlarmAutoSubscribeWorkerClient worker = new();
SessionManager manager = NewManager(worker, alarms: new AlarmsOptions
{
Enabled = true,
DefaultArea = "DEV",
});
await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", CancellationToken.None);
Assert.Equal(1, worker.SubscribeAlarmsInvokeCount);
Assert.Contains(@"\Galaxy!DEV",
worker.LastSubscribeAlarmsCommand!.SubscriptionExpression);
}
[Fact]
public async Task OpenSessionAsync_Succeeds_WhenAutoSubscribeFailsWithRequireOff()
{
// Worker rejects the SubscribeAlarms command. With RequireSubscribeOnOpen=false
// (the default), the session still opens — alarm-side commands later return
// "not subscribed", but data subscriptions work.
AlarmAutoSubscribeWorkerClient worker = new()
{
SubscribeAlarmsReplyFactory = _ => new MxCommandReply
{
Kind = MxCommandKind.SubscribeAlarms,
ProtocolStatus = new ProtocolStatus
{
Code = ProtocolStatusCode.MxaccessFailure,
Message = "wnwrap subscribe failed",
},
DiagnosticMessage = "alarm provider unavailable",
},
};
SessionManager manager = NewManager(worker, alarms: new AlarmsOptions
{
Enabled = true,
SubscriptionExpression = @"\\HOST\Galaxy!Area1",
RequireSubscribeOnOpen = false,
});
GatewaySession session = await manager.OpenSessionAsync(
CreateOpenRequest(), "client-1", CancellationToken.None);
Assert.Equal(SessionState.Ready, session.State);
Assert.Equal(1, worker.SubscribeAlarmsInvokeCount);
}
[Fact]
public async Task OpenSessionAsync_Throws_WhenAutoSubscribeFailsWithRequireOn()
{
AlarmAutoSubscribeWorkerClient worker = new()
{
SubscribeAlarmsReplyFactory = _ => new MxCommandReply
{
Kind = MxCommandKind.SubscribeAlarms,
ProtocolStatus = new ProtocolStatus
{
Code = ProtocolStatusCode.MxaccessFailure,
Message = "wnwrap subscribe failed",
},
},
};
SessionManager manager = NewManager(worker, alarms: new AlarmsOptions
{
Enabled = true,
SubscriptionExpression = @"\\HOST\Galaxy!Area1",
RequireSubscribeOnOpen = true,
});
await Assert.ThrowsAsync<SessionManagerException>(
async () => await manager.OpenSessionAsync(
CreateOpenRequest(), "client-1", CancellationToken.None));
}
[Fact]
public async Task OpenSessionAsync_Throws_WhenEnabledButNoExpressionAndRequireOn()
{
AlarmAutoSubscribeWorkerClient worker = new();
SessionManager manager = NewManager(worker, alarms: new AlarmsOptions
{
Enabled = true,
// No SubscriptionExpression and no DefaultArea.
RequireSubscribeOnOpen = true,
});
await Assert.ThrowsAsync<SessionManagerException>(
async () => await manager.OpenSessionAsync(
CreateOpenRequest(), "client-1", CancellationToken.None));
Assert.Equal(0, worker.SubscribeAlarmsInvokeCount);
}
[Fact]
public async Task OpenSessionAsync_Succeeds_WhenEnabledButNoExpressionAndRequireOff()
{
AlarmAutoSubscribeWorkerClient worker = new();
SessionManager manager = NewManager(worker, alarms: new AlarmsOptions
{
Enabled = true,
// No SubscriptionExpression and no DefaultArea — default require=false.
});
GatewaySession session = await manager.OpenSessionAsync(
CreateOpenRequest(), "client-1", CancellationToken.None);
Assert.Equal(SessionState.Ready, session.State);
Assert.Equal(0, worker.SubscribeAlarmsInvokeCount);
}
private static SessionManager NewManager(
AlarmAutoSubscribeWorkerClient worker,
AlarmsOptions alarms)
{
FakeSessionWorkerClientFactory factory = new(worker);
GatewayOptions options = new GatewayOptions
{
Sessions = new SessionOptions
{
DefaultCommandTimeoutSeconds = 30,
MaxSessions = 64,
DefaultLeaseSeconds = 1800,
},
Worker = new WorkerOptions
{
StartupTimeoutSeconds = 30,
ShutdownTimeoutSeconds = 10,
},
Alarms = alarms,
};
return new SessionManager(
new SessionRegistry(),
factory,
Options.Create(options),
new GatewayMetrics());
}
private static SessionOpenRequest CreateOpenRequest()
{
return new SessionOpenRequest(
RequestedBackend: null,
ClientSessionName: "test-session",
ClientCorrelationId: "client-correlation-1",
CommandTimeout: Duration.FromTimeSpan(TimeSpan.FromSeconds(5)));
}
private sealed class FakeSessionWorkerClientFactory(IWorkerClient client) : ISessionWorkerClientFactory
{
public Task<IWorkerClient> CreateAsync(
GatewaySession session,
CancellationToken cancellationToken)
{
return Task.FromResult(client);
}
}
private sealed class AlarmAutoSubscribeWorkerClient : IWorkerClient
{
public string SessionId { get; } = "session-1";
public int? ProcessId { get; } = 1234;
public WorkerClientState State { get; set; } = WorkerClientState.Ready;
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow;
public int SubscribeAlarmsInvokeCount { get; private set; }
public SubscribeAlarmsCommand? LastSubscribeAlarmsCommand { get; private set; }
public Func<WorkerCommand, MxCommandReply>? SubscribeAlarmsReplyFactory { get; init; }
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task<WorkerCommandReply> InvokeAsync(
WorkerCommand command, TimeSpan timeout, CancellationToken cancellationToken)
{
if (command.Command?.Kind == MxCommandKind.SubscribeAlarms)
{
SubscribeAlarmsInvokeCount++;
LastSubscribeAlarmsCommand = command.Command.SubscribeAlarms;
MxCommandReply reply = SubscribeAlarmsReplyFactory?.Invoke(command)
?? new MxCommandReply
{
Kind = MxCommandKind.SubscribeAlarms,
ProtocolStatus = new ProtocolStatus
{
Code = ProtocolStatusCode.Ok,
Message = "OK",
},
};
return Task.FromResult(new WorkerCommandReply { Reply = reply });
}
return Task.FromResult(new WorkerCommandReply
{
Reply = new MxCommandReply
{
Kind = command.Command?.Kind ?? MxCommandKind.Unspecified,
ProtocolStatus = new ProtocolStatus
{
Code = ProtocolStatusCode.Ok,
Message = "OK",
},
},
});
}
public async IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.CompletedTask;
yield break;
}
public Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken)
=> Task.CompletedTask;
public void Kill(string reason) { }
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
}
@@ -1,374 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using MxGateway.Contracts.Proto;
using MxGateway.Server.Sessions;
using MxGateway.Server.Workers;
namespace MxGateway.Tests.Gateway.Sessions;
/// <summary>
/// Pins the production <see cref="WorkerAlarmRpcDispatcher"/>'s behaviour:
/// resolves the session by id, issues the matching MxCommand over the
/// worker pipe, and unwraps the reply into AcknowledgeAlarmReply or the
/// ActiveAlarmSnapshot stream.
/// </summary>
public sealed class WorkerAlarmRpcDispatcherTests
{
[Fact]
public async Task AcknowledgeAsync_returns_session_not_found_when_session_missing()
{
SessionRegistry registry = new SessionRegistry();
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
new AcknowledgeAlarmRequest
{
SessionId = "missing",
ClientCorrelationId = "c1",
AlarmFullReference = Guid.NewGuid().ToString(),
},
CancellationToken.None);
Assert.Equal(ProtocolStatusCode.SessionNotFound, reply.ProtocolStatus.Code);
}
[Fact]
public async Task AcknowledgeAsync_forwards_guid_and_returns_native_status()
{
SessionRegistry registry = new SessionRegistry();
Guid alarmGuid = Guid.NewGuid();
FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient
{
ReplyFactory = command =>
{
Assert.Equal(MxCommandKind.AcknowledgeAlarm, command.Command.Kind);
Assert.Equal(alarmGuid.ToString(), command.Command.AcknowledgeAlarmCommand.AlarmGuid);
Assert.Equal("ack", command.Command.AcknowledgeAlarmCommand.Comment);
Assert.Equal("alice", command.Command.AcknowledgeAlarmCommand.OperatorUser);
return new MxCommandReply
{
Kind = MxCommandKind.AcknowledgeAlarm,
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" },
Hresult = 0,
AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload { NativeStatus = 0 },
};
},
};
GatewaySession session = NewSession("s1");
session.AttachWorkerClient(worker);
session.MarkReady();
registry.TryAdd(session);
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
new AcknowledgeAlarmRequest
{
SessionId = "s1",
ClientCorrelationId = "c1",
AlarmFullReference = alarmGuid.ToString(),
Comment = "ack",
OperatorUser = "alice",
},
CancellationToken.None);
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.Equal(0, reply.Hresult);
Assert.Equal("s1", reply.SessionId);
Assert.Equal("c1", reply.CorrelationId);
Assert.Equal(1, worker.InvokeCount);
}
[Fact]
public async Task AcknowledgeAsync_propagates_worker_diagnostic_on_failure()
{
SessionRegistry registry = new SessionRegistry();
FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient
{
ReplyFactory = _ => new MxCommandReply
{
Kind = MxCommandKind.AcknowledgeAlarm,
ProtocolStatus = new ProtocolStatus
{
Code = ProtocolStatusCode.MxaccessFailure,
Message = "AVEVA Acknowledge failed.",
},
Hresult = -123,
DiagnosticMessage = "AVEVA AlarmAckByGUID returned non-zero status -123.",
},
};
GatewaySession session = NewSession("s1");
session.AttachWorkerClient(worker);
session.MarkReady();
registry.TryAdd(session);
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
new AcknowledgeAlarmRequest
{
SessionId = "s1",
AlarmFullReference = Guid.NewGuid().ToString(),
},
CancellationToken.None);
Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code);
Assert.Equal(-123, reply.Hresult);
Assert.Contains("-123", reply.DiagnosticMessage);
}
[Theory]
[InlineData("Galaxy!TestArea.TestMachine_001.TestAlarm001", "Galaxy", "TestArea", "TestMachine_001.TestAlarm001")]
[InlineData("Galaxy!Area.Tag", "Galaxy", "Area", "Tag")]
[InlineData("Provider!Group.Tag.With.Dots", "Provider", "Group", "Tag.With.Dots")]
public void TryParseAlarmReference_decomposes_provider_group_tag(
string reference, string expectedProvider, string expectedGroup, string expectedName)
{
Assert.True(WorkerAlarmRpcDispatcher.TryParseAlarmReference(
reference, out string provider, out string group, out string name));
Assert.Equal(expectedProvider, provider);
Assert.Equal(expectedGroup, group);
Assert.Equal(expectedName, name);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
[InlineData("no-bang-here")]
[InlineData("!Group.Tag")] // empty provider
[InlineData("Galaxy!")] // bang at end
[InlineData("Galaxy!Group")] // missing dot
[InlineData("Galaxy!.Tag")] // empty group
[InlineData("Galaxy!Group.")] // empty tag
public void TryParseAlarmReference_rejects_malformed_references(string? reference)
{
Assert.False(WorkerAlarmRpcDispatcher.TryParseAlarmReference(
reference, out _, out _, out _));
}
[Fact]
public async Task AcknowledgeAsync_routes_provider_group_tag_via_AckByName()
{
SessionRegistry registry = new SessionRegistry();
AcknowledgeAlarmByNameCommand? observed = null;
FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient
{
ReplyFactory = command =>
{
Assert.Equal(MxCommandKind.AcknowledgeAlarmByName, command.Command.Kind);
observed = command.Command.AcknowledgeAlarmByNameCommand;
return new MxCommandReply
{
Kind = MxCommandKind.AcknowledgeAlarmByName,
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" },
Hresult = 0,
AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload { NativeStatus = 0 },
};
},
};
GatewaySession session = NewSession("s1");
session.AttachWorkerClient(worker);
session.MarkReady();
registry.TryAdd(session);
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
new AcknowledgeAlarmRequest
{
SessionId = "s1",
ClientCorrelationId = "c1",
AlarmFullReference = "Galaxy!TestArea.TestMachine_001.TestAlarm001",
Comment = "ack-by-name",
OperatorUser = "bob",
},
CancellationToken.None);
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.NotNull(observed);
Assert.Equal("TestMachine_001.TestAlarm001", observed!.AlarmName);
Assert.Equal("Galaxy", observed.ProviderName);
Assert.Equal("TestArea", observed.GroupName);
Assert.Equal("bob", observed.OperatorUser);
Assert.Equal("ack-by-name", observed.Comment);
}
[Fact]
public async Task AcknowledgeAsync_returns_invalid_request_for_unparseable_reference()
{
SessionRegistry registry = new SessionRegistry();
FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient();
GatewaySession session = NewSession("s1");
session.AttachWorkerClient(worker);
session.MarkReady();
registry.TryAdd(session);
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
new AcknowledgeAlarmRequest
{
SessionId = "s1",
AlarmFullReference = "no-bang-no-dot",
},
CancellationToken.None);
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
Assert.Equal(0, worker.InvokeCount);
}
[Fact]
public async Task QueryActiveAlarmsAsync_yields_each_snapshot_from_payload()
{
SessionRegistry registry = new SessionRegistry();
FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient
{
ReplyFactory = command =>
{
Assert.Equal(MxCommandKind.QueryActiveAlarms, command.Command.Kind);
QueryActiveAlarmsReplyPayload payload = new QueryActiveAlarmsReplyPayload();
payload.Snapshots.Add(new ActiveAlarmSnapshot
{
AlarmFullReference = "Galaxy!A.T1",
CurrentState = AlarmConditionState.Active,
Severity = 500,
});
payload.Snapshots.Add(new ActiveAlarmSnapshot
{
AlarmFullReference = "Galaxy!A.T2",
CurrentState = AlarmConditionState.ActiveAcked,
Severity = 100,
});
return new MxCommandReply
{
Kind = MxCommandKind.QueryActiveAlarms,
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" },
QueryActiveAlarms = payload,
};
},
};
GatewaySession session = NewSession("s1");
session.AttachWorkerClient(worker);
session.MarkReady();
registry.TryAdd(session);
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
List<ActiveAlarmSnapshot> collected = new List<ActiveAlarmSnapshot>();
await foreach (ActiveAlarmSnapshot snap in dispatcher.QueryActiveAlarmsAsync(
new QueryActiveAlarmsRequest { SessionId = "s1" },
CancellationToken.None))
{
collected.Add(snap);
}
Assert.Equal(2, collected.Count);
Assert.Equal("Galaxy!A.T1", collected[0].AlarmFullReference);
Assert.Equal("Galaxy!A.T2", collected[1].AlarmFullReference);
}
[Fact]
public async Task QueryActiveAlarmsAsync_yields_empty_when_session_missing()
{
SessionRegistry registry = new SessionRegistry();
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
List<ActiveAlarmSnapshot> collected = new List<ActiveAlarmSnapshot>();
await foreach (ActiveAlarmSnapshot snap in dispatcher.QueryActiveAlarmsAsync(
new QueryActiveAlarmsRequest { SessionId = "missing" },
CancellationToken.None))
{
collected.Add(snap);
}
Assert.Empty(collected);
}
[Fact]
public async Task QueryActiveAlarmsAsync_yields_empty_on_worker_failure()
{
SessionRegistry registry = new SessionRegistry();
FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient
{
ReplyFactory = _ => new MxCommandReply
{
Kind = MxCommandKind.QueryActiveAlarms,
ProtocolStatus = new ProtocolStatus
{
Code = ProtocolStatusCode.MxaccessFailure,
Message = "alarm consumer not subscribed",
},
},
};
GatewaySession session = NewSession("s1");
session.AttachWorkerClient(worker);
session.MarkReady();
registry.TryAdd(session);
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
List<ActiveAlarmSnapshot> collected = new List<ActiveAlarmSnapshot>();
await foreach (ActiveAlarmSnapshot snap in dispatcher.QueryActiveAlarmsAsync(
new QueryActiveAlarmsRequest { SessionId = "s1" },
CancellationToken.None))
{
collected.Add(snap);
}
Assert.Empty(collected);
}
private static GatewaySession NewSession(string sessionId)
{
return new GatewaySession(
sessionId,
"mxaccess",
$"mxaccess-gateway-1-{sessionId}",
"nonce",
"client-1",
"test-session",
"client-correlation-1",
commandTimeout: TimeSpan.FromSeconds(30),
startupTimeout: TimeSpan.FromSeconds(5),
shutdownTimeout: TimeSpan.FromSeconds(5),
leaseDuration: TimeSpan.FromMinutes(30),
openedAt: DateTimeOffset.UtcNow);
}
private sealed class FakeAlarmWorkerClient : IWorkerClient
{
public string SessionId { get; } = "session-1";
public int? ProcessId { get; } = 1;
public WorkerClientState State { get; } = WorkerClientState.Ready;
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow;
public Func<WorkerCommand, MxCommandReply>? ReplyFactory { get; set; }
public int InvokeCount { get; private set; }
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task<WorkerCommandReply> InvokeAsync(
WorkerCommand command, TimeSpan timeout, CancellationToken cancellationToken)
{
InvokeCount++;
MxCommandReply reply = ReplyFactory?.Invoke(command) ?? new MxCommandReply();
return Task.FromResult(new WorkerCommandReply { Reply = reply });
}
public async IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.CompletedTask;
yield break;
}
public Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken) => Task.CompletedTask;
public void Kill(string reason) { }
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
}
@@ -1,298 +0,0 @@
using Grpc.Core;
using Microsoft.Extensions.Options;
using MxGateway.Contracts.Proto;
using MxGateway.Server.Configuration;
using MxGateway.Server.Security.Authentication;
using MxGateway.Server.Security.Authorization;
namespace MxGateway.Tests.Security.Authorization;
public sealed class GatewayGrpcAuthorizationInterceptorTests
{
/// <summary>Verifies that missing API key returns unauthenticated status.</summary>
[Fact]
public async Task UnaryServerHandler_MissingApiKey_ReturnsUnauthenticated()
{
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
new FakeApiKeyVerifier(ApiKeyVerificationResult.Fail(
ApiKeyVerificationFailure.MissingOrMalformedCredentials)),
new GatewayRequestIdentityAccessor());
RpcException exception = await Assert.ThrowsAsync<RpcException>(
() => interceptor.UnaryServerHandler(
new OpenSessionRequest(),
new TestServerCallContext([]),
(_, _) => Task.FromResult(new OpenSessionReply())));
Assert.Equal(StatusCode.Unauthenticated, exception.StatusCode);
Assert.DoesNotContain("secret", exception.Status.Detail, StringComparison.OrdinalIgnoreCase);
}
/// <summary>Verifies that invalid API key error does not expose raw credentials.</summary>
[Fact]
public async Task UnaryServerHandler_InvalidApiKey_DoesNotExposeRawCredentialInStatus()
{
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
new FakeApiKeyVerifier(ApiKeyVerificationResult.Fail(ApiKeyVerificationFailure.SecretMismatch)),
new GatewayRequestIdentityAccessor());
RpcException exception = await Assert.ThrowsAsync<RpcException>(
() => interceptor.UnaryServerHandler(
new OpenSessionRequest(),
ContextWithAuthorization("Bearer mxgw_operator01_super-secret"),
(_, _) => Task.FromResult(new OpenSessionReply())));
Assert.Equal(StatusCode.Unauthenticated, exception.StatusCode);
Assert.DoesNotContain("super-secret", exception.Status.Detail, StringComparison.Ordinal);
}
/// <summary>Verifies that valid key without required scope returns permission denied.</summary>
[Fact]
public async Task UnaryServerHandler_ValidApiKeyMissingScope_ReturnsPermissionDenied()
{
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.EventsRead)),
new GatewayRequestIdentityAccessor());
RpcException exception = await Assert.ThrowsAsync<RpcException>(
() => interceptor.UnaryServerHandler(
new OpenSessionRequest(),
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
(_, _) => Task.FromResult(new OpenSessionReply())));
Assert.Equal(StatusCode.PermissionDenied, exception.StatusCode);
Assert.Contains(GatewayScopes.SessionOpen, exception.Status.Detail, StringComparison.Ordinal);
}
/// <summary>Verifies that valid key with scope sets request identity for the handler.</summary>
[Fact]
public async Task UnaryServerHandler_ValidApiKeyWithScope_SetsRequestIdentity()
{
GatewayRequestIdentityAccessor identityAccessor = new();
ApiKeyIdentity? identitySeenByHandler = null;
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.SessionOpen)),
identityAccessor);
OpenSessionReply reply = await interceptor.UnaryServerHandler(
new OpenSessionRequest(),
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
(_, _) =>
{
identitySeenByHandler = identityAccessor.Current;
return Task.FromResult(new OpenSessionReply { SessionId = "session-1" });
});
Assert.Equal("session-1", reply.SessionId);
Assert.NotNull(identitySeenByHandler);
Assert.Equal("operator01", identitySeenByHandler.KeyId);
Assert.Null(identityAccessor.Current);
}
/// <summary>Verifies that server stream handler requires proper scope.</summary>
[Fact]
public async Task ServerStreamingServerHandler_ValidApiKeyMissingScope_ReturnsPermissionDenied()
{
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.SessionOpen)),
new GatewayRequestIdentityAccessor());
RpcException exception = await Assert.ThrowsAsync<RpcException>(
() => interceptor.ServerStreamingServerHandler(
new StreamEventsRequest(),
new TestServerStreamWriter<MxEvent>(),
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
(_, _, _) => Task.CompletedTask));
Assert.Equal(StatusCode.PermissionDenied, exception.StatusCode);
Assert.Contains(GatewayScopes.EventsRead, exception.Status.Detail, StringComparison.Ordinal);
}
/// <summary>Verifies that server stream handler allows streams with proper scope.</summary>
[Fact]
public async Task ServerStreamingServerHandler_ValidApiKeyWithScope_AllowsStream()
{
GatewayRequestIdentityAccessor identityAccessor = new();
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.EventsRead)),
identityAccessor);
TestServerStreamWriter<MxEvent> streamWriter = new();
await interceptor.ServerStreamingServerHandler(
new StreamEventsRequest(),
streamWriter,
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
async (_, writer, _) =>
{
Assert.Equal("operator01", identityAccessor.Current?.KeyId);
await writer.WriteAsync(new MxEvent { SessionId = "session-1" });
});
MxEvent eventMessage = Assert.Single(streamWriter.Messages);
Assert.Equal("session-1", eventMessage.SessionId);
Assert.Null(identityAccessor.Current);
}
/// <summary>Verifies that disabled authentication skips API key verification.</summary>
[Fact]
public async Task UnaryServerHandler_AuthenticationDisabled_SkipsApiKeyVerification()
{
GatewayRequestIdentityAccessor identityAccessor = new();
FakeApiKeyVerifier verifier = new(ApiKeyVerificationResult.Fail(
ApiKeyVerificationFailure.MissingOrMalformedCredentials));
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
verifier,
identityAccessor,
AuthenticationMode.Disabled);
OpenSessionReply reply = await interceptor.UnaryServerHandler(
new OpenSessionRequest(),
new TestServerCallContext([]),
(_, _) => Task.FromResult(new OpenSessionReply { SessionId = "session-1" }));
Assert.Equal("session-1", reply.SessionId);
Assert.False(verifier.WasCalled);
Assert.Null(identityAccessor.Current);
}
private static GatewayGrpcAuthorizationInterceptor CreateInterceptor(
IApiKeyVerifier apiKeyVerifier,
IGatewayRequestIdentityAccessor identityAccessor,
AuthenticationMode authenticationMode = AuthenticationMode.ApiKey)
{
return new GatewayGrpcAuthorizationInterceptor(
apiKeyVerifier,
new GatewayGrpcScopeResolver(),
identityAccessor,
Options.Create(new GatewayOptions
{
Authentication = new AuthenticationOptions
{
Mode = authenticationMode
}
}));
}
private static ApiKeyVerificationResult SuccessWithScopes(params string[] scopes)
{
return ApiKeyVerificationResult.Success(new ApiKeyIdentity(
KeyId: "operator01",
KeyPrefix: "mxgw_operator01",
DisplayName: "Operator Key",
Scopes: new HashSet<string>(scopes, StringComparer.Ordinal)));
}
private static TestServerCallContext ContextWithAuthorization(string authorizationHeader)
{
return new TestServerCallContext([new Metadata.Entry("authorization", authorizationHeader)]);
}
private sealed class FakeApiKeyVerifier(ApiKeyVerificationResult result) : IApiKeyVerifier
{
/// <summary>Gets whether the verifier was called.</summary>
public bool WasCalled { get; private set; }
/// <summary>Gets the last authorization header seen by the verifier.</summary>
public string? LastAuthorizationHeader { get; private set; }
/// <summary>Verifies the authorization header against stored result.</summary>
/// <param name="authorizationHeader">The authorization header to verify.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Configured verification result.</returns>
public Task<ApiKeyVerificationResult> VerifyAsync(
string? authorizationHeader,
CancellationToken cancellationToken)
{
WasCalled = true;
LastAuthorizationHeader = authorizationHeader;
return Task.FromResult(result);
}
}
private sealed class TestServerStreamWriter<T> : IServerStreamWriter<T>
{
/// <summary>Gets messages written to the stream.</summary>
public List<T> Messages { get; } = [];
/// <summary>Gets or sets write options for the stream.</summary>
public WriteOptions? WriteOptions { get; set; }
/// <summary>Writes a message to the stream.</summary>
/// <param name="message">The message to write.</param>
/// <returns>Task representing the write operation.</returns>
public Task WriteAsync(T message)
{
Messages.Add(message);
return Task.CompletedTask;
}
}
private sealed class TestServerCallContext(
Metadata requestHeaders,
CancellationToken cancellationToken = default) : ServerCallContext
{
private readonly Metadata responseTrailers = [];
private readonly Dictionary<object, object> userState = [];
private Status status;
private WriteOptions? writeOptions;
/// <inheritdoc />
protected override string MethodCore => "/mxaccess_gateway.v1.MxAccessGateway/Test";
/// <inheritdoc />
protected override string HostCore => "localhost";
/// <inheritdoc />
protected override string PeerCore => "ipv4:127.0.0.1:5000";
/// <inheritdoc />
protected override DateTime DeadlineCore => DateTime.UtcNow.AddMinutes(1);
/// <inheritdoc />
protected override Metadata RequestHeadersCore => requestHeaders;
/// <inheritdoc />
protected override CancellationToken CancellationTokenCore => cancellationToken;
/// <inheritdoc />
protected override Metadata ResponseTrailersCore => responseTrailers;
/// <inheritdoc />
protected override Status StatusCore
{
get => status;
set => status = value;
}
/// <inheritdoc />
protected override WriteOptions? WriteOptionsCore
{
get => writeOptions;
set => writeOptions = value;
}
/// <inheritdoc />
protected override AuthContext AuthContextCore { get; } = new(
string.Empty,
new Dictionary<string, List<AuthProperty>>(StringComparer.Ordinal));
/// <inheritdoc />
protected override IDictionary<object, object> UserStateCore => userState;
/// <inheritdoc />
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)
{
return Task.CompletedTask;
}
/// <inheritdoc />
protected override ContextPropagationToken CreatePropagationTokenCore(
ContextPropagationOptions? options)
{
throw new NotSupportedException();
}
}
}
@@ -1,169 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Google.Protobuf;
using MxGateway.Contracts;
using MxGateway.Contracts.Proto;
using MxGateway.Worker.Ipc;
namespace MxGateway.Worker.Tests.Ipc;
public sealed class WorkerFrameProtocolTests
{
private const string SessionId = "session-1";
private const string Nonce = "nonce-secret";
/// <summary>Verifies that valid envelopes round-trip through write and read.</summary>
[Fact]
public async Task WriteAndReadAsync_WithValidEnvelope_RoundTripsFrame()
{
WorkerFrameProtocolOptions options = CreateOptions();
MemoryStream stream = new();
WorkerEnvelope original = CreateGatewayHelloEnvelope();
WorkerFrameWriter writer = new(stream, options);
await writer.WriteAsync(original);
stream.Position = 0;
WorkerFrameReader reader = new(stream, options);
WorkerEnvelope parsed = await reader.ReadAsync();
Assert.Equal(original, parsed);
}
/// <summary>Verifies that wrong protocol version throws mismatch error.</summary>
[Fact]
public async Task ReadAsync_WithWrongProtocolVersion_ThrowsProtocolVersionMismatch()
{
WorkerFrameProtocolOptions options = CreateOptions();
WorkerEnvelope envelope = CreateGatewayHelloEnvelope();
envelope.ProtocolVersion++;
MemoryStream stream = new(CreateFrame(envelope));
WorkerFrameReader reader = new(stream, options);
WorkerFrameProtocolException exception =
await Assert.ThrowsAsync<WorkerFrameProtocolException>(
async () => await reader.ReadAsync());
Assert.Equal(WorkerFrameProtocolErrorCode.ProtocolVersionMismatch, exception.ErrorCode);
}
/// <summary>Verifies that wrong session ID throws mismatch error.</summary>
[Fact]
public async Task ReadAsync_WithWrongSessionId_ThrowsSessionMismatch()
{
WorkerFrameProtocolOptions options = CreateOptions();
WorkerEnvelope envelope = CreateGatewayHelloEnvelope();
envelope.SessionId = "different-session";
MemoryStream stream = new(CreateFrame(envelope));
WorkerFrameReader reader = new(stream, options);
WorkerFrameProtocolException exception =
await Assert.ThrowsAsync<WorkerFrameProtocolException>(
async () => await reader.ReadAsync());
Assert.Equal(WorkerFrameProtocolErrorCode.SessionMismatch, exception.ErrorCode);
}
/// <summary>Verifies that malformed length throws error.</summary>
[Fact]
public async Task ReadAsync_WithMalformedLength_ThrowsMalformedLength()
{
WorkerFrameProtocolOptions options = CreateOptions();
MemoryStream stream = new(new byte[sizeof(uint)]);
WorkerFrameReader reader = new(stream, options);
WorkerFrameProtocolException exception =
await Assert.ThrowsAsync<WorkerFrameProtocolException>(
async () => await reader.ReadAsync());
Assert.Equal(WorkerFrameProtocolErrorCode.MalformedLength, exception.ErrorCode);
}
/// <summary>Verifies that malformed payload throws invalid envelope error.</summary>
[Fact]
public async Task ReadAsync_WithMalformedPayload_ThrowsInvalidEnvelope()
{
WorkerFrameProtocolOptions options = CreateOptions();
MemoryStream stream = new(CreateFrame(new byte[] { 0x80 }));
WorkerFrameReader reader = new(stream, options);
WorkerFrameProtocolException exception =
await Assert.ThrowsAsync<WorkerFrameProtocolException>(
async () => await reader.ReadAsync());
Assert.Equal(WorkerFrameProtocolErrorCode.InvalidEnvelope, exception.ErrorCode);
}
/// <summary>Verifies that concurrent writes produce complete serialized frames.</summary>
[Fact]
public async Task WriteAsync_WithConcurrentCalls_SerializesCompleteFrames()
{
WorkerFrameProtocolOptions options = CreateOptions();
MemoryStream stream = new();
WorkerFrameWriter writer = new(stream, options);
await Task.WhenAll(
writer.WriteAsync(CreateGatewayHelloEnvelope(sequence: 1)),
writer.WriteAsync(CreateGatewayHelloEnvelope(sequence: 2)),
writer.WriteAsync(CreateGatewayHelloEnvelope(sequence: 3)));
stream.Position = 0;
WorkerFrameReader reader = new(stream, options);
WorkerEnvelope first = await reader.ReadAsync();
WorkerEnvelope second = await reader.ReadAsync();
WorkerEnvelope third = await reader.ReadAsync();
Assert.Equal(new ulong[] { 1, 2, 3 }, new[] { first.Sequence, second.Sequence, third.Sequence }.OrderBy(sequence => sequence));
}
private static WorkerFrameProtocolOptions CreateOptions()
{
return new WorkerFrameProtocolOptions(
SessionId,
GatewayContractInfo.WorkerProtocolVersion,
Nonce);
}
private static WorkerEnvelope CreateGatewayHelloEnvelope(ulong sequence = 1)
{
return new WorkerEnvelope
{
ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
SessionId = SessionId,
Sequence = sequence,
GatewayHello = new GatewayHello
{
SupportedProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
Nonce = Nonce,
GatewayVersion = "test-gateway",
},
};
}
private static byte[] CreateFrame(IMessage message)
{
return CreateFrame(message.ToByteArray());
}
private static byte[] CreateFrame(byte[] payload)
{
byte[] frame = new byte[sizeof(uint) + payload.Length];
WriteUInt32LittleEndian(frame, (uint)payload.Length);
payload.CopyTo(frame, sizeof(uint));
return frame;
}
private static void WriteUInt32LittleEndian(
byte[] buffer,
uint value)
{
buffer[0] = (byte)value;
buffer[1] = (byte)(value >> 8);
buffer[2] = (byte)(value >> 16);
buffer[3] = (byte)(value >> 24);
}
}
@@ -1,202 +0,0 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using MxGateway.Contracts.Proto;
using MxGateway.Worker.MxAccess;
using MxGateway.Worker.Sta;
namespace MxGateway.Worker.Tests.MxAccess;
/// <summary>
/// Tests for <see cref="MxAccessStaSession"/>.
/// </summary>
public sealed class MxAccessStaSessionTests
{
/// <summary>
/// Verifies that StartAsync creates the MXAccess COM object and attaches the event sink on the STA thread.
/// </summary>
[Fact]
public async Task StartAsync_CreatesComObjectAndAttachesEventSinkOnStaThread()
{
FakeMxAccessComObjectFactory factory = new();
FakeMxAccessEventSink eventSink = new();
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, eventSink);
WorkerReady ready = await session.StartAsync("session-1", workerProcessId: 1234);
Assert.Equal(1234, ready.WorkerProcessId);
Assert.Equal(MxAccessInteropInfo.ProgId, ready.MxaccessProgid);
Assert.Equal(MxAccessInteropInfo.Clsid, ready.MxaccessClsid);
Assert.NotNull(ready.ReadyTimestamp);
Assert.Equal(runtime.StaThreadId, factory.CreateThreadId);
Assert.Equal(runtime.StaThreadId, eventSink.AttachThreadId);
Assert.Equal(ApartmentState.STA, factory.CreateApartmentState);
Assert.Same(factory.CreatedObject, eventSink.AttachedObject);
Assert.Equal("session-1", eventSink.SessionId);
}
/// <summary>
/// Verifies that StartAsync maps creation exceptions with HResult when the factory fails.
/// </summary>
[Fact]
public async Task StartAsync_WhenFactoryFails_MapsCreationExceptionWithHResult()
{
const int hresult = unchecked((int)0x80040154);
FakeMxAccessComObjectFactory factory = new(new COMException("Class not registered.", hresult));
FakeMxAccessEventSink eventSink = new();
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, eventSink);
MxAccessCreationException exception = await Assert.ThrowsAsync<MxAccessCreationException>(
() => session.StartAsync(workerProcessId: 1234));
Assert.Equal(hresult, exception.CapturedHResult);
Assert.Equal(MxAccessInteropInfo.ProgId, exception.AttemptedProgId);
Assert.Equal(MxAccessInteropInfo.Clsid, exception.AttemptedClsid);
Assert.Null(eventSink.AttachedObject);
}
/// <summary>
/// Verifies that Dispose detaches the event sink on the STA thread.
/// </summary>
[Fact]
public async Task Dispose_DetachesEventSinkOnStaThread()
{
FakeMxAccessComObjectFactory factory = new();
FakeMxAccessEventSink eventSink = new();
using StaRuntime runtime = CreateRuntime();
MxAccessStaSession session = new(runtime, factory, eventSink);
await session.StartAsync(workerProcessId: 1234);
session.Dispose();
Assert.Equal(runtime.StaThreadId, eventSink.DetachThreadId);
}
private static StaRuntime CreateRuntime()
{
return new StaRuntime(
new NoopComApartmentInitializer(),
new StaMessagePump(),
TimeSpan.FromMilliseconds(25));
}
/// <summary>
/// Fake MXAccess COM object factory for testing.
/// </summary>
private sealed class FakeMxAccessComObjectFactory : IMxAccessComObjectFactory
{
private readonly Exception? exception;
/// <summary>
/// Initializes a fake factory that optionally throws an exception.
/// </summary>
/// <param name="exception">Exception to throw when Create is called; null to succeed.</param>
public FakeMxAccessComObjectFactory(Exception? exception = null)
{
this.exception = exception;
}
/// <summary>
/// Gets the COM object created by this factory.
/// </summary>
public object CreatedObject { get; } = new();
/// <summary>
/// Gets the managed thread ID when Create was called.
/// </summary>
public int? CreateThreadId { get; private set; }
/// <summary>
/// Gets the apartment state when Create was called.
/// </summary>
public ApartmentState? CreateApartmentState { get; private set; }
/// <summary>
/// Creates the COM object or throws the configured exception.
/// </summary>
public object Create()
{
CreateThreadId = Thread.CurrentThread.ManagedThreadId;
CreateApartmentState = Thread.CurrentThread.GetApartmentState();
if (exception is not null)
{
throw exception;
}
return CreatedObject;
}
}
/// <summary>
/// Fake MXAccess event sink for testing.
/// </summary>
private sealed class FakeMxAccessEventSink : IMxAccessEventSink
{
/// <summary>
/// Gets the attached MXAccess COM object.
/// </summary>
public object? AttachedObject { get; private set; }
/// <summary>
/// Gets the managed thread ID when Attach was called.
/// </summary>
public int? AttachThreadId { get; private set; }
/// <summary>
/// Gets the managed thread ID when Detach was called.
/// </summary>
public int? DetachThreadId { get; private set; }
/// <summary>
/// Gets the session identifier.
/// </summary>
public string? SessionId { get; private set; }
/// <summary>
/// Attaches the MXAccess COM object and records thread context.
/// </summary>
/// <param name="mxAccessComObject">MXAccess COM object to attach.</param>
/// <param name="sessionId">Identifier of the session.</param>
public void Attach(
object mxAccessComObject,
string sessionId)
{
AttachedObject = mxAccessComObject;
AttachThreadId = Thread.CurrentThread.ManagedThreadId;
SessionId = sessionId;
}
/// <summary>
/// Detaches the MXAccess COM object and records thread context.
/// </summary>
public void Detach()
{
DetachThreadId = Thread.CurrentThread.ManagedThreadId;
AttachedObject = null;
}
}
/// <summary>
/// Noop STA COM apartment initializer for testing.
/// </summary>
private sealed class NoopComApartmentInitializer : IStaComApartmentInitializer
{
/// <summary>
/// Initializes the COM apartment (no-op).
/// </summary>
public void Initialize()
{
}
/// <summary>
/// Uninitializes the COM apartment (no-op).
/// </summary>
public void Uninitialize()
{
}
}
}
@@ -1,112 +0,0 @@
using System;
using System.Linq;
using MxGateway.Worker.MxAccess;
namespace MxGateway.Worker.Tests.MxAccess;
/// <summary>
/// Unit-test coverage for <see cref="WnWrapAlarmConsumer"/>'s pure
/// parsing helpers — XML payload → <see cref="MxAlarmSnapshotRecord"/>
/// dictionary, and the 32-char-hex GUID round-trip. The COM-side
/// polling loop is verified separately by the Skip-gated
/// <c>WnWrapConsumerProbeTests</c> on a live AVEVA install.
/// </summary>
public sealed class WnWrapAlarmConsumerXmlTests
{
/// <summary>Captured XML from the dev rig (probe run 2026-05-01).</summary>
private const string SingleAlarmActiveXml =
"<?xml version=\"1.0\"?><ALARM_RECORDS COUNT=\"1\">" +
"<ALARM><GUID>BCC4705395424D65BDAABCDEA6A32A73</GUID>" +
"<DATE>2026/5/1</DATE><TIME>13:26:14.709</TIME>" +
"<GMTOFFSET>240</GMTOFFSET><DSTADJUST>0</DSTADJUST>" +
"<PROVIDER_NODE>DESKTOP-6JL3KKO</PROVIDER_NODE>" +
"<PROVIDER_NAME>Galaxy</PROVIDER_NAME>" +
"<GROUP>TestArea</GROUP>" +
"<TAGNAME>TestMachine_001.TestAlarm001</TAGNAME>" +
"<TYPE>DSC</TYPE><VALUE>true</VALUE><LIMIT>true</LIMIT>" +
"<PRIORITY>500</PRIORITY><STATE>UNACK_ALM</STATE>" +
"<OPERATOR_NODE></OPERATOR_NODE><OPERATOR_NAME></OPERATOR_NAME>" +
"<ALARM_COMMENT>Test alarm #1</ALARM_COMMENT></ALARM>" +
"</ALARM_RECORDS>";
private const string EmptyXml =
"<?xml version=\"1.0\"?><ALARM_RECORDS COUNT=\"0\"></ALARM_RECORDS>";
[Fact]
public void ParseSnapshotXml_returns_empty_dictionary_for_empty_payload()
{
var records = WnWrapAlarmConsumer.ParseSnapshotXml(EmptyXml);
Assert.Empty(records);
}
[Fact]
public void ParseSnapshotXml_returns_empty_dictionary_for_null_or_whitespace()
{
Assert.Empty(WnWrapAlarmConsumer.ParseSnapshotXml(""));
Assert.Empty(WnWrapAlarmConsumer.ParseSnapshotXml(" "));
}
[Fact]
public void ParseSnapshotXml_decodes_single_active_alarm_record()
{
var records = WnWrapAlarmConsumer.ParseSnapshotXml(SingleAlarmActiveXml);
Assert.Single(records);
Guid expectedGuid = new Guid("BCC47053-9542-4D65-BDAA-BCDEA6A32A73");
var record = records[expectedGuid];
Assert.Equal(expectedGuid, record.AlarmGuid);
Assert.Equal("DESKTOP-6JL3KKO", record.ProviderNode);
Assert.Equal("Galaxy", record.ProviderName);
Assert.Equal("TestArea", record.Group);
Assert.Equal("TestMachine_001.TestAlarm001", record.TagName);
Assert.Equal("DSC", record.Type);
Assert.Equal("true", record.Value);
Assert.Equal("true", record.Limit);
Assert.Equal(500, record.Priority);
Assert.Equal(MxAlarmStateKind.UnackAlm, record.State);
Assert.Equal("Test alarm #1", record.AlarmComment);
Assert.Equal(DateTimeKind.Utc, record.TransitionTimestampUtc.Kind);
// 13:26:14.709 EDT (UTC-4, DSTADJUST=0) + 240 minutes = 17:26:14.709 UTC.
Assert.Equal(17, record.TransitionTimestampUtc.Hour);
Assert.Equal(26, record.TransitionTimestampUtc.Minute);
}
[Fact]
public void ParseSnapshotXml_silently_drops_records_with_invalid_guids()
{
string xml = SingleAlarmActiveXml.Replace(
"<GUID>BCC4705395424D65BDAABCDEA6A32A73</GUID>",
"<GUID>not-a-guid</GUID>");
Assert.Empty(WnWrapAlarmConsumer.ParseSnapshotXml(xml));
}
[Theory]
[InlineData("BCC4705395424D65BDAABCDEA6A32A73", "BCC47053-9542-4D65-BDAA-BCDEA6A32A73")]
[InlineData("00000000000000000000000000000000", "00000000-0000-0000-0000-000000000000")]
public void TryParseHexGuid_handles_dashless_32_char_hex(string hex, string expected)
{
Assert.True(WnWrapAlarmConsumer.TryParseHexGuid(hex, out Guid guid));
Assert.Equal(new Guid(expected), guid);
}
[Theory]
[InlineData("BCC47053-9542-4D65-BDAA-BCDEA6A32A73")]
public void TryParseHexGuid_accepts_canonical_dashed_form(string canonical)
{
Assert.True(WnWrapAlarmConsumer.TryParseHexGuid(canonical, out Guid guid));
Assert.Equal(new Guid(canonical), guid);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("nope")]
[InlineData("0123456789ABCDEF")] // too short
[InlineData("BCC4705395424D65BDAABCDEA6A32A73XX")] // too long
public void TryParseHexGuid_rejects_invalid_input(string? hex)
{
Assert.False(WnWrapAlarmConsumer.TryParseHexGuid(hex, out Guid guid));
Assert.Equal(Guid.Empty, guid);
}
}
@@ -1,43 +0,0 @@
using System;
namespace MxGateway.Worker.Ipc;
/// <summary>Configuration options for worker pipe sessions including heartbeat parameters.</summary>
public sealed class WorkerPipeSessionOptions
{
/// <summary>Default heartbeat interval (5 seconds).</summary>
public static readonly TimeSpan DefaultHeartbeatInterval = TimeSpan.FromSeconds(5);
/// <summary>Default heartbeat grace period (15 seconds).</summary>
public static readonly TimeSpan DefaultHeartbeatGrace = TimeSpan.FromSeconds(15);
/// <summary>Initializes a new instance of the WorkerPipeSessionOptions class with default values.</summary>
public WorkerPipeSessionOptions()
{
HeartbeatInterval = DefaultHeartbeatInterval;
HeartbeatGrace = DefaultHeartbeatGrace;
}
/// <summary>Gets or sets the heartbeat interval.</summary>
public TimeSpan HeartbeatInterval { get; set; }
/// <summary>Gets or sets the heartbeat grace period.</summary>
public TimeSpan HeartbeatGrace { get; set; }
/// <summary>Validates the session options.</summary>
public void Validate()
{
if (HeartbeatInterval <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(
nameof(HeartbeatInterval),
"Worker heartbeat interval must be greater than zero.");
}
if (HeartbeatGrace <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(
nameof(HeartbeatGrace),
"Worker heartbeat grace must be greater than zero.");
}
}
}
@@ -1,59 +0,0 @@
namespace MxGateway.Worker.MxAccess;
public interface IMxAccessServer
{
/// <summary>Registers a client and returns a server handle.</summary>
/// <param name="clientName">Name of the client requesting registration.</param>
/// <returns>Server handle for subsequent operations.</returns>
int Register(string clientName);
/// <summary>Unregisters a server handle.</summary>
/// <param name="serverHandle">Server handle to unregister.</param>
void Unregister(int serverHandle);
/// <summary>Adds an item to a server and returns an item handle.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemDefinition">Item definition string.</param>
/// <returns>Item handle for the added item.</returns>
int AddItem(
int serverHandle,
string itemDefinition);
/// <summary>Adds an item with context to a server and returns an item handle.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemDefinition">Item definition string.</param>
/// <param name="itemContext">Item context string.</param>
/// <returns>Item handle for the added item.</returns>
int AddItem2(
int serverHandle,
string itemDefinition,
string itemContext);
/// <summary>Removes an item from a server.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to remove.</param>
void RemoveItem(
int serverHandle,
int itemHandle);
/// <summary>Subscribes to change notifications for an item.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to subscribe to.</param>
void Advise(
int serverHandle,
int itemHandle);
/// <summary>Unsubscribes from change notifications for an item.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to unsubscribe from.</param>
void UnAdvise(
int serverHandle,
int itemHandle);
/// <summary>Subscribes to supervisory change notifications for an item.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to subscribe to.</param>
void AdviseSupervisory(
int serverHandle,
int itemHandle);
}
@@ -1,160 +0,0 @@
using System;
using ArchestrA.MxAccess;
using Proto = MxGateway.Contracts.Proto;
namespace MxGateway.Worker.MxAccess;
/// <summary>Sink for MXAccess COM events that converts them to protobuf format.</summary>
public sealed class MxAccessBaseEventSink : IMxAccessEventSink
{
private readonly MxAccessEventMapper eventMapper;
private readonly MxAccessEventQueue eventQueue;
private LMXProxyServerClass? server;
private string sessionId = string.Empty;
/// <summary>Initializes a new instance of the MxAccessBaseEventSink class with a default queue.</summary>
public MxAccessBaseEventSink()
: this(new MxAccessEventQueue())
{
}
/// <summary>Initializes a new instance of the MxAccessBaseEventSink class with a provided queue.</summary>
/// <param name="eventQueue">Queue for buffering converted MXAccess events.</param>
public MxAccessBaseEventSink(MxAccessEventQueue eventQueue)
: this(eventQueue, new MxAccessEventMapper())
{
}
/// <summary>Initializes a new instance of the MxAccessBaseEventSink class with provided queue and mapper.</summary>
/// <param name="eventQueue">Queue for buffering converted MXAccess events.</param>
/// <param name="eventMapper">Converter for MXAccess events to protobuf format.</param>
public MxAccessBaseEventSink(
MxAccessEventQueue eventQueue,
MxAccessEventMapper eventMapper)
{
this.eventQueue = eventQueue ?? throw new ArgumentNullException(nameof(eventQueue));
this.eventMapper = eventMapper ?? throw new ArgumentNullException(nameof(eventMapper));
}
/// <inheritdoc />
public void Attach(
object mxAccessComObject,
string sessionId)
{
this.sessionId = sessionId ?? string.Empty;
server = (LMXProxyServerClass)mxAccessComObject;
server.OnDataChange += OnDataChange;
server.OnWriteComplete += OnWriteComplete;
server.OperationComplete += OperationComplete;
server.OnBufferedDataChange += OnBufferedDataChange;
}
/// <inheritdoc />
public void Detach()
{
if (server is null)
{
return;
}
server.OnDataChange -= OnDataChange;
server.OnWriteComplete -= OnWriteComplete;
server.OperationComplete -= OperationComplete;
server.OnBufferedDataChange -= OnBufferedDataChange;
server = null;
sessionId = string.Empty;
}
private void OnDataChange(
int hLMXServerHandle,
int phItemHandle,
object pvItemValue,
int pwItemQuality,
object pftItemTimeStamp,
ref MXSTATUS_PROXY[] pVars)
{
MXSTATUS_PROXY[] statuses = pVars;
EnqueueEvent(() => eventMapper.CreateOnDataChange(
sessionId,
hLMXServerHandle,
phItemHandle,
pvItemValue,
pwItemQuality,
pftItemTimeStamp,
statuses));
}
private void OnWriteComplete(
int hLMXServerHandle,
int phItemHandle,
ref MXSTATUS_PROXY[] pVars)
{
MXSTATUS_PROXY[] statuses = pVars;
EnqueueEvent(() => eventMapper.CreateOnWriteComplete(
sessionId,
hLMXServerHandle,
phItemHandle,
statuses));
}
private void OperationComplete(
int hLMXServerHandle,
int phItemHandle,
ref MXSTATUS_PROXY[] pVars)
{
MXSTATUS_PROXY[] statuses = pVars;
EnqueueEvent(() => eventMapper.CreateOperationComplete(
sessionId,
hLMXServerHandle,
phItemHandle,
statuses));
}
private void OnBufferedDataChange(
int hLMXServerHandle,
int phItemHandle,
MxDataType dtDataType,
object pvItemValue,
object pwItemQuality,
object pftItemTimeStamp,
ref MXSTATUS_PROXY[] pVars)
{
MXSTATUS_PROXY[] statuses = pVars;
EnqueueEvent(() => eventMapper.CreateOnBufferedDataChange(
sessionId,
hLMXServerHandle,
phItemHandle,
(int)dtDataType,
pvItemValue,
pwItemQuality,
pftItemTimeStamp,
statuses));
}
private void EnqueueEvent(Func<Proto.MxEvent> createEvent)
{
try
{
eventQueue.Enqueue(createEvent());
}
catch (Exception exception)
{
eventQueue.RecordFault(CreateEventConversionFault(exception));
}
}
private Proto.WorkerFault CreateEventConversionFault(Exception exception)
{
return new Proto.WorkerFault
{
Category = Proto.WorkerFaultCategory.MxaccessEventConversionFailed,
ExceptionType = exception.GetType().FullName ?? string.Empty,
DiagnosticMessage = $"{exception.GetType().FullName}: HRESULT 0x{unchecked((uint)exception.HResult):X8}",
ProtocolStatus = new Proto.ProtocolStatus
{
Code = Proto.ProtocolStatusCode.MxaccessFailure,
Message = "MXAccess event conversion failed.",
},
};
}
}
@@ -1,151 +0,0 @@
using System;
using System.Reflection;
using System.Runtime.ExceptionServices;
using ArchestrA.MxAccess;
namespace MxGateway.Worker.MxAccess;
/// <summary>
/// Adapter exposing MXAccess COM object methods through the IMxAccessServer interface.
/// </summary>
public sealed class MxAccessComServer : IMxAccessServer
{
private readonly object mxAccessComObject;
/// <summary>
/// Initializes the adapter with the MXAccess COM object.
/// </summary>
/// <param name="mxAccessComObject">MXAccess COM object instance.</param>
public MxAccessComServer(object mxAccessComObject)
{
this.mxAccessComObject = mxAccessComObject ?? throw new ArgumentNullException(nameof(mxAccessComObject));
}
/// <inheritdoc />
public int Register(string clientName)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
{
return mxAccessServer.Register(clientName);
}
return (int)Invoke(nameof(Register), clientName);
}
/// <inheritdoc />
public void Unregister(int serverHandle)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
{
mxAccessServer.Unregister(serverHandle);
return;
}
Invoke(nameof(Unregister), serverHandle);
}
/// <inheritdoc />
public int AddItem(
int serverHandle,
string itemDefinition)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
{
return mxAccessServer.AddItem(serverHandle, itemDefinition);
}
return (int)Invoke(nameof(AddItem), serverHandle, itemDefinition);
}
/// <inheritdoc />
public int AddItem2(
int serverHandle,
string itemDefinition,
string itemContext)
{
if (mxAccessComObject is ILMXProxyServer3 mxAccessServer)
{
return mxAccessServer.AddItem2(serverHandle, itemDefinition, itemContext);
}
return (int)Invoke(nameof(AddItem2), serverHandle, itemDefinition, itemContext);
}
/// <inheritdoc />
public void RemoveItem(
int serverHandle,
int itemHandle)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
{
mxAccessServer.RemoveItem(serverHandle, itemHandle);
return;
}
Invoke(nameof(RemoveItem), serverHandle, itemHandle);
}
/// <inheritdoc />
public void Advise(
int serverHandle,
int itemHandle)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
{
mxAccessServer.Advise(serverHandle, itemHandle);
return;
}
Invoke(nameof(Advise), serverHandle, itemHandle);
}
/// <inheritdoc />
public void UnAdvise(
int serverHandle,
int itemHandle)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
{
mxAccessServer.UnAdvise(serverHandle, itemHandle);
return;
}
Invoke(nameof(UnAdvise), serverHandle, itemHandle);
}
/// <inheritdoc />
public void AdviseSupervisory(
int serverHandle,
int itemHandle)
{
if (mxAccessComObject is ILMXProxyServer4 mxAccessServer)
{
mxAccessServer.AdviseSupervisory(serverHandle, itemHandle);
return;
}
Invoke(nameof(AdviseSupervisory), serverHandle, itemHandle);
}
private object Invoke(
string methodName,
params object[] arguments)
{
try
{
return mxAccessComObject
.GetType()
.InvokeMember(
methodName,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod,
binder: null,
target: mxAccessComObject,
args: arguments);
}
catch (TargetInvocationException exception) when (exception.InnerException is not null)
{
ExceptionDispatchInfo.Capture(exception.InnerException).Throw();
throw;
}
}
}
@@ -1,59 +0,0 @@
using System;
namespace MxGateway.Worker.MxAccess;
/// <summary>
/// Library-agnostic alarm-state enum. Mirrors the four <c>STATE</c>
/// values returned by AVEVA's <c>WNWRAPCONSUMERLib</c> XML payload —
/// <c>UNACK_ALM</c>, <c>ACK_ALM</c>, <c>UNACK_RTN</c>, <c>ACK_RTN</c>.
/// Decoupling the consumer from any specific COM library keeps the
/// proto-build path testable without an AVEVA install.
/// </summary>
public enum MxAlarmStateKind
{
Unspecified = 0,
UnackAlm = 1,
AckAlm = 2,
UnackRtn = 3,
AckRtn = 4,
}
/// <summary>
/// Single alarm record as emitted by the wnwrapConsumer XML stream.
/// Field names match the captured XML schema (see
/// <c>docs/AlarmClientDiscovery.md</c> "Option A — captured" section).
/// </summary>
public sealed class MxAlarmSnapshotRecord
{
public Guid AlarmGuid { get; set; }
public DateTime TransitionTimestampUtc { get; set; }
public string ProviderNode { get; set; } = string.Empty;
public string ProviderName { get; set; } = string.Empty;
public string Group { get; set; } = string.Empty;
public string TagName { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public string Limit { get; set; } = string.Empty;
public int Priority { get; set; }
public MxAlarmStateKind State { get; set; }
public string OperatorNode { get; set; } = string.Empty;
public string OperatorName { get; set; } = string.Empty;
public string AlarmComment { get; set; } = string.Empty;
}
/// <summary>
/// One transition emitted by the consumer's snapshot diff. Pairs the
/// latest record with its previous state so the proto layer can decide
/// whether the transition is a Raise / Acknowledge / Clear.
/// </summary>
public sealed class MxAlarmTransitionEvent : EventArgs
{
public MxAlarmSnapshotRecord Record { get; set; } = new MxAlarmSnapshotRecord();
/// <summary>
/// The state on the consumer's previous polled snapshot, or
/// <see cref="MxAlarmStateKind.Unspecified"/> when this is the
/// first time the GUID has been observed.
/// </summary>
public MxAlarmStateKind PreviousState { get; set; }
}
-104
View File
@@ -1,104 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Contracts", "MxGateway.Contracts\MxGateway.Contracts.csproj", "{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Server", "MxGateway.Server\MxGateway.Server.csproj", "{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Tests", "MxGateway.Tests\MxGateway.Tests.csproj", "{6E069780-A892-487E-AEED-051E26C829A4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.IntegrationTests", "MxGateway.IntegrationTests\MxGateway.IntegrationTests.csproj", "{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Worker", "MxGateway.Worker\MxGateway.Worker.csproj", "{5F2E4C90-B101-4D5D-A9D4-F9F7B53C1A85}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Worker.Tests", "MxGateway.Worker.Tests\MxGateway.Worker.Tests.csproj", "{91255F30-8D43-47C9-AC52-AA0DDA4E9348}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|x64.ActiveCfg = Debug|Any CPU
{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|x64.Build.0 = Debug|Any CPU
{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|x86.ActiveCfg = Debug|Any CPU
{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|x86.Build.0 = Debug|Any CPU
{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|Any CPU.Build.0 = Release|Any CPU
{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|x64.ActiveCfg = Release|Any CPU
{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|x64.Build.0 = Release|Any CPU
{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|x86.ActiveCfg = Release|Any CPU
{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|x86.Build.0 = Release|Any CPU
{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|x64.ActiveCfg = Debug|Any CPU
{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|x64.Build.0 = Debug|Any CPU
{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|x86.ActiveCfg = Debug|Any CPU
{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|x86.Build.0 = Debug|Any CPU
{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|Any CPU.Build.0 = Release|Any CPU
{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|x64.ActiveCfg = Release|Any CPU
{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|x64.Build.0 = Release|Any CPU
{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|x86.ActiveCfg = Release|Any CPU
{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|x86.Build.0 = Release|Any CPU
{6E069780-A892-487E-AEED-051E26C829A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6E069780-A892-487E-AEED-051E26C829A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6E069780-A892-487E-AEED-051E26C829A4}.Debug|x64.ActiveCfg = Debug|Any CPU
{6E069780-A892-487E-AEED-051E26C829A4}.Debug|x64.Build.0 = Debug|Any CPU
{6E069780-A892-487E-AEED-051E26C829A4}.Debug|x86.ActiveCfg = Debug|Any CPU
{6E069780-A892-487E-AEED-051E26C829A4}.Debug|x86.Build.0 = Debug|Any CPU
{6E069780-A892-487E-AEED-051E26C829A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6E069780-A892-487E-AEED-051E26C829A4}.Release|Any CPU.Build.0 = Release|Any CPU
{6E069780-A892-487E-AEED-051E26C829A4}.Release|x64.ActiveCfg = Release|Any CPU
{6E069780-A892-487E-AEED-051E26C829A4}.Release|x64.Build.0 = Release|Any CPU
{6E069780-A892-487E-AEED-051E26C829A4}.Release|x86.ActiveCfg = Release|Any CPU
{6E069780-A892-487E-AEED-051E26C829A4}.Release|x86.Build.0 = Release|Any CPU
{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|x64.ActiveCfg = Debug|Any CPU
{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|x64.Build.0 = Debug|Any CPU
{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|x86.ActiveCfg = Debug|Any CPU
{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|x86.Build.0 = Debug|Any CPU
{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|Any CPU.Build.0 = Release|Any CPU
{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|x64.ActiveCfg = Release|Any CPU
{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|x64.Build.0 = Release|Any CPU
{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|x86.ActiveCfg = Release|Any CPU
{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|x86.Build.0 = Release|Any CPU
{5F2E4C90-B101-4D5D-A9D4-F9F7B53C1A85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F2E4C90-B101-4D5D-A9D4-F9F7B53C1A85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F2E4C90-B101-4D5D-A9D4-F9F7B53C1A85}.Debug|x64.ActiveCfg = Debug|Any CPU
{5F2E4C90-B101-4D5D-A9D4-F9F7B53C1A85}.Debug|x64.Build.0 = Debug|Any CPU
{5F2E4C90-B101-4D5D-A9D4-F9F7B53C1A85}.Debug|x86.ActiveCfg = Debug|Any CPU
{5F2E4C90-B101-4D5D-A9D4-F9F7B53C1A85}.Debug|x86.Build.0 = Debug|Any CPU
{5F2E4C90-B101-4D5D-A9D4-F9F7B53C1A85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F2E4C90-B101-4D5D-A9D4-F9F7B53C1A85}.Release|Any CPU.Build.0 = Release|Any CPU
{5F2E4C90-B101-4D5D-A9D4-F9F7B53C1A85}.Release|x64.ActiveCfg = Release|Any CPU
{5F2E4C90-B101-4D5D-A9D4-F9F7B53C1A85}.Release|x64.Build.0 = Release|Any CPU
{5F2E4C90-B101-4D5D-A9D4-F9F7B53C1A85}.Release|x86.ActiveCfg = Release|Any CPU
{5F2E4C90-B101-4D5D-A9D4-F9F7B53C1A85}.Release|x86.Build.0 = Release|Any CPU
{91255F30-8D43-47C9-AC52-AA0DDA4E9348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91255F30-8D43-47C9-AC52-AA0DDA4E9348}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91255F30-8D43-47C9-AC52-AA0DDA4E9348}.Debug|x64.ActiveCfg = Debug|Any CPU
{91255F30-8D43-47C9-AC52-AA0DDA4E9348}.Debug|x64.Build.0 = Debug|Any CPU
{91255F30-8D43-47C9-AC52-AA0DDA4E9348}.Debug|x86.ActiveCfg = Debug|Any CPU
{91255F30-8D43-47C9-AC52-AA0DDA4E9348}.Debug|x86.Build.0 = Debug|Any CPU
{91255F30-8D43-47C9-AC52-AA0DDA4E9348}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91255F30-8D43-47C9-AC52-AA0DDA4E9348}.Release|Any CPU.Build.0 = Release|Any CPU
{91255F30-8D43-47C9-AC52-AA0DDA4E9348}.Release|x64.ActiveCfg = Release|Any CPU
{91255F30-8D43-47C9-AC52-AA0DDA4E9348}.Release|x64.Build.0 = Release|Any CPU
{91255F30-8D43-47C9-AC52-AA0DDA4E9348}.Release|x86.ActiveCfg = Release|Any CPU
{91255F30-8D43-47C9-AC52-AA0DDA4E9348}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
@@ -0,0 +1,26 @@
namespace ZB.MOM.WW.MxGateway.Contracts;
/// <summary>
/// Holds the protocol version constants shared by gateway components.
/// <see cref="GatewayProtocolVersion"/> is advertised to clients in
/// <c>OpenSessionReply</c>; <see cref="WorkerProtocolVersion"/> is used to
/// validate <c>WorkerEnvelope</c> protocol framing on the gateway↔worker pipe.
/// </summary>
public static class GatewayContractInfo
{
public const uint GatewayProtocolVersion = 3;
public const uint WorkerProtocolVersion = 1;
public const string DefaultBackendName = "mxaccess-worker";
/// <summary>
/// Environment variable name that opts an xUnit suite into running live
/// MXAccess COM tests. Single source of truth shared by both
/// <c>ZB.MOM.WW.MxGateway.IntegrationTests.LiveMxAccessFactAttribute</c> and
/// <c>ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport.LiveMxAccessFactAttribute</c>
/// so any future opt-in tweak does not silently leave one project
/// behind — see Worker.Tests-025.
/// </summary>
public const string LiveMxAccessOptInVariableName = "MXGATEWAY_RUN_LIVE_MXACCESS_TESTS";
}
@@ -9,7 +9,7 @@ using pb = global::Google.Protobuf;
using pbc = global::Google.Protobuf.Collections;
using pbr = global::Google.Protobuf.Reflection;
using scg = global::System.Collections.Generic;
namespace MxGateway.Contracts.Proto.Galaxy {
namespace ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy {
/// <summary>Holder for reflection information generated from galaxy_repository.proto</summary>
public static partial class GalaxyRepositoryReflection {
@@ -72,21 +72,21 @@ namespace MxGateway.Contracts.Proto.Galaxy {
"dBosLmdhbGF4eV9yZXBvc2l0b3J5LnYxLkRpc2NvdmVySGllcmFyY2h5UmVw",
"bHkSaAoRV2F0Y2hEZXBsb3lFdmVudHMSLi5nYWxheHlfcmVwb3NpdG9yeS52",
"MS5XYXRjaERlcGxveUV2ZW50c1JlcXVlc3QaIS5nYWxheHlfcmVwb3NpdG9y",
"eS52MS5EZXBsb3lFdmVudDABQiOqAiBNeEdhdGV3YXkuQ29udHJhY3RzLlBy",
"b3RvLkdhbGF4eWIGcHJvdG8z"));
"eS52MS5EZXBsb3lFdmVudDABQi2qAipaQi5NT00uV1cuTXhHYXRld2F5LkNv",
"bnRyYWN0cy5Qcm90by5HYWxheHliBnByb3RvMw=="));
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.WrappersReflection.Descriptor, },
new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] {
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest), global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest.Parser, null, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply), global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply.Parser, new[]{ "Ok" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest), global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest.Parser, null, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply), global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply.Parser, new[]{ "Present", "TimeOfLastDeploy" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest), global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest.Parser, new[]{ "PageSize", "PageToken", "RootGobjectId", "RootTagName", "RootContainedPath", "MaxDepth", "CategoryIds", "TemplateChainContains", "TagNameGlob", "IncludeAttributes", "AlarmBearingOnly", "HistorizedOnly" }, new[]{ "Root", "IncludeAttributes" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply), global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply.Parser, new[]{ "Objects", "NextPageToken", "TotalObjectCount" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest), global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest.Parser, new[]{ "LastSeenDeployTime" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DeployEvent), global::MxGateway.Contracts.Proto.Galaxy.DeployEvent.Parser, new[]{ "Sequence", "ObservedAt", "TimeOfLastDeploy", "TimeOfLastDeployPresent", "ObjectCount", "AttributeCount" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject), global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject.Parser, new[]{ "GobjectId", "TagName", "ContainedName", "BrowseName", "ParentGobjectId", "IsArea", "CategoryId", "HostedByGobjectId", "TemplateChain", "Attributes" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute), global::MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute.Parser, new[]{ "AttributeName", "FullTagReference", "MxDataType", "DataTypeName", "IsArray", "ArrayDimension", "ArrayDimensionPresent", "MxAttributeCategory", "SecurityClassification", "IsHistorized", "IsAlarm" }, null, null, null, null)
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest), global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest.Parser, null, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionReply), global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionReply.Parser, new[]{ "Ok" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest), global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest.Parser, null, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply), global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply.Parser, new[]{ "Present", "TimeOfLastDeploy" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest), global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest.Parser, new[]{ "PageSize", "PageToken", "RootGobjectId", "RootTagName", "RootContainedPath", "MaxDepth", "CategoryIds", "TemplateChainContains", "TagNameGlob", "IncludeAttributes", "AlarmBearingOnly", "HistorizedOnly" }, new[]{ "Root", "IncludeAttributes" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply), global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply.Parser, new[]{ "Objects", "NextPageToken", "TotalObjectCount" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest), global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest.Parser, new[]{ "LastSeenDeployTime" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DeployEvent), global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DeployEvent.Parser, new[]{ "Sequence", "ObservedAt", "TimeOfLastDeploy", "TimeOfLastDeployPresent", "ObjectCount", "AttributeCount" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyObject), global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyObject.Parser, new[]{ "GobjectId", "TagName", "ContainedName", "BrowseName", "ParentGobjectId", "IsArea", "CategoryId", "HostedByGobjectId", "TemplateChain", "Attributes" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute), global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute.Parser, new[]{ "AttributeName", "FullTagReference", "MxDataType", "DataTypeName", "IsArray", "ArrayDimension", "ArrayDimensionPresent", "MxAttributeCategory", "SecurityClassification", "IsHistorized", "IsAlarm" }, null, null, null, null)
}));
}
#endregion
@@ -108,7 +108,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[0]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[0]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -269,7 +269,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[1]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[1]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -467,7 +467,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[2]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[2]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -628,7 +628,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[3]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[3]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -873,7 +873,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[4]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[4]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -1589,7 +1589,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[5]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[5]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -1623,12 +1623,12 @@ namespace MxGateway.Contracts.Proto.Galaxy {
/// <summary>Field number for the "objects" field.</summary>
public const int ObjectsFieldNumber = 1;
private static readonly pb::FieldCodec<global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject> _repeated_objects_codec
= pb::FieldCodec.ForMessage(10, global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject.Parser);
private readonly pbc::RepeatedField<global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject> objects_ = new pbc::RepeatedField<global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject>();
private static readonly pb::FieldCodec<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyObject> _repeated_objects_codec
= pb::FieldCodec.ForMessage(10, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyObject.Parser);
private readonly pbc::RepeatedField<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyObject> objects_ = new pbc::RepeatedField<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyObject>();
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public pbc::RepeatedField<global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject> Objects {
public pbc::RepeatedField<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyObject> Objects {
get { return objects_; }
}
@@ -1856,7 +1856,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[6]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[6]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -2067,7 +2067,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[7]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[7]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -2477,7 +2477,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[8]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[8]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -2625,12 +2625,12 @@ namespace MxGateway.Contracts.Proto.Galaxy {
/// <summary>Field number for the "attributes" field.</summary>
public const int AttributesFieldNumber = 10;
private static readonly pb::FieldCodec<global::MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute> _repeated_attributes_codec
= pb::FieldCodec.ForMessage(82, global::MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute.Parser);
private readonly pbc::RepeatedField<global::MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute> attributes_ = new pbc::RepeatedField<global::MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute>();
private static readonly pb::FieldCodec<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute> _repeated_attributes_codec
= pb::FieldCodec.ForMessage(82, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute.Parser);
private readonly pbc::RepeatedField<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute> attributes_ = new pbc::RepeatedField<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute>();
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public pbc::RepeatedField<global::MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute> Attributes {
public pbc::RepeatedField<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyAttribute> Attributes {
get { return attributes_; }
}
@@ -2986,7 +2986,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[9]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.MessageTypes[9]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -3053,6 +3053,14 @@ namespace MxGateway.Contracts.Proto.Galaxy {
/// <summary>Field number for the "mx_data_type" field.</summary>
public const int MxDataTypeFieldNumber = 3;
private int mxDataType_;
/// <summary>
/// Raw Galaxy SQL `dbo.data_type` identifier, passed through unchanged.
/// This is NOT a member of `mxaccess_gateway.v1.MxDataType` — Galaxy's
/// type enumeration is distinct from MXAccess's wire data-type enum and
/// the two must not be cast or compared. The GalaxyRepository service is
/// metadata-only and deliberately does not share types with
/// mxaccess_gateway.proto. See docs/GalaxyRepository.md.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public int MxDataType {
@@ -3065,6 +3073,10 @@ namespace MxGateway.Contracts.Proto.Galaxy {
/// <summary>Field number for the "data_type_name" field.</summary>
public const int DataTypeNameFieldNumber = 4;
private string dataTypeName_ = "";
/// <summary>
/// Human-readable name from Galaxy's `dbo.data_type` table (e.g. "Float",
/// "Integer", "Boolean"). Free-form Galaxy text; not a stable enum.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public string DataTypeName {
@@ -3113,6 +3125,11 @@ namespace MxGateway.Contracts.Proto.Galaxy {
/// <summary>Field number for the "mx_attribute_category" field.</summary>
public const int MxAttributeCategoryFieldNumber = 8;
private int mxAttributeCategory_;
/// <summary>
/// Raw Galaxy SQL attribute-category identifier, passed through unchanged.
/// Galaxy-specific; not mapped to any gateway enum. See
/// docs/GalaxyRepository.md.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public int MxAttributeCategory {
@@ -3125,6 +3142,11 @@ namespace MxGateway.Contracts.Proto.Galaxy {
/// <summary>Field number for the "security_classification" field.</summary>
public const int SecurityClassificationFieldNumber = 9;
private int securityClassification_;
/// <summary>
/// Raw Galaxy SQL security-classification identifier, passed through
/// unchanged. Galaxy-specific; not mapped to any gateway enum. See
/// docs/GalaxyRepository.md.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public int SecurityClassification {
@@ -7,7 +7,7 @@
using grpc = global::Grpc.Core;
namespace MxGateway.Contracts.Proto.Galaxy {
namespace ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy {
/// <summary>
/// Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL
/// database). Lets clients enumerate the deployed object hierarchy and each
@@ -52,24 +52,24 @@ namespace MxGateway.Contracts.Proto.Galaxy {
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest> __Marshaller_galaxy_repository_v1_TestConnectionRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest.Parser));
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest> __Marshaller_galaxy_repository_v1_TestConnectionRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply> __Marshaller_galaxy_repository_v1_TestConnectionReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply.Parser));
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionReply> __Marshaller_galaxy_repository_v1_TestConnectionReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionReply.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest> __Marshaller_galaxy_repository_v1_GetLastDeployTimeRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest.Parser));
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest> __Marshaller_galaxy_repository_v1_GetLastDeployTimeRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply> __Marshaller_galaxy_repository_v1_GetLastDeployTimeReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply.Parser));
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply> __Marshaller_galaxy_repository_v1_GetLastDeployTimeReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest> __Marshaller_galaxy_repository_v1_DiscoverHierarchyRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest.Parser));
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest> __Marshaller_galaxy_repository_v1_DiscoverHierarchyRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply> __Marshaller_galaxy_repository_v1_DiscoverHierarchyReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply.Parser));
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply> __Marshaller_galaxy_repository_v1_DiscoverHierarchyReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest> __Marshaller_galaxy_repository_v1_WatchDeployEventsRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest.Parser));
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest> __Marshaller_galaxy_repository_v1_WatchDeployEventsRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::MxGateway.Contracts.Proto.Galaxy.DeployEvent> __Marshaller_galaxy_repository_v1_DeployEvent = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.Galaxy.DeployEvent.Parser));
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DeployEvent> __Marshaller_galaxy_repository_v1_DeployEvent = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DeployEvent.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest, global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply> __Method_TestConnection = new grpc::Method<global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest, global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply>(
static readonly grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionReply> __Method_TestConnection = new grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionReply>(
grpc::MethodType.Unary,
__ServiceName,
"TestConnection",
@@ -77,7 +77,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
__Marshaller_galaxy_repository_v1_TestConnectionReply);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest, global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply> __Method_GetLastDeployTime = new grpc::Method<global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest, global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply>(
static readonly grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply> __Method_GetLastDeployTime = new grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply>(
grpc::MethodType.Unary,
__ServiceName,
"GetLastDeployTime",
@@ -85,7 +85,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
__Marshaller_galaxy_repository_v1_GetLastDeployTimeReply);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest, global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply> __Method_DiscoverHierarchy = new grpc::Method<global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest, global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply>(
static readonly grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply> __Method_DiscoverHierarchy = new grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply>(
grpc::MethodType.Unary,
__ServiceName,
"DiscoverHierarchy",
@@ -93,7 +93,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
__Marshaller_galaxy_repository_v1_DiscoverHierarchyReply);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest, global::MxGateway.Contracts.Proto.Galaxy.DeployEvent> __Method_WatchDeployEvents = new grpc::Method<global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest, global::MxGateway.Contracts.Proto.Galaxy.DeployEvent>(
static readonly grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DeployEvent> __Method_WatchDeployEvents = new grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DeployEvent>(
grpc::MethodType.ServerStreaming,
__ServiceName,
"WatchDeployEvents",
@@ -103,7 +103,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
/// <summary>Service descriptor</summary>
public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor
{
get { return global::MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.Services[0]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection.Descriptor.Services[0]; }
}
/// <summary>Base class for server-side implementations of GalaxyRepository</summary>
@@ -111,19 +111,19 @@ namespace MxGateway.Contracts.Proto.Galaxy {
public abstract partial class GalaxyRepositoryBase
{
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task<global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply> TestConnection(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::ServerCallContext context)
public virtual global::System.Threading.Tasks.Task<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionReply> TestConnection(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task<global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply> GetLastDeployTime(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::ServerCallContext context)
public virtual global::System.Threading.Tasks.Task<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply> GetLastDeployTime(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task<global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply> DiscoverHierarchy(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::ServerCallContext context)
public virtual global::System.Threading.Tasks.Task<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply> DiscoverHierarchy(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
@@ -141,7 +141,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
/// <param name="context">The context of the server-side call handler being invoked.</param>
/// <returns>A task indicating completion of the handler.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task WatchDeployEvents(global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest request, grpc::IServerStreamWriter<global::MxGateway.Contracts.Proto.Galaxy.DeployEvent> responseStream, grpc::ServerCallContext context)
public virtual global::System.Threading.Tasks.Task WatchDeployEvents(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest request, grpc::IServerStreamWriter<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DeployEvent> responseStream, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
@@ -176,62 +176,62 @@ namespace MxGateway.Contracts.Proto.Galaxy {
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply TestConnection(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionReply TestConnection(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return TestConnection(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply TestConnection(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::CallOptions options)
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionReply TestConnection(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_TestConnection, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply> TestConnectionAsync(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionReply> TestConnectionAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return TestConnectionAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply> TestConnectionAsync(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::CallOptions options)
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionReply> TestConnectionAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_TestConnection, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply GetLastDeployTime(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply GetLastDeployTime(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return GetLastDeployTime(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply GetLastDeployTime(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::CallOptions options)
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply GetLastDeployTime(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_GetLastDeployTime, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply> GetLastDeployTimeAsync(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply> GetLastDeployTimeAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return GetLastDeployTimeAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply> GetLastDeployTimeAsync(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::CallOptions options)
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply> GetLastDeployTimeAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_GetLastDeployTime, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply DiscoverHierarchy(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply DiscoverHierarchy(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return DiscoverHierarchy(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply DiscoverHierarchy(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::CallOptions options)
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply DiscoverHierarchy(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_DiscoverHierarchy, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply> DiscoverHierarchyAsync(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply> DiscoverHierarchyAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return DiscoverHierarchyAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply> DiscoverHierarchyAsync(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::CallOptions options)
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply> DiscoverHierarchyAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_DiscoverHierarchy, null, options, request);
}
@@ -249,7 +249,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
/// <param name="cancellationToken">An optional token for canceling the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncServerStreamingCall<global::MxGateway.Contracts.Proto.Galaxy.DeployEvent> WatchDeployEvents(global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
public virtual grpc::AsyncServerStreamingCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DeployEvent> WatchDeployEvents(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return WatchDeployEvents(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
@@ -265,7 +265,7 @@ namespace MxGateway.Contracts.Proto.Galaxy {
/// <param name="options">The options for the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncServerStreamingCall<global::MxGateway.Contracts.Proto.Galaxy.DeployEvent> WatchDeployEvents(global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest request, grpc::CallOptions options)
public virtual grpc::AsyncServerStreamingCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DeployEvent> WatchDeployEvents(global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncServerStreamingCall(__Method_WatchDeployEvents, null, options, request);
}
@@ -296,10 +296,10 @@ namespace MxGateway.Contracts.Proto.Galaxy {
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public static void BindService(grpc::ServiceBinderBase serviceBinder, GalaxyRepositoryBase serviceImpl)
{
serviceBinder.AddMethod(__Method_TestConnection, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest, global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply>(serviceImpl.TestConnection));
serviceBinder.AddMethod(__Method_GetLastDeployTime, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest, global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply>(serviceImpl.GetLastDeployTime));
serviceBinder.AddMethod(__Method_DiscoverHierarchy, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest, global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply>(serviceImpl.DiscoverHierarchy));
serviceBinder.AddMethod(__Method_WatchDeployEvents, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod<global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest, global::MxGateway.Contracts.Proto.Galaxy.DeployEvent>(serviceImpl.WatchDeployEvents));
serviceBinder.AddMethod(__Method_TestConnection, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.TestConnectionReply>(serviceImpl.TestConnection));
serviceBinder.AddMethod(__Method_GetLastDeployTime, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply>(serviceImpl.GetLastDeployTime));
serviceBinder.AddMethod(__Method_DiscoverHierarchy, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply>(serviceImpl.DiscoverHierarchy));
serviceBinder.AddMethod(__Method_WatchDeployEvents, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod<global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.DeployEvent>(serviceImpl.WatchDeployEvents));
}
}
@@ -0,0 +1,371 @@
// <auto-generated>
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: mxaccess_gateway.proto
// </auto-generated>
#pragma warning disable 0414, 1591, 8981, 0612
#region Designer generated code
using grpc = global::Grpc.Core;
namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
/// <summary>
/// Public client API for MXAccess sessions hosted by the gateway.
/// </summary>
public static partial class MxAccessGateway
{
static readonly string __ServiceName = "mxaccess_gateway.v1.MxAccessGateway";
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static void __Helper_SerializeMessage(global::Google.Protobuf.IMessage message, grpc::SerializationContext context)
{
#if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION
if (message is global::Google.Protobuf.IBufferMessage)
{
context.SetPayloadLength(message.CalculateSize());
global::Google.Protobuf.MessageExtensions.WriteTo(message, context.GetBufferWriter());
context.Complete();
return;
}
#endif
context.Complete(global::Google.Protobuf.MessageExtensions.ToByteArray(message));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static class __Helper_MessageCache<T>
{
public static readonly bool IsBufferMessage = global::System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(global::Google.Protobuf.IBufferMessage)).IsAssignableFrom(typeof(T));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static T __Helper_DeserializeMessage<T>(grpc::DeserializationContext context, global::Google.Protobuf.MessageParser<T> parser) where T : global::Google.Protobuf.IMessage<T>
{
#if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION
if (__Helper_MessageCache<T>.IsBufferMessage)
{
return parser.ParseFrom(context.PayloadAsReadOnlySequence());
}
#endif
return parser.ParseFrom(context.PayloadAsNewBuffer());
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionRequest> __Marshaller_mxaccess_gateway_v1_OpenSessionRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionReply> __Marshaller_mxaccess_gateway_v1_OpenSessionReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionReply.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionRequest> __Marshaller_mxaccess_gateway_v1_CloseSessionRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionReply> __Marshaller_mxaccess_gateway_v1_CloseSessionReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionReply.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandRequest> __Marshaller_mxaccess_gateway_v1_MxCommandRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply> __Marshaller_mxaccess_gateway_v1_MxCommandReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamEventsRequest> __Marshaller_mxaccess_gateway_v1_StreamEventsRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamEventsRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent> __Marshaller_mxaccess_gateway_v1_MxEvent = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest> __Marshaller_mxaccess_gateway_v1_AcknowledgeAlarmRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply> __Marshaller_mxaccess_gateway_v1_AcknowledgeAlarmReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamAlarmsRequest> __Marshaller_mxaccess_gateway_v1_StreamAlarmsRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamAlarmsRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage> __Marshaller_mxaccess_gateway_v1_AlarmFeedMessage = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionReply> __Method_OpenSession = new grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionReply>(
grpc::MethodType.Unary,
__ServiceName,
"OpenSession",
__Marshaller_mxaccess_gateway_v1_OpenSessionRequest,
__Marshaller_mxaccess_gateway_v1_OpenSessionReply);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionReply> __Method_CloseSession = new grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionReply>(
grpc::MethodType.Unary,
__ServiceName,
"CloseSession",
__Marshaller_mxaccess_gateway_v1_CloseSessionRequest,
__Marshaller_mxaccess_gateway_v1_CloseSessionReply);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply> __Method_Invoke = new grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply>(
grpc::MethodType.Unary,
__ServiceName,
"Invoke",
__Marshaller_mxaccess_gateway_v1_MxCommandRequest,
__Marshaller_mxaccess_gateway_v1_MxCommandReply);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamEventsRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent> __Method_StreamEvents = new grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamEventsRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent>(
grpc::MethodType.ServerStreaming,
__ServiceName,
"StreamEvents",
__Marshaller_mxaccess_gateway_v1_StreamEventsRequest,
__Marshaller_mxaccess_gateway_v1_MxEvent);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply> __Method_AcknowledgeAlarm = new grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply>(
grpc::MethodType.Unary,
__ServiceName,
"AcknowledgeAlarm",
__Marshaller_mxaccess_gateway_v1_AcknowledgeAlarmRequest,
__Marshaller_mxaccess_gateway_v1_AcknowledgeAlarmReply);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamAlarmsRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage> __Method_StreamAlarms = new grpc::Method<global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamAlarmsRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage>(
grpc::MethodType.ServerStreaming,
__ServiceName,
"StreamAlarms",
__Marshaller_mxaccess_gateway_v1_StreamAlarmsRequest,
__Marshaller_mxaccess_gateway_v1_AlarmFeedMessage);
/// <summary>Service descriptor</summary>
public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor
{
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.Services[0]; }
}
/// <summary>Base class for server-side implementations of MxAccessGateway</summary>
[grpc::BindServiceMethod(typeof(MxAccessGateway), "BindService")]
public abstract partial class MxAccessGatewayBase
{
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task<global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionReply> OpenSession(global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionRequest request, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task<global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionReply> CloseSession(global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionRequest request, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply> Invoke(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandRequest request, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task StreamEvents(global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamEventsRequest request, grpc::IServerStreamWriter<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent> responseStream, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task<global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply> AcknowledgeAlarm(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest request, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
/// <summary>
/// Session-less central alarm feed. The stream opens with the current
/// active-alarm snapshot (one `active_alarm` per alarm), then a single
/// `snapshot_complete`, then a `transition` for every subsequent change.
/// Served by the gateway's always-on alarm monitor; any number of clients
/// fan out from the single monitor without opening a worker session.
/// </summary>
/// <param name="request">The request received from the client.</param>
/// <param name="responseStream">Used for sending responses back to the client.</param>
/// <param name="context">The context of the server-side call handler being invoked.</param>
/// <returns>A task indicating completion of the handler.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task StreamAlarms(global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamAlarmsRequest request, grpc::IServerStreamWriter<global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage> responseStream, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
}
/// <summary>Client for MxAccessGateway</summary>
public partial class MxAccessGatewayClient : grpc::ClientBase<MxAccessGatewayClient>
{
/// <summary>Creates a new client for MxAccessGateway</summary>
/// <param name="channel">The channel to use to make remote calls.</param>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public MxAccessGatewayClient(grpc::ChannelBase channel) : base(channel)
{
}
/// <summary>Creates a new client for MxAccessGateway that uses a custom <c>CallInvoker</c>.</summary>
/// <param name="callInvoker">The callInvoker to use to make remote calls.</param>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public MxAccessGatewayClient(grpc::CallInvoker callInvoker) : base(callInvoker)
{
}
/// <summary>Protected parameterless constructor to allow creation of test doubles.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
protected MxAccessGatewayClient() : base()
{
}
/// <summary>Protected constructor to allow creation of configured clients.</summary>
/// <param name="configuration">The client configuration.</param>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
protected MxAccessGatewayClient(ClientBaseConfiguration configuration) : base(configuration)
{
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionReply OpenSession(global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return OpenSession(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionReply OpenSession(global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_OpenSession, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionReply> OpenSessionAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return OpenSessionAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionReply> OpenSessionAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_OpenSession, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionReply CloseSession(global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return CloseSession(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionReply CloseSession(global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_CloseSession, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionReply> CloseSessionAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return CloseSessionAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionReply> CloseSessionAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_CloseSession, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply Invoke(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return Invoke(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply Invoke(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_Invoke, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply> InvokeAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return InvokeAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply> InvokeAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_Invoke, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncServerStreamingCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent> StreamEvents(global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamEventsRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return StreamEvents(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncServerStreamingCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent> StreamEvents(global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamEventsRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncServerStreamingCall(__Method_StreamEvents, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply AcknowledgeAlarm(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return AcknowledgeAlarm(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply AcknowledgeAlarm(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_AcknowledgeAlarm, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply> AcknowledgeAlarmAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return AcknowledgeAlarmAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply> AcknowledgeAlarmAsync(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_AcknowledgeAlarm, null, options, request);
}
/// <summary>
/// Session-less central alarm feed. The stream opens with the current
/// active-alarm snapshot (one `active_alarm` per alarm), then a single
/// `snapshot_complete`, then a `transition` for every subsequent change.
/// Served by the gateway's always-on alarm monitor; any number of clients
/// fan out from the single monitor without opening a worker session.
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="headers">The initial metadata to send with the call. This parameter is optional.</param>
/// <param name="deadline">An optional deadline for the call. The call will be cancelled if deadline is hit.</param>
/// <param name="cancellationToken">An optional token for canceling the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncServerStreamingCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage> StreamAlarms(global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamAlarmsRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return StreamAlarms(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
/// <summary>
/// Session-less central alarm feed. The stream opens with the current
/// active-alarm snapshot (one `active_alarm` per alarm), then a single
/// `snapshot_complete`, then a `transition` for every subsequent change.
/// Served by the gateway's always-on alarm monitor; any number of clients
/// fan out from the single monitor without opening a worker session.
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="options">The options for the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncServerStreamingCall<global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage> StreamAlarms(global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamAlarmsRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncServerStreamingCall(__Method_StreamAlarms, null, options, request);
}
/// <summary>Creates a new instance of client from given <c>ClientBaseConfiguration</c>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
protected override MxAccessGatewayClient NewInstance(ClientBaseConfiguration configuration)
{
return new MxAccessGatewayClient(configuration);
}
}
/// <summary>Creates service definition that can be registered with a server</summary>
/// <param name="serviceImpl">An object implementing the server-side handling logic.</param>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public static grpc::ServerServiceDefinition BindService(MxAccessGatewayBase serviceImpl)
{
return grpc::ServerServiceDefinition.CreateBuilder()
.AddMethod(__Method_OpenSession, serviceImpl.OpenSession)
.AddMethod(__Method_CloseSession, serviceImpl.CloseSession)
.AddMethod(__Method_Invoke, serviceImpl.Invoke)
.AddMethod(__Method_StreamEvents, serviceImpl.StreamEvents)
.AddMethod(__Method_AcknowledgeAlarm, serviceImpl.AcknowledgeAlarm)
.AddMethod(__Method_StreamAlarms, serviceImpl.StreamAlarms).Build();
}
/// <summary>Register service method with a service binder with or without implementation. Useful when customizing the service binding logic.
/// Note: this method is part of an experimental API that can change or be removed without any prior notice.</summary>
/// <param name="serviceBinder">Service methods will be bound by calling <c>AddMethod</c> on this object.</param>
/// <param name="serviceImpl">An object implementing the server-side handling logic.</param>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public static void BindService(grpc::ServiceBinderBase serviceBinder, MxAccessGatewayBase serviceImpl)
{
serviceBinder.AddMethod(__Method_OpenSession, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.OpenSessionReply>(serviceImpl.OpenSession));
serviceBinder.AddMethod(__Method_CloseSession, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.CloseSessionReply>(serviceImpl.CloseSession));
serviceBinder.AddMethod(__Method_Invoke, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply>(serviceImpl.Invoke));
serviceBinder.AddMethod(__Method_StreamEvents, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod<global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamEventsRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent>(serviceImpl.StreamEvents));
serviceBinder.AddMethod(__Method_AcknowledgeAlarm, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply>(serviceImpl.AcknowledgeAlarm));
serviceBinder.AddMethod(__Method_StreamAlarms, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod<global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamAlarmsRequest, global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage>(serviceImpl.StreamAlarms));
}
}
}
#endregion
@@ -9,7 +9,7 @@ using pb = global::Google.Protobuf;
using pbc = global::Google.Protobuf.Collections;
using pbr = global::Google.Protobuf.Reflection;
using scg = global::System.Collections.Generic;
namespace MxGateway.Contracts.Proto {
namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
/// <summary>Holder for reflection information generated from mxaccess_worker.proto</summary>
public static partial class MxaccessWorkerReflection {
@@ -94,23 +94,23 @@ namespace MxGateway.Contracts.Proto {
"X0NBVEVHT1JZX01YQUNDRVNTX0VWRU5UX0NPTlZFUlNJT05fRkFJTEVEEAgS",
"IgoeV09SS0VSX0ZBVUxUX0NBVEVHT1JZX1NUQV9IVU5HEAkSKAokV09SS0VS",
"X0ZBVUxUX0NBVEVHT1JZX1FVRVVFX09WRVJGTE9XEAoSKgomV09SS0VSX0ZB",
"VUxUX0NBVEVHT1JZX1NIVVRET1dOX1RJTUVPVVQQC0IcqgIZTXhHYXRld2F5",
"LkNvbnRyYWN0cy5Qcm90b2IGcHJvdG8z"));
"VUxUX0NBVEVHT1JZX1NIVVRET1dOX1RJTUVPVVQQC0ImqgIjWkIuTU9NLldX",
"Lk14R2F0ZXdheS5Db250cmFjdHMuUHJvdG9iBnByb3RvMw=="));
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.DurationReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor, },
new pbr::GeneratedClrTypeInfo(new[] {typeof(global::MxGateway.Contracts.Proto.WorkerState), typeof(global::MxGateway.Contracts.Proto.WorkerFaultCategory), }, null, new pbr::GeneratedClrTypeInfo[] {
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.WorkerEnvelope), global::MxGateway.Contracts.Proto.WorkerEnvelope.Parser, new[]{ "ProtocolVersion", "SessionId", "Sequence", "CorrelationId", "GatewayHello", "WorkerHello", "WorkerReady", "WorkerCommand", "WorkerCommandReply", "WorkerCancel", "WorkerShutdown", "WorkerShutdownAck", "WorkerEvent", "WorkerHeartbeat", "WorkerFault" }, new[]{ "Body" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.GatewayHello), global::MxGateway.Contracts.Proto.GatewayHello.Parser, new[]{ "SupportedProtocolVersion", "Nonce", "GatewayVersion" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.WorkerHello), global::MxGateway.Contracts.Proto.WorkerHello.Parser, new[]{ "ProtocolVersion", "Nonce", "WorkerProcessId", "WorkerVersion" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.WorkerReady), global::MxGateway.Contracts.Proto.WorkerReady.Parser, new[]{ "WorkerProcessId", "MxaccessProgid", "MxaccessClsid", "ReadyTimestamp" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.WorkerCommand), global::MxGateway.Contracts.Proto.WorkerCommand.Parser, new[]{ "Command", "EnqueueTimestamp" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.WorkerCommandReply), global::MxGateway.Contracts.Proto.WorkerCommandReply.Parser, new[]{ "Reply", "CompletedTimestamp" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.WorkerCancel), global::MxGateway.Contracts.Proto.WorkerCancel.Parser, new[]{ "Reason" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.WorkerShutdown), global::MxGateway.Contracts.Proto.WorkerShutdown.Parser, new[]{ "GracePeriod", "Reason" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.WorkerShutdownAck), global::MxGateway.Contracts.Proto.WorkerShutdownAck.Parser, new[]{ "Status" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.WorkerEvent), global::MxGateway.Contracts.Proto.WorkerEvent.Parser, new[]{ "Event" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.WorkerHeartbeat), global::MxGateway.Contracts.Proto.WorkerHeartbeat.Parser, new[]{ "WorkerProcessId", "State", "LastStaActivityTimestamp", "PendingCommandCount", "OutboundEventQueueDepth", "LastEventSequence", "CurrentCommandCorrelationId" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.WorkerFault), global::MxGateway.Contracts.Proto.WorkerFault.Parser, new[]{ "Category", "CommandMethod", "Hresult", "ExceptionType", "DiagnosticMessage", "ProtocolStatus" }, new[]{ "Hresult" }, null, null, null)
new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.DurationReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor, },
new pbr::GeneratedClrTypeInfo(new[] {typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerState), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFaultCategory), }, null, new pbr::GeneratedClrTypeInfo[] {
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerEnvelope), global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerEnvelope.Parser, new[]{ "ProtocolVersion", "SessionId", "Sequence", "CorrelationId", "GatewayHello", "WorkerHello", "WorkerReady", "WorkerCommand", "WorkerCommandReply", "WorkerCancel", "WorkerShutdown", "WorkerShutdownAck", "WorkerEvent", "WorkerHeartbeat", "WorkerFault" }, new[]{ "Body" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.GatewayHello), global::ZB.MOM.WW.MxGateway.Contracts.Proto.GatewayHello.Parser, new[]{ "SupportedProtocolVersion", "Nonce", "GatewayVersion" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHello), global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHello.Parser, new[]{ "ProtocolVersion", "Nonce", "WorkerProcessId", "WorkerVersion" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerReady), global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerReady.Parser, new[]{ "WorkerProcessId", "MxaccessProgid", "MxaccessClsid", "ReadyTimestamp" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommand), global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommand.Parser, new[]{ "Command", "EnqueueTimestamp" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommandReply), global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommandReply.Parser, new[]{ "Reply", "CompletedTimestamp" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCancel), global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCancel.Parser, new[]{ "Reason" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdown), global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdown.Parser, new[]{ "GracePeriod", "Reason" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdownAck), global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdownAck.Parser, new[]{ "Status" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerEvent), global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerEvent.Parser, new[]{ "Event" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHeartbeat), global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHeartbeat.Parser, new[]{ "WorkerProcessId", "State", "LastStaActivityTimestamp", "PendingCommandCount", "OutboundEventQueueDepth", "LastEventSequence", "CurrentCommandCorrelationId" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFault), global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFault.Parser, new[]{ "Category", "CommandMethod", "Hresult", "ExceptionType", "DiagnosticMessage", "ProtocolStatus" }, new[]{ "Hresult" }, null, null, null)
}));
}
#endregion
@@ -166,7 +166,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[0]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[0]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -287,8 +287,8 @@ namespace MxGateway.Contracts.Proto {
public const int GatewayHelloFieldNumber = 10;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.GatewayHello GatewayHello {
get { return bodyCase_ == BodyOneofCase.GatewayHello ? (global::MxGateway.Contracts.Proto.GatewayHello) body_ : null; }
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.GatewayHello GatewayHello {
get { return bodyCase_ == BodyOneofCase.GatewayHello ? (global::ZB.MOM.WW.MxGateway.Contracts.Proto.GatewayHello) body_ : null; }
set {
body_ = value;
bodyCase_ = value == null ? BodyOneofCase.None : BodyOneofCase.GatewayHello;
@@ -299,8 +299,8 @@ namespace MxGateway.Contracts.Proto {
public const int WorkerHelloFieldNumber = 11;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.WorkerHello WorkerHello {
get { return bodyCase_ == BodyOneofCase.WorkerHello ? (global::MxGateway.Contracts.Proto.WorkerHello) body_ : null; }
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHello WorkerHello {
get { return bodyCase_ == BodyOneofCase.WorkerHello ? (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHello) body_ : null; }
set {
body_ = value;
bodyCase_ = value == null ? BodyOneofCase.None : BodyOneofCase.WorkerHello;
@@ -311,8 +311,8 @@ namespace MxGateway.Contracts.Proto {
public const int WorkerReadyFieldNumber = 12;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.WorkerReady WorkerReady {
get { return bodyCase_ == BodyOneofCase.WorkerReady ? (global::MxGateway.Contracts.Proto.WorkerReady) body_ : null; }
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerReady WorkerReady {
get { return bodyCase_ == BodyOneofCase.WorkerReady ? (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerReady) body_ : null; }
set {
body_ = value;
bodyCase_ = value == null ? BodyOneofCase.None : BodyOneofCase.WorkerReady;
@@ -323,8 +323,8 @@ namespace MxGateway.Contracts.Proto {
public const int WorkerCommandFieldNumber = 13;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.WorkerCommand WorkerCommand {
get { return bodyCase_ == BodyOneofCase.WorkerCommand ? (global::MxGateway.Contracts.Proto.WorkerCommand) body_ : null; }
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommand WorkerCommand {
get { return bodyCase_ == BodyOneofCase.WorkerCommand ? (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommand) body_ : null; }
set {
body_ = value;
bodyCase_ = value == null ? BodyOneofCase.None : BodyOneofCase.WorkerCommand;
@@ -335,8 +335,8 @@ namespace MxGateway.Contracts.Proto {
public const int WorkerCommandReplyFieldNumber = 14;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.WorkerCommandReply WorkerCommandReply {
get { return bodyCase_ == BodyOneofCase.WorkerCommandReply ? (global::MxGateway.Contracts.Proto.WorkerCommandReply) body_ : null; }
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommandReply WorkerCommandReply {
get { return bodyCase_ == BodyOneofCase.WorkerCommandReply ? (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommandReply) body_ : null; }
set {
body_ = value;
bodyCase_ = value == null ? BodyOneofCase.None : BodyOneofCase.WorkerCommandReply;
@@ -347,8 +347,8 @@ namespace MxGateway.Contracts.Proto {
public const int WorkerCancelFieldNumber = 15;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.WorkerCancel WorkerCancel {
get { return bodyCase_ == BodyOneofCase.WorkerCancel ? (global::MxGateway.Contracts.Proto.WorkerCancel) body_ : null; }
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCancel WorkerCancel {
get { return bodyCase_ == BodyOneofCase.WorkerCancel ? (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCancel) body_ : null; }
set {
body_ = value;
bodyCase_ = value == null ? BodyOneofCase.None : BodyOneofCase.WorkerCancel;
@@ -359,8 +359,8 @@ namespace MxGateway.Contracts.Proto {
public const int WorkerShutdownFieldNumber = 16;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.WorkerShutdown WorkerShutdown {
get { return bodyCase_ == BodyOneofCase.WorkerShutdown ? (global::MxGateway.Contracts.Proto.WorkerShutdown) body_ : null; }
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdown WorkerShutdown {
get { return bodyCase_ == BodyOneofCase.WorkerShutdown ? (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdown) body_ : null; }
set {
body_ = value;
bodyCase_ = value == null ? BodyOneofCase.None : BodyOneofCase.WorkerShutdown;
@@ -371,8 +371,8 @@ namespace MxGateway.Contracts.Proto {
public const int WorkerShutdownAckFieldNumber = 17;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.WorkerShutdownAck WorkerShutdownAck {
get { return bodyCase_ == BodyOneofCase.WorkerShutdownAck ? (global::MxGateway.Contracts.Proto.WorkerShutdownAck) body_ : null; }
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdownAck WorkerShutdownAck {
get { return bodyCase_ == BodyOneofCase.WorkerShutdownAck ? (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdownAck) body_ : null; }
set {
body_ = value;
bodyCase_ = value == null ? BodyOneofCase.None : BodyOneofCase.WorkerShutdownAck;
@@ -383,8 +383,8 @@ namespace MxGateway.Contracts.Proto {
public const int WorkerEventFieldNumber = 18;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.WorkerEvent WorkerEvent {
get { return bodyCase_ == BodyOneofCase.WorkerEvent ? (global::MxGateway.Contracts.Proto.WorkerEvent) body_ : null; }
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerEvent WorkerEvent {
get { return bodyCase_ == BodyOneofCase.WorkerEvent ? (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerEvent) body_ : null; }
set {
body_ = value;
bodyCase_ = value == null ? BodyOneofCase.None : BodyOneofCase.WorkerEvent;
@@ -395,8 +395,8 @@ namespace MxGateway.Contracts.Proto {
public const int WorkerHeartbeatFieldNumber = 19;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.WorkerHeartbeat WorkerHeartbeat {
get { return bodyCase_ == BodyOneofCase.WorkerHeartbeat ? (global::MxGateway.Contracts.Proto.WorkerHeartbeat) body_ : null; }
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHeartbeat WorkerHeartbeat {
get { return bodyCase_ == BodyOneofCase.WorkerHeartbeat ? (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHeartbeat) body_ : null; }
set {
body_ = value;
bodyCase_ = value == null ? BodyOneofCase.None : BodyOneofCase.WorkerHeartbeat;
@@ -407,8 +407,8 @@ namespace MxGateway.Contracts.Proto {
public const int WorkerFaultFieldNumber = 20;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.WorkerFault WorkerFault {
get { return bodyCase_ == BodyOneofCase.WorkerFault ? (global::MxGateway.Contracts.Proto.WorkerFault) body_ : null; }
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFault WorkerFault {
get { return bodyCase_ == BodyOneofCase.WorkerFault ? (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFault) body_ : null; }
set {
body_ = value;
bodyCase_ = value == null ? BodyOneofCase.None : BodyOneofCase.WorkerFault;
@@ -729,67 +729,67 @@ namespace MxGateway.Contracts.Proto {
switch (other.BodyCase) {
case BodyOneofCase.GatewayHello:
if (GatewayHello == null) {
GatewayHello = new global::MxGateway.Contracts.Proto.GatewayHello();
GatewayHello = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.GatewayHello();
}
GatewayHello.MergeFrom(other.GatewayHello);
break;
case BodyOneofCase.WorkerHello:
if (WorkerHello == null) {
WorkerHello = new global::MxGateway.Contracts.Proto.WorkerHello();
WorkerHello = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHello();
}
WorkerHello.MergeFrom(other.WorkerHello);
break;
case BodyOneofCase.WorkerReady:
if (WorkerReady == null) {
WorkerReady = new global::MxGateway.Contracts.Proto.WorkerReady();
WorkerReady = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerReady();
}
WorkerReady.MergeFrom(other.WorkerReady);
break;
case BodyOneofCase.WorkerCommand:
if (WorkerCommand == null) {
WorkerCommand = new global::MxGateway.Contracts.Proto.WorkerCommand();
WorkerCommand = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommand();
}
WorkerCommand.MergeFrom(other.WorkerCommand);
break;
case BodyOneofCase.WorkerCommandReply:
if (WorkerCommandReply == null) {
WorkerCommandReply = new global::MxGateway.Contracts.Proto.WorkerCommandReply();
WorkerCommandReply = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommandReply();
}
WorkerCommandReply.MergeFrom(other.WorkerCommandReply);
break;
case BodyOneofCase.WorkerCancel:
if (WorkerCancel == null) {
WorkerCancel = new global::MxGateway.Contracts.Proto.WorkerCancel();
WorkerCancel = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCancel();
}
WorkerCancel.MergeFrom(other.WorkerCancel);
break;
case BodyOneofCase.WorkerShutdown:
if (WorkerShutdown == null) {
WorkerShutdown = new global::MxGateway.Contracts.Proto.WorkerShutdown();
WorkerShutdown = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdown();
}
WorkerShutdown.MergeFrom(other.WorkerShutdown);
break;
case BodyOneofCase.WorkerShutdownAck:
if (WorkerShutdownAck == null) {
WorkerShutdownAck = new global::MxGateway.Contracts.Proto.WorkerShutdownAck();
WorkerShutdownAck = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdownAck();
}
WorkerShutdownAck.MergeFrom(other.WorkerShutdownAck);
break;
case BodyOneofCase.WorkerEvent:
if (WorkerEvent == null) {
WorkerEvent = new global::MxGateway.Contracts.Proto.WorkerEvent();
WorkerEvent = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerEvent();
}
WorkerEvent.MergeFrom(other.WorkerEvent);
break;
case BodyOneofCase.WorkerHeartbeat:
if (WorkerHeartbeat == null) {
WorkerHeartbeat = new global::MxGateway.Contracts.Proto.WorkerHeartbeat();
WorkerHeartbeat = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHeartbeat();
}
WorkerHeartbeat.MergeFrom(other.WorkerHeartbeat);
break;
case BodyOneofCase.WorkerFault:
if (WorkerFault == null) {
WorkerFault = new global::MxGateway.Contracts.Proto.WorkerFault();
WorkerFault = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFault();
}
WorkerFault.MergeFrom(other.WorkerFault);
break;
@@ -831,7 +831,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 82: {
global::MxGateway.Contracts.Proto.GatewayHello subBuilder = new global::MxGateway.Contracts.Proto.GatewayHello();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.GatewayHello subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.GatewayHello();
if (bodyCase_ == BodyOneofCase.GatewayHello) {
subBuilder.MergeFrom(GatewayHello);
}
@@ -840,7 +840,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 90: {
global::MxGateway.Contracts.Proto.WorkerHello subBuilder = new global::MxGateway.Contracts.Proto.WorkerHello();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHello subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHello();
if (bodyCase_ == BodyOneofCase.WorkerHello) {
subBuilder.MergeFrom(WorkerHello);
}
@@ -849,7 +849,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 98: {
global::MxGateway.Contracts.Proto.WorkerReady subBuilder = new global::MxGateway.Contracts.Proto.WorkerReady();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerReady subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerReady();
if (bodyCase_ == BodyOneofCase.WorkerReady) {
subBuilder.MergeFrom(WorkerReady);
}
@@ -858,7 +858,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 106: {
global::MxGateway.Contracts.Proto.WorkerCommand subBuilder = new global::MxGateway.Contracts.Proto.WorkerCommand();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommand subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommand();
if (bodyCase_ == BodyOneofCase.WorkerCommand) {
subBuilder.MergeFrom(WorkerCommand);
}
@@ -867,7 +867,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 114: {
global::MxGateway.Contracts.Proto.WorkerCommandReply subBuilder = new global::MxGateway.Contracts.Proto.WorkerCommandReply();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommandReply subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommandReply();
if (bodyCase_ == BodyOneofCase.WorkerCommandReply) {
subBuilder.MergeFrom(WorkerCommandReply);
}
@@ -876,7 +876,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 122: {
global::MxGateway.Contracts.Proto.WorkerCancel subBuilder = new global::MxGateway.Contracts.Proto.WorkerCancel();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCancel subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCancel();
if (bodyCase_ == BodyOneofCase.WorkerCancel) {
subBuilder.MergeFrom(WorkerCancel);
}
@@ -885,7 +885,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 130: {
global::MxGateway.Contracts.Proto.WorkerShutdown subBuilder = new global::MxGateway.Contracts.Proto.WorkerShutdown();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdown subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdown();
if (bodyCase_ == BodyOneofCase.WorkerShutdown) {
subBuilder.MergeFrom(WorkerShutdown);
}
@@ -894,7 +894,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 138: {
global::MxGateway.Contracts.Proto.WorkerShutdownAck subBuilder = new global::MxGateway.Contracts.Proto.WorkerShutdownAck();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdownAck subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdownAck();
if (bodyCase_ == BodyOneofCase.WorkerShutdownAck) {
subBuilder.MergeFrom(WorkerShutdownAck);
}
@@ -903,7 +903,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 146: {
global::MxGateway.Contracts.Proto.WorkerEvent subBuilder = new global::MxGateway.Contracts.Proto.WorkerEvent();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerEvent subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerEvent();
if (bodyCase_ == BodyOneofCase.WorkerEvent) {
subBuilder.MergeFrom(WorkerEvent);
}
@@ -912,7 +912,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 154: {
global::MxGateway.Contracts.Proto.WorkerHeartbeat subBuilder = new global::MxGateway.Contracts.Proto.WorkerHeartbeat();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHeartbeat subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHeartbeat();
if (bodyCase_ == BodyOneofCase.WorkerHeartbeat) {
subBuilder.MergeFrom(WorkerHeartbeat);
}
@@ -921,7 +921,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 162: {
global::MxGateway.Contracts.Proto.WorkerFault subBuilder = new global::MxGateway.Contracts.Proto.WorkerFault();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFault subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFault();
if (bodyCase_ == BodyOneofCase.WorkerFault) {
subBuilder.MergeFrom(WorkerFault);
}
@@ -965,7 +965,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 82: {
global::MxGateway.Contracts.Proto.GatewayHello subBuilder = new global::MxGateway.Contracts.Proto.GatewayHello();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.GatewayHello subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.GatewayHello();
if (bodyCase_ == BodyOneofCase.GatewayHello) {
subBuilder.MergeFrom(GatewayHello);
}
@@ -974,7 +974,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 90: {
global::MxGateway.Contracts.Proto.WorkerHello subBuilder = new global::MxGateway.Contracts.Proto.WorkerHello();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHello subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHello();
if (bodyCase_ == BodyOneofCase.WorkerHello) {
subBuilder.MergeFrom(WorkerHello);
}
@@ -983,7 +983,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 98: {
global::MxGateway.Contracts.Proto.WorkerReady subBuilder = new global::MxGateway.Contracts.Proto.WorkerReady();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerReady subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerReady();
if (bodyCase_ == BodyOneofCase.WorkerReady) {
subBuilder.MergeFrom(WorkerReady);
}
@@ -992,7 +992,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 106: {
global::MxGateway.Contracts.Proto.WorkerCommand subBuilder = new global::MxGateway.Contracts.Proto.WorkerCommand();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommand subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommand();
if (bodyCase_ == BodyOneofCase.WorkerCommand) {
subBuilder.MergeFrom(WorkerCommand);
}
@@ -1001,7 +1001,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 114: {
global::MxGateway.Contracts.Proto.WorkerCommandReply subBuilder = new global::MxGateway.Contracts.Proto.WorkerCommandReply();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommandReply subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCommandReply();
if (bodyCase_ == BodyOneofCase.WorkerCommandReply) {
subBuilder.MergeFrom(WorkerCommandReply);
}
@@ -1010,7 +1010,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 122: {
global::MxGateway.Contracts.Proto.WorkerCancel subBuilder = new global::MxGateway.Contracts.Proto.WorkerCancel();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCancel subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerCancel();
if (bodyCase_ == BodyOneofCase.WorkerCancel) {
subBuilder.MergeFrom(WorkerCancel);
}
@@ -1019,7 +1019,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 130: {
global::MxGateway.Contracts.Proto.WorkerShutdown subBuilder = new global::MxGateway.Contracts.Proto.WorkerShutdown();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdown subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdown();
if (bodyCase_ == BodyOneofCase.WorkerShutdown) {
subBuilder.MergeFrom(WorkerShutdown);
}
@@ -1028,7 +1028,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 138: {
global::MxGateway.Contracts.Proto.WorkerShutdownAck subBuilder = new global::MxGateway.Contracts.Proto.WorkerShutdownAck();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdownAck subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerShutdownAck();
if (bodyCase_ == BodyOneofCase.WorkerShutdownAck) {
subBuilder.MergeFrom(WorkerShutdownAck);
}
@@ -1037,7 +1037,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 146: {
global::MxGateway.Contracts.Proto.WorkerEvent subBuilder = new global::MxGateway.Contracts.Proto.WorkerEvent();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerEvent subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerEvent();
if (bodyCase_ == BodyOneofCase.WorkerEvent) {
subBuilder.MergeFrom(WorkerEvent);
}
@@ -1046,7 +1046,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 154: {
global::MxGateway.Contracts.Proto.WorkerHeartbeat subBuilder = new global::MxGateway.Contracts.Proto.WorkerHeartbeat();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHeartbeat subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerHeartbeat();
if (bodyCase_ == BodyOneofCase.WorkerHeartbeat) {
subBuilder.MergeFrom(WorkerHeartbeat);
}
@@ -1055,7 +1055,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 162: {
global::MxGateway.Contracts.Proto.WorkerFault subBuilder = new global::MxGateway.Contracts.Proto.WorkerFault();
global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFault subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFault();
if (bodyCase_ == BodyOneofCase.WorkerFault) {
subBuilder.MergeFrom(WorkerFault);
}
@@ -1085,7 +1085,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[1]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[1]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -1357,7 +1357,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[2]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[2]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -1666,7 +1666,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[3]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[3]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -1984,7 +1984,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[4]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[4]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -2017,10 +2017,10 @@ namespace MxGateway.Contracts.Proto {
/// <summary>Field number for the "command" field.</summary>
public const int CommandFieldNumber = 1;
private global::MxGateway.Contracts.Proto.MxCommand command_;
private global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommand command_;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.MxCommand Command {
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommand Command {
get { return command_; }
set {
command_ = value;
@@ -2139,7 +2139,7 @@ namespace MxGateway.Contracts.Proto {
}
if (other.command_ != null) {
if (command_ == null) {
Command = new global::MxGateway.Contracts.Proto.MxCommand();
Command = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommand();
}
Command.MergeFrom(other.Command);
}
@@ -2170,7 +2170,7 @@ namespace MxGateway.Contracts.Proto {
break;
case 10: {
if (command_ == null) {
Command = new global::MxGateway.Contracts.Proto.MxCommand();
Command = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommand();
}
input.ReadMessage(Command);
break;
@@ -2203,7 +2203,7 @@ namespace MxGateway.Contracts.Proto {
break;
case 10: {
if (command_ == null) {
Command = new global::MxGateway.Contracts.Proto.MxCommand();
Command = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommand();
}
input.ReadMessage(Command);
break;
@@ -2237,7 +2237,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[5]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[5]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -2270,10 +2270,10 @@ namespace MxGateway.Contracts.Proto {
/// <summary>Field number for the "reply" field.</summary>
public const int ReplyFieldNumber = 1;
private global::MxGateway.Contracts.Proto.MxCommandReply reply_;
private global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply reply_;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.MxCommandReply Reply {
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply Reply {
get { return reply_; }
set {
reply_ = value;
@@ -2392,7 +2392,7 @@ namespace MxGateway.Contracts.Proto {
}
if (other.reply_ != null) {
if (reply_ == null) {
Reply = new global::MxGateway.Contracts.Proto.MxCommandReply();
Reply = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply();
}
Reply.MergeFrom(other.Reply);
}
@@ -2423,7 +2423,7 @@ namespace MxGateway.Contracts.Proto {
break;
case 10: {
if (reply_ == null) {
Reply = new global::MxGateway.Contracts.Proto.MxCommandReply();
Reply = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply();
}
input.ReadMessage(Reply);
break;
@@ -2456,7 +2456,7 @@ namespace MxGateway.Contracts.Proto {
break;
case 10: {
if (reply_ == null) {
Reply = new global::MxGateway.Contracts.Proto.MxCommandReply();
Reply = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandReply();
}
input.ReadMessage(Reply);
break;
@@ -2490,7 +2490,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[6]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[6]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -2688,7 +2688,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[7]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[7]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -2932,7 +2932,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[8]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[8]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -2964,10 +2964,10 @@ namespace MxGateway.Contracts.Proto {
/// <summary>Field number for the "status" field.</summary>
public const int StatusFieldNumber = 1;
private global::MxGateway.Contracts.Proto.ProtocolStatus status_;
private global::ZB.MOM.WW.MxGateway.Contracts.Proto.ProtocolStatus status_;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.ProtocolStatus Status {
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.ProtocolStatus Status {
get { return status_; }
set {
status_ = value;
@@ -3061,7 +3061,7 @@ namespace MxGateway.Contracts.Proto {
}
if (other.status_ != null) {
if (status_ == null) {
Status = new global::MxGateway.Contracts.Proto.ProtocolStatus();
Status = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.ProtocolStatus();
}
Status.MergeFrom(other.Status);
}
@@ -3086,7 +3086,7 @@ namespace MxGateway.Contracts.Proto {
break;
case 10: {
if (status_ == null) {
Status = new global::MxGateway.Contracts.Proto.ProtocolStatus();
Status = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.ProtocolStatus();
}
input.ReadMessage(Status);
break;
@@ -3112,7 +3112,7 @@ namespace MxGateway.Contracts.Proto {
break;
case 10: {
if (status_ == null) {
Status = new global::MxGateway.Contracts.Proto.ProtocolStatus();
Status = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.ProtocolStatus();
}
input.ReadMessage(Status);
break;
@@ -3139,7 +3139,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[9]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[9]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -3171,10 +3171,10 @@ namespace MxGateway.Contracts.Proto {
/// <summary>Field number for the "event" field.</summary>
public const int EventFieldNumber = 1;
private global::MxGateway.Contracts.Proto.MxEvent event_;
private global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent event_;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.MxEvent Event {
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent Event {
get { return event_; }
set {
event_ = value;
@@ -3268,7 +3268,7 @@ namespace MxGateway.Contracts.Proto {
}
if (other.event_ != null) {
if (event_ == null) {
Event = new global::MxGateway.Contracts.Proto.MxEvent();
Event = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent();
}
Event.MergeFrom(other.Event);
}
@@ -3293,7 +3293,7 @@ namespace MxGateway.Contracts.Proto {
break;
case 10: {
if (event_ == null) {
Event = new global::MxGateway.Contracts.Proto.MxEvent();
Event = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent();
}
input.ReadMessage(Event);
break;
@@ -3319,7 +3319,7 @@ namespace MxGateway.Contracts.Proto {
break;
case 10: {
if (event_ == null) {
Event = new global::MxGateway.Contracts.Proto.MxEvent();
Event = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent();
}
input.ReadMessage(Event);
break;
@@ -3346,7 +3346,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[10]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[10]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -3396,10 +3396,10 @@ namespace MxGateway.Contracts.Proto {
/// <summary>Field number for the "state" field.</summary>
public const int StateFieldNumber = 2;
private global::MxGateway.Contracts.Proto.WorkerState state_ = global::MxGateway.Contracts.Proto.WorkerState.Unspecified;
private global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerState state_ = global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerState.Unspecified;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.WorkerState State {
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerState State {
get { return state_; }
set {
state_ = value;
@@ -3496,7 +3496,7 @@ namespace MxGateway.Contracts.Proto {
public override int GetHashCode() {
int hash = 1;
if (WorkerProcessId != 0) hash ^= WorkerProcessId.GetHashCode();
if (State != global::MxGateway.Contracts.Proto.WorkerState.Unspecified) hash ^= State.GetHashCode();
if (State != global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerState.Unspecified) hash ^= State.GetHashCode();
if (lastStaActivityTimestamp_ != null) hash ^= LastStaActivityTimestamp.GetHashCode();
if (PendingCommandCount != 0) hash ^= PendingCommandCount.GetHashCode();
if (OutboundEventQueueDepth != 0) hash ^= OutboundEventQueueDepth.GetHashCode();
@@ -3524,7 +3524,7 @@ namespace MxGateway.Contracts.Proto {
output.WriteRawTag(8);
output.WriteInt32(WorkerProcessId);
}
if (State != global::MxGateway.Contracts.Proto.WorkerState.Unspecified) {
if (State != global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerState.Unspecified) {
output.WriteRawTag(16);
output.WriteEnum((int) State);
}
@@ -3562,7 +3562,7 @@ namespace MxGateway.Contracts.Proto {
output.WriteRawTag(8);
output.WriteInt32(WorkerProcessId);
}
if (State != global::MxGateway.Contracts.Proto.WorkerState.Unspecified) {
if (State != global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerState.Unspecified) {
output.WriteRawTag(16);
output.WriteEnum((int) State);
}
@@ -3599,7 +3599,7 @@ namespace MxGateway.Contracts.Proto {
if (WorkerProcessId != 0) {
size += 1 + pb::CodedOutputStream.ComputeInt32Size(WorkerProcessId);
}
if (State != global::MxGateway.Contracts.Proto.WorkerState.Unspecified) {
if (State != global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerState.Unspecified) {
size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) State);
}
if (lastStaActivityTimestamp_ != null) {
@@ -3632,7 +3632,7 @@ namespace MxGateway.Contracts.Proto {
if (other.WorkerProcessId != 0) {
WorkerProcessId = other.WorkerProcessId;
}
if (other.State != global::MxGateway.Contracts.Proto.WorkerState.Unspecified) {
if (other.State != global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerState.Unspecified) {
State = other.State;
}
if (other.lastStaActivityTimestamp_ != null) {
@@ -3677,7 +3677,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 16: {
State = (global::MxGateway.Contracts.Proto.WorkerState) input.ReadEnum();
State = (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerState) input.ReadEnum();
break;
}
case 26: {
@@ -3727,7 +3727,7 @@ namespace MxGateway.Contracts.Proto {
break;
}
case 16: {
State = (global::MxGateway.Contracts.Proto.WorkerState) input.ReadEnum();
State = (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerState) input.ReadEnum();
break;
}
case 26: {
@@ -3776,7 +3776,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[11]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessWorkerReflection.Descriptor.MessageTypes[11]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -3814,10 +3814,10 @@ namespace MxGateway.Contracts.Proto {
/// <summary>Field number for the "category" field.</summary>
public const int CategoryFieldNumber = 1;
private global::MxGateway.Contracts.Proto.WorkerFaultCategory category_ = global::MxGateway.Contracts.Proto.WorkerFaultCategory.Unspecified;
private global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFaultCategory category_ = global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFaultCategory.Unspecified;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.WorkerFaultCategory Category {
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFaultCategory Category {
get { return category_; }
set {
category_ = value;
@@ -3889,10 +3889,10 @@ namespace MxGateway.Contracts.Proto {
/// <summary>Field number for the "protocol_status" field.</summary>
public const int ProtocolStatusFieldNumber = 6;
private global::MxGateway.Contracts.Proto.ProtocolStatus protocolStatus_;
private global::ZB.MOM.WW.MxGateway.Contracts.Proto.ProtocolStatus protocolStatus_;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::MxGateway.Contracts.Proto.ProtocolStatus ProtocolStatus {
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.ProtocolStatus ProtocolStatus {
get { return protocolStatus_; }
set {
protocolStatus_ = value;
@@ -3927,7 +3927,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override int GetHashCode() {
int hash = 1;
if (Category != global::MxGateway.Contracts.Proto.WorkerFaultCategory.Unspecified) hash ^= Category.GetHashCode();
if (Category != global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFaultCategory.Unspecified) hash ^= Category.GetHashCode();
if (CommandMethod.Length != 0) hash ^= CommandMethod.GetHashCode();
if (HasHresult) hash ^= Hresult.GetHashCode();
if (ExceptionType.Length != 0) hash ^= ExceptionType.GetHashCode();
@@ -3951,7 +3951,7 @@ namespace MxGateway.Contracts.Proto {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
output.WriteRawMessage(this);
#else
if (Category != global::MxGateway.Contracts.Proto.WorkerFaultCategory.Unspecified) {
if (Category != global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFaultCategory.Unspecified) {
output.WriteRawTag(8);
output.WriteEnum((int) Category);
}
@@ -3985,7 +3985,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
if (Category != global::MxGateway.Contracts.Proto.WorkerFaultCategory.Unspecified) {
if (Category != global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFaultCategory.Unspecified) {
output.WriteRawTag(8);
output.WriteEnum((int) Category);
}
@@ -4019,7 +4019,7 @@ namespace MxGateway.Contracts.Proto {
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public int CalculateSize() {
int size = 0;
if (Category != global::MxGateway.Contracts.Proto.WorkerFaultCategory.Unspecified) {
if (Category != global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFaultCategory.Unspecified) {
size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) Category);
}
if (CommandMethod.Length != 0) {
@@ -4049,7 +4049,7 @@ namespace MxGateway.Contracts.Proto {
if (other == null) {
return;
}
if (other.Category != global::MxGateway.Contracts.Proto.WorkerFaultCategory.Unspecified) {
if (other.Category != global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFaultCategory.Unspecified) {
Category = other.Category;
}
if (other.CommandMethod.Length != 0) {
@@ -4066,7 +4066,7 @@ namespace MxGateway.Contracts.Proto {
}
if (other.protocolStatus_ != null) {
if (protocolStatus_ == null) {
ProtocolStatus = new global::MxGateway.Contracts.Proto.ProtocolStatus();
ProtocolStatus = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.ProtocolStatus();
}
ProtocolStatus.MergeFrom(other.ProtocolStatus);
}
@@ -4090,7 +4090,7 @@ namespace MxGateway.Contracts.Proto {
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
break;
case 8: {
Category = (global::MxGateway.Contracts.Proto.WorkerFaultCategory) input.ReadEnum();
Category = (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFaultCategory) input.ReadEnum();
break;
}
case 18: {
@@ -4111,7 +4111,7 @@ namespace MxGateway.Contracts.Proto {
}
case 50: {
if (protocolStatus_ == null) {
ProtocolStatus = new global::MxGateway.Contracts.Proto.ProtocolStatus();
ProtocolStatus = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.ProtocolStatus();
}
input.ReadMessage(ProtocolStatus);
break;
@@ -4136,7 +4136,7 @@ namespace MxGateway.Contracts.Proto {
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
break;
case 8: {
Category = (global::MxGateway.Contracts.Proto.WorkerFaultCategory) input.ReadEnum();
Category = (global::ZB.MOM.WW.MxGateway.Contracts.Proto.WorkerFaultCategory) input.ReadEnum();
break;
}
case 18: {
@@ -4157,7 +4157,7 @@ namespace MxGateway.Contracts.Proto {
}
case 50: {
if (protocolStatus_ == null) {
ProtocolStatus = new global::MxGateway.Contracts.Proto.ProtocolStatus();
ProtocolStatus = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.ProtocolStatus();
}
input.ReadMessage(ProtocolStatus);
break;
@@ -2,11 +2,18 @@ syntax = "proto3";
package galaxy_repository.v1;
option csharp_namespace = "MxGateway.Contracts.Proto.Galaxy";
option csharp_namespace = "ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
// Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
// additively only. Never renumber or repurpose an existing field number or
// enum value. When a field or enum value is removed, add a `reserved` range
// (and `reserved` name) covering it in the same change so a future editor
// cannot accidentally reuse the retired tag. There are no `reserved`
// declarations today because no field or enum value has ever been removed.
// Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL
// database). Lets clients enumerate the deployed object hierarchy and each
// object's dynamic attributes so they know what tag references to subscribe
@@ -110,12 +117,26 @@ message GalaxyObject {
message GalaxyAttribute {
string attribute_name = 1;
string full_tag_reference = 2;
// Raw Galaxy SQL `dbo.data_type` identifier, passed through unchanged.
// This is NOT a member of `mxaccess_gateway.v1.MxDataType` Galaxy's
// type enumeration is distinct from MXAccess's wire data-type enum and
// the two must not be cast or compared. The GalaxyRepository service is
// metadata-only and deliberately does not share types with
// mxaccess_gateway.proto. See docs/GalaxyRepository.md.
int32 mx_data_type = 3;
// Human-readable name from Galaxy's `dbo.data_type` table (e.g. "Float",
// "Integer", "Boolean"). Free-form Galaxy text; not a stable enum.
string data_type_name = 4;
bool is_array = 5;
int32 array_dimension = 6;
bool array_dimension_present = 7;
// Raw Galaxy SQL attribute-category identifier, passed through unchanged.
// Galaxy-specific; not mapped to any gateway enum. See
// docs/GalaxyRepository.md.
int32 mx_attribute_category = 8;
// Raw Galaxy SQL security-classification identifier, passed through
// unchanged. Galaxy-specific; not mapped to any gateway enum. See
// docs/GalaxyRepository.md.
int32 security_classification = 9;
bool is_historized = 10;
bool is_alarm = 11;
@@ -2,11 +2,17 @@ syntax = "proto3";
package mxaccess_gateway.v1;
option csharp_namespace = "MxGateway.Contracts.Proto";
option csharp_namespace = "ZB.MOM.WW.MxGateway.Contracts.Proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
// Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
// additively only. Never renumber or repurpose an existing field number or
// enum value. When a field or enum value is removed, add a `reserved` range
// (and `reserved` name) covering it in the same change so a future editor
// cannot accidentally reuse the retired tag.
// Public client API for MXAccess sessions hosted by the gateway.
service MxAccessGateway {
rpc OpenSession(OpenSessionRequest) returns (OpenSessionReply);
@@ -14,7 +20,12 @@ service MxAccessGateway {
rpc Invoke(MxCommandRequest) returns (MxCommandReply);
rpc StreamEvents(StreamEventsRequest) returns (stream MxEvent);
rpc AcknowledgeAlarm(AcknowledgeAlarmRequest) returns (AcknowledgeAlarmReply);
rpc QueryActiveAlarms(QueryActiveAlarmsRequest) returns (stream ActiveAlarmSnapshot);
// Session-less central alarm feed. The stream opens with the current
// active-alarm snapshot (one `active_alarm` per alarm), then a single
// `snapshot_complete`, then a `transition` for every subsequent change.
// Served by the gateway's always-on alarm monitor; any number of clients
// fan out from the single monitor without opening a worker session.
rpc StreamAlarms(StreamAlarmsRequest) returns (stream AlarmFeedMessage);
}
message OpenSessionRequest {
@@ -93,6 +104,11 @@ message MxCommand {
AcknowledgeAlarmCommand acknowledge_alarm_command = 36;
QueryActiveAlarmsCommand query_active_alarms_command = 37;
AcknowledgeAlarmByNameCommand acknowledge_alarm_by_name_command = 38;
WriteBulkCommand write_bulk = 39;
Write2BulkCommand write2_bulk = 40;
WriteSecuredBulkCommand write_secured_bulk = 41;
WriteSecured2BulkCommand write_secured2_bulk = 42;
ReadBulkCommand read_bulk = 43;
PingCommand ping = 100;
GetSessionStateCommand get_session_state = 101;
GetWorkerInfoCommand get_worker_info = 102;
@@ -132,6 +148,11 @@ enum MxCommandKind {
MX_COMMAND_KIND_ACKNOWLEDGE_ALARM = 27;
MX_COMMAND_KIND_QUERY_ACTIVE_ALARMS = 28;
MX_COMMAND_KIND_ACKNOWLEDGE_ALARM_BY_NAME = 29;
MX_COMMAND_KIND_WRITE_BULK = 30;
MX_COMMAND_KIND_WRITE2_BULK = 31;
MX_COMMAND_KIND_WRITE_SECURED_BULK = 32;
MX_COMMAND_KIND_WRITE_SECURED2_BULK = 33;
MX_COMMAND_KIND_READ_BULK = 34;
MX_COMMAND_KIND_PING = 100;
MX_COMMAND_KIND_GET_SESSION_STATE = 101;
MX_COMMAND_KIND_GET_WORKER_INFO = 102;
@@ -335,6 +356,88 @@ message UnsubscribeBulkCommand {
repeated int32 item_handles = 2;
}
// Bulk Write sequential MXAccess Write per entry, on the worker's STA.
// MXAccess has no native bulk write; each entry round-trips through the same
// single-item Write path the gateway uses today. Per-item failures appear as
// BulkWriteResult entries with `was_successful = false` and never throw.
message WriteBulkCommand {
int32 server_handle = 1;
repeated WriteBulkEntry entries = 2;
}
message WriteBulkEntry {
int32 item_handle = 1;
MxValue value = 2;
int32 user_id = 3;
}
// Bulk Write2 sequential MXAccess Write2 (timestamped) per entry.
message Write2BulkCommand {
int32 server_handle = 1;
repeated Write2BulkEntry entries = 2;
}
message Write2BulkEntry {
int32 item_handle = 1;
MxValue value = 2;
MxValue timestamp_value = 3;
int32 user_id = 4;
}
// Bulk WriteSecured sequential MXAccess WriteSecured per entry.
// Credential-sensitive values (`value`) MUST be kept out of logs, metrics
// labels, command lines, and diagnostics same redaction rules as the
// single-item WriteSecured contract.
message WriteSecuredBulkCommand {
int32 server_handle = 1;
repeated WriteSecuredBulkEntry entries = 2;
}
message WriteSecuredBulkEntry {
int32 item_handle = 1;
int32 current_user_id = 2;
int32 verifier_user_id = 3;
// Credential-sensitive write value. Implementations must not log this field
// unless an explicit redacted value-logging path is enabled.
MxValue value = 4;
}
// Bulk WriteSecured2 sequential MXAccess WriteSecured2 (timestamped) per
// entry. Same redaction rules apply.
message WriteSecured2BulkCommand {
int32 server_handle = 1;
repeated WriteSecured2BulkEntry entries = 2;
}
message WriteSecured2BulkEntry {
int32 item_handle = 1;
int32 current_user_id = 2;
int32 verifier_user_id = 3;
// Credential-sensitive write value. Implementations must not log this field
// unless an explicit redacted value-logging path is enabled.
MxValue value = 4;
MxValue timestamp_value = 5;
}
// Bulk Read snapshot the current value for each requested tag. MXAccess COM
// has no synchronous Read; the worker implements ReadBulk as:
//
// - If the tag is already in the session's item registry AND that item is
// currently advised AND the worker has a cached OnDataChange for it, the
// reply returns the cached value WITHOUT modifying the existing
// subscription (was_cached = true).
// - Otherwise the worker takes the snapshot lifecycle itself: AddItem +
// Advise, wait up to `timeout_ms` for the first OnDataChange, then
// UnAdvise + RemoveItem before returning. The session is left exactly
// as it was before the call (was_cached = false).
//
// `timeout_ms == 0` uses the gateway-configured default (1000 ms).
message ReadBulkCommand {
int32 server_handle = 1;
repeated string tag_addresses = 2;
uint32 timeout_ms = 3;
}
message PingCommand {
string message = 1;
}
@@ -381,8 +484,22 @@ message MxCommandReply {
BulkSubscribeReply un_advise_item_bulk = 31;
BulkSubscribeReply subscribe_bulk = 32;
BulkSubscribeReply unsubscribe_bulk = 33;
// Reply payload for BOTH MX_COMMAND_KIND_ACKNOWLEDGE_ALARM (by GUID)
// and MX_COMMAND_KIND_ACKNOWLEDGE_ALARM_BY_NAME. There is intentionally
// no by-name-specific reply case: the by-name ack carries no outcome
// detail beyond the native ack return code, so the worker reuses this
// `acknowledge_alarm` payload for both command kinds (the worker's
// MxAccessCommandExecutor sets `acknowledge_alarm` for the by-name arm
// too). Consumers must dispatch on MxCommandReply.kind, not on the
// payload case, to tell the two acks apart. The top-level `hresult`
// mirrors AcknowledgeAlarmReplyPayload.native_status and is preferred.
AcknowledgeAlarmReplyPayload acknowledge_alarm = 34;
QueryActiveAlarmsReplyPayload query_active_alarms = 35;
BulkWriteReply write_bulk = 36;
BulkWriteReply write2_bulk = 37;
BulkWriteReply write_secured_bulk = 38;
BulkWriteReply write_secured2_bulk = 39;
BulkReadReply read_bulk = 40;
SessionStateReply session_state = 100;
WorkerInfoReply worker_info = 101;
DrainEventsReply drain_events = 102;
@@ -433,6 +550,61 @@ message BulkSubscribeReply {
repeated SubscribeResult results = 1;
}
// Per-item result for the four bulk write families. `item_handle` mirrors the
// request entry's item_handle so callers can correlate inputs to outputs even
// when the gateway's per-entry `IConstraintEnforcer.CheckWriteHandleAsync`
// filter (see `MxAccessGatewayService.ReplaceWriteBulkEntries` and
// `docs/Authorization.md`) dropped some entries before reaching the worker.
// Per-item failures populate `error_message` + `hresult` and never raise
// callers iterate and inspect each entry.
message BulkWriteResult {
int32 server_handle = 1;
int32 item_handle = 2;
bool was_successful = 3;
optional int32 hresult = 4;
repeated MxStatusProxy statuses = 5;
string error_message = 6;
}
message BulkWriteReply {
repeated BulkWriteResult results = 1;
}
// Per-tag result for ReadBulk. `was_cached` is true when the value came from
// an existing live subscription's last OnDataChange (the worker did not touch
// the subscription); false when the worker took the AddItem + Advise + wait +
// UnAdvise + RemoveItem snapshot lifecycle itself.
//
// On `was_successful = true`, `value`, `quality`, `source_timestamp`, and
// `statuses` carry the read data (from the cached subscription or the snapshot
// lifecycle, depending on `was_cached`) and `error_message` is empty. On
// `was_successful = false`, only `server_handle`, `tag_address`, `item_handle`
// (when allocated), `was_cached`, and `error_message` are populated; `value`,
// `quality`, `source_timestamp`, and `statuses` are left at their proto3
// defaults (null / 0 / null / empty) and must not be read as data they are
// wire-indistinguishable from "value is null with quality bad" data and serve
// only as absent markers. ReadBulk has no `hresult` field by design (its
// outcomes are timeout / cache / lifecycle states, not MXAccess COM return
// codes see `docs/DesignDecisions.md` "Bulk Command Family"). Per-tag
// failures populate `error_message` and never raise callers iterate and
// inspect each entry.
message BulkReadResult {
int32 server_handle = 1;
string tag_address = 2;
int32 item_handle = 3;
bool was_successful = 4;
bool was_cached = 5;
MxValue value = 6;
int32 quality = 7;
google.protobuf.Timestamp source_timestamp = 8;
repeated MxStatusProxy statuses = 9;
string error_message = 10;
}
message BulkReadReply {
repeated BulkReadResult results = 1;
}
message SessionStateReply {
SessionState state = 1;
}
@@ -448,12 +620,16 @@ message DrainEventsReply {
repeated MxEvent events = 1;
}
// Reply payload for AcknowledgeAlarmCommand. Surfaces AVEVA's native
// AlarmAckByGUID return code; 0 means success. The MxCommandReply's
// hresult field carries the same value and is preferred for protocol
// consumers this payload exists so the gateway-side
// WorkerAlarmRpcDispatcher can echo native_status into
// AcknowledgeAlarmReply.hresult without unpacking the outer envelope.
// Reply payload for AcknowledgeAlarmCommand AND
// AcknowledgeAlarmByNameCommand both ack command kinds reuse this
// payload case (`MxCommandReply.acknowledge_alarm`); there is no
// dedicated by-name reply case. Surfaces AVEVA's native ack return
// code (AlarmAckByGUID for the GUID arm, AlarmAckByName for the
// by-name arm); 0 means success. The MxCommandReply's hresult field
// carries the same value and is preferred for protocol consumers
// this payload exists so the gateway-side WorkerAlarmRpcDispatcher
// can echo native_status into AcknowledgeAlarmReply.hresult without
// unpacking the outer envelope.
message AcknowledgeAlarmReplyPayload {
int32 native_status = 1;
}
@@ -613,7 +789,10 @@ enum AlarmConditionState {
}
message AcknowledgeAlarmRequest {
string session_id = 1;
// Retired: acknowledgement is session-less it routes to the gateway's
// central alarm monitor, not a client worker session.
reserved 1;
reserved "session_id";
string client_correlation_id = 2;
// Fully-qualified alarm reference matching OnAlarmTransitionEvent.alarm_full_reference.
string alarm_full_reference = 3;
@@ -625,25 +804,60 @@ message AcknowledgeAlarmRequest {
}
message AcknowledgeAlarmReply {
string session_id = 1;
// Retired: see AcknowledgeAlarmRequest acknowledgement is session-less.
reserved 1;
reserved "session_id";
string correlation_id = 2;
ProtocolStatus protocol_status = 3;
// HRESULT captured from MXAccess if the ack failed at the COM layer.
// Native ack return code echoed from the worker. The worker carries the
// ack outcome as a single int32 (AcknowledgeAlarmReplyPayload.native_status,
// = AlarmAckByName / AlarmAckByGUID return code; 0 = success); the gateway's
// WorkerAlarmRpcDispatcher copies that value here. This is the authoritative
// ack-outcome field for the public RPC. Absent only when the worker reply
// omitted the value entirely (a protocol violation).
optional int32 hresult = 4;
// Native MxAccess status describing the outcome of the ack.
// Reserved for a structured MxStatusProxy view of the ack outcome. The
// worker by-name/by-GUID ack path produces only the int32 return code
// (see `hresult`), so the current gateway leaves this field UNSET on every
// reply. Clients must read `hresult` (and `protocol_status`) for the ack
// result and must not depend on `status` being populated.
MxStatusProxy status = 5;
string diagnostic_message = 6;
}
message QueryActiveAlarmsRequest {
string session_id = 1;
string client_correlation_id = 2;
// Optional alarm-reference prefix used to scope a partial ConditionRefresh
// (e.g. equipment sub-tree). Empty means full refresh.
string alarm_filter_prefix = 3;
// Request to attach to the gateway's central alarm feed (StreamAlarms).
message StreamAlarmsRequest {
string client_correlation_id = 1;
// Optional alarm-reference prefix scoping the feed to an equipment
// sub-tree. Empty streams every active alarm.
string alarm_filter_prefix = 2;
}
// One message on the StreamAlarms feed. The stream opens with one
// `active_alarm` per currently-active alarm, then a single
// `snapshot_complete`, then a `transition` for every subsequent change.
message AlarmFeedMessage {
oneof payload {
// Part of the initial active-alarm snapshot (ConditionRefresh).
ActiveAlarmSnapshot active_alarm = 1;
// Sentinel: the initial snapshot is fully delivered and `transition`
// messages follow. Always true when present.
bool snapshot_complete = 2;
// A live alarm state change (raise / acknowledge / clear).
OnAlarmTransitionEvent transition = 3;
}
}
message MxStatusProxy {
// Mirrors the `success` member of the MXAccess MXSTATUS_PROXY struct
// (a 16-bit signed value in the COM struct, widened to int32 on the
// wire). Despite the name it is NOT a boolean it is the raw numeric
// indicator the worker reads off the COM struct without reinterpretation.
// It is carried verbatim for diagnostics; the authoritative success/
// failure of the operation is `category` (MX_STATUS_CATEGORY_OK marks
// success), with `detail`, `diagnostic_text`, `raw_category`, and
// `raw_detected_by` describing any non-OK outcome. Clients should branch
// on `category`, not on a specific `success` value.
int32 success = 1;
MxStatusCategory category = 2;
MxStatusSource detected_by = 3;
@@ -2,12 +2,19 @@ syntax = "proto3";
package mxaccess_worker.v1;
option csharp_namespace = "MxGateway.Contracts.Proto";
option csharp_namespace = "ZB.MOM.WW.MxGateway.Contracts.Proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "mxaccess_gateway.proto";
// Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
// additively only. Never renumber or repurpose an existing field number or
// enum value. When a field or enum value is removed, add a `reserved` range
// (and `reserved` name) covering it in the same change so a future editor
// cannot accidentally reuse the retired tag. There are no `reserved`
// declarations today because no field or enum value has ever been removed.
// Gateway-to-worker IPC envelope. Named-pipe framing prepends a little-endian
// uint32 payload length to this protobuf payload.
message WorkerEnvelope {
@@ -0,0 +1,112 @@
using System.Security.Claims;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Dashboard;
namespace ZB.MOM.WW.MxGateway.IntegrationTests;
[Collection(LiveResourcesCollection.Name)]
[Trait("Category", "LiveLdap")]
public sealed class DashboardLdapLiveTests
{
[LiveLdapFact]
public async Task AuthenticateAsync_AdminInGwAdminGroup_Succeeds()
{
DashboardAuthenticator authenticator = CreateAuthenticator();
DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
"admin",
"admin123",
CancellationToken.None);
Assert.True(result.Succeeded);
Assert.NotNull(result.Principal);
Assert.Equal("admin", result.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value);
Assert.Contains(result.Principal.Claims, claim =>
claim.Type == DashboardAuthenticationDefaults.LdapGroupClaimType
&& claim.Value.Contains("GwAdmin", StringComparison.OrdinalIgnoreCase));
}
[LiveLdapFact]
public async Task AuthenticateAsync_ReadOnlyUserMissingGwAdminGroup_Fails()
{
DashboardAuthenticator authenticator = CreateAuthenticator();
DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
"readonly",
"readonly123",
CancellationToken.None);
Assert.False(result.Succeeded);
Assert.Null(result.Principal);
Assert.DoesNotContain("readonly123", result.FailureMessage, StringComparison.Ordinal);
}
[LiveLdapFact]
public async Task AuthenticateAsync_AdminWithWrongPassword_FailsWithoutLeakingPassword()
{
// Exercises the LdapException branch: the user exists and the service
// account search succeeds, but the candidate bind is rejected.
const string wrongPassword = "definitely-not-the-admin-password";
DashboardAuthenticator authenticator = CreateAuthenticator();
DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
"admin",
wrongPassword,
CancellationToken.None);
Assert.False(result.Succeeded);
Assert.Null(result.Principal);
Assert.DoesNotContain(wrongPassword, result.FailureMessage, StringComparison.Ordinal);
}
[LiveLdapFact]
public async Task AuthenticateAsync_UnknownUsername_Fails()
{
// Exercises the `candidate is null` branch: the service-account search
// returns no entry, so no candidate bind is attempted.
DashboardAuthenticator authenticator = CreateAuthenticator();
DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
"no-such-user-9f3c1",
"irrelevant-password",
CancellationToken.None);
Assert.False(result.Succeeded);
Assert.Null(result.Principal);
}
[LiveLdapFact]
public async Task AuthenticateAsync_ServerUnreachable_FailsWithoutThrowing()
{
// Exercises the connect-failure path: a closed loopback port produces a
// connection error that DashboardAuthenticator must absorb into a Fail
// result rather than propagating an exception to the dashboard.
DashboardAuthenticator authenticator = new(
Options.Create(new GatewayOptions
{
Ldap = new LdapOptions
{
// 1 is a reserved port number that no LDAP server listens on.
Port = 1,
},
}),
NullLogger<DashboardAuthenticator>.Instance);
DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
"admin",
"admin123",
CancellationToken.None);
Assert.False(result.Succeeded);
Assert.Null(result.Principal);
}
private static DashboardAuthenticator CreateAuthenticator()
{
return new DashboardAuthenticator(
Options.Create(new GatewayOptions()),
NullLogger<DashboardAuthenticator>.Instance);
}
}
@@ -1,12 +1,13 @@
using MxGateway.Server.Galaxy;
using ZB.MOM.WW.MxGateway.Server.Galaxy;
namespace MxGateway.IntegrationTests.Galaxy;
namespace ZB.MOM.WW.MxGateway.IntegrationTests.Galaxy;
[Collection(LiveResourcesCollection.Name)]
[Trait("Category", "LiveGalaxy")]
public sealed class GalaxyRepositoryLiveTests
{
/// <summary>Verifies that the Galaxy Repository can establish a live connection to the ZB database.</summary>
[LiveGalaxyRepositoryFact]
[Trait("Category", "LiveGalaxy")]
public async Task TestConnection_AgainstZb_Succeeds()
{
GalaxyRepository repository = CreateRepository();
@@ -18,7 +19,6 @@ public sealed class GalaxyRepositoryLiveTests
/// <summary>Verifies that the last deploy time can be retrieved from the ZB database.</summary>
[LiveGalaxyRepositoryFact]
[Trait("Category", "LiveGalaxy")]
public async Task GetLastDeployTime_AgainstZb_ReturnsTimestamp()
{
GalaxyRepository repository = CreateRepository();
@@ -30,7 +30,6 @@ public sealed class GalaxyRepositoryLiveTests
/// <summary>Verifies that the hierarchy can be retrieved from the ZB database.</summary>
[LiveGalaxyRepositoryFact]
[Trait("Category", "LiveGalaxy")]
public async Task GetHierarchy_AgainstZb_ReturnsObjects()
{
GalaxyRepository repository = CreateRepository();
@@ -48,7 +47,6 @@ public sealed class GalaxyRepositoryLiveTests
/// <summary>Verifies that object attributes can be retrieved from the ZB database.</summary>
[LiveGalaxyRepositoryFact]
[Trait("Category", "LiveGalaxy")]
public async Task GetAttributes_AgainstZb_ReturnsAtLeastOneAttribute()
{
GalaxyRepository repository = CreateRepository();
@@ -1,4 +1,6 @@
namespace MxGateway.IntegrationTests.Galaxy;
using ZB.MOM.WW.MxGateway.Server.Galaxy;
namespace ZB.MOM.WW.MxGateway.IntegrationTests.Galaxy;
/// <summary>Fact attribute that skips tests unless live Galaxy Repository tests are explicitly enabled.</summary>
public sealed class LiveGalaxyRepositoryFactAttribute : FactAttribute
@@ -18,14 +20,14 @@ public sealed class LiveGalaxyRepositoryFactAttribute : FactAttribute
}
/// <summary>Gets a value indicating whether live Galaxy Repository tests are enabled.</summary>
public static bool Enabled =>
string.Equals(
Environment.GetEnvironmentVariable(EnableVariableName),
"1",
StringComparison.Ordinal);
public static bool Enabled => IntegrationTestEnvironment.IsEnabled(EnableVariableName);
/// <summary>Gets the Galaxy Repository connection string from environment or default.</summary>
/// <summary>
/// Gets the Galaxy Repository connection string from environment or the production
/// default. The default is sourced from <see cref="GalaxyRepositoryOptions.DefaultConnectionString"/>
/// so the live-test fallback cannot drift away from the production default.
/// </summary>
public static string ConnectionString =>
Environment.GetEnvironmentVariable(ConnectionStringVariableName)
?? "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;";
?? GalaxyRepositoryOptions.DefaultConnectionString;
}
@@ -1,17 +1,34 @@
namespace MxGateway.IntegrationTests;
using ZB.MOM.WW.MxGateway.Contracts;
namespace ZB.MOM.WW.MxGateway.IntegrationTests;
public static class IntegrationTestEnvironment
{
public const string LiveMxAccessVariableName = "MXGATEWAY_RUN_LIVE_MXACCESS_TESTS";
/// <summary>
/// Sourced from <see cref="GatewayContractInfo.LiveMxAccessOptInVariableName"/>
/// so the env-var literal is shared with
/// <c>ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport.LiveMxAccessFactAttribute</c>
/// (Worker.Tests-025).
/// </summary>
public const string LiveMxAccessVariableName = GatewayContractInfo.LiveMxAccessOptInVariableName;
public const string LiveMxAccessWorkerExecutableVariableName = "MXGATEWAY_LIVE_MXACCESS_WORKER_EXE";
public const string LiveMxAccessItemVariableName = "MXGATEWAY_LIVE_MXACCESS_ITEM";
public const string LiveMxAccessClientNameVariableName = "MXGATEWAY_LIVE_MXACCESS_CLIENT_NAME";
public const string LiveMxAccessEventTimeoutSecondsVariableName = "MXGATEWAY_LIVE_MXACCESS_EVENT_TIMEOUT_SECONDS";
/// <summary>Gets whether live MXAccess tests are enabled.</summary>
public static bool LiveMxAccessTestsEnabled =>
public static bool LiveMxAccessTestsEnabled => IsEnabled(LiveMxAccessVariableName);
/// <summary>
/// Gets whether an opt-in live-test suite is enabled, by comparing the named
/// environment variable to <c>1</c>. Shared by every <c>Live*FactAttribute</c>
/// so the opt-in check has a single implementation.
/// </summary>
/// <param name="variableName">The environment variable that gates the suite.</param>
/// <returns><see langword="true"/> when the variable is exactly <c>1</c>.</returns>
public static bool IsEnabled(string variableName) =>
string.Equals(
Environment.GetEnvironmentVariable(LiveMxAccessVariableName),
Environment.GetEnvironmentVariable(variableName),
"1",
StringComparison.Ordinal);
@@ -25,7 +42,7 @@ public static class IntegrationTestEnvironment
public static string LiveMxAccessClientName =>
GetOptionalEnvironmentVariable(
LiveMxAccessClientNameVariableName,
"MxGateway.IntegrationTests");
"ZB.MOM.WW.MxGateway.IntegrationTests");
/// <summary>Gets the timeout for waiting on events in live tests.</summary>
public static TimeSpan LiveMxAccessEventTimeout =>
@@ -34,7 +51,7 @@ public static class IntegrationTestEnvironment
defaultValue: 15));
/// <summary>Resolves the path to the worker executable for live tests.</summary>
/// <returns>Path to MxGateway.Worker.exe.</returns>
/// <returns>Path to ZB.MOM.WW.MxGateway.Worker.exe.</returns>
public static string ResolveLiveMxAccessWorkerExecutablePath()
{
string? configuredPath = Environment.GetEnvironmentVariable(LiveMxAccessWorkerExecutableVariableName);
@@ -46,11 +63,11 @@ public static class IntegrationTestEnvironment
string repositoryRoot = ResolveRepositoryRoot(AppContext.BaseDirectory);
string[] candidatePaths =
[
Path.Combine(repositoryRoot, "src", "MxGateway.Worker", "bin", "x86", "Debug", "net48", "MxGateway.Worker.exe"),
Path.Combine(repositoryRoot, "src", "MxGateway.Worker", "bin", "Debug", "net48", "MxGateway.Worker.exe"),
Path.Combine(repositoryRoot, "src", "MxGateway.Worker", "bin", "x86", "Release", "net48", "MxGateway.Worker.exe"),
Path.Combine(repositoryRoot, "src", "MxGateway.Worker", "bin", "Release", "net48", "MxGateway.Worker.exe"),
Path.Combine(repositoryRoot, "src", "MxGateway.Worker", "bin", "x86", "Release", "MxGateway.Worker.exe"),
Path.Combine(repositoryRoot, "src", "ZB.MOM.WW.MxGateway.Worker", "bin", "x86", "Debug", "net48", "ZB.MOM.WW.MxGateway.Worker.exe"),
Path.Combine(repositoryRoot, "src", "ZB.MOM.WW.MxGateway.Worker", "bin", "Debug", "net48", "ZB.MOM.WW.MxGateway.Worker.exe"),
Path.Combine(repositoryRoot, "src", "ZB.MOM.WW.MxGateway.Worker", "bin", "x86", "Release", "net48", "ZB.MOM.WW.MxGateway.Worker.exe"),
Path.Combine(repositoryRoot, "src", "ZB.MOM.WW.MxGateway.Worker", "bin", "Release", "net48", "ZB.MOM.WW.MxGateway.Worker.exe"),
Path.Combine(repositoryRoot, "src", "ZB.MOM.WW.MxGateway.Worker", "bin", "x86", "Release", "ZB.MOM.WW.MxGateway.Worker.exe"),
];
return candidatePaths.FirstOrDefault(File.Exists)
@@ -80,7 +97,7 @@ public static class IntegrationTestEnvironment
return defaultValue;
}
/// <summary>Resolves the root directory of the repository by searching for .git and src directories.</summary>
/// <summary>Resolves the root directory of the repository by walking parents for a src/ directory next to either a .git marker or a .sln/.slnx file.</summary>
/// <param name="startDirectory">Starting directory to search from.</param>
/// <returns>The repository root path, or the start directory if not found.</returns>
internal static string ResolveRepositoryRoot(string startDirectory)
@@ -88,9 +105,7 @@ public static class IntegrationTestEnvironment
DirectoryInfo? directory = new(startDirectory);
while (directory is not null)
{
if ((Directory.Exists(Path.Combine(directory.FullName, ".git"))
|| File.Exists(Path.Combine(directory.FullName, ".git")))
&& Directory.Exists(Path.Combine(directory.FullName, "src")))
if (IsRepositoryRoot(directory))
{
return directory.FullName;
}
@@ -100,4 +115,25 @@ public static class IntegrationTestEnvironment
return Directory.GetCurrentDirectory();
}
private static bool IsRepositoryRoot(DirectoryInfo directory)
{
string srcPath = Path.Combine(directory.FullName, "src");
if (!Directory.Exists(srcPath))
{
return false;
}
// Accept a checked-out git repo OR an unpacked working tree that ships a
// .sln/.slnx alongside src/. The .sln/.slnx fallback lets the integration
// tests run in copies that have no .git folder (e.g. an extracted zip).
if (Directory.Exists(Path.Combine(directory.FullName, ".git"))
|| File.Exists(Path.Combine(directory.FullName, ".git")))
{
return true;
}
return Directory.EnumerateFiles(srcPath, "*.slnx").Any()
|| Directory.EnumerateFiles(srcPath, "*.sln").Any();
}
}
@@ -1,4 +1,4 @@
namespace MxGateway.IntegrationTests;
namespace ZB.MOM.WW.MxGateway.IntegrationTests;
public sealed class IntegrationTestEnvironmentTests
{
@@ -0,0 +1,16 @@
namespace ZB.MOM.WW.MxGateway.IntegrationTests;
public sealed class LiveLdapFactAttribute : FactAttribute
{
public const string EnableVariableName = "MXGATEWAY_RUN_LIVE_LDAP_TESTS";
public LiveLdapFactAttribute()
{
if (!Enabled)
{
Skip = $"Set {EnableVariableName}=1 to run live LDAP tests.";
}
}
public static bool Enabled => IntegrationTestEnvironment.IsEnabled(EnableVariableName);
}
@@ -1,4 +1,4 @@
namespace MxGateway.IntegrationTests;
namespace ZB.MOM.WW.MxGateway.IntegrationTests;
/// <summary>Marks an xUnit test as requiring installed MXAccess COM and live provider state.</summary>
public sealed class LiveMxAccessFactAttribute : FactAttribute
@@ -0,0 +1,16 @@
namespace ZB.MOM.WW.MxGateway.IntegrationTests;
/// <summary>
/// xUnit collection that serializes every live integration-test class. The live
/// suites contend for genuinely shared singletons — one MXAccess COM provider,
/// one <c>ZB</c> SQL database, and one GLAuth instance with a per-IP failure
/// lockout — so they must not run in parallel with one another. Placing each
/// live class in this collection disables xUnit's default cross-class
/// parallelism for them while leaving non-live tests free to parallelize.
/// </summary>
[CollectionDefinition(Name, DisableParallelization = true)]
public sealed class LiveResourcesCollection
{
/// <summary>The collection name applied via <c>[Collection]</c> on live test classes.</summary>
public const string Name = "Live external resources";
}
File diff suppressed because it is too large Load Diff
@@ -17,8 +17,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MxGateway.Contracts\MxGateway.Contracts.csproj" />
<ProjectReference Include="..\MxGateway.Server\MxGateway.Server.csproj" />
<ProjectReference Include="..\ZB.MOM.WW.MxGateway.Contracts\ZB.MOM.WW.MxGateway.Contracts.csproj" />
<ProjectReference Include="..\ZB.MOM.WW.MxGateway.Server\ZB.MOM.WW.MxGateway.Server.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,22 @@
namespace ZB.MOM.WW.MxGateway.Server.Alarms;
/// <summary>Service-collection wiring for the gateway's central alarm monitor.</summary>
public static class AlarmsServiceCollectionExtensions
{
/// <summary>
/// Registers the always-on <see cref="GatewayAlarmMonitor"/> as both
/// the <see cref="IGatewayAlarmService"/> singleton and a hosted
/// service, so it starts with the gateway host and is shared by the
/// gRPC alarm surface and the dashboard.
/// </summary>
/// <param name="services">Service collection to register services in.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddGatewayAlarms(this IServiceCollection services)
{
services.AddSingleton<GatewayAlarmMonitor>();
services.AddSingleton<IGatewayAlarmService>(provider => provider.GetRequiredService<GatewayAlarmMonitor>());
services.AddHostedService(provider => provider.GetRequiredService<GatewayAlarmMonitor>());
return services;
}
}
@@ -0,0 +1,693 @@
using System.Threading.Channels;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Sessions;
namespace ZB.MOM.WW.MxGateway.Server.Alarms;
/// <summary>
/// The gateway's always-on alarm monitor and broker. It owns one
/// gateway-managed worker session dedicated to alarms, keeps an in-process
/// cache of the active-alarm set fed by that session's transition events
/// (reconciled periodically against the worker's snapshot), and fans the
/// feed out to any number of <see cref="StreamAsync"/> subscribers.
/// The session is re-opened transparently if the worker faults.
/// </summary>
public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmService
{
private const string MonitorClientName = "gateway-alarm-monitor";
private const string BackendName = "Galaxy";
private const int SubscriberQueueCapacity = 2048;
private static readonly TimeSpan RestartBackoff = TimeSpan.FromSeconds(5);
private static readonly TimeSpan StartupGrace = TimeSpan.FromSeconds(2);
private readonly ISessionManager _sessionManager;
private readonly AlarmsOptions _options;
private readonly ILogger<GatewayAlarmMonitor> _logger;
private readonly object _sync = new();
private readonly Dictionary<string, ActiveAlarmSnapshot> _alarms = new(StringComparer.Ordinal);
private readonly List<Subscriber> _subscribers = [];
private volatile GatewayAlarmMonitorState _state = GatewayAlarmMonitorState.Disabled;
private volatile string? _lastError;
private GatewaySession? _session;
/// <summary>Initializes the gateway alarm monitor.</summary>
/// <param name="sessionManager">Gateway session manager.</param>
/// <param name="options">Gateway options carrying the alarm configuration.</param>
/// <param name="logger">Diagnostic logger.</param>
public GatewayAlarmMonitor(
ISessionManager sessionManager,
IOptions<GatewayOptions> options,
ILogger<GatewayAlarmMonitor> logger)
{
_sessionManager = sessionManager ?? throw new ArgumentNullException(nameof(sessionManager));
_options = (options ?? throw new ArgumentNullException(nameof(options))).Value.Alarms;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public GatewayAlarmMonitorState State => _state;
/// <inheritdoc />
public string? LastError => _lastError;
/// <inheritdoc />
public int? WorkerProcessId
{
get { lock (_sync) { return _session?.WorkerProcessId; } }
}
/// <inheritdoc />
public IReadOnlyList<ActiveAlarmSnapshot> CurrentAlarms
{
get
{
lock (_sync)
{
return _alarms.Values.Select(alarm => alarm.Clone()).ToArray();
}
}
}
/// <inheritdoc />
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (!_options.Enabled)
{
_state = GatewayAlarmMonitorState.Disabled;
_logger.LogInformation("Gateway alarm monitor disabled (MxGateway:Alarms:Enabled is false).");
return;
}
string subscription = ResolveSubscription();
if (string.IsNullOrWhiteSpace(subscription))
{
_state = GatewayAlarmMonitorState.Faulted;
_lastError = "MxGateway:Alarms is enabled but no SubscriptionExpression / DefaultArea is configured.";
_logger.LogError("{Diagnostic}", _lastError);
return;
}
// Brief grace so worker-process launching and startup orphan cleanup
// settle before the monitor opens its own session.
try
{
await Task.Delay(StartupGrace, stoppingToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return;
}
while (!stoppingToken.IsCancellationRequested)
{
try
{
await RunMonitorAsync(subscription, stoppingToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception exception)
{
_state = GatewayAlarmMonitorState.Faulted;
_lastError = exception.Message;
_logger.LogWarning(
exception,
"Gateway alarm monitor lifecycle faulted; restarting in {Backoff}.",
RestartBackoff);
try
{
await Task.Delay(RestartBackoff, stoppingToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
break;
}
}
}
_state = GatewayAlarmMonitorState.Disabled;
}
// One monitoring lifecycle: open a session, subscribe alarms, reconcile,
// then consume transition events until the session ends or is cancelled.
private async Task RunMonitorAsync(string subscription, CancellationToken stoppingToken)
{
_state = GatewayAlarmMonitorState.Starting;
GatewaySession session = await _sessionManager.OpenSessionAsync(
new SessionOpenRequest(BackendName, MonitorClientName, Guid.NewGuid().ToString("N"), CommandTimeout: null),
MonitorClientName,
stoppingToken)
.ConfigureAwait(false);
lock (_sync) { _session = session; }
try
{
await SubscribeAlarmsAsync(session.SessionId, subscription, stoppingToken).ConfigureAwait(false);
await ReconcileAsync(session.SessionId, stoppingToken).ConfigureAwait(false);
_state = GatewayAlarmMonitorState.Monitoring;
_lastError = null;
_logger.LogInformation(
"Gateway alarm monitor active on {Subscription} (session {SessionId}, worker pid {WorkerPid}).",
subscription,
session.SessionId,
session.WorkerProcessId);
using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
Task reconcileLoop = ReconcileLoopAsync(session.SessionId, linked.Token);
try
{
await foreach (WorkerEvent workerEvent in _sessionManager
.ReadEventsAsync(session.SessionId, linked.Token)
.ConfigureAwait(false))
{
MxEvent? mxEvent = workerEvent.Event;
if (mxEvent is { BodyCase: MxEvent.BodyOneofCase.OnAlarmTransition }
&& mxEvent.OnAlarmTransition is not null)
{
ApplyTransition(mxEvent.OnAlarmTransition);
}
}
}
finally
{
await linked.CancelAsync().ConfigureAwait(false);
try
{
await reconcileLoop.ConfigureAwait(false);
}
catch
{
// Reconcile-loop teardown errors are not actionable here.
}
}
// The event stream ended without cancellation — the worker session
// closed or faulted. Surface it so the supervisor loop restarts.
throw new InvalidOperationException("Alarm monitor worker event stream ended.");
}
finally
{
lock (_sync) { _session = null; }
ClearCache();
try
{
await _sessionManager.CloseSessionAsync(session.SessionId, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception exception)
{
_logger.LogDebug(exception, "Closing alarm monitor session {SessionId} failed.", session.SessionId);
}
}
}
private async Task SubscribeAlarmsAsync(string sessionId, string subscription, CancellationToken cancellationToken)
{
WorkerCommandReply reply = await _sessionManager.InvokeAsync(
sessionId,
new WorkerCommand
{
Command = new MxCommand
{
Kind = MxCommandKind.SubscribeAlarms,
SubscribeAlarms = new SubscribeAlarmsCommand { SubscriptionExpression = subscription },
},
},
cancellationToken)
.ConfigureAwait(false);
ProtocolStatusCode? code = reply.Reply?.ProtocolStatus?.Code;
if (code != ProtocolStatusCode.Ok)
{
string diagnostic = reply.Reply?.DiagnosticMessage
?? reply.Reply?.ProtocolStatus?.Message
?? $"status {code}";
throw new InvalidOperationException($"Worker rejected SubscribeAlarms: {diagnostic}");
}
}
private async Task ReconcileLoopAsync(string sessionId, CancellationToken cancellationToken)
{
try
{
int seconds = Math.Max(5, _options.ReconcileIntervalSeconds);
using PeriodicTimer timer = new(TimeSpan.FromSeconds(seconds));
while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
{
try
{
await ReconcileAsync(sessionId, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exception)
{
_logger.LogDebug(exception, "Alarm reconcile pass failed; keeping the current cache.");
}
}
}
catch (OperationCanceledException)
{
}
}
private async Task ReconcileAsync(string sessionId, CancellationToken cancellationToken)
{
WorkerCommandReply reply = await _sessionManager.InvokeAsync(
sessionId,
new WorkerCommand
{
Command = new MxCommand
{
Kind = MxCommandKind.QueryActiveAlarms,
QueryActiveAlarmsCommand = new QueryActiveAlarmsCommand { AlarmFilterPrefix = string.Empty },
},
},
cancellationToken)
.ConfigureAwait(false);
if (reply.Reply?.ProtocolStatus?.Code != ProtocolStatusCode.Ok)
{
return;
}
QueryActiveAlarmsReplyPayload? payload = reply.Reply.QueryActiveAlarms;
if (payload is not null)
{
ApplyReconcile(payload.Snapshots);
}
}
// Applies a live transition to the cache and broadcasts it to subscribers.
private void ApplyTransition(OnAlarmTransitionEvent transition)
{
string reference = transition.AlarmFullReference ?? string.Empty;
if (reference.Length == 0)
{
return;
}
lock (_sync)
{
if (transition.TransitionKind == AlarmTransitionKind.Clear)
{
_alarms.Remove(reference);
}
else
{
_alarms[reference] = SnapshotFromTransition(transition);
}
Broadcast(new AlarmFeedMessage { Transition = transition }, reference);
}
}
// Replaces the cache with the worker's authoritative snapshot, broadcasting
// a synthetic transition for any alarm the live stream missed.
private void ApplyReconcile(IEnumerable<ActiveAlarmSnapshot> snapshots)
{
Dictionary<string, ActiveAlarmSnapshot> next = new(StringComparer.Ordinal);
foreach (ActiveAlarmSnapshot snapshot in snapshots)
{
if (!string.IsNullOrEmpty(snapshot.AlarmFullReference))
{
next[snapshot.AlarmFullReference] = snapshot;
}
}
lock (_sync)
{
foreach (KeyValuePair<string, ActiveAlarmSnapshot> existing in _alarms)
{
if (!next.ContainsKey(existing.Key))
{
Broadcast(
new AlarmFeedMessage { Transition = TransitionFromSnapshot(existing.Value, AlarmTransitionKind.Clear) },
existing.Key);
}
}
foreach (KeyValuePair<string, ActiveAlarmSnapshot> incoming in next)
{
if (!_alarms.ContainsKey(incoming.Key))
{
Broadcast(
new AlarmFeedMessage { Transition = TransitionFromSnapshot(incoming.Value, AlarmTransitionKind.Raise) },
incoming.Key);
}
}
_alarms.Clear();
foreach (KeyValuePair<string, ActiveAlarmSnapshot> incoming in next)
{
_alarms[incoming.Key] = incoming.Value;
}
}
}
// Caller holds _sync. Pushes a feed message to every matching subscriber;
// a subscriber that has fallen behind is completed with an error and dropped.
private void Broadcast(AlarmFeedMessage message, string reference)
{
for (int index = _subscribers.Count - 1; index >= 0; index--)
{
Subscriber subscriber = _subscribers[index];
if (!subscriber.Matches(reference))
{
continue;
}
if (!subscriber.Channel.Writer.TryWrite(message))
{
subscriber.Channel.Writer.TryComplete(new InvalidOperationException(
"Alarm feed subscriber fell behind and was dropped; reconnect to re-snapshot."));
_subscribers.RemoveAt(index);
}
}
}
private void ClearCache()
{
lock (_sync)
{
_alarms.Clear();
}
}
/// <inheritdoc />
public async IAsyncEnumerable<AlarmFeedMessage> StreamAsync(
string? alarmFilterPrefix,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
string prefix = alarmFilterPrefix ?? string.Empty;
Channel<AlarmFeedMessage> channel = Channel.CreateBounded<AlarmFeedMessage>(
new BoundedChannelOptions(SubscriberQueueCapacity)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = true,
SingleWriter = false,
});
Subscriber subscriber = new(channel, prefix);
ActiveAlarmSnapshot[] snapshot;
lock (_sync)
{
// Register before snapshotting under the same lock so no transition
// can slip between the snapshot and the live stream.
_subscribers.Add(subscriber);
snapshot = _alarms.Values
.Where(alarm => prefix.Length == 0
|| alarm.AlarmFullReference.StartsWith(prefix, StringComparison.Ordinal))
.Select(alarm => alarm.Clone())
.ToArray();
}
try
{
foreach (ActiveAlarmSnapshot alarm in snapshot)
{
yield return new AlarmFeedMessage { ActiveAlarm = alarm };
}
yield return new AlarmFeedMessage { SnapshotComplete = true };
await foreach (AlarmFeedMessage message in channel.Reader
.ReadAllAsync(cancellationToken)
.ConfigureAwait(false))
{
yield return message;
}
}
finally
{
lock (_sync) { _subscribers.Remove(subscriber); }
channel.Writer.TryComplete();
}
}
/// <inheritdoc />
public async Task<AcknowledgeAlarmReply> AcknowledgeAsync(
AcknowledgeAlarmRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
string? sessionId;
lock (_sync) { sessionId = _session?.SessionId; }
if (sessionId is null || _state != GatewayAlarmMonitorState.Monitoring)
{
return new AcknowledgeAlarmReply
{
CorrelationId = request.ClientCorrelationId,
ProtocolStatus = new ProtocolStatus
{
Code = ProtocolStatusCode.WorkerUnavailable,
Message = "Gateway alarm monitor is not currently active.",
},
DiagnosticMessage = _lastError ?? "Alarm monitor is not running.",
};
}
MxCommand? command = BuildAcknowledgeCommand(request, out string? parseError);
if (command is null)
{
return new AcknowledgeAlarmReply
{
CorrelationId = request.ClientCorrelationId,
ProtocolStatus = new ProtocolStatus
{
Code = ProtocolStatusCode.InvalidRequest,
Message = parseError ?? "Invalid acknowledge request.",
},
DiagnosticMessage = parseError ?? "Invalid acknowledge request.",
};
}
WorkerCommandReply workerReply = await _sessionManager
.InvokeAsync(sessionId, new WorkerCommand { Command = command }, cancellationToken)
.ConfigureAwait(false);
MxCommandReply mxReply = workerReply.Reply ?? new MxCommandReply
{
ProtocolStatus = new ProtocolStatus
{
Code = ProtocolStatusCode.ProtocolViolation,
Message = "Worker reply did not include an MxCommandReply.",
},
};
AcknowledgeAlarmReply reply = new()
{
CorrelationId = request.ClientCorrelationId,
ProtocolStatus = mxReply.ProtocolStatus ?? new ProtocolStatus { Code = ProtocolStatusCode.Ok },
DiagnosticMessage = mxReply.DiagnosticMessage ?? string.Empty,
};
if (mxReply.HasHresult)
{
reply.Hresult = mxReply.Hresult;
}
return reply;
}
private string ResolveSubscription()
{
if (!string.IsNullOrWhiteSpace(_options.SubscriptionExpression))
{
return _options.SubscriptionExpression;
}
if (!string.IsNullOrWhiteSpace(_options.DefaultArea))
{
return $@"\\{Environment.MachineName}\Galaxy!{_options.DefaultArea}";
}
return string.Empty;
}
private static MxCommand? BuildAcknowledgeCommand(AcknowledgeAlarmRequest request, out string? parseError)
{
parseError = null;
if (string.IsNullOrWhiteSpace(request.AlarmFullReference))
{
parseError = "alarm_full_reference is required.";
return null;
}
string comment = request.Comment ?? string.Empty;
string operatorUser = request.OperatorUser ?? string.Empty;
if (Guid.TryParse(request.AlarmFullReference, out Guid guid))
{
return new MxCommand
{
Kind = MxCommandKind.AcknowledgeAlarm,
AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand
{
AlarmGuid = guid.ToString(),
Comment = comment,
OperatorUser = operatorUser,
OperatorNode = string.Empty,
OperatorDomain = string.Empty,
OperatorFullName = string.Empty,
},
};
}
if (TryParseAlarmReference(request.AlarmFullReference, out string provider, out string group, out string alarm))
{
return new MxCommand
{
Kind = MxCommandKind.AcknowledgeAlarmByName,
AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand
{
AlarmName = alarm,
ProviderName = provider,
GroupName = group,
Comment = comment,
OperatorUser = operatorUser,
OperatorNode = string.Empty,
OperatorDomain = string.Empty,
OperatorFullName = string.Empty,
},
};
}
parseError = "alarm_full_reference must be a canonical GUID or 'Provider!Group.Tag' format.";
return null;
}
/// <summary>
/// Parses an alarm reference of the form <c>Provider!Group.Tag</c>: the
/// first <c>!</c> splits provider from <c>Group.Tag</c>; the first
/// <c>.</c> after the <c>!</c> splits group from tag.
/// </summary>
/// <param name="reference">The full alarm reference.</param>
/// <param name="providerName">The parsed provider.</param>
/// <param name="groupName">The parsed group/area.</param>
/// <param name="alarmName">The parsed tag/alarm name.</param>
/// <returns>true on a well-formed reference; otherwise false.</returns>
public static bool TryParseAlarmReference(
string? reference,
out string providerName,
out string groupName,
out string alarmName)
{
providerName = string.Empty;
groupName = string.Empty;
alarmName = string.Empty;
if (string.IsNullOrWhiteSpace(reference))
{
return false;
}
int bang = reference!.IndexOf('!', StringComparison.Ordinal);
if (bang <= 0 || bang == reference.Length - 1)
{
return false;
}
string left = reference[..bang];
string right = reference[(bang + 1)..];
int dot = right.IndexOf('.', StringComparison.Ordinal);
if (dot <= 0 || dot == right.Length - 1)
{
return false;
}
providerName = left;
groupName = right[..dot];
alarmName = right[(dot + 1)..];
return true;
}
private static ActiveAlarmSnapshot SnapshotFromTransition(OnAlarmTransitionEvent transition)
{
ActiveAlarmSnapshot snapshot = new()
{
AlarmFullReference = transition.AlarmFullReference,
SourceObjectReference = transition.SourceObjectReference,
AlarmTypeName = transition.AlarmTypeName,
Severity = transition.Severity,
CurrentState = transition.TransitionKind == AlarmTransitionKind.Acknowledge
? AlarmConditionState.ActiveAcked
: AlarmConditionState.Active,
Category = transition.Category,
Description = transition.Description,
OperatorUser = transition.OperatorUser,
OperatorComment = transition.OperatorComment,
};
if (transition.OriginalRaiseTimestamp is not null)
{
snapshot.OriginalRaiseTimestamp = transition.OriginalRaiseTimestamp;
}
if (transition.TransitionTimestamp is not null)
{
snapshot.LastTransitionTimestamp = transition.TransitionTimestamp;
}
if (transition.CurrentValue is not null)
{
snapshot.CurrentValue = transition.CurrentValue;
}
if (transition.LimitValue is not null)
{
snapshot.LimitValue = transition.LimitValue;
}
return snapshot;
}
private static OnAlarmTransitionEvent TransitionFromSnapshot(
ActiveAlarmSnapshot snapshot,
AlarmTransitionKind kind)
{
OnAlarmTransitionEvent transition = new()
{
AlarmFullReference = snapshot.AlarmFullReference,
SourceObjectReference = snapshot.SourceObjectReference,
AlarmTypeName = snapshot.AlarmTypeName,
TransitionKind = kind,
Severity = snapshot.Severity,
Category = snapshot.Category,
Description = snapshot.Description,
OperatorUser = snapshot.OperatorUser,
OperatorComment = snapshot.OperatorComment,
};
if (snapshot.OriginalRaiseTimestamp is not null)
{
transition.OriginalRaiseTimestamp = snapshot.OriginalRaiseTimestamp;
}
if (snapshot.LastTransitionTimestamp is not null)
{
transition.TransitionTimestamp = snapshot.LastTransitionTimestamp;
}
if (snapshot.CurrentValue is not null)
{
transition.CurrentValue = snapshot.CurrentValue;
}
if (snapshot.LimitValue is not null)
{
transition.LimitValue = snapshot.LimitValue;
}
return transition;
}
private sealed class Subscriber(Channel<AlarmFeedMessage> channel, string prefix)
{
public Channel<AlarmFeedMessage> Channel { get; } = channel;
public bool Matches(string reference)
{
return prefix.Length == 0 || reference.StartsWith(prefix, StringComparison.Ordinal);
}
}
}
@@ -0,0 +1,63 @@
using ZB.MOM.WW.MxGateway.Contracts.Proto;
namespace ZB.MOM.WW.MxGateway.Server.Alarms;
/// <summary>Lifecycle state of the gateway's central alarm monitor.</summary>
public enum GatewayAlarmMonitorState
{
/// <summary>Alarm monitoring is switched off (<c>MxGateway:Alarms:Enabled</c> is false).</summary>
Disabled,
/// <summary>The monitor is opening or re-opening its worker session.</summary>
Starting,
/// <summary>The monitor is connected and tracking the active-alarm set.</summary>
Monitoring,
/// <summary>The monitor's last lifecycle attempt failed; a restart is pending.</summary>
Faulted,
}
/// <summary>
/// The gateway's always-on alarm broker. A single gateway-owned worker
/// session monitors the AVEVA alarm provider; this service caches the
/// current active-alarm set and fans it out to any number of clients —
/// no client needs to open its own worker session to see alarms.
/// </summary>
public interface IGatewayAlarmService
{
/// <summary>Current monitor lifecycle state.</summary>
GatewayAlarmMonitorState State { get; }
/// <summary>Diagnostic message from the most recent fault, or null.</summary>
string? LastError { get; }
/// <summary>Process id of the worker backing the monitor, when one is attached.</summary>
int? WorkerProcessId { get; }
/// <summary>A point-in-time copy of the current active-alarm set.</summary>
IReadOnlyList<ActiveAlarmSnapshot> CurrentAlarms { get; }
/// <summary>
/// Attaches to the central alarm feed. The returned stream yields one
/// <see cref="AlarmFeedMessage"/> per currently-active alarm, then a
/// single <c>snapshot_complete</c> sentinel, then a <c>transition</c>
/// for every subsequent change.
/// </summary>
/// <param name="alarmFilterPrefix">Optional alarm-reference prefix scoping the feed.</param>
/// <param name="cancellationToken">Token that ends the subscription.</param>
IAsyncEnumerable<AlarmFeedMessage> StreamAsync(
string? alarmFilterPrefix,
CancellationToken cancellationToken);
/// <summary>
/// Acknowledges an alarm through the monitor's worker session. Never
/// throws — transport and monitor-state failures surface in the
/// reply's <see cref="AcknowledgeAlarmReply.ProtocolStatus"/>.
/// </summary>
/// <param name="request">The acknowledge request.</param>
/// <param name="cancellationToken">Token to cancel the call.</param>
Task<AcknowledgeAlarmReply> AcknowledgeAsync(
AcknowledgeAlarmRequest request,
CancellationToken cancellationToken);
}
@@ -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;
}
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public enum AuthenticationMode
{
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class AuthenticationOptions
{
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class DashboardOptions
{
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveAuthenticationConfiguration(
string Mode,
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveDashboardConfiguration(
bool Enabled,
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveEventConfiguration(
int QueueCapacity,
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveGatewayConfiguration(
EffectiveAuthenticationConfiguration Authentication,
@@ -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);
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveProtocolConfiguration(
uint WorkerProtocolVersion,
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveSessionConfiguration(
int DefaultCommandTimeoutSeconds,
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed record EffectiveWorkerConfiguration(
string ExecutablePath,
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public enum EventBackpressurePolicy
{
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class EventOptions
{
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Options;
namespace MxGateway.Server.Configuration;
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
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Options;
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public static class GatewayConfigurationServiceCollectionExtensions
{
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class GatewayOptions
{
@@ -1,7 +1,7 @@
using Microsoft.Extensions.Options;
using MxGateway.Contracts;
using ZB.MOM.WW.MxGateway.Contracts;
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class GatewayOptionsValidator : IValidateOptions<GatewayOptions>
{
@@ -25,6 +25,7 @@ public sealed class GatewayOptionsValidator : IValidateOptions<GatewayOptions>
ValidateEvents(options.Events, failures);
ValidateDashboard(options.Dashboard, failures);
ValidateProtocol(options.Protocol, failures);
ValidateAlarms(options.Alarms, failures);
return failures.Count == 0
? ValidateOptionsResult.Success
@@ -228,6 +229,32 @@ public sealed class GatewayOptionsValidator : IValidateOptions<GatewayOptions>
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)
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
/// <summary>
/// Provides the effective gateway configuration, applying defaults and validations.
@@ -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";
}
@@ -1,6 +1,6 @@
using MxGateway.Contracts;
using ZB.MOM.WW.MxGateway.Contracts;
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
/// <summary>
/// Configuration options for the worker protocol version.
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public sealed class SessionOptions
{
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
public enum WorkerArchitecture
{
@@ -1,10 +1,10 @@
namespace MxGateway.Server.Configuration;
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\MxGateway.Worker\bin\x86\Release\MxGateway.Worker.exe";
@"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; }
@@ -7,6 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<base href="@DashboardBaseHref" />
<link rel="stylesheet" href="/lib/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="/css/theme.css" />
<link rel="stylesheet" href="/css/dashboard.css" />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
@@ -1,4 +1,4 @@
namespace MxGateway.Server.Dashboard.Components;
namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Components;
public static class DashboardDisplay
{
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components;
namespace MxGateway.Server.Dashboard.Components;
namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Components;
/// <summary>
/// Base class for Blazor dashboard pages that watch gateway metrics snapshots.
@@ -0,0 +1,50 @@
@inherits LayoutComponentBase
@inject IOptions<GatewayOptions> GatewayOptions
<div class="dashboard-shell">
<header class="app-bar">
<a class="brand" href=""><span class="mark">&#9646;</span> MXAccess Gateway</a>
<nav class="app-nav">
<NavLink href="" Match="NavLinkMatch.All">Overview</NavLink>
<NavLink href="sessions">Sessions</NavLink>
<NavLink href="workers">Workers</NavLink>
<NavLink href="events">Events</NavLink>
<NavLink href="galaxy">Galaxy</NavLink>
<NavLink href="browse">Browse</NavLink>
<NavLink href="alarms">Alarms</NavLink>
<NavLink href="apikeys">API Keys</NavLink>
<NavLink href="settings">Settings</NavLink>
</nav>
<span class="spacer"></span>
<AuthorizeView>
<Authorized Context="authState">
<div class="app-user">
<span class="meta">@authState.User.Identity?.Name</span>
<form method="post" action="@DashboardPath("/logout")">
<AntiforgeryToken />
<button class="btn btn-outline-secondary btn-sm" type="submit">Sign out</button>
</form>
</div>
</Authorized>
<NotAuthorized>
<a class="btn btn-outline-secondary btn-sm" href="@DashboardPath("/login")">Sign in</a>
</NotAuthorized>
</AuthorizeView>
</header>
<main class="page">
@Body
</main>
</div>
@code {
private string DashboardPath(string relativePath)
{
string pathBase = GatewayOptions.Value.Dashboard.PathBase.TrimEnd('/');
if (string.IsNullOrWhiteSpace(pathBase))
{
pathBase = "/dashboard";
}
return $"{pathBase}{relativePath}";
}
}
@@ -0,0 +1,286 @@
@page "/alarms"
@page "/dashboard/alarms"
@implements IAsyncDisposable
@inject IDashboardLiveDataService LiveData
@inject IOptions<GatewayOptions> GatewayOptions
<PageTitle>Dashboard Alarms</PageTitle>
<div class="dashboard-page-header">
<div>
<h1>Alarms</h1>
<div class="text-secondary">@HeaderLine()</div>
</div>
</div>
@if (!GatewayOptions.Value.Alarms.Enabled)
{
<div class="alert alert-danger">
Alarm auto-subscribe is disabled (<code>MxGateway:Alarms:Enabled</code> is false). The
dashboard session is not subscribed to any alarm provider, so this list will stay empty.
Enable alarms in configuration and restart the gateway.
</div>
}
@if (!string.IsNullOrWhiteSpace(_queryError))
{
<div class="alert alert-danger">Alarm query failed: @_queryError</div>
}
<section class="metric-grid compact">
<MetricCard Label="Active (unacked)" Value="@_unackedCount.ToString("N0")" />
<MetricCard Label="Acknowledged" Value="@_ackedCount.ToString("N0")" />
<MetricCard Label="Total Active" Value="@_alarms.Count.ToString("N0")" />
<MetricCard Label="Showing" Value="@FilteredAlarms().Count.ToString("N0")" />
</section>
<section class="dashboard-section">
<div class="section-heading">
<h2>Filters</h2>
</div>
<div class="alarm-filters">
<label class="alarm-filter-check">
<input type="checkbox" @bind="_showActive" />
<span>Active (unacked)</span>
</label>
<label class="alarm-filter-check">
<input type="checkbox" @bind="_showAcked" />
<span>Acknowledged</span>
</label>
<div class="alarm-filter-field">
<label class="form-label" for="alarm-area">Area</label>
<select id="alarm-area" class="form-select form-select-sm" @bind="_areaFilter">
<option value="">All areas</option>
@foreach (string area in Areas())
{
<option value="@area">@area</option>
}
</select>
</div>
<div class="alarm-filter-field">
<label class="form-label" for="alarm-sev-min">Min severity</label>
<input id="alarm-sev-min" type="number" class="form-control form-control-sm" @bind="_minSeverity" />
</div>
<div class="alarm-filter-field">
<label class="form-label" for="alarm-sev-max">Max severity</label>
<input id="alarm-sev-max" type="number" class="form-control form-control-sm" @bind="_maxSeverity" />
</div>
<div class="alarm-filter-field alarm-filter-grow">
<label class="form-label" for="alarm-search">Search</label>
<input id="alarm-search" class="form-control form-control-sm"
placeholder="Reference, source or description…"
@bind="_search" @bind:event="oninput" />
</div>
</div>
</section>
<section class="dashboard-section">
<div class="section-heading">
<h2>Active Alarms</h2>
</div>
@{
IReadOnlyList<DashboardActiveAlarm> rows = FilteredAlarms();
}
@if (rows.Count == 0)
{
<div class="empty-state">
@if (_alarms.Count == 0)
{
<span>No alarms are currently Active or ActiveAcked.</span>
}
else
{
<span>No alarms match the current filters.</span>
}
</div>
}
else
{
<div class="table-responsive">
<table class="table table-sm dashboard-table">
<thead>
<tr>
<th scope="col">State</th>
<th scope="col">Severity</th>
<th scope="col">Alarm Reference</th>
<th scope="col">Source</th>
<th scope="col">Type</th>
<th scope="col">Area</th>
<th scope="col">Last Transition</th>
<th scope="col">Operator</th>
</tr>
</thead>
<tbody>
@foreach (DashboardActiveAlarm alarm in rows)
{
<tr>
<td><span class="alarm-state @StateClass(alarm.State)">@StateText(alarm.State)</span></td>
<td class="alarm-severity">@alarm.Severity</td>
<td>
<code>@alarm.Reference</code>
@if (!string.IsNullOrWhiteSpace(alarm.Description))
{
<div class="alarm-desc">@alarm.Description</div>
}
</td>
<td>@DashboardDisplay.Text(alarm.Source)</td>
<td>@DashboardDisplay.Text(alarm.AlarmType)</td>
<td>@DashboardDisplay.Text(alarm.Area)</td>
<td>@(alarm.LastTransition is { } ts ? DashboardDisplay.DateTime(ts) : "-")</td>
<td>
@DashboardDisplay.Text(alarm.OperatorUser)
@if (!string.IsNullOrWhiteSpace(alarm.OperatorComment))
{
<div class="alarm-desc">@alarm.OperatorComment</div>
}
</td>
</tr>
}
</tbody>
</table>
</div>
}
<div class="browse-search-note">
Cleared alarms are not retained — this list reflects only alarms currently Active or
ActiveAcked, refreshed every 3 seconds.
</div>
</section>
@code {
private readonly List<DashboardActiveAlarm> _alarms = [];
private string? _queryError;
private int? _workerPid;
private DateTimeOffset? _lastRefresh;
private int _unackedCount;
private int _ackedCount;
private bool _showActive = true;
private bool _showAcked;
private string _areaFilter = string.Empty;
private int _minSeverity;
private int _maxSeverity = 1000;
private string _search = string.Empty;
private readonly CancellationTokenSource _cts = new();
private Task? _pollTask;
/// <inheritdoc />
protected override void OnInitialized()
{
_pollTask = PollLoopAsync();
}
private string HeaderLine()
{
string refreshed = _lastRefresh is { } at
? $"refreshed {DashboardDisplay.DateTime(at)}"
: "awaiting first refresh";
return _workerPid is int pid ? $"{refreshed} · worker pid {pid}" : refreshed;
}
private IReadOnlyList<string> Areas()
{
return _alarms
.Select(alarm => alarm.Area)
.Where(area => !string.IsNullOrWhiteSpace(area))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(area => area, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private IReadOnlyList<DashboardActiveAlarm> FilteredAlarms()
{
string query = _search.Trim();
return _alarms
.Where(MatchesState)
.Where(alarm => _areaFilter.Length == 0
|| string.Equals(alarm.Area, _areaFilter, StringComparison.OrdinalIgnoreCase))
.Where(alarm => alarm.Severity >= _minSeverity && alarm.Severity <= _maxSeverity)
.Where(alarm => query.Length == 0
|| alarm.Reference.Contains(query, StringComparison.OrdinalIgnoreCase)
|| alarm.Source.Contains(query, StringComparison.OrdinalIgnoreCase)
|| alarm.Description.Contains(query, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(alarm => alarm.Severity)
.ThenByDescending(alarm => alarm.LastTransition ?? DateTimeOffset.MinValue)
.ToArray();
}
private bool MatchesState(DashboardActiveAlarm alarm)
{
return alarm.State switch
{
AlarmConditionState.Active => _showActive,
AlarmConditionState.ActiveAcked => _showAcked,
_ => true,
};
}
private static string StateText(AlarmConditionState state)
{
return state switch
{
AlarmConditionState.Active => "Active",
AlarmConditionState.ActiveAcked => "Acked",
AlarmConditionState.Inactive => "Inactive",
_ => "Unknown",
};
}
private static string StateClass(AlarmConditionState state)
{
return state switch
{
AlarmConditionState.Active => "alarm-state-active",
AlarmConditionState.ActiveAcked => "alarm-state-acked",
_ => "alarm-state-other",
};
}
private async Task PollLoopAsync()
{
try
{
await InvokeAsync(RefreshAlarmsAsync).ConfigureAwait(false);
using PeriodicTimer timer = new(TimeSpan.FromSeconds(3));
while (await timer.WaitForNextTickAsync(_cts.Token).ConfigureAwait(false))
{
await InvokeAsync(RefreshAlarmsAsync).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
}
}
private async Task RefreshAlarmsAsync()
{
DashboardAlarmQueryResult result = await LiveData.QueryAlarmsAsync(_cts.Token);
_queryError = result.Error;
_workerPid = result.WorkerProcessId;
_lastRefresh = DateTimeOffset.UtcNow;
_alarms.Clear();
_alarms.AddRange(result.Alarms);
_unackedCount = _alarms.Count(alarm => alarm.State == AlarmConditionState.Active);
_ackedCount = _alarms.Count(alarm => alarm.State == AlarmConditionState.ActiveAcked);
StateHasChanged();
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
await _cts.CancelAsync();
if (_pollTask is not null)
{
try
{
await _pollTask;
}
catch (OperationCanceledException)
{
}
}
_cts.Dispose();
GC.SuppressFinalize(this);
}
}
@@ -0,0 +1,466 @@
@page "/apikeys"
@page "/dashboard/apikeys"
@inherits DashboardPageBase
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IDashboardApiKeyManagementService ApiKeyManagementService
<PageTitle>Dashboard API Keys</PageTitle>
@if (Snapshot is null)
{
<div class="empty-state">Loading API keys.</div>
}
else
{
<div class="dashboard-page-header">
<div>
<h1>API Keys</h1>
<div class="text-secondary">@Snapshot.ApiKeys.Count key rows</div>
</div>
@if (CanManageApiKeys)
{
<button type="button" class="btn btn-primary" @onclick="OpenCreateDialog">
Create API Key
</button>
}
</div>
@if (CanManageApiKeys)
{
@if (!string.IsNullOrWhiteSpace(ResultMessage))
{
<div class="alert @(LastOperationSucceeded ? "alert-success" : "alert-danger")" role="alert">
@ResultMessage
@if (!string.IsNullOrWhiteSpace(LastGeneratedApiKey))
{
<div class="mt-2">
<code class="one-time-secret">@LastGeneratedApiKey</code>
</div>
}
</div>
}
@if (IsCreateDialogOpen)
{
<div class="modal-backdrop fade show"></div>
<div class="modal fade show api-key-create-modal" role="dialog" aria-modal="true" aria-labelledby="createApiKeyTitle">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<EditForm Model="@CreateModel" OnSubmit="@CreateApiKeyAsync">
<div class="modal-header">
<h2 class="modal-title h5" id="createApiKeyTitle">Create API Key</h2>
<button type="button" class="btn-close" aria-label="Close" @onclick="CloseCreateDialog"></button>
</div>
<div class="modal-body">
<div class="api-key-management-grid">
<div class="mb-3">
<label for="keyId" class="form-label">Key ID</label>
<input id="keyId" class="form-control" @bind="CreateModel.KeyId" @bind:event="oninput" />
</div>
<div class="mb-3">
<label for="displayName" class="form-label">Display Name</label>
<input id="displayName" class="form-control" @bind="CreateModel.DisplayName" @bind:event="oninput" />
</div>
</div>
<fieldset class="mb-3">
<legend class="form-label">Scopes</legend>
<div class="scope-grid">
@foreach (string scope in AvailableScopes)
{
<label class="form-check">
<input class="form-check-input" type="checkbox"
checked="@IsScopeSelected(scope)"
@onchange="eventArgs => SetScope(scope, eventArgs)" />
<span class="form-check-label">@scope</span>
</label>
}
</div>
</fieldset>
<div class="api-key-management-grid">
<div class="mb-3">
<label for="readSubtrees" class="form-label">Read subtrees</label>
<textarea id="readSubtrees" class="form-control" rows="2" @bind="CreateModel.ReadSubtrees" @bind:event="oninput"></textarea>
</div>
<div class="mb-3">
<label for="writeSubtrees" class="form-label">Write subtrees</label>
<textarea id="writeSubtrees" class="form-control" rows="2" @bind="CreateModel.WriteSubtrees" @bind:event="oninput"></textarea>
</div>
<div class="mb-3">
<label for="readTagGlobs" class="form-label">Read tag globs</label>
<textarea id="readTagGlobs" class="form-control" rows="2" @bind="CreateModel.ReadTagGlobs" @bind:event="oninput"></textarea>
</div>
<div class="mb-3">
<label for="writeTagGlobs" class="form-label">Write tag globs</label>
<textarea id="writeTagGlobs" class="form-control" rows="2" @bind="CreateModel.WriteTagGlobs" @bind:event="oninput"></textarea>
</div>
<div class="mb-3">
<label for="browseSubtrees" class="form-label">Browse subtrees</label>
<textarea id="browseSubtrees" class="form-control" rows="2" @bind="CreateModel.BrowseSubtrees" @bind:event="oninput"></textarea>
</div>
<div class="mb-3">
<label for="maxWriteClassification" class="form-label">Max write classification</label>
<input id="maxWriteClassification" class="form-control" @bind="CreateModel.MaxWriteClassification" @bind:event="oninput" />
</div>
</div>
<div class="d-flex flex-wrap gap-3">
<label class="form-check">
<InputCheckbox class="form-check-input" @bind-Value="CreateModel.ReadAlarmOnly" />
<span class="form-check-label">Read alarm only</span>
</label>
<label class="form-check">
<InputCheckbox class="form-check-input" @bind-Value="CreateModel.ReadHistorizedOnly" />
<span class="form-check-label">Read historized only</span>
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" disabled="@IsBusy" @onclick="CloseCreateDialog">
Cancel
</button>
<button type="submit" class="btn btn-primary" disabled="@IsBusy">Create Key</button>
</div>
</EditForm>
</div>
</div>
</div>
}
}
<section class="dashboard-section">
@if (Snapshot.ApiKeys.Count == 0)
{
<div class="empty-state">No API keys are available for display.</div>
}
else
{
<div class="table-responsive">
<table class="table table-sm align-middle dashboard-table">
<thead>
<tr>
<th scope="col">Key</th>
<th scope="col">Status</th>
<th scope="col">Display Name</th>
<th scope="col">Scopes</th>
<th scope="col">Constraints</th>
<th scope="col">Created</th>
<th scope="col">Last Used</th>
@if (CanManageApiKeys)
{
<th scope="col">Actions</th>
}
</tr>
</thead>
<tbody>
@foreach (DashboardApiKeySummary key in Snapshot.ApiKeys)
{
<tr>
<td><code>@key.KeyId</code></td>
<td><StatusBadge Text="@(key.RevokedUtc is null ? "Active" : "Revoked")" /></td>
<td>@DashboardDisplay.Text(key.DisplayName)</td>
<td>@DashboardDisplay.Text(string.Join(", ", key.Scopes.Order(StringComparer.Ordinal)))</td>
<td>@DashboardDisplay.Text(ConstraintText(key.Constraints))</td>
<td>@DashboardDisplay.DateTime(key.CreatedUtc)</td>
<td>@DashboardDisplay.DateTime(key.LastUsedUtc)</td>
@if (CanManageApiKeys)
{
<td>
<div class="btn-group btn-group-sm" role="group" aria-label="API key actions">
@if (key.RevokedUtc is null)
{
@* Rotate clears revoked_utc, which would silently reactivate a
deliberately revoked key. Only offer it for active keys so a
revoked key is not un-revoked as a side effect of rotation. *@
<button type="button" class="btn btn-outline-secondary"
disabled="@IsBusy"
@onclick="() => RotateApiKeyAsync(key.KeyId)">
Rotate
</button>
<button type="button" class="btn btn-outline-danger"
disabled="@IsBusy"
@onclick="() => RevokeApiKeyAsync(key.KeyId)">
Revoke
</button>
}
else
{
<span class="text-muted small">No actions</span>
}
</div>
</td>
}
</tr>
}
</tbody>
</table>
</div>
}
</section>
}
@code {
private static readonly string[] AvailableScopes =
[
GatewayScopes.SessionOpen,
GatewayScopes.SessionClose,
GatewayScopes.InvokeRead,
GatewayScopes.InvokeWrite,
GatewayScopes.InvokeSecure,
GatewayScopes.EventsRead,
GatewayScopes.MetadataRead,
GatewayScopes.Admin
];
private ApiKeyCreateModel CreateModel { get; } = new();
private bool CanManageApiKeys { get; set; }
private bool IsBusy { get; set; }
private bool IsCreateDialogOpen { get; set; }
private string? ResultMessage { get; set; }
private bool LastOperationSucceeded { get; set; }
private string? LastGeneratedApiKey { get; set; }
protected override async Task OnInitializedAsync()
{
AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync()
.ConfigureAwait(false);
CanManageApiKeys = ApiKeyManagementService.CanManage(authenticationState.User);
}
private async Task CreateApiKeyAsync()
{
if (IsBusy)
{
return;
}
if (!TryBuildCreateRequest(out DashboardApiKeyManagementRequest? request, out string? validationMessage))
{
SetResult(DashboardApiKeyManagementResult.Fail(validationMessage ?? "API key request is invalid."));
return;
}
await RunManagementActionAsync(user => ApiKeyManagementService.CreateAsync(
user,
request,
CancellationToken.None))
.ConfigureAwait(false);
}
private async Task RevokeApiKeyAsync(string keyId)
{
await RunManagementActionAsync(user => ApiKeyManagementService.RevokeAsync(
user,
keyId,
CancellationToken.None))
.ConfigureAwait(false);
}
private async Task RotateApiKeyAsync(string keyId)
{
await RunManagementActionAsync(user => ApiKeyManagementService.RotateAsync(
user,
keyId,
CancellationToken.None))
.ConfigureAwait(false);
}
private async Task RunManagementActionAsync(
Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardApiKeyManagementResult>> action)
{
if (IsBusy)
{
return;
}
IsBusy = true;
try
{
AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync()
.ConfigureAwait(false);
CanManageApiKeys = ApiKeyManagementService.CanManage(authenticationState.User);
DashboardApiKeyManagementResult result = await action(authenticationState.User).ConfigureAwait(false);
SetResult(result);
if (result.Succeeded && result.ApiKey is not null)
{
CreateModel.Reset();
IsCreateDialogOpen = false;
}
}
finally
{
IsBusy = false;
}
}
private void SetResult(DashboardApiKeyManagementResult result)
{
LastOperationSucceeded = result.Succeeded;
ResultMessage = result.Message;
LastGeneratedApiKey = result.ApiKey;
}
private void OpenCreateDialog()
{
IsCreateDialogOpen = true;
}
private void CloseCreateDialog()
{
if (!IsBusy)
{
IsCreateDialogOpen = false;
}
}
private bool TryBuildCreateRequest(
[System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out DashboardApiKeyManagementRequest? request,
out string? validationMessage)
{
request = null;
validationMessage = null;
if (!string.IsNullOrWhiteSpace(CreateModel.MaxWriteClassification)
&& !int.TryParse(
CreateModel.MaxWriteClassification,
System.Globalization.NumberStyles.Integer,
System.Globalization.CultureInfo.InvariantCulture,
out int _))
{
validationMessage = "Max write classification must be an integer.";
return false;
}
int? maxWriteClassification = string.IsNullOrWhiteSpace(CreateModel.MaxWriteClassification)
? null
: int.Parse(
CreateModel.MaxWriteClassification,
System.Globalization.NumberStyles.Integer,
System.Globalization.CultureInfo.InvariantCulture);
request = new DashboardApiKeyManagementRequest(
KeyId: CreateModel.KeyId,
DisplayName: CreateModel.DisplayName,
Scopes: CreateModel.SelectedScopes,
Constraints: new ZB.MOM.WW.MxGateway.Server.Security.Authentication.ApiKeyConstraints(
ReadSubtrees: ParseList(CreateModel.ReadSubtrees),
WriteSubtrees: ParseList(CreateModel.WriteSubtrees),
ReadTagGlobs: ParseList(CreateModel.ReadTagGlobs),
WriteTagGlobs: ParseList(CreateModel.WriteTagGlobs),
MaxWriteClassification: maxWriteClassification,
BrowseSubtrees: ParseList(CreateModel.BrowseSubtrees),
ReadAlarmOnly: CreateModel.ReadAlarmOnly,
ReadHistorizedOnly: CreateModel.ReadHistorizedOnly));
return true;
}
private bool IsScopeSelected(string scope)
{
return CreateModel.SelectedScopes.Contains(scope);
}
private void SetScope(string scope, ChangeEventArgs eventArgs)
{
bool selected = eventArgs.Value is bool value && value;
if (selected)
{
CreateModel.SelectedScopes.Add(scope);
}
else
{
CreateModel.SelectedScopes.Remove(scope);
}
}
private static string ConstraintText(ZB.MOM.WW.MxGateway.Server.Security.Authentication.ApiKeyConstraints constraints)
{
if (constraints.IsEmpty)
{
return "unconstrained";
}
List<string> parts = [];
AddList(parts, "read_subtrees", constraints.ReadSubtrees);
AddList(parts, "write_subtrees", constraints.WriteSubtrees);
AddList(parts, "read_tag_globs", constraints.ReadTagGlobs);
AddList(parts, "write_tag_globs", constraints.WriteTagGlobs);
AddList(parts, "browse_subtrees", constraints.BrowseSubtrees);
if (constraints.MaxWriteClassification is { } max)
{
parts.Add($"max_write_classification={max}");
}
if (constraints.ReadAlarmOnly)
{
parts.Add("read_alarm_only");
}
if (constraints.ReadHistorizedOnly)
{
parts.Add("read_historized_only");
}
return string.Join("; ", parts);
}
private static void AddList(List<string> parts, string name, IReadOnlyList<string> values)
{
if (values.Count > 0)
{
parts.Add($"{name}=[{string.Join(", ", values)}]");
}
}
private static IReadOnlyList<string> ParseList(string? value)
{
return (value ?? string.Empty)
.Split([',', ';', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(item => !string.IsNullOrWhiteSpace(item))
.ToArray();
}
private sealed class ApiKeyCreateModel
{
public string KeyId { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public HashSet<string> SelectedScopes { get; } = new(StringComparer.Ordinal);
public string ReadSubtrees { get; set; } = string.Empty;
public string WriteSubtrees { get; set; } = string.Empty;
public string ReadTagGlobs { get; set; } = string.Empty;
public string WriteTagGlobs { get; set; } = string.Empty;
public string BrowseSubtrees { get; set; } = string.Empty;
public string MaxWriteClassification { get; set; } = string.Empty;
public bool ReadAlarmOnly { get; set; }
public bool ReadHistorizedOnly { get; set; }
public void Reset()
{
KeyId = string.Empty;
DisplayName = string.Empty;
SelectedScopes.Clear();
ReadSubtrees = string.Empty;
WriteSubtrees = string.Empty;
ReadTagGlobs = string.Empty;
WriteTagGlobs = string.Empty;
BrowseSubtrees = string.Empty;
MaxWriteClassification = string.Empty;
ReadAlarmOnly = false;
ReadHistorizedOnly = false;
}
}
}
@@ -0,0 +1,423 @@
@page "/browse"
@page "/dashboard/browse"
@implements IAsyncDisposable
@inject IGalaxyHierarchyCache GalaxyCache
@inject IDashboardLiveDataService LiveData
@using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy
@using ZB.MOM.WW.MxGateway.Server.Galaxy
<PageTitle>Dashboard Browse</PageTitle>
<div class="dashboard-page-header">
<div>
<h1>Browse</h1>
<div class="text-secondary">@HeaderLine()</div>
</div>
<StatusBadge Text="@GalaxyCache.Current.Status.ToString()" />
</div>
<div class="browse-layout">
<section class="dashboard-section browse-panel">
<div class="section-heading">
<h2>Galaxy Hierarchy</h2>
</div>
<input class="form-control form-control-sm browse-search"
placeholder="Filter attributes by name or reference…"
@bind="Search"
@bind:event="oninput" />
@if (_roots.Count == 0)
{
<div class="empty-state">
No Galaxy hierarchy is cached yet. The hierarchy refreshes from the
Galaxy Repository in the background — check the Galaxy tab for status.
</div>
}
else if (!string.IsNullOrWhiteSpace(Search))
{
@if (_searchMatches.Count == 0)
{
<div class="empty-state">No attributes match &ldquo;@Search&rdquo;.</div>
}
else
{
<div class="browse-tree browse-search-results">
@foreach (GalaxyAttribute hit in _searchMatches)
{
GalaxyAttribute row = hit;
<div class="tree-attr"
title="@row.FullTagReference"
@oncontextmenu:preventDefault="true"
@oncontextmenu="@(args => ShowMenu(args, row))"
@ondblclick="@(() => AddTagAsync(row.FullTagReference))">
<span class="attr-icon">·</span>
<span class="attr-name">@row.FullTagReference</span>
<span class="attr-type">@FormatType(row)</span>
@if (row.IsAlarm)
{
<span class="attr-flag attr-flag-alarm">alarm</span>
}
@if (row.IsHistorized)
{
<span class="attr-flag attr-flag-hist">hist</span>
}
</div>
}
</div>
@if (_searchMatches.Count >= SearchResultLimit)
{
<div class="browse-search-note">Showing the first @SearchResultLimit matches — refine the filter.</div>
}
}
}
else
{
<div class="browse-tree">
@foreach (DashboardBrowseNode root in _roots)
{
<BrowseTreeNodeView Node="root"
OnAddTag="AddTagAsync"
OnTagContextMenu="OnTagContextMenu" />
}
</div>
<div class="browse-search-note">Double-click a tag, or right-click for the menu.</div>
}
</section>
<section class="dashboard-section browse-panel">
<div class="section-heading">
<h2>Subscription Panel</h2>
</div>
<div class="sub-panel-meta">
@if (_subscribed.Count > 0)
{
<span>@_subscribed.Count subscribed</span>
<span>·</span>
<span>refresh 2s</span>
@if (_workerPid is int pid)
{
<span>·</span>
<span>worker pid @pid</span>
}
<button type="button" class="btn btn-outline-secondary btn-sm sub-clear" @onclick="ClearAll">Clear all</button>
}
</div>
@if (!string.IsNullOrWhiteSpace(_readError))
{
<div class="alert alert-danger">Live read failed: @_readError</div>
}
@if (_subscribed.Count == 0)
{
<div class="empty-state">
No tags subscribed. Right-click a tag in the hierarchy and choose
<strong>Add to subscription panel</strong> (or double-click it) to watch its
live value, quality and source timestamp here.
</div>
}
else
{
<div class="table-responsive">
<table class="table table-sm dashboard-table sub-table">
<thead>
<tr>
<th scope="col">Tag</th>
<th scope="col">Value</th>
<th scope="col">Type</th>
<th scope="col">Quality</th>
<th scope="col">Updated</th>
<th scope="col" class="sub-actions-col"></th>
</tr>
</thead>
<tbody>
@foreach (string tag in _subscribed)
{
string key = tag;
DashboardTagValue? value = _values.GetValueOrDefault(key);
<tr>
<td><code>@key</code></td>
<td class="sub-value">@(value?.ValueText ?? "…")</td>
<td>@(value?.DataType ?? "-")</td>
<td>
@if (value is null)
{
<span class="text-secondary">…</span>
}
else
{
<span class="quality-chip @(value.QualityGood ? "quality-good" : "quality-bad")">
@value.Quality
</span>
@if (!string.IsNullOrWhiteSpace(value.Error))
{
<span class="sub-error" title="@value.Error">!</span>
}
}
</td>
<td title="@TimestampTooltip(value)">@TimestampText(key, value)</td>
<td class="sub-actions-col">
<button type="button"
class="btn btn-outline-secondary btn-sm"
@onclick="@(() => RemoveTag(key))">Remove</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>
</div>
@if (_menuVisible)
{
<div class="context-menu-overlay"
@onclick="HideMenu"
@oncontextmenu:preventDefault="true"
@oncontextmenu="HideMenu"></div>
<div class="context-menu" style="left:@(_menuX)px; top:@(_menuY)px;">
<div class="context-menu-head">@(_menuAttribute?.AttributeName)</div>
<button type="button" class="context-menu-item" @onclick="AddMenuTagAsync">
Add to subscription panel
</button>
</div>
}
@code {
private const int SearchResultLimit = 300;
private IReadOnlyList<DashboardBrowseNode> _roots = [];
private string _search = string.Empty;
private IReadOnlyList<GalaxyAttribute> _searchMatches = [];
private readonly List<string> _subscribed = [];
private readonly Dictionary<string, DashboardTagValue> _values = new(StringComparer.Ordinal);
// Per-tag bookkeeping for the Updated column: the signature of the value
// last seen, and when that value/quality was first observed. Lets the
// column move only on a real change, not on every 2s poll.
private readonly Dictionary<string, string> _valueSignature = new(StringComparer.Ordinal);
private readonly Dictionary<string, DateTimeOffset> _observedChangeAt = new(StringComparer.Ordinal);
private string? _readError;
private int? _workerPid;
private bool _menuVisible;
private int _menuX;
private int _menuY;
private GalaxyAttribute? _menuAttribute;
private readonly CancellationTokenSource _cts = new();
private Task? _pollTask;
/// <inheritdoc />
protected override void OnInitialized()
{
_roots = DashboardBrowseTreeBuilder.Build(GalaxyCache.Current.Objects);
_pollTask = PollLoopAsync();
}
private string HeaderLine()
{
GalaxyHierarchyCacheEntry entry = GalaxyCache.Current;
return $"{entry.ObjectCount:N0} objects · {entry.AttributeCount:N0} attributes · "
+ $"{entry.AlarmAttributeCount:N0} alarm attributes";
}
private string Search
{
get => _search;
set
{
_search = value ?? string.Empty;
_searchMatches = ComputeSearch(_search);
}
}
private IReadOnlyList<GalaxyAttribute> ComputeSearch(string rawQuery)
{
string query = rawQuery.Trim();
if (query.Length == 0)
{
return [];
}
List<GalaxyAttribute> matches = [];
foreach (GalaxyObject galaxyObject in GalaxyCache.Current.Objects)
{
foreach (GalaxyAttribute attr in galaxyObject.Attributes)
{
if (attr.FullTagReference.Contains(query, StringComparison.OrdinalIgnoreCase)
|| attr.AttributeName.Contains(query, StringComparison.OrdinalIgnoreCase))
{
matches.Add(attr);
if (matches.Count >= SearchResultLimit)
{
return matches;
}
}
}
}
return matches;
}
private static string FormatType(GalaxyAttribute attr)
{
string baseType = string.IsNullOrWhiteSpace(attr.DataTypeName) ? "type?" : attr.DataTypeName;
if (!attr.IsArray)
{
return baseType;
}
return attr.ArrayDimensionPresent ? $"{baseType}[{attr.ArrayDimension}]" : $"{baseType}[]";
}
private Task OnTagContextMenu((MouseEventArgs Event, GalaxyAttribute Attribute) args)
{
ShowMenu(args.Event, args.Attribute);
return Task.CompletedTask;
}
private void ShowMenu(MouseEventArgs args, GalaxyAttribute attr)
{
_menuAttribute = attr;
_menuX = (int)args.ClientX;
_menuY = (int)args.ClientY;
_menuVisible = true;
}
private void HideMenu()
{
_menuVisible = false;
_menuAttribute = null;
}
private async Task AddMenuTagAsync()
{
GalaxyAttribute? attr = _menuAttribute;
HideMenu();
if (attr is not null)
{
await AddTagAsync(attr.FullTagReference);
}
}
private async Task AddTagAsync(string fullReference)
{
if (string.IsNullOrWhiteSpace(fullReference)
|| _subscribed.Contains(fullReference, StringComparer.Ordinal))
{
return;
}
_subscribed.Add(fullReference);
await RefreshValuesAsync();
}
private void RemoveTag(string tag)
{
_subscribed.Remove(tag);
_values.Remove(tag);
_valueSignature.Remove(tag);
_observedChangeAt.Remove(tag);
}
private void ClearAll()
{
_subscribed.Clear();
_values.Clear();
_valueSignature.Clear();
_observedChangeAt.Clear();
_readError = null;
}
// The MXAccess source timestamp when the worker supplies one, otherwise the
// time the dashboard first observed the current value/quality.
private string TimestampText(string tag, DashboardTagValue? value)
{
if (value is null)
{
return "…";
}
if (value.SourceTimestamp is { } source)
{
return DashboardDisplay.DateTime(source);
}
return _observedChangeAt.TryGetValue(tag, out DateTimeOffset observed)
? DashboardDisplay.DateTime(observed)
: "-";
}
private static string TimestampTooltip(DashboardTagValue? value)
{
return value?.SourceTimestamp is not null
? "MXAccess source timestamp."
: "When the dashboard first observed this value — MXAccess did not supply a source timestamp for this tag.";
}
private async Task PollLoopAsync()
{
try
{
using PeriodicTimer timer = new(TimeSpan.FromSeconds(2));
while (await timer.WaitForNextTickAsync(_cts.Token).ConfigureAwait(false))
{
await InvokeAsync(RefreshValuesAsync).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
}
}
private async Task RefreshValuesAsync()
{
if (_subscribed.Count == 0)
{
return;
}
string[] tags = [.. _subscribed];
DashboardLiveReadResult result = await LiveData.ReadAsync(tags, _cts.Token);
_readError = result.Error;
_workerPid = result.WorkerProcessId;
DateTimeOffset now = DateTimeOffset.UtcNow;
foreach (DashboardTagValue value in result.Values)
{
// Stamp the observed-change time only when the value/quality
// signature actually changes, so the Updated column does not
// tick on every poll for a static tag.
string signature = $"{value.ValueText}{value.Quality}{value.Ok}";
if (!_valueSignature.TryGetValue(value.TagAddress, out string? previous)
|| previous != signature)
{
_valueSignature[value.TagAddress] = signature;
_observedChangeAt[value.TagAddress] = now;
}
_values[value.TagAddress] = value;
}
StateHasChanged();
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
await _cts.CancelAsync();
if (_pollTask is not null)
{
try
{
await _pollTask;
}
catch (OperationCanceledException)
{
}
}
_cts.Dispose();
GC.SuppressFinalize(this);
}
}
@@ -19,12 +19,12 @@ else
</div>
<section class="metric-grid">
<MetricCard Label="Last Deploy" Value="@DashboardDisplay.DateTime(Snapshot.Galaxy.LastDeployTime)" Detail="@DeployAge()" />
<MetricCard Label="Last Deploy" Value="@DashboardDisplay.DateTime(Snapshot.Galaxy.LastDeployTime)" Detail="@DeployAge()" Wide="true" />
<MetricCard Label="Last Refresh" Value="@DashboardDisplay.DateTime(Snapshot.Galaxy.LastSuccessAt)" Detail="@LastAttemptDetail()" Wide="true" />
<MetricCard Label="Objects" Value="@DashboardDisplay.Count(Snapshot.Galaxy.ObjectCount)" Detail="@($"{Snapshot.Galaxy.AreaCount:N0} areas")" />
<MetricCard Label="Attributes" Value="@DashboardDisplay.Count(Snapshot.Galaxy.AttributeCount)" Detail="dynamic, deployed" />
<MetricCard Label="Historized" Value="@DashboardDisplay.Count(Snapshot.Galaxy.HistorizedAttributeCount)" />
<MetricCard Label="Alarms" Value="@DashboardDisplay.Count(Snapshot.Galaxy.AlarmAttributeCount)" />
<MetricCard Label="Last Refresh" Value="@DashboardDisplay.DateTime(Snapshot.Galaxy.LastSuccessAt)" Detail="@LastAttemptDetail()" />
</section>
@if (Snapshot.Galaxy.Status == DashboardGalaxyStatus.Unknown)
@@ -141,7 +141,7 @@ else
@code {
[Inject]
private IOptions<MxGateway.Server.Galaxy.GalaxyRepositoryOptions> GalaxyOptions { get; set; } = null!;
private IOptions<ZB.MOM.WW.MxGateway.Server.Galaxy.GalaxyRepositoryOptions> GalaxyOptions { get; set; } = null!;
private string RefreshHeading()
{
@@ -0,0 +1,102 @@
@using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy
@*
Recursive Browse hierarchy node. Renders one Galaxy object, its child
objects (recursively), and its attributes as right-clickable tag rows.
Expansion state is local; children render only while expanded.
*@
<div class="tree-node">
<div class="tree-row @(Node.IsArea ? "tree-row-area" : "tree-row-object")">
@if (Node.HasChildren)
{
<button type="button" class="tree-toggle" @onclick="Toggle" aria-label="Toggle">
@(_expanded ? "▾" : "▸")
</button>
}
else
{
<span class="tree-toggle tree-toggle-empty"></span>
}
<span class="tree-label" @onclick="Toggle">
<span class="tree-icon">@(Node.IsArea ? "▣" : "◇")</span>
<span class="tree-name">@Node.DisplayName</span>
@if (!string.IsNullOrWhiteSpace(Node.Object.TagName)
&& !string.Equals(Node.Object.TagName, Node.DisplayName, StringComparison.Ordinal))
{
<code class="tree-tag">@Node.Object.TagName</code>
}
</span>
</div>
@if (_expanded)
{
<div class="tree-children">
@foreach (DashboardBrowseNode child in Node.Children)
{
<BrowseTreeNodeView Node="child" OnAddTag="OnAddTag" OnTagContextMenu="OnTagContextMenu" />
}
@foreach (GalaxyAttribute attr in Node.Attributes)
{
GalaxyAttribute row = attr;
<div class="tree-attr"
title="@row.FullTagReference"
@oncontextmenu:preventDefault="true"
@oncontextmenu="@(args => OnTagContextMenu.InvokeAsync((args, row)))"
@ondblclick="@(() => OnAddTag.InvokeAsync(row.FullTagReference))">
<span class="tree-toggle tree-toggle-empty"></span>
<span class="attr-icon">·</span>
<span class="attr-name">@row.AttributeName</span>
<span class="attr-type">@DisplayType(row)</span>
@if (row.IsAlarm)
{
<span class="attr-flag attr-flag-alarm">alarm</span>
}
@if (row.IsHistorized)
{
<span class="attr-flag attr-flag-hist">hist</span>
}
</div>
}
</div>
}
</div>
@code {
/// <summary>The hierarchy node this view renders.</summary>
[Parameter]
[EditorRequired]
public DashboardBrowseNode Node { get; set; } = null!;
/// <summary>Raised with a tag's full reference when the operator double-clicks it.</summary>
[Parameter]
public EventCallback<string> OnAddTag { get; set; }
/// <summary>Raised when an attribute row is right-clicked, for the context menu.</summary>
[Parameter]
public EventCallback<(MouseEventArgs Event, GalaxyAttribute Attribute)> OnTagContextMenu { get; set; }
private bool _expanded;
private void Toggle()
{
if (Node.HasChildren)
{
_expanded = !_expanded;
}
}
private static string DisplayType(GalaxyAttribute attribute)
{
string baseType = string.IsNullOrWhiteSpace(attribute.DataTypeName)
? "type?"
: attribute.DataTypeName;
if (attribute.IsArray)
{
return attribute.ArrayDimensionPresent
? $"{baseType}[{attribute.ArrayDimension}]"
: $"{baseType}[]";
}
return baseType;
}
}
@@ -1,4 +1,4 @@
<div class="card metric-card h-100">
<div class="card metric-card h-100@(Wide ? " metric-card-wide" : string.Empty)">
<div class="card-body">
<div class="metric-label">@Label</div>
<div class="metric-value">@Value</div>
@@ -18,4 +18,8 @@
[Parameter]
public string? Detail { get; set; }
/// <summary>Spans the card across two grid columns for long values such as timestamps.</summary>
[Parameter]
public bool Wide { get; set; }
}
@@ -0,0 +1,16 @@
<span class="chip @CssClass">@Text</span>
@code {
[Parameter]
public string? Text { get; set; }
private string CssClass => Text switch
{
"Ready" or "Healthy" or "Active" => "chip-ok",
"Creating" or "StartingWorker" or "WaitingForPipe" or "InitializingWorker" or "Closing" => "chip-warn",
"Stale" or "Degraded" => "chip-warn",
"Faulted" or "Unavailable" => "chip-bad",
"Closed" or "Revoked" or "Unknown" => "chip-idle",
_ => "chip-idle"
};
}
@@ -0,0 +1,13 @@
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.Extensions.Options
@using ZB.MOM.WW.MxGateway.Contracts.Proto
@using ZB.MOM.WW.MxGateway.Server.Configuration
@using ZB.MOM.WW.MxGateway.Server.Dashboard
@using ZB.MOM.WW.MxGateway.Server.Dashboard.Components.Layout
@using ZB.MOM.WW.MxGateway.Server.Dashboard.Components.Shared
@using ZB.MOM.WW.MxGateway.Server.Security.Authorization
@using ZB.MOM.WW.MxGateway.Server.Workers
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@@ -0,0 +1,63 @@
using ZB.MOM.WW.MxGateway.Contracts.Proto;
namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
/// <summary>
/// One active-alarm row as shown on the dashboard Alarms tab. Projected
/// from an <see cref="ActiveAlarmSnapshot"/> so the Razor component never
/// touches protobuf types directly.
/// </summary>
public sealed record DashboardActiveAlarm(
string Reference,
string Provider,
string Area,
string Source,
string AlarmType,
int Severity,
AlarmConditionState State,
DateTimeOffset? LastTransition,
string OperatorUser,
string OperatorComment,
string Description)
{
/// <summary>Projects a worker active-alarm snapshot into a dashboard alarm row.</summary>
/// <param name="snapshot">The snapshot returned by <c>QueryActiveAlarms</c>.</param>
/// <returns>The projected dashboard alarm.</returns>
public static DashboardActiveAlarm FromSnapshot(ActiveAlarmSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(snapshot);
string provider = string.Empty;
string reference = snapshot.AlarmFullReference ?? string.Empty;
int bang = reference.IndexOf('!', StringComparison.Ordinal);
if (bang > 0)
{
provider = reference[..bang];
}
return new DashboardActiveAlarm(
Reference: reference,
Provider: provider,
Area: snapshot.Category ?? string.Empty,
Source: snapshot.SourceObjectReference ?? string.Empty,
AlarmType: snapshot.AlarmTypeName ?? string.Empty,
Severity: snapshot.Severity,
State: snapshot.CurrentState,
LastTransition: snapshot.LastTransitionTimestamp?.ToDateTimeOffset(),
OperatorUser: snapshot.OperatorUser ?? string.Empty,
OperatorComment: snapshot.OperatorComment ?? string.Empty,
Description: snapshot.Description ?? string.Empty);
}
/// <summary>True when this alarm is active and not yet acknowledged.</summary>
public bool IsUnacknowledged => State == AlarmConditionState.Active;
}
/// <summary>Result of a dashboard active-alarm query.</summary>
/// <param name="Alarms">The active alarms, or an empty list on error.</param>
/// <param name="Error">A diagnostic message when the query failed; otherwise null.</param>
/// <param name="WorkerProcessId">The worker process id backing the dashboard session, when available.</param>
public sealed record DashboardAlarmQueryResult(
IReadOnlyList<DashboardActiveAlarm> Alarms,
string? Error,
int? WorkerProcessId);

Some files were not shown because too many files have changed in this diff Show More