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:
@@ -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>\\<machine>\Galaxy!<area></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>\\<MachineName>\Galaxy!<DefaultArea></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;
|
||||
}
|
||||
}
|
||||
-298
@@ -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; }
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
+53
-31
@@ -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 {
|
||||
+36
-36
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+5931
-1211
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
+127
-127
@@ -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;
|
||||
+22
-1
@@ -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;
|
||||
+232
-18
@@ -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;
|
||||
+8
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
-6
@@ -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();
|
||||
+10
-8
@@ -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;
|
||||
}
|
||||
+51
-15
@@ -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
-1
@@ -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
-1
@@ -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
+2
-2
@@ -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>\\<machine>\Galaxy!<area></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>\\<MachineName>\Galaxy!<DefaultArea></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
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public enum AuthenticationMode
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public sealed class AuthenticationOptions
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public sealed class DashboardOptions
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public sealed record EffectiveAuthenticationConfiguration(
|
||||
string Mode,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public sealed record EffectiveDashboardConfiguration(
|
||||
bool Enabled,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public sealed record EffectiveEventConfiguration(
|
||||
int QueueCapacity,
|
||||
+1
-1
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public sealed record EffectiveProtocolConfiguration(
|
||||
uint WorkerProtocolVersion,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public sealed record EffectiveSessionConfiguration(
|
||||
int DefaultCommandTimeoutSeconds,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public sealed record EffectiveWorkerConfiguration(
|
||||
string ExecutablePath,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public enum EventBackpressurePolicy
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public sealed class EventOptions
|
||||
{
|
||||
+1
-1
@@ -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
-1
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public static class GatewayConfigurationServiceCollectionExtensions
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public sealed class GatewayOptions
|
||||
{
|
||||
+29
-2
@@ -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
-1
@@ -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";
|
||||
}
|
||||
+2
-2
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public sealed class SessionOptions
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Configuration;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
public enum WorkerArchitecture
|
||||
{
|
||||
+2
-2
@@ -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; }
|
||||
+1
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
namespace MxGateway.Server.Dashboard.Components;
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Components;
|
||||
|
||||
public static class DashboardDisplay
|
||||
{
|
||||
+1
-1
@@ -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">▮</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 “@Search”.</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);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -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
Reference in New Issue
Block a user