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:
@@ -3,7 +3,7 @@
|
||||
"contractName": "mxaccess-gateway",
|
||||
"gatewayProtocolVersion": 3,
|
||||
"workerProtocolVersion": 1,
|
||||
"protoRoot": "src/MxGateway.Contracts/Protos",
|
||||
"protoRoot": "src/ZB.MOM.WW.MxGateway.Contracts/Protos",
|
||||
"sourceFiles": [
|
||||
{
|
||||
"path": "mxaccess_gateway.proto",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# aaAlarmManagedClient discovery — public surface, 2026-05-01
|
||||
|
||||
Result of running
|
||||
`MxGateway.Worker.Tests.AlarmClientDiscoveryTests.DumpAlarmClientPublicSurface`
|
||||
`ZB.MOM.WW.MxGateway.Worker.Tests.AlarmClientDiscoveryTests.DumpAlarmClientPublicSurface`
|
||||
against the deployed AVEVA assembly:
|
||||
|
||||
- File:
|
||||
@@ -68,7 +68,7 @@ list.
|
||||
## What this means
|
||||
|
||||
The architecture comment on
|
||||
`src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs` (PR A.5) is
|
||||
`src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmClientConsumer.cs` (PR A.5) is
|
||||
**wrong against this deployed assembly**:
|
||||
|
||||
> "The AVEVA alarm-manager surface (`IAlarmMgrDataProvider`) exposes
|
||||
@@ -89,7 +89,7 @@ never gets invoked at runtime. Until A.2 lands a WM_APP pump,
|
||||
|
||||
## Live runtime probe — 2026-05-01
|
||||
|
||||
`MxGateway.Worker.Tests.AlarmClientWmProbeTests.ProbeAlarmClientWmMessages`
|
||||
`ZB.MOM.WW.MxGateway.Worker.Tests.AlarmClientWmProbeTests.ProbeAlarmClientWmMessages`
|
||||
is a Skip-gated runtime probe that creates a real message-only
|
||||
window, calls `AlarmClient.RegisterConsumer(hWnd, …)` +
|
||||
`Subscribe(@"\Galaxy!", …)`, and pumps for 20s while logging every
|
||||
@@ -505,7 +505,7 @@ Interop.WNWRAPCONSUMERLib.dll`). The COM class is registered in
|
||||
Apartment` — `new wwAlarmConsumerClass()` succeeds via
|
||||
`CoCreateInstance`.
|
||||
|
||||
The probe `MxGateway.Worker.Tests/WnWrapConsumerProbeTests.cs`
|
||||
The probe `ZB.MOM.WW.MxGateway.Worker.Tests/WnWrapConsumerProbeTests.cs`
|
||||
(Skip-gated, archival) drove the captured run. Lifecycle:
|
||||
|
||||
1. `new wwAlarmConsumerClass()` — instantiated.
|
||||
@@ -622,7 +622,7 @@ Replacing `aaAlarmManagedClient.AlarmClient` with
|
||||
alarm-consumer surface unblocks A.2 fully. Outline:
|
||||
|
||||
1. **Reference path:** drop `aaAlarmManagedClient.dll` reference
|
||||
from `MxGateway.Worker.csproj`; add `Interop.WNWRAPCONSUMERLib.dll`
|
||||
from `ZB.MOM.WW.MxGateway.Worker.csproj`; add `Interop.WNWRAPCONSUMERLib.dll`
|
||||
reference from `mxaccessgw/lib/`. (Or commit the interop dll
|
||||
in-tree under `lib/` and reference relatively.)
|
||||
2. **`AlarmClientConsumer` → `WnWrapAlarmConsumer`:** rewrite
|
||||
|
||||
+16
-18
@@ -107,29 +107,20 @@ The gateway keeps API key state in a dedicated SQLite database. SQLite is suffic
|
||||
|
||||
### Connection factory
|
||||
|
||||
`AuthSqliteConnectionFactory` reads `GatewayOptions.Authentication.SqlitePath`, ensures the parent directory exists, and opens the connection in `ReadWriteCreate` mode so first-run installations can create the file without manual provisioning:
|
||||
`AuthSqliteConnectionFactory` reads `GatewayOptions.Authentication.SqlitePath`, ensures the parent directory exists, and builds a connection string in `ReadWriteCreate` mode so first-run installations can create the file without manual provisioning. Connection pooling is enabled and the connection string carries a non-zero `DefaultTimeout`:
|
||||
|
||||
```csharp
|
||||
public SqliteConnection CreateConnection()
|
||||
SqliteConnectionStringBuilder builder = new()
|
||||
{
|
||||
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());
|
||||
}
|
||||
DataSource = sqlitePath,
|
||||
Mode = SqliteOpenMode.ReadWriteCreate,
|
||||
Pooling = true,
|
||||
DefaultTimeout = (int)BusyTimeout.TotalSeconds,
|
||||
};
|
||||
```
|
||||
|
||||
Every store opens its connection through `OpenConnectionAsync`, which opens the connection and then applies `PRAGMA journal_mode=WAL` and `PRAGMA busy_timeout`. WAL is a persistent database-level setting so re-applying it per connection is a cheap no-op; `busy_timeout` is per-connection state. Because `MarkKeyUsedAsync` runs on every authenticated request and `SqliteApiKeyAuditStore` appends on every denial, this lets concurrent readers and writers retry briefly instead of surfacing `SQLITE_BUSY` as a hard failure on the request path.
|
||||
|
||||
### Schema
|
||||
|
||||
`SqliteAuthSchema` declares table names and the current schema version as constants. Three tables are involved:
|
||||
@@ -166,6 +157,8 @@ public static ApiKeyRecord Read(SqliteDataReader reader)
|
||||
|
||||
`SqliteApiKeyAdminStore` (`IApiKeyAdminStore`) implements administrative mutations: `CreateAsync` accepts an `ApiKeyCreateRequest`, `RevokeAsync` sets `revoked_utc` only when not already revoked, and `RotateAsync` replaces `secret_hash`, clears `last_used_utc`, and clears `revoked_utc` so a rotated key is immediately usable.
|
||||
|
||||
Because `RotateAsync` clears `revoked_utc`, rotating a previously revoked key reactivates it. The dashboard API Keys page therefore offers the Rotate (and Revoke) action only for keys whose status is `Active`; a revoked key shows no actions, so an operator cannot un-revoke a deliberately disabled key as a side effect of a rotation.
|
||||
|
||||
### Audit trail
|
||||
|
||||
`SqliteApiKeyAuditStore` (`IApiKeyAuditStore`) appends `ApiKeyAuditEntry` values to the `api_key_audit` table and stamps each row with a UTC timestamp inside the store rather than trusting the caller. `ListRecentAsync` returns the most recent rows ordered by `audit_id` descending and projects them into `ApiKeyAuditRecord`. Rows are kept even after the referenced key is revoked because the audit history is the durable record of administrative action; the `key_id` column is nullable to accommodate non-key-scoped events such as `init-db`.
|
||||
@@ -223,6 +216,10 @@ constraints remain fully unconstrained after migration.
|
||||
|
||||
Key ids are restricted by the parser to ASCII letters, digits, periods, and hyphens so they remain safe to embed in the token format and in URL paths used by administrative tooling.
|
||||
|
||||
The CLI is not the only management surface: the dashboard API Keys page
|
||||
creates, rotates, and revokes keys through the same `IApiKeyAdminStore`. See
|
||||
[Gateway Dashboard Design](./GatewayDashboardDesign.md#api-keys-page).
|
||||
|
||||
## Scope Serialization
|
||||
|
||||
Scopes are persisted as a single TEXT column rather than a join table because the set is small, never queried by membership at the database level, and changes atomically with the owning row. `ApiKeyScopeSerializer.Serialize` writes a JSON array sorted with `StringComparer.Ordinal` so equivalent scope sets produce byte-identical column values, which makes audit diffing and database comparisons deterministic:
|
||||
@@ -276,4 +273,5 @@ Singletons are safe because each operation opens its own short-lived `SqliteConn
|
||||
|
||||
- [Gateway Configuration](./GatewayConfiguration.md)
|
||||
- [Authorization](./Authorization.md)
|
||||
- [Gateway Dashboard Design](./GatewayDashboardDesign.md)
|
||||
- [Diagnostics](./Diagnostics.md)
|
||||
|
||||
+37
-13
@@ -8,7 +8,7 @@ what an authenticated API key can browse, read, or write inside the Galaxy.
|
||||
|
||||
Authorization runs as a single gRPC server interceptor registered for every call on the gateway. It pulls the authenticated identity for the current request, derives the scope that the request type requires, and either lets the call continue or fails the call with a gRPC status. The pipeline keeps service classes free of cross-cutting checks, which matches the `gateway.md` "thin gRPC layer" rule that service handlers translate between contracts and domain code without owning policy.
|
||||
|
||||
The participating types live under `src/MxGateway.Server/Security/Authorization/`:
|
||||
The participating types live under `src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/`:
|
||||
|
||||
- `GatewayGrpcAuthorizationInterceptor` runs the authenticate-then-authorize pipeline for unary and server-streaming calls.
|
||||
- `GatewayGrpcScopeResolver` maps a request message (and, for `MxCommandRequest`, the inner `MxCommandKind`) to the scope string that must be present on the caller.
|
||||
@@ -102,12 +102,18 @@ public string ResolveRequiredScope(object request)
|
||||
CloseSessionRequest => GatewayScopes.SessionClose,
|
||||
StreamEventsRequest => GatewayScopes.EventsRead,
|
||||
MxCommandRequest commandRequest => ResolveCommandScope(commandRequest.Command?.Kind ?? MxCommandKind.Unspecified),
|
||||
AcknowledgeAlarmRequest => GatewayScopes.InvokeWrite,
|
||||
StreamAlarmsRequest => GatewayScopes.EventsRead,
|
||||
TestConnectionRequest or
|
||||
GetLastDeployTimeRequest or
|
||||
DiscoverHierarchyRequest or
|
||||
WatchDeployEventsRequest => GatewayScopes.MetadataRead,
|
||||
_ => GatewayScopes.Admin
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The `_ => GatewayScopes.Admin` fallback is intentional: any future request type that the resolver does not recognize fails closed, requiring the strongest scope until the resolver is updated.
|
||||
The `_ => GatewayScopes.Admin` fallback is intentional: any future request type that the resolver does not recognize fails closed, requiring the strongest scope until the resolver is updated. `AcknowledgeAlarm` is treated as a write — it mutates alarm state, mirroring `MxCommandKind.Write*` — and `StreamAlarms` shares the alarm/event surface with `StreamEvents` and `MxCommandKind.DrainEvents`, so it carries `events:read`. Both alarm RPCs are session-less: the scope check is the only authorization gate, since there is no per-session ownership to enforce.
|
||||
|
||||
`MxCommandRequest` is special because it multiplexes many MxAccess operations through a single RPC. The resolver inspects the embedded `MxCommandKind` so each operation gets its own scope:
|
||||
|
||||
@@ -117,10 +123,14 @@ private static string ResolveCommandScope(MxCommandKind kind)
|
||||
return kind switch
|
||||
{
|
||||
MxCommandKind.Write or
|
||||
MxCommandKind.Write2 => GatewayScopes.InvokeWrite,
|
||||
MxCommandKind.Write2 or
|
||||
MxCommandKind.WriteBulk or
|
||||
MxCommandKind.Write2Bulk => GatewayScopes.InvokeWrite,
|
||||
|
||||
MxCommandKind.WriteSecured or
|
||||
MxCommandKind.WriteSecured2 or
|
||||
MxCommandKind.WriteSecuredBulk or
|
||||
MxCommandKind.WriteSecured2Bulk or
|
||||
MxCommandKind.AuthenticateUser => GatewayScopes.InvokeSecure,
|
||||
|
||||
MxCommandKind.ArchestraUserToId or
|
||||
@@ -135,7 +145,7 @@ private static string ResolveCommandScope(MxCommandKind kind)
|
||||
}
|
||||
```
|
||||
|
||||
Reads (`Register`, `AddItem`, `Advise`, and any other unspecified kind) fall through to `InvokeRead`, which keeps the matrix small while still separating reads from writes, secured writes, metadata lookups, event drains, and worker shutdown.
|
||||
Reads (`Register`, `AddItem`, `Advise`, `ReadBulk`, and any other unspecified kind) fall through to `InvokeRead`, which keeps the matrix small while still separating reads from writes, secured writes, metadata lookups, event drains, and worker shutdown. The four bulk-write families (`WriteBulk`, `Write2Bulk`, `WriteSecuredBulk`, `WriteSecured2Bulk`) are mapped explicitly so a missing arm cannot silently demote a bulk write to a read scope.
|
||||
|
||||
## Constraint Enforcement
|
||||
|
||||
@@ -161,12 +171,25 @@ Glob matching is anchored, case-insensitive, and supports `*` and `?`.
|
||||
Subtree and tag glob lists are alternatives: matching either list allows that
|
||||
scope dimension. Empty lists mean unconstrained for that dimension.
|
||||
|
||||
Constraints are set when a key is created — through the `apikey create-key`
|
||||
flags (see [Authentication](./Authentication.md)) or the dashboard API Keys
|
||||
page create dialog (see
|
||||
[Gateway Dashboard Design](./GatewayDashboardDesign.md#api-keys-page)). The
|
||||
dashboard API Keys page also renders each key's effective constraints.
|
||||
|
||||
The service checks read constraints for `AddItem`, `AddItem2`, `AddItemBulk`,
|
||||
`SubscribeBulk`, and `AdviseItemBulk`. It checks write constraints for
|
||||
`Write`, `Write2`, `WriteSecured`, and `WriteSecured2`. Successful item
|
||||
registrations are tracked per session so later item-handle commands resolve
|
||||
back to the original tag address. If a constrained key presents an unknown item
|
||||
handle, the gateway fails closed.
|
||||
`SubscribeBulk`, `AdviseItemBulk`, and `ReadBulk`. It checks write constraints
|
||||
for `Write`, `Write2`, `WriteSecured`, `WriteSecured2`, `WriteBulk`,
|
||||
`Write2Bulk`, `WriteSecuredBulk`, and `WriteSecured2Bulk`. Bulk commands run
|
||||
through `BulkConstraintPlan` (`ReadBulkConstraintPlan`,
|
||||
`WriteBulkConstraintPlan`, `SubscribeBulkConstraintPlan`), which preserves the
|
||||
caller's input order: each entry is evaluated against the constraint surface,
|
||||
and `BulkConstraintPlan.MergeDeniedInto` re-merges denied entries back into
|
||||
their original index positions so the reply slot at `entries[i]` always
|
||||
corresponds to the request slot at `entries[i]`. Successful item registrations
|
||||
are tracked per session so later item-handle commands resolve back to the
|
||||
original tag address. If a constrained key presents an unknown item handle,
|
||||
the gateway fails closed.
|
||||
|
||||
Non-bulk constraint failures return gRPC `PermissionDenied`. Bulk read
|
||||
commands preserve input order and return a failed `SubscribeResult` for each
|
||||
@@ -182,10 +205,10 @@ blocking constraint; secured values and raw credentials are never logged.
|
||||
|----------|-------|--------------|
|
||||
| `SessionOpen` | `session:open` | `OpenSessionRequest` |
|
||||
| `SessionClose` | `session:close` | `CloseSessionRequest` |
|
||||
| `EventsRead` | `events:read` | `StreamEventsRequest`, `MxCommandKind.DrainEvents` |
|
||||
| `InvokeRead` | `invoke:read` | `MxCommandRequest` for read-style command kinds (`Register`, `AddItem`, `Advise`, and any kind not otherwise mapped) |
|
||||
| `InvokeWrite` | `invoke:write` | `MxCommandKind.Write`, `MxCommandKind.Write2` |
|
||||
| `InvokeSecure` | `invoke:secure` | `MxCommandKind.WriteSecured`, `MxCommandKind.WriteSecured2`, `MxCommandKind.AuthenticateUser` |
|
||||
| `EventsRead` | `events:read` | `StreamEventsRequest`, `StreamAlarmsRequest`, `MxCommandKind.DrainEvents` |
|
||||
| `InvokeRead` | `invoke:read` | `MxCommandRequest` for read-style command kinds (`Register`, `AddItem`, `Advise`, `ReadBulk`, and any kind not otherwise mapped) |
|
||||
| `InvokeWrite` | `invoke:write` | `AcknowledgeAlarmRequest`, `MxCommandKind.Write`, `MxCommandKind.Write2`, `MxCommandKind.WriteBulk`, `MxCommandKind.Write2Bulk` |
|
||||
| `InvokeSecure` | `invoke:secure` | `MxCommandKind.WriteSecured`, `MxCommandKind.WriteSecured2`, `MxCommandKind.WriteSecuredBulk`, `MxCommandKind.WriteSecured2Bulk`, `MxCommandKind.AuthenticateUser` |
|
||||
| `MetadataRead` | `metadata:read` | `MxCommandKind.ArchestraUserToId`, `MxCommandKind.GetSessionState`, `MxCommandKind.GetWorkerInfo`, `GalaxyRepository.TestConnection`, `GalaxyRepository.GetLastDeployTime`, `GalaxyRepository.DiscoverHierarchy`, `GalaxyRepository.WatchDeployEvents` |
|
||||
| `Admin` | `admin` | `MxCommandKind.ShutdownWorker`, the default for any unrecognized request type, and the dashboard authorization policy |
|
||||
|
||||
@@ -252,6 +275,7 @@ Singleton lifetimes are appropriate because none of the three classes hold per-r
|
||||
## Related Documentation
|
||||
|
||||
- [Authentication](./Authentication.md)
|
||||
- [Gateway Dashboard Design](./GatewayDashboardDesign.md)
|
||||
- [Grpc](./Grpc.md)
|
||||
- [GatewayConfiguration](./GatewayConfiguration.md)
|
||||
- [Galaxy Repository Browse](./GalaxyRepository.md)
|
||||
|
||||
@@ -398,7 +398,7 @@ README.md
|
||||
examples/
|
||||
```
|
||||
|
||||
Generated code should be reproducible from `src/MxGateway.Contracts/Protos/`.
|
||||
Generated code should be reproducible from `src/ZB.MOM.WW.MxGateway.Contracts/Protos/`.
|
||||
Do not hand-edit generated code.
|
||||
|
||||
The stable client proto manifest defines the generated-code directories:
|
||||
|
||||
+10
-10
@@ -8,7 +8,7 @@ in [Toolchain Links](./ToolchainLinks.md) when a command is missing from
|
||||
## Shared Inputs
|
||||
|
||||
All clients generate bindings from the shared protobuf files under
|
||||
`src/MxGateway.Contracts/Protos`. Regenerate the published client descriptor
|
||||
`src/ZB.MOM.WW.MxGateway.Contracts/Protos`. Regenerate the published client descriptor
|
||||
after changing either `.proto` file or `clients/proto/proto-inputs.json`:
|
||||
|
||||
```powershell
|
||||
@@ -35,37 +35,37 @@ machine boundary or uses a production certificate.
|
||||
## .NET
|
||||
|
||||
The .NET client uses .NET 10 and references
|
||||
`src/MxGateway.Contracts/MxGateway.Contracts.csproj` for generated C# contract
|
||||
`src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj` for generated C# contract
|
||||
types. `clients/dotnet/generated` remains reserved for client-local generator
|
||||
output if the client later decouples from the contracts project.
|
||||
|
||||
Regenerate the generated C# contract types:
|
||||
|
||||
```powershell
|
||||
dotnet build src/MxGateway.Contracts/MxGateway.Contracts.csproj
|
||||
dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj
|
||||
```
|
||||
|
||||
Build and test from the repository root:
|
||||
|
||||
```powershell
|
||||
dotnet build clients/dotnet/MxGateway.Client.sln
|
||||
dotnet test clients/dotnet/MxGateway.Client.sln --no-build
|
||||
dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.sln
|
||||
dotnet test clients/dotnet/ZB.MOM.WW.MxGateway.Client.sln --no-build
|
||||
```
|
||||
|
||||
Create local package artifacts:
|
||||
|
||||
```powershell
|
||||
$dotnetPackageOutput = Join-Path (Get-Location) 'artifacts/clients/dotnet'
|
||||
dotnet pack clients/dotnet/MxGateway.Client/MxGateway.Client.csproj -c Release -p:PackageOutputPath="$dotnetPackageOutput"
|
||||
dotnet publish clients/dotnet/MxGateway.Client.Cli/MxGateway.Client.Cli.csproj -c Release -o artifacts/clients/dotnet/mxgw-dotnet
|
||||
dotnet pack clients/dotnet/ZB.MOM.WW.MxGateway.Client/ZB.MOM.WW.MxGateway.Client.csproj -c Release -p:PackageOutputPath="$dotnetPackageOutput"
|
||||
dotnet publish clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/ZB.MOM.WW.MxGateway.Client.Cli.csproj -c Release -o artifacts/clients/dotnet/mxgw-dotnet
|
||||
```
|
||||
|
||||
Run the CLI from source:
|
||||
|
||||
```powershell
|
||||
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- version --json
|
||||
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- smoke --endpoint "http://$env:MXGATEWAY_ENDPOINT" --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
|
||||
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- smoke --endpoint "https://mxgateway.example.local:5001" --tls --ca-file C:\certs\mxgateway-ca.pem --server-name mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
|
||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- version --json
|
||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint "http://$env:MXGATEWAY_ENDPOINT" --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
|
||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint "https://mxgateway.example.local:5001" --tls --ca-file C:\certs\mxgateway-ca.pem --server-name mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
|
||||
```
|
||||
|
||||
## Go
|
||||
|
||||
@@ -21,9 +21,9 @@ records:
|
||||
|
||||
The source files listed by the manifest are:
|
||||
|
||||
- `src/MxGateway.Contracts/Protos/mxaccess_gateway.proto`
|
||||
- `src/MxGateway.Contracts/Protos/mxaccess_worker.proto`
|
||||
- `src/MxGateway.Contracts/Protos/galaxy_repository.proto`
|
||||
- `src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto`
|
||||
- `src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_worker.proto`
|
||||
- `src/ZB.MOM.WW.MxGateway.Contracts/Protos/galaxy_repository.proto`
|
||||
|
||||
`mxaccess_gateway.proto` defines the public gRPC service and shared DTOs.
|
||||
`mxaccess_worker.proto` is included in the descriptor because worker-aware
|
||||
@@ -86,7 +86,7 @@ issues.
|
||||
|
||||
## Language Generation Inputs
|
||||
|
||||
All generators use `src/MxGateway.Contracts/Protos` as the protobuf import
|
||||
All generators use `src/ZB.MOM.WW.MxGateway.Contracts/Protos` as the protobuf import
|
||||
root. The checked-in descriptor is available when a language build prefers a
|
||||
descriptor input, but the `.proto` files remain canonical.
|
||||
|
||||
@@ -94,7 +94,7 @@ Use these commands to regenerate language-specific client bindings:
|
||||
|
||||
| Client | Command |
|
||||
|--------|---------|
|
||||
| .NET | `dotnet build src/MxGateway.Contracts/MxGateway.Contracts.csproj` |
|
||||
| .NET | `dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj` |
|
||||
| Go | `Push-Location clients/go; ./generate-proto.ps1; Pop-Location` |
|
||||
| Rust | `Push-Location clients/rust; cargo check --workspace; Pop-Location` |
|
||||
| Python | `Push-Location clients/python; ./generate-proto.ps1; Pop-Location` |
|
||||
@@ -103,10 +103,10 @@ Use these commands to regenerate language-specific client bindings:
|
||||
.NET generation currently runs through the contracts project:
|
||||
|
||||
```powershell
|
||||
dotnet build src/MxGateway.Contracts/MxGateway.Contracts.csproj
|
||||
dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj
|
||||
```
|
||||
|
||||
Future .NET client projects may either reference `MxGateway.Contracts` or
|
||||
Future .NET client projects may either reference `ZB.MOM.WW.MxGateway.Contracts` or
|
||||
generate client-local files into `clients/dotnet/generated` with `Grpc.Tools`.
|
||||
|
||||
Go clients should generate `mxaccess_gateway.proto` and
|
||||
|
||||
+49
-7
@@ -6,7 +6,7 @@ recreated by the contracts project build.
|
||||
|
||||
## Files
|
||||
|
||||
`src/MxGateway.Contracts/Protos/mxaccess_gateway.proto` defines the public
|
||||
`src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto` defines the public
|
||||
`MxAccessGateway` gRPC service, command payloads, command replies, event DTOs,
|
||||
`MxValue`, `MxArray`, and `MxStatusProxy`.
|
||||
|
||||
@@ -23,19 +23,61 @@ the corresponding MXAccess `AddItem`, `Advise`, `UnAdvise`, and `RemoveItem`
|
||||
calls sequentially on the session STA and preserves input order in the result
|
||||
list.
|
||||
|
||||
`src/MxGateway.Contracts/Protos/mxaccess_worker.proto` defines the named-pipe
|
||||
The command model also includes bulk write/read command kinds:
|
||||
`WriteBulk`, `Write2Bulk`, `WriteSecuredBulk`, `WriteSecured2Bulk`, and
|
||||
`ReadBulk`. They are unary `Invoke` payloads on the same `MxAccessGateway`
|
||||
surface (not separate gRPC methods) and exist so a caller can submit one list
|
||||
of items per round trip while preserving MXAccess parity per entry.
|
||||
|
||||
- `WriteBulkCommand` / `Write2BulkCommand` / `WriteSecuredBulkCommand` /
|
||||
`WriteSecured2BulkCommand` each carry a `server_handle` and a `repeated`
|
||||
list of entries (`WriteBulkEntry`, `Write2BulkEntry`,
|
||||
`WriteSecuredBulkEntry`, `WriteSecured2BulkEntry`). Each entry mirrors the
|
||||
single-item command shape — `item_handle` + `value` (+ `timestamp_value` on
|
||||
the `*2` variants, + `current_user_id` / `verifier_user_id` on the secured
|
||||
variants). All four replies use `BulkWriteReply`, which carries
|
||||
`repeated BulkWriteResult`. A `BulkWriteResult` has `server_handle`,
|
||||
`item_handle`, `was_successful`, `optional int32 hresult`, `repeated
|
||||
MxStatusProxy statuses`, and `error_message`. Per-entry failures populate
|
||||
`error_message` + `hresult` and never raise — callers iterate and inspect
|
||||
each entry. The credential-sensitive redaction rules for `WriteSecured` /
|
||||
`WriteSecured2` apply to every `value` inside `WriteSecuredBulkEntry` and
|
||||
`WriteSecured2BulkEntry`.
|
||||
|
||||
- `ReadBulkCommand` carries `server_handle`, `repeated string tag_addresses`,
|
||||
and `uint32 timeout_ms` (0 means use the gateway-configured default). The
|
||||
reply is `BulkReadReply` carrying `repeated BulkReadResult`. A
|
||||
`BulkReadResult` has `server_handle`, `tag_address`, `item_handle`,
|
||||
`was_successful`, `was_cached`, `value`, `quality`, `source_timestamp`,
|
||||
`repeated MxStatusProxy statuses`, and `error_message`. MXAccess has no
|
||||
synchronous `Read`, so `ReadBulk` is dual-mode per entry: when a tag is
|
||||
already advised in the session the worker returns the cached
|
||||
`OnDataChange` payload without touching the subscription
|
||||
(`was_cached = true`); otherwise the worker takes a full
|
||||
`AddItem` + `Advise` + wait-for-first-`OnDataChange` + `UnAdvise` +
|
||||
`RemoveItem` snapshot lifecycle and returns the result
|
||||
(`was_cached = false`). The asymmetry that `BulkReadResult` has no
|
||||
`hresult` field is intentional — `ReadBulk` outcomes are timeout / cache
|
||||
/ lifecycle states rather than MXAccess COM return codes.
|
||||
|
||||
See `gateway.md` for the full cached-vs-snapshot `ReadBulk` lifecycle and the
|
||||
per-command scope requirements, and `docs/DesignDecisions.md` "Bulk Command
|
||||
Family" for the rationale behind the per-entry result shape (independent
|
||||
success tracking, input-order preservation, no partial-failure exceptions).
|
||||
|
||||
`src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_worker.proto` defines the named-pipe
|
||||
worker IPC envelope and control messages. It imports
|
||||
`mxaccess_gateway.proto` so the worker and gateway use the same command, reply,
|
||||
event, value, and status shapes.
|
||||
|
||||
`src/MxGateway.Contracts/Protos/galaxy_repository.proto` defines the
|
||||
`src/ZB.MOM.WW.MxGateway.Contracts/Protos/galaxy_repository.proto` defines the
|
||||
`GalaxyRepository` service used by clients to browse the Galaxy Repository
|
||||
(deployed object hierarchy and dynamic attributes). The service is metadata-
|
||||
only and does not share types with `mxaccess_gateway.proto`. See
|
||||
[Galaxy Repository Browse](./GalaxyRepository.md) for the RPC catalog and
|
||||
behavior.
|
||||
|
||||
Generated C# output is written to `src/MxGateway.Contracts/Generated/`. Do not
|
||||
Generated C# output is written to `src/ZB.MOM.WW.MxGateway.Contracts/Generated/`. Do not
|
||||
hand-edit generated files.
|
||||
|
||||
Client generation inputs are published through
|
||||
@@ -49,20 +91,20 @@ generation inputs, output directories, and golden protobuf JSON fixtures.
|
||||
Run the contracts build to regenerate C# protobuf and gRPC code:
|
||||
|
||||
```bash
|
||||
dotnet build src/MxGateway.Contracts/MxGateway.Contracts.csproj
|
||||
dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj
|
||||
```
|
||||
|
||||
Run the focused contract tests after changing either `.proto` file:
|
||||
|
||||
```bash
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter ProtobufContractRoundTripTests
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj --filter ProtobufContractRoundTripTests
|
||||
```
|
||||
|
||||
The full solution build also regenerates the C# contracts before compiling
|
||||
gateway and test projects:
|
||||
|
||||
```bash
|
||||
dotnet build src/MxGateway.sln
|
||||
dotnet build src/ZB.MOM.WW.MxGateway.slnx
|
||||
```
|
||||
|
||||
Regenerate the client descriptor after changing either `.proto` file:
|
||||
|
||||
@@ -85,7 +85,7 @@ The explicit sequence remains the parity baseline for issue-level validation.
|
||||
Run the matrix shape tests after changing the smoke matrix:
|
||||
|
||||
```bash
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~CrossLanguageSmokeMatrixTests
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj --filter FullyQualifiedName~CrossLanguageSmokeMatrixTests
|
||||
```
|
||||
|
||||
Live execution remains a separate opt-in step because it depends on a running
|
||||
|
||||
@@ -82,6 +82,18 @@ fan-out may be added later with explicit backpressure semantics.
|
||||
Rationale: one subscriber preserves simple event ordering and failure behavior
|
||||
while parity is being proven.
|
||||
|
||||
### Alarms — superseded for the alarm subsystem
|
||||
|
||||
The single-subscriber rule above no longer applies to alarms. The gateway runs
|
||||
an always-on central alarm monitor (`GatewayAlarmMonitor`) that owns one
|
||||
gateway-managed worker session, caches the active-alarm set, and fans it out to
|
||||
any number of clients through the session-less `StreamAlarms` RPC. Per-session
|
||||
alarm auto-subscribe is removed; `AcknowledgeAlarm` is session-less and routes
|
||||
through the monitor. Data-side `StreamEvents` remains one subscriber per
|
||||
session. Rationale: alarm state is gateway-wide, not session-scoped — every
|
||||
client wants the same current set plus updates, and forcing each to own a
|
||||
worker would multiply AVEVA polling load for no benefit.
|
||||
|
||||
## Authentication
|
||||
|
||||
Decision: API key authentication for the public gateway.
|
||||
@@ -199,6 +211,57 @@ and failure behavior are easy to compare against direct MXAccess.
|
||||
|
||||
Batch tag registration can be added later if measured setup latency requires it.
|
||||
|
||||
## Bulk Command Family
|
||||
|
||||
Decision: the gateway exposes a fixed set of *bulk* command kinds —
|
||||
`AddItemBulk`, `AdviseItemBulk`, `RemoveItemBulk`, `UnAdviseItemBulk`,
|
||||
`SubscribeBulk`, `UnsubscribeBulk`, `WriteBulk`, `Write2Bulk`,
|
||||
`WriteSecuredBulk`, `WriteSecured2Bulk`, `ReadBulk` — that carry a list of
|
||||
entries in one round-trip and return one per-entry result. Each command kind
|
||||
runs the corresponding single-item MXAccess COM call sequentially on the
|
||||
worker STA; per-entry failures populate `was_successful = false` with the
|
||||
underlying HRESULT and never throw. There is no transactional / fail-fast
|
||||
semantic — bulk here means "one round-trip, per-entry results", not
|
||||
"atomic".
|
||||
|
||||
Rationale: MXAccess COM itself has no native bulk API for any of these
|
||||
operations. Surfacing the per-entry result list keeps parity transparent —
|
||||
the caller sees the same per-item HRESULT they would see calling MXAccess
|
||||
N times directly — while the bulk shape collapses the gateway/IPC overhead
|
||||
to one round-trip per batch and lets the worker keep the STA hot.
|
||||
|
||||
`ReadBulk` is the only bulk command without a 1:1 MXAccess analogue. Two
|
||||
choices were considered:
|
||||
|
||||
1. **Cache-then-snapshot** (chosen): when a requested tag is already in the
|
||||
session's item registry AND advised, the worker returns the last cached
|
||||
`OnDataChange` value without touching the subscription
|
||||
(`was_cached = true`). Otherwise it takes the full `AddItem + Advise +
|
||||
wait-for-first-OnDataChange + UnAdvise + RemoveItem` lifecycle itself
|
||||
(`was_cached = false`) and leaves the session exactly as it was before
|
||||
the call. The cache lives on a per-session `MxAccessValueCache`,
|
||||
populated by `MxAccessBaseEventSink` on every `OnDataChange` after the
|
||||
event clears the outbound queue.
|
||||
|
||||
2. **Always-snapshot**: take the AddItem-through-RemoveItem lifecycle for
|
||||
every requested tag. Cleaner conceptually but pays the full lifecycle
|
||||
cost on every call and would interfere with existing subscriptions if
|
||||
MXAccess reuses item handles.
|
||||
|
||||
The chosen behavior matches what callers actually want from "current
|
||||
value" — a free read of an already-streaming tag, and a one-shot snapshot
|
||||
otherwise — and never disturbs subscriptions the caller did not create.
|
||||
The decision intentionally does NOT synthesize an `OnDataChange` event
|
||||
from the snapshot path: the snapshot value reaches the caller through
|
||||
`ReadBulk`'s reply payload only, not through the event stream. This
|
||||
preserves the "Don't synthesize events" rule that scopes the rest of the
|
||||
worker.
|
||||
|
||||
`ReadBulk`'s wait loop pumps Windows messages on the worker STA
|
||||
(`StaRuntime.PumpPendingMessages`) on every poll iteration so the inbound
|
||||
MXAccess COM event can dispatch while the bulk executor still holds the
|
||||
thread — without the pump the OnDataChange would never deliver.
|
||||
|
||||
## Graceful Worker Shutdown
|
||||
|
||||
Decision: best-effort cleanup before COM release.
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
# Gateway Diagnostics
|
||||
|
||||
The diagnostics subsystem provides structured logging, credential redaction, and request-scoped log enrichment for the gateway. It lives under `src/MxGateway.Server/Diagnostics/` and is wired into the ASP.NET Core pipeline so every gRPC and HTTP request carries the same correlation fields.
|
||||
The diagnostics subsystem provides structured logging, credential redaction, and request-scoped log enrichment for the gateway. It lives under `src/ZB.MOM.WW.MxGateway.Server/Diagnostics/` and is wired into the ASP.NET Core pipeline so every gRPC and HTTP request carries the same correlation fields.
|
||||
|
||||
## Goals
|
||||
|
||||
@@ -162,7 +162,7 @@ public static IApplicationBuilder UseGatewayRequestLoggingScope(this IApplicatio
|
||||
{
|
||||
ILogger logger = context.RequestServices
|
||||
.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger("MxGateway.Request");
|
||||
.CreateLogger("ZB.MOM.WW.MxGateway.Request");
|
||||
|
||||
using IDisposable? scope = logger.BeginGatewayScope(new GatewayLogScope(
|
||||
SessionId: ReadHeader(context, SessionIdHeaderName),
|
||||
@@ -188,7 +188,7 @@ The scope is keyed off four custom headers and the standard `authorization` head
|
||||
|
||||
The numeric headers use `int.TryParse` and `ulong.TryParse`; missing or unparseable values become `null` and are dropped by `GatewayLogScope.ToDictionary`. This keeps the middleware tolerant of clients that do not yet emit every header, which matters because the earliest call in a session (`OpenSession`) has no `SessionId` to send.
|
||||
|
||||
The logger category is `MxGateway.Request`, which lets operators filter the request scope events independently from per-component categories.
|
||||
The logger category is `ZB.MOM.WW.MxGateway.Request`, which lets operators filter the request scope events independently from per-component categories.
|
||||
|
||||
### Pipeline ordering
|
||||
|
||||
@@ -213,7 +213,7 @@ The order matters: putting the logging scope first ensures that authentication f
|
||||
|
||||
- `GatewayLogScope.ToDictionary` redacts `ClientIdentity` whenever a scope is materialized.
|
||||
- `DashboardRedactor.Redact` delegates to `RedactClientIdentity` for any value containing the `mxgw_` marker, then falls back to a marker-keyword check for fields like `password` or `token`. This keeps dashboard renders aligned with log redaction.
|
||||
- `MxGateway.Tests/Diagnostics/GatewayLogRedactorTests.cs` covers each redaction branch, including the assertion that `WriteSecured` values stay redacted even when `valueLoggingEnabled` is true.
|
||||
- `ZB.MOM.WW.MxGateway.Tests/Diagnostics/GatewayLogRedactorTests.cs` covers each redaction branch, including the assertion that `WriteSecured` values stay redacted even when `valueLoggingEnabled` is true.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
|
||||
+91
-19
@@ -2,7 +2,7 @@
|
||||
|
||||
The gateway exposes a read-only browse surface over the AVEVA System Platform
|
||||
Galaxy Repository (the SQL Server database named `ZB`). Clients use it to
|
||||
enumerate the deployed object hierarchy and each object's dynamic attributes
|
||||
enumerate the deployed object hierarchy and each object's attributes
|
||||
before subscribing to runtime values via the existing `MxAccessGateway` RPCs.
|
||||
|
||||
This is a metadata layer: it never reads or writes runtime tag values, never
|
||||
@@ -19,20 +19,22 @@ ArchestrA IDE renders the deployment tree. Surfacing that data over gRPC lets
|
||||
remote clients build a navigable address space without any coupling to the
|
||||
COM layer or the host platform.
|
||||
|
||||
The query bodies are kept byte-for-byte identical to the equivalent OPC UA
|
||||
server in the OtOpcUa project so the two consumers see the same row sets.
|
||||
`HierarchySql` is the object-hierarchy query originally ported from the
|
||||
equivalent OPC UA server in the OtOpcUa project. `AttributesSql` has since
|
||||
diverged from OtOpcUa — see [Built-in vs configured attributes](#built-in-vs-configured-attributes)
|
||||
— and is no longer kept in sync with it.
|
||||
|
||||
## RPC Surface
|
||||
|
||||
The service is defined in
|
||||
`src/MxGateway.Contracts/Protos/galaxy_repository.proto` under package
|
||||
`src/ZB.MOM.WW.MxGateway.Contracts/Protos/galaxy_repository.proto` under package
|
||||
`galaxy_repository.v1`.
|
||||
|
||||
| RPC | Purpose |
|
||||
|-----|---------|
|
||||
| `TestConnection` | Connectivity probe. Returns `{ ok: bool }` after a `SELECT 1`. Does not throw on SQL failure — returns `ok = false`. Always hits SQL directly so it remains a true health check. |
|
||||
| `GetLastDeployTime` | Returns the cached `galaxy.time_of_last_deploy`. Served from the shared hierarchy cache; refreshed in the background. |
|
||||
| `DiscoverHierarchy` | Returns one page of the deployed hierarchy plus each returned object's dynamic attributes. **Served from cache** — see [Hierarchy Cache](#hierarchy-cache). |
|
||||
| `DiscoverHierarchy` | Returns one page of the deployed hierarchy plus each returned object's attributes (configured and built-in — see [Built-in vs configured attributes](#built-in-vs-configured-attributes)). **Served from cache** — see [Hierarchy Cache](#hierarchy-cache). |
|
||||
| `WatchDeployEvents` | **Server-streaming.** The server emits the current state immediately on subscribe (so clients can bootstrap without waiting), then emits one event per detected deploy change. See [Deploy Notifications](#deploy-notifications). |
|
||||
|
||||
`DiscoverHierarchy` is a paged unary RPC. The raw request accepts `page_size`
|
||||
@@ -53,7 +55,7 @@ reports the post-filter count.
|
||||
## Hierarchy Cache
|
||||
|
||||
The gateway holds a single shared `IGalaxyHierarchyCache`
|
||||
(`src/MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs`) — every
|
||||
(`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs`) — every
|
||||
`DiscoverHierarchy` and `GetLastDeployTime` request reads from this cache
|
||||
rather than hitting SQL. Many clients can browse concurrently with at most
|
||||
one SQL query in flight.
|
||||
@@ -87,10 +89,40 @@ load to complete before returning. If the first load fails or times out,
|
||||
the client gets `Unavailable` with a short reason. Once any load completes
|
||||
(success or failure), this wait is skipped on subsequent calls.
|
||||
|
||||
### On-disk snapshot
|
||||
|
||||
The gateway may lose connectivity to the Galaxy database — and the database is
|
||||
often unreachable right when the gateway itself restarts. To keep browse
|
||||
working across that gap, the cache persists its dataset to disk:
|
||||
|
||||
- After every successful **heavy** refresh (a deploy change), the raw
|
||||
hierarchy and attribute rowsets are written to
|
||||
`MxGateway:Galaxy:SnapshotCachePath`
|
||||
(default `C:\ProgramData\MxGateway\galaxy-snapshot.json`). The write is
|
||||
atomic — a temp file plus rename — so a crash mid-write cannot corrupt the
|
||||
snapshot. Cheap no-change ticks write nothing; the file is already current.
|
||||
- On the **first** refresh after startup, before any SQL runs, the cache
|
||||
reloads that file. The restored data is served with `Stale` status —
|
||||
it is last-known data, not live — so clients can browse immediately even
|
||||
when the Galaxy database is unreachable.
|
||||
- The first live query then reconciles: if it observes the **same**
|
||||
`time_of_last_deploy` the snapshot was saved at, the entry is promoted to
|
||||
`Healthy` with no heavy re-query (the snapshot is provably current); if it
|
||||
observes a newer deploy, the heavy queries run and replace the snapshot; if
|
||||
the database is still unreachable, the entry stays `Stale`.
|
||||
|
||||
`is_alarm` / `is_historized` filters, paging, and the dashboard summary all
|
||||
work against a restored snapshot exactly as against a live pull — the restore
|
||||
path runs the same materialization. Persistence is disabled by setting
|
||||
`MxGateway:Galaxy:PersistSnapshot` to `false`; the snapshot file is then
|
||||
neither written nor read, and a cold start with an unreachable database comes
|
||||
up `Unavailable` as before. The on-disk file is a cache, not a system of
|
||||
record: deleting it only forces the next cold start to wait for live SQL.
|
||||
|
||||
## Deploy Notifications
|
||||
|
||||
`WatchDeployEvents` is a server-streaming RPC backed by
|
||||
`IGalaxyDeployNotifier` (`src/MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs`).
|
||||
`IGalaxyDeployNotifier` (`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs`).
|
||||
The notifier maintains a private bounded channel per subscriber so a slow
|
||||
client cannot back-pressure other subscribers or the publisher.
|
||||
|
||||
@@ -176,6 +208,43 @@ message DiscoverHierarchyReply {
|
||||
}
|
||||
```
|
||||
|
||||
### Built-in vs configured attributes
|
||||
|
||||
Each `GalaxyObject` carries two kinds of attribute, both surfaced the same way
|
||||
in the `attributes` list:
|
||||
|
||||
- **Configured (dynamic) attributes** — attributes added in the ArchestrA IDE
|
||||
attribute editor. Stored in the Galaxy `dynamic_attribute` table.
|
||||
- **Built-in attributes** — attributes every object inherits from its
|
||||
primitives: the object framework, the engine/platform primitives, and the
|
||||
per-attribute extensions (Alarm, History, Boolean, …). Stored in
|
||||
`attribute_definition` and reached through `primitive_instance`.
|
||||
|
||||
Built-in attributes are why an `AppEngine` or `WinPlatform` object reports its
|
||||
`Engine.*` and `Alarm*` attributes, and why an alarmed attribute such as
|
||||
`TestAlarm001` reports its extension leaves `TestAlarm001.Acked`,
|
||||
`TestAlarm001.AckMsg`, `TestAlarm001.ActiveAlarmState`, and so on. An earlier
|
||||
version of the browse query returned only configured attributes, so those
|
||||
objects came back empty or partial; including built-ins makes the browse
|
||||
surface match what System Platform's own Object Viewer shows. Expect roughly
|
||||
seven times as many attributes as configured-only — the dashboard attribute
|
||||
count reflects this.
|
||||
|
||||
Two rules govern the built-in rows:
|
||||
|
||||
- **No category filter.** `attribute_definition` uses a different
|
||||
`mx_attribute_category` numbering than `dynamic_attribute`, so only the
|
||||
`_`-prefixed-name and `.Description` exclusions apply to built-ins. (The
|
||||
configured-attribute category allow-list is unchanged.)
|
||||
- **`is_historized` / `is_alarm` are always `false` for built-in rows.** Those
|
||||
flags identify a configured attribute that *anchors* a history or alarm
|
||||
extension (e.g. `TestAlarm001`), not the extension's machinery leaves
|
||||
(`TestAlarm001.Acked`). `alarm_bearing_only` and `historized_only` therefore
|
||||
still select the anchor attributes, not their built-in children.
|
||||
|
||||
When a configured attribute and a built-in attribute resolve to the same
|
||||
reference, the configured attribute wins.
|
||||
|
||||
### Contained name vs tag name
|
||||
|
||||
Galaxy objects carry two names. `tag_name` is globally unique and is what
|
||||
@@ -201,7 +270,7 @@ fields cannot express null. Use it to distinguish "no dimension reported" from
|
||||
|
||||
```text
|
||||
gRPC client(s)
|
||||
-> GalaxyRepositoryGrpcService (src/MxGateway.Server/Grpc/)
|
||||
-> GalaxyRepositoryGrpcService (src/ZB.MOM.WW.MxGateway.Server/Grpc/)
|
||||
DiscoverHierarchy, GetLastDeployTime -> IGalaxyHierarchyCache.Current
|
||||
WatchDeployEvents -> IGalaxyDeployNotifier
|
||||
TestConnection -> GalaxyRepository (direct SQL)
|
||||
@@ -218,29 +287,30 @@ GalaxyHierarchyRefreshService (BackgroundService)
|
||||
|
||||
Component breakdown:
|
||||
|
||||
- `GalaxyRepository` (`src/MxGateway.Server/Galaxy/GalaxyRepository.cs`) holds
|
||||
the SQL. Its constants `HierarchySql` and `AttributesSql` are copied verbatim
|
||||
from the OtOpcUa project; do not edit them in isolation here. The two
|
||||
queries walk template-derivation and package-derivation chains via
|
||||
recursive CTEs and pick the most-derived attribute override per object.
|
||||
- `GalaxyRepository` (`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyRepository.cs`) holds
|
||||
the SQL. Both `HierarchySql` and `AttributesSql` walk template-derivation and
|
||||
package-derivation chains via recursive CTEs and pick the most-derived
|
||||
override per object. `HierarchySql` still matches the OtOpcUa original;
|
||||
`AttributesSql` does not — it additionally enumerates built-in primitive
|
||||
attributes (see [Built-in vs configured attributes](#built-in-vs-configured-attributes)).
|
||||
- `GalaxyHierarchyCache`
|
||||
(`src/MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs`) holds the most
|
||||
(`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs`) holds the most
|
||||
recent immutable `GalaxyHierarchyCacheEntry` (materialized objects +
|
||||
precomputed dashboard summary + counts + status). All gRPC clients share the
|
||||
same entry.
|
||||
- `GalaxyHierarchyRefreshService`
|
||||
(`src/MxGateway.Server/Galaxy/GalaxyHierarchyRefreshService.cs`) is a
|
||||
(`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyHierarchyRefreshService.cs`) is a
|
||||
hosted `BackgroundService` that drives `RefreshAsync` on the configured
|
||||
interval, with deploy-time gating to avoid unnecessary heavy queries.
|
||||
- `GalaxyDeployNotifier`
|
||||
(`src/MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs`) is a thin
|
||||
(`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs`) is a thin
|
||||
per-subscriber-channel fan-out for streaming clients.
|
||||
- `GalaxyProtoMapper`
|
||||
(`src/MxGateway.Server/Grpc/GalaxyProtoMapper.cs`) converts row models to
|
||||
(`src/ZB.MOM.WW.MxGateway.Server/Grpc/GalaxyProtoMapper.cs`) converts row models to
|
||||
proto messages. Used by the cache during refresh to materialize the reply
|
||||
once.
|
||||
- `GalaxyRepositoryGrpcService`
|
||||
(`src/MxGateway.Server/Grpc/GalaxyRepositoryGrpcService.cs`) implements
|
||||
(`src/ZB.MOM.WW.MxGateway.Server/Grpc/GalaxyRepositoryGrpcService.cs`) implements
|
||||
the four RPCs.
|
||||
|
||||
## Configuration
|
||||
@@ -251,6 +321,8 @@ Bound to `MxGateway:Galaxy` via `GalaxyRepositoryOptions`.
|
||||
|--------|---------|-------------|
|
||||
| `MxGateway:Galaxy:ConnectionString` | `Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;` | SQL Server connection string for the Galaxy Repository. Integrated Security against `localhost` is the dev default; production deployments should override this through the standard double-underscore environment variable form, e.g. `MxGateway__Galaxy__ConnectionString`. |
|
||||
| `MxGateway:Galaxy:CommandTimeoutSeconds` | `60` | Per-command SQL timeout. Applies to all three RPCs. |
|
||||
| `MxGateway:Galaxy:PersistSnapshot` | `true` | Persists each successful browse dataset to disk and reloads it at startup. See [On-disk snapshot](#on-disk-snapshot). |
|
||||
| `MxGateway:Galaxy:SnapshotCachePath` | `C:\ProgramData\MxGateway\galaxy-snapshot.json` | File path for the persisted browse snapshot. Ignored when `PersistSnapshot` is `false`. |
|
||||
|
||||
The connection string is not treated as a secret in dev (`Integrated
|
||||
Security`), but production deployments that use SQL authentication should set
|
||||
@@ -306,7 +378,7 @@ that as a yellow or red status badge plus the truncated error.
|
||||
- Failures to reach the Galaxy database surface as `Unavailable`. Detailed
|
||||
SQL exceptions are logged at `Warning` and never returned to clients.
|
||||
- Integration tests live in
|
||||
`src/MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs`. Set
|
||||
`src/ZB.MOM.WW.MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs`. Set
|
||||
`MXGATEWAY_RUN_LIVE_GALAXY_TESTS=1` (and optionally
|
||||
`MXGATEWAY_LIVE_GALAXY_CONN`) to run them; otherwise they skip.
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ paths, timeouts, queue sizes, enum values, or protocol values are invalid.
|
||||
"RunMigrationsOnStartup": true
|
||||
},
|
||||
"Worker": {
|
||||
"ExecutablePath": "src\\MxGateway.Worker\\bin\\x86\\Release\\MxGateway.Worker.exe",
|
||||
"ExecutablePath": "src\\ZB.MOM.WW.MxGateway.Worker\\bin\\x86\\Release\\ZB.MOM.WW.MxGateway.Worker.exe",
|
||||
"WorkingDirectory": null,
|
||||
"RequiredArchitecture": "X86",
|
||||
"StartupTimeoutSeconds": 30,
|
||||
@@ -60,7 +60,15 @@ paths, timeouts, queue sizes, enum values, or protocol values are invalid.
|
||||
"Galaxy": {
|
||||
"ConnectionString": "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;",
|
||||
"CommandTimeoutSeconds": 60,
|
||||
"DashboardRefreshIntervalSeconds": 30
|
||||
"DashboardRefreshIntervalSeconds": 30,
|
||||
"PersistSnapshot": true,
|
||||
"SnapshotCachePath": "C:\\ProgramData\\MxGateway\\galaxy-snapshot.json"
|
||||
},
|
||||
"Alarms": {
|
||||
"Enabled": false,
|
||||
"SubscriptionExpression": "",
|
||||
"DefaultArea": "",
|
||||
"ReconcileIntervalSeconds": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +94,7 @@ When `Mode` is `ApiKey`, `SqlitePath` and `PepperSecretName` must be present.
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `MxGateway:Worker:ExecutablePath` | `src\MxGateway.Worker\bin\x86\Release\MxGateway.Worker.exe` | Path to the x86 worker executable launched for each gateway session. The path must be valid and point to a `.exe` file. |
|
||||
| `MxGateway:Worker:ExecutablePath` | `src\ZB.MOM.WW.MxGateway.Worker\bin\x86\Release\ZB.MOM.WW.MxGateway.Worker.exe` | Path to the x86 worker executable launched for each gateway session. The path must be valid and point to a `.exe` file. |
|
||||
| `MxGateway:Worker:WorkingDirectory` | `null` | Optional working directory for the worker process. When set, it must be a valid filesystem path. |
|
||||
| `MxGateway:Worker:RequiredArchitecture` | `X86` | Required Portable Executable architecture for the worker. Supported values are `X86` and `X64`; MXAccess parity uses `X86`. |
|
||||
| `MxGateway:Worker:StartupTimeoutSeconds` | `30` | Total startup budget for process launch, startup probe, pipe connect, handshake, and worker readiness. |
|
||||
@@ -164,10 +172,24 @@ at startup.
|
||||
| `MxGateway:Galaxy:ConnectionString` | `Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;` | SQL Server connection string for the Galaxy Repository (`ZB`) used by the `GalaxyRepository` browse RPCs. Override in production via `MxGateway__Galaxy__ConnectionString`. |
|
||||
| `MxGateway:Galaxy:CommandTimeoutSeconds` | `60` | Per-command SQL timeout for all Galaxy browse RPCs. |
|
||||
| `MxGateway:Galaxy:DashboardRefreshIntervalSeconds` | `30` | Interval between background refreshes of the dashboard Galaxy summary cache. SQL is hit at most once per interval regardless of dashboard render rate. |
|
||||
| `MxGateway:Galaxy:PersistSnapshot` | `true` | Persists the latest successful Galaxy browse dataset to disk. When `true`, the cache reloads that snapshot at startup so clients can still browse last-known data while the Galaxy database is unreachable. The restored data is served with `Stale` status until a live query confirms it. |
|
||||
| `MxGateway:Galaxy:SnapshotCachePath` | `C:\ProgramData\MxGateway\galaxy-snapshot.json` | File path for the persisted Galaxy browse snapshot. Ignored when `PersistSnapshot` is `false`. The snapshot is written atomically (temp file plus rename). |
|
||||
|
||||
See [Galaxy Repository Browse](./GalaxyRepository.md) for the RPC surface and
|
||||
behavior.
|
||||
|
||||
## Alarm Options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `MxGateway:Alarms:Enabled` | `false` | Gates the gateway's always-on central alarm monitor. When `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 `StreamAlarms` RPC — no client opens its own session to see alarms. |
|
||||
| `MxGateway:Alarms:SubscriptionExpression` | _(empty)_ | AVEVA alarm-subscription expression the monitor subscribes on startup, in canonical `\\<machine>\Galaxy!<area>` form. The literal `Galaxy` provider is correct regardless of the Galaxy database name. When empty and `Enabled` is `true`, the gateway falls back to `\\<MachineName>\Galaxy!<DefaultArea>` if `DefaultArea` is set. |
|
||||
| `MxGateway:Alarms:DefaultArea` | _(empty)_ | Area name used to compose a default subscription when `SubscriptionExpression` is empty. If both are empty while `Enabled` is `true`, the monitor faults with a configuration diagnostic. |
|
||||
| `MxGateway:Alarms:ReconcileIntervalSeconds` | `30` | How often the monitor reconciles its in-process alarm cache against the worker's authoritative active-alarm snapshot, catching transitions the live poll-and-diff feed missed. Floored at 5 seconds. |
|
||||
|
||||
The alarm monitor is independent of client sessions: `AcknowledgeAlarm` and
|
||||
`StreamAlarms` are session-less RPCs served by the monitor.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
|
||||
|
||||
@@ -34,7 +34,7 @@ SignalR circuit. Bootstrap is sufficient for a basic dashboard.
|
||||
|
||||
## Hosting Model
|
||||
|
||||
The dashboard is hosted by `MxGateway.Server` alongside the gRPC API. When
|
||||
The dashboard is hosted by `ZB.MOM.WW.MxGateway.Server` alongside the gRPC API. When
|
||||
`MxGateway:Dashboard:Enabled` is `true`, `MapGatewayDashboard()` maps the
|
||||
configured `Dashboard:PathBase` to the Blazor Server app and maps the login,
|
||||
logout, and access-denied HTTP endpoints beside it. When dashboard hosting is
|
||||
@@ -49,6 +49,7 @@ Endpoint layout:
|
||||
/dashboard/workers
|
||||
/dashboard/events
|
||||
/dashboard/galaxy
|
||||
/dashboard/apikeys
|
||||
/dashboard/settings
|
||||
/dashboard/_blazor
|
||||
```
|
||||
@@ -68,7 +69,7 @@ dashboard as the default web page. Otherwise leave gRPC/API hosting unaffected.
|
||||
## High-Level Components
|
||||
|
||||
```text
|
||||
MxGateway.Server
|
||||
ZB.MOM.WW.MxGateway.Server
|
||||
Dashboard/
|
||||
Components/
|
||||
App.razor
|
||||
@@ -83,6 +84,7 @@ MxGateway.Server
|
||||
SessionDetailsPage.razor
|
||||
WorkersPage.razor
|
||||
EventsPage.razor
|
||||
ApiKeysPage.razor
|
||||
SettingsPage.razor
|
||||
Shared/
|
||||
MetricCard.razor
|
||||
@@ -91,6 +93,9 @@ MxGateway.Server
|
||||
DashboardSnapshotService.cs
|
||||
DashboardAuthorizationHandler.cs
|
||||
DashboardAuthenticator.cs
|
||||
DashboardApiKeyAuthorization.cs
|
||||
DashboardApiKeyManagementService.cs
|
||||
DashboardApiKeySummary.cs
|
||||
DashboardSnapshot.cs
|
||||
DashboardSessionSummary.cs
|
||||
DashboardWorkerSummary.cs
|
||||
@@ -249,6 +254,99 @@ Show aggregate event diagnostics:
|
||||
Do not display full tag values by default. If value display is later added, make
|
||||
it opt-in and redacted.
|
||||
|
||||
### Browse page
|
||||
|
||||
`/dashboard/browse` lets an operator explore the Galaxy tag hierarchy and watch
|
||||
live values. The tree is built in-process by `DashboardBrowseTreeBuilder` from
|
||||
`IGalaxyHierarchyCache.Current` — the same cache the Galaxy page reads — so a
|
||||
render costs no gRPC call and no SQL round-trip. Each node shows its child
|
||||
objects and, when expanded, its attributes with attribute name, data type
|
||||
(including array dimension), and the alarm / historized flags. Galaxy SQL
|
||||
carries no attribute description, so none is shown. A filter box switches the
|
||||
tree to a flat list of matching attributes.
|
||||
|
||||
Right-clicking an attribute (or double-clicking it) adds it to the subscription
|
||||
panel. The panel shows each subscribed tag's live value, MXAccess data type,
|
||||
quality and source timestamp, refreshed every two seconds. The subscription
|
||||
panel is the explicit opt-in tag-value surface: it always shows values
|
||||
regardless of `Dashboard:ShowTagValues`, which continues to govern only the
|
||||
diagnostic session/worker views.
|
||||
|
||||
### Alarms page
|
||||
|
||||
`/dashboard/alarms` lists the alarms the gateway's central alarm monitor
|
||||
currently holds as Active or ActiveAcked, refreshed every three seconds. It
|
||||
defaults to showing unacknowledged `Active` alarms; filters add acknowledged
|
||||
alarms and narrow by area, severity range, and a reference/source/description
|
||||
text search. Cleared alarms are not retained — the gateway holds no
|
||||
alarm-history store, so the page reflects only the live active set. The page is
|
||||
read-only; it does not acknowledge alarms. If `MxGateway:Alarms:Enabled` is
|
||||
false the central monitor never starts, and the page says so instead of showing
|
||||
an empty list with no explanation.
|
||||
|
||||
### Live data source
|
||||
|
||||
Both the Browse subscription panel and the Alarms page read live MXAccess data
|
||||
through `IDashboardLiveDataService` (`DashboardLiveDataService`). For tag data
|
||||
it owns one shared gateway session for the whole dashboard, opened lazily on
|
||||
first use via `ISessionManager` and re-opened transparently when it faults or
|
||||
its lease expires. One session means one worker process backs every dashboard
|
||||
circuit; all access is serialised so the worker sees one in-flight command at a
|
||||
time. Tag reads go through `GatewaySession.SubscribeBulkAsync` / `ReadBulkAsync`.
|
||||
|
||||
The Alarms page does **not** use the dashboard session: alarm data comes from
|
||||
the gateway's always-on central monitor. `QueryAlarmsAsync` reads
|
||||
`IGatewayAlarmService.CurrentAlarms` — the monitor's in-process cache — so the
|
||||
dashboard sees the same active-alarm set as every `StreamAlarms` client, with
|
||||
no per-dashboard alarm subscription. When `MxGateway:Alarms:Enabled` is false
|
||||
the monitor never starts and the cache stays empty.
|
||||
|
||||
### API keys page
|
||||
|
||||
`/dashboard/apikeys` lists the gateway's API keys and, for authorized
|
||||
operators, manages them. It reads key metadata through the same
|
||||
`IApiKeyAdminStore` the `apikey` CLI uses, so the dashboard and the CLI act
|
||||
on one source of truth.
|
||||
|
||||
The table shows one row per key:
|
||||
|
||||
- key id,
|
||||
- status (`Active` or `Revoked`),
|
||||
- display name,
|
||||
- scopes,
|
||||
- constraints (rendered as `unconstrained` when none are set),
|
||||
- created timestamp,
|
||||
- last-used timestamp.
|
||||
|
||||
Key secrets are never listed. Only the peppered hash is stored, and the page
|
||||
never reconstructs a key. See [Authorization](./Authorization.md#constraint-enforcement)
|
||||
for what each constraint means and how it is enforced on the gRPC path.
|
||||
|
||||
#### Management actions
|
||||
|
||||
Create, Rotate, and Revoke controls render only when the signed-in user is
|
||||
authorized. `DashboardApiKeyAuthorization.CanManage` requires an authenticated
|
||||
principal that is a member of the LDAP `MxGateway:Ldap:RequiredGroup` — the
|
||||
same group the dashboard login enforces. An anonymous localhost viewer can read
|
||||
the table but sees no action controls.
|
||||
|
||||
- **Create** opens a dialog for the key id, display name, scope checkboxes
|
||||
(the `GatewayScopes` catalog), and the optional constraint fields: read and
|
||||
write subtrees, read and write tag globs, browse subtrees, max write
|
||||
classification, and the read-alarm-only / read-historized-only flags.
|
||||
- **Rotate** issues a new secret for an existing key id and invalidates the
|
||||
old one.
|
||||
- **Revoke** marks a key revoked; a revoked key cannot be un-revoked.
|
||||
|
||||
Create and Rotate return the assembled `mxgw_<keyId>_<secret>` token **once**,
|
||||
in a one-time banner. It is never shown again, so the operator must copy it
|
||||
immediately. This mirrors the `apikey create-key` / `rotate-key` CLI.
|
||||
|
||||
Every management action appends an `api_key_audit` entry
|
||||
(`dashboard-create-key`, `dashboard-rotate-key`, `dashboard-revoke-key`) with
|
||||
the key id and the caller's remote address. Secrets and pepper values are never
|
||||
logged.
|
||||
|
||||
### Settings page
|
||||
|
||||
Show read-only effective configuration:
|
||||
@@ -330,8 +428,8 @@ Suggested configuration:
|
||||
## Styling
|
||||
|
||||
The dashboard serves Bootstrap 5.3.3 assets from
|
||||
`src/MxGateway.Server/wwwroot/lib/bootstrap/` and local layout/status styling
|
||||
from `src/MxGateway.Server/wwwroot/css/dashboard.css`.
|
||||
`src/ZB.MOM.WW.MxGateway.Server/wwwroot/lib/bootstrap/` and local layout/status styling
|
||||
from `src/ZB.MOM.WW.MxGateway.Server/wwwroot/css/dashboard.css`.
|
||||
|
||||
Recommended visual language:
|
||||
|
||||
@@ -377,7 +475,7 @@ Integration tests should verify:
|
||||
|
||||
The first dashboard slice implements:
|
||||
|
||||
1. Blazor Server hosting in `MxGateway.Server`.
|
||||
1. Blazor Server hosting in `ZB.MOM.WW.MxGateway.Server`.
|
||||
2. local Bootstrap static assets.
|
||||
3. dashboard configuration binding.
|
||||
4. dashboard auth using API key login and HTTP-only cookie.
|
||||
|
||||
@@ -59,7 +59,7 @@ Those belong to the worker.
|
||||
## High-Level Components
|
||||
|
||||
```text
|
||||
MxGateway.Server
|
||||
ZB.MOM.WW.MxGateway.Server
|
||||
Program / Host
|
||||
Configuration
|
||||
Grpc
|
||||
@@ -677,7 +677,7 @@ development only.
|
||||
Dashboard authentication reuses the API-key verifier and scope model. The
|
||||
dashboard login endpoint accepts the key in a form post, checks `admin` scope
|
||||
when `Dashboard:RequireAdminScope` is enabled, and signs in with the
|
||||
`MxGateway.Dashboard` cookie scheme. The cookie is HTTP-only, secure, strict
|
||||
`ZB.MOM.WW.MxGateway.Dashboard` cookie scheme. The cookie is HTTP-only, secure, strict
|
||||
SameSite, and scoped with the `__Host-MxGatewayDashboard` name. Logout clears
|
||||
that cookie. Login and logout posts use anti-forgery validation, and dashboard
|
||||
API keys are not accepted in query strings. `Dashboard:AllowAnonymousLocalhost`
|
||||
@@ -703,15 +703,15 @@ gRPC admin API. It should initialize the auth database, create keys, list keys
|
||||
without secrets, revoke keys, rotate keys, and print raw secrets only once at
|
||||
creation.
|
||||
|
||||
`MxGateway.Server` exposes local API-key administration as an `apikey`
|
||||
`ZB.MOM.WW.MxGateway.Server` exposes local API-key administration as an `apikey`
|
||||
subcommand before the web host starts:
|
||||
|
||||
```bash
|
||||
MxGateway.Server apikey init-db --sqlite-path C:\ProgramData\MxGateway\gateway-auth.db
|
||||
MxGateway.Server apikey create-key --key-id operator01 --display-name Operator --scopes session:open,events:read
|
||||
MxGateway.Server apikey list-keys --json
|
||||
MxGateway.Server apikey revoke-key --key-id operator01
|
||||
MxGateway.Server apikey rotate-key --key-id operator01 --json
|
||||
ZB.MOM.WW.MxGateway.Server apikey init-db --sqlite-path C:\ProgramData\MxGateway\gateway-auth.db
|
||||
ZB.MOM.WW.MxGateway.Server apikey create-key --key-id operator01 --display-name Operator --scopes session:open,events:read
|
||||
ZB.MOM.WW.MxGateway.Server apikey list-keys --json
|
||||
ZB.MOM.WW.MxGateway.Server apikey revoke-key --key-id operator01
|
||||
ZB.MOM.WW.MxGateway.Server apikey rotate-key --key-id operator01 --json
|
||||
```
|
||||
|
||||
The subcommands accept `--sqlite-path`, `--pepper`, and `--json`. `--pepper`
|
||||
@@ -846,7 +846,7 @@ Suggested configuration shape:
|
||||
"RunMigrationsOnStartup": true
|
||||
},
|
||||
"Worker": {
|
||||
"ExecutablePath": "src/MxGateway.Worker/bin/x86/Release/MxGateway.Worker.exe",
|
||||
"ExecutablePath": "src/ZB.MOM.WW.MxGateway.Worker/bin/x86/Release/ZB.MOM.WW.MxGateway.Worker.exe",
|
||||
"WorkingDirectory": null,
|
||||
"RequiredArchitecture": "X86",
|
||||
"StartupTimeoutSeconds": 30,
|
||||
@@ -887,7 +887,7 @@ Suggested configuration shape:
|
||||
|
||||
Do not scatter connection or path constants through implementation code.
|
||||
|
||||
`MxGateway.Server` binds this section to `GatewayOptions` at startup and
|
||||
`ZB.MOM.WW.MxGateway.Server` binds this section to `GatewayOptions` at startup and
|
||||
registers validation with `ValidateOnStart()`. Startup fails before the gateway
|
||||
begins serving traffic when required authentication settings are missing,
|
||||
timeouts or queue sizes are not positive, dashboard settings are malformed, or
|
||||
|
||||
+260
-25
@@ -7,13 +7,13 @@ provider state.
|
||||
|
||||
## Fake Worker Harness
|
||||
|
||||
`FakeWorkerHarness` in `src/MxGateway.Tests/Gateway/Workers/Fakes/` provides an
|
||||
`FakeWorkerHarness` in `src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/Fakes/` provides an
|
||||
in-process worker side for named-pipe IPC tests. It uses the same
|
||||
`WorkerFrameReader`, `WorkerFrameWriter`, and `WorkerEnvelope` contract as the
|
||||
gateway so tests exercise real frame validation and worker-client state changes.
|
||||
|
||||
Use the harness when a gateway or session test needs worker behavior without
|
||||
starting `MxGateway.Worker.exe` or loading MXAccess COM. The harness scripts:
|
||||
starting `ZB.MOM.WW.MxGateway.Worker.exe` or loading MXAccess COM. The harness scripts:
|
||||
|
||||
- `WorkerHello` and `WorkerReady` startup,
|
||||
- command replies with matching correlation ids,
|
||||
@@ -37,43 +37,196 @@ event, and `CloseSession` without loading MXAccess COM.
|
||||
|
||||
## Live MXAccess Smoke
|
||||
|
||||
`WorkerLiveMxAccessSmokeTests` in `src/MxGateway.IntegrationTests/` composes the
|
||||
`WorkerLiveMxAccessSmokeTests` in `src/ZB.MOM.WW.MxGateway.IntegrationTests/` composes the
|
||||
real gRPC service, `SessionManager`, `SessionWorkerClientFactory`,
|
||||
`WorkerClient`, `WorkerProcessLauncher`, and `MxGateway.Worker.exe`. It is
|
||||
`WorkerClient`, `WorkerProcessLauncher`, and `ZB.MOM.WW.MxGateway.Worker.exe`. It is
|
||||
skipped unless `MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1` is set because it creates
|
||||
the installed MXAccess COM object and depends on live provider state.
|
||||
|
||||
The live smoke opens a gateway session, launches the x86 worker, runs
|
||||
`Register`, `AddItem`, and `Advise`, waits a bounded time for one
|
||||
`OnDataChange`, and closes the session in a `finally` block so the worker gets a
|
||||
graceful shutdown request even when a command or event assertion fails.
|
||||
`Register`, `AddItem`, and `Advise`, waits a bounded time for the first
|
||||
`OnDataChange` event (skipping any earlier bootstrap/registration-state event),
|
||||
and closes the session in a `finally` block so the worker gets a graceful
|
||||
shutdown request even when a command or event assertion fails. Cleanup failures
|
||||
in that `finally` block are logged rather than thrown, so a real assertion
|
||||
failure is never masked by a shutdown timeout.
|
||||
|
||||
`WorkerLiveMxAccessSmokeTests` additionally covers five MXAccess parity paths the
|
||||
fake-worker tests cannot validate:
|
||||
|
||||
- a `Write` round-trip against an advised item, asserting both that the reply is
|
||||
`Ok` / `MxCommandKind.Write` *and* that the worker emits a matching
|
||||
`OnWriteComplete` event for the targeted (server, item) handle pair — the
|
||||
same round-trip proof used by `scripts/run-client-e2e-tests.ps1`,
|
||||
- an `AddItem` against an invalid server handle, asserting the MXAccess failure
|
||||
surfaces in the command reply without faulting the gateway transport,
|
||||
- the `UnAdvise` → `RemoveItem` → `Unregister` teardown chain, asserting each
|
||||
step replies `Ok` with the matching `MxCommandKind`, that no further
|
||||
`OnDataChange` events arrive for the un-advised pair, and that a second
|
||||
`RemoveItem` against the freed handle relays a non-`Ok` MXAccess failure,
|
||||
- a `WriteSecured` round-trip after `AuthenticateUser`, asserting the reply
|
||||
carries `MxCommandKind.WriteSecured` and the credential password never
|
||||
appears in the diagnostic message (parity for both the secured-write
|
||||
ordering rule and the "do not log secrets" contract), and
|
||||
- an abnormal worker exit (the worker process is killed mid-session) where the
|
||||
gateway must transition the session to `SessionState.Faulted` with a
|
||||
non-empty fault description carrying a known worker-client classification
|
||||
(pipe disconnected / worker faulted / end-of-stream / heartbeat expired).
|
||||
|
||||
All six tests are gated by the same `MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1`
|
||||
opt-in variable.
|
||||
|
||||
Build the worker before running the smoke:
|
||||
|
||||
```bash
|
||||
dotnet build src/MxGateway.Worker/MxGateway.Worker.csproj -p:Platform=x86
|
||||
dotnet build src/ZB.MOM.WW.MxGateway.Worker/ZB.MOM.WW.MxGateway.Worker.csproj -p:Platform=x86
|
||||
```
|
||||
|
||||
Run the smoke explicitly:
|
||||
|
||||
```bash
|
||||
$env:MXGATEWAY_RUN_LIVE_MXACCESS_TESTS = "1"
|
||||
dotnet test src/MxGateway.IntegrationTests/MxGateway.IntegrationTests.csproj --filter FullyQualifiedName~WorkerLiveMxAccessSmokeTests
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.IntegrationTests/ZB.MOM.WW.MxGateway.IntegrationTests.csproj --filter FullyQualifiedName~WorkerLiveMxAccessSmokeTests
|
||||
```
|
||||
|
||||
Optional live smoke variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MXGATEWAY_LIVE_MXACCESS_WORKER_EXE` | First existing `MxGateway.Worker.exe` under `src/MxGateway.Worker/bin/...` | Worker executable path. Set this when running against a packaged worker or a non-default build output. |
|
||||
| `MXGATEWAY_LIVE_MXACCESS_WORKER_EXE` | First existing `ZB.MOM.WW.MxGateway.Worker.exe` under `src/ZB.MOM.WW.MxGateway.Worker/bin/...` | Worker executable path. Set this when running against a packaged worker or a non-default build output. |
|
||||
| `MXGATEWAY_LIVE_MXACCESS_ITEM` | `TestChildObject.TestInt` | MXAccess item reference used by `AddItem`. |
|
||||
| `MXGATEWAY_LIVE_MXACCESS_CLIENT_NAME` | `MxGateway.IntegrationTests` | Client name passed to `Register`. |
|
||||
| `MXGATEWAY_LIVE_MXACCESS_EVENT_TIMEOUT_SECONDS` | `15` | Maximum wait for the first `OnDataChange`. |
|
||||
| `MXGATEWAY_LIVE_MXACCESS_CLIENT_NAME` | `ZB.MOM.WW.MxGateway.IntegrationTests` | Client name passed to `Register`. |
|
||||
| `MXGATEWAY_LIVE_MXACCESS_EVENT_TIMEOUT_SECONDS` | `15` | Maximum wait for the first `OnDataChange` (also used for the `OnWriteComplete` round-trip and the abnormal-exit fault transition). |
|
||||
| `MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_USER` | `admin` | ArchestrA user name passed to `AuthenticateUser` before the `WriteSecured` parity step. |
|
||||
| `MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_PASSWORD` | `admin123` | Password paired with the user above. Never logged; the test asserts the value does not appear in the WriteSecured diagnostic message. |
|
||||
|
||||
The test output includes session id, worker process id, command status,
|
||||
HRESULT/status diagnostics, event sequence and handles, close status, and worker
|
||||
stdout/stderr lines emitted during the run.
|
||||
|
||||
## Dev-rig Probes
|
||||
|
||||
`src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/` partitions runtime probes from the regular
|
||||
Worker.Tests regression suite. The folder is its own
|
||||
`ZB.MOM.WW.MxGateway.Worker.Tests.Probes` namespace so a discovery filter (e.g. `dotnet
|
||||
test --filter FullyQualifiedName~ZB.MOM.WW.MxGateway.Worker.Tests.Probes`) can target or
|
||||
exclude them without enumerating individual class names. The probes are
|
||||
`[Fact(Skip = "...")]` by default and exist to characterize live AVEVA
|
||||
behavior on the dev rig, not to gate CI — flip `Skip = null` on the dev box
|
||||
with installed MXAccess + a running Galaxy provider when running them:
|
||||
|
||||
- `AlarmsLiveSmokeTests` — end-to-end smoke for the alarms-over-gateway
|
||||
pipeline (`WnWrapAlarmConsumer` + `AlarmDispatcher` +
|
||||
`MxAccessAlarmEventSink`) against `\\<machine>\Galaxy!DEV` with the dev rig's
|
||||
10-second flip script writing `TestMachine_001.TestAlarm001`.
|
||||
- `AlarmClientWmProbeTests` — registers as an `AlarmClient` consumer on a real
|
||||
hidden message-only window and logs every Win32 message that arrives during
|
||||
a fixed pump window. Used to identify the `WM_APP` /
|
||||
`RegisterWindowMessage` IDs alarm callbacks use.
|
||||
- `WnWrapConsumerProbeTests` — instantiates AVEVA's standalone `wnwrapConsumer`
|
||||
COM class, subscribes to the dev rig's `\\<machine>\Galaxy!DEV` provider,
|
||||
and polls `GetXmlCurrentAlarms2`. The XML payload bypasses the
|
||||
`FILETIME→DateTime` auto-marshaling that crashes
|
||||
`aaAlarmManagedClient.AlarmClient.GetHighPriAlarm` on this rig.
|
||||
|
||||
The probes share the Worker.Tests project (so they can use its `net48`/`x86`
|
||||
configuration and the installed `ArchestrA.MxAccess` / `aaAlarmManagedClient`
|
||||
references), but they are not part of the regression contract — a Worker.Tests
|
||||
run with `Skip` left in place passes them as skipped.
|
||||
|
||||
## Live Galaxy Repository
|
||||
|
||||
`GalaxyRepositoryLiveTests` in `src/ZB.MOM.WW.MxGateway.IntegrationTests/Galaxy/` exercises
|
||||
`GalaxyRepository` directly against the `ZB` Galaxy Repository SQL database. It is
|
||||
skipped unless `MXGATEWAY_RUN_LIVE_GALAXY_TESTS=1` is set because it depends on a
|
||||
reachable SQL Server instance and deployed Galaxy state — fake-worker tests cannot
|
||||
cover the SQL browse RPCs.
|
||||
|
||||
The suite covers `TestConnectionAsync`, `GetLastDeployTimeAsync`,
|
||||
`GetHierarchyAsync`, and `GetAttributesAsync`. `GetHierarchyAsync` and
|
||||
`GetAttributesAsync` assert a non-empty result, so the connected `ZB` database
|
||||
must contain a deployed Galaxy, not just an empty schema.
|
||||
|
||||
Run the Galaxy live tests explicitly:
|
||||
|
||||
```bash
|
||||
$env:MXGATEWAY_RUN_LIVE_GALAXY_TESTS = "1"
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.IntegrationTests/ZB.MOM.WW.MxGateway.IntegrationTests.csproj --filter FullyQualifiedName~GalaxyRepositoryLiveTests
|
||||
```
|
||||
|
||||
Optional live Galaxy variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MXGATEWAY_LIVE_GALAXY_CONN` | `Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;` | Galaxy Repository connection string. Set this when the `ZB` database is on a non-default instance or needs SQL authentication. |
|
||||
|
||||
The default connection string targets `ZB` on `localhost` with Windows
|
||||
authentication, which matches the Galaxy Repository conventions in CLAUDE.md.
|
||||
|
||||
## Galaxy Filter Safety
|
||||
|
||||
`GalaxyFilterInputSafetyTests` in `src/ZB.MOM.WW.MxGateway.Tests/Galaxy/` covers adversarial
|
||||
input handling for the Galaxy Repository browse filter layer. It runs in the
|
||||
unit-test project (no live SQL needed) and complements the live SQL coverage in
|
||||
`GalaxyRepositoryLiveTests`.
|
||||
|
||||
The test class re-frames the original "Galaxy SQL injection" concern (Tests-002 in
|
||||
`code-reviews/Tests/findings.md`). `GalaxyRepository` issues only four *constant*
|
||||
SQL statements (`HierarchySql`, `AttributesSql`, `SELECT 1`,
|
||||
`SELECT time_of_last_deploy FROM galaxy`) — no `DiscoverHierarchyRequest` field
|
||||
is ever concatenated into a SQL string, so there is no dynamic SQL surface and no
|
||||
`LIKE`-escaping helper to test. All filters (`TagNameGlob`, `RootTagName`,
|
||||
template-chain, category, contained-path) are applied **in memory** by
|
||||
`GalaxyHierarchyProjector` / `GalaxyGlobMatcher` against the cached snapshot.
|
||||
|
||||
The adversarial-input matrix (`'`, `' OR '1'='1`, `'; DROP TABLE gobject;--`,
|
||||
`%`, `_`, `100%_off`, `[abc]`, `Pump'001`) pins the following invariants:
|
||||
|
||||
- SQL metacharacters (`'`, `;`) and `LIKE`-wildcards (`%`, `_`) are treated as
|
||||
opaque literals by `GalaxyGlobMatcher` — they never act as wildcards, never
|
||||
spuriously match unrelated text.
|
||||
- Only `*` and `?` are glob wildcards.
|
||||
- `GalaxyGlobMatcher` applies a 100 ms regex timeout so a pathological glob
|
||||
(e.g. 5 000 `a` characters plus a literal `!`) completes promptly rather than
|
||||
catastrophically backtracking.
|
||||
- `GalaxyHierarchyProjector` returns zero matches (rather than the whole
|
||||
hierarchy) for an adversarial `TagNameGlob` or `TemplateChainContains`, and
|
||||
surfaces `NotFound` for an adversarial `RootTagName`.
|
||||
- The `DiscoverHierarchy` RPC end-to-end returns zero matches for adversarial
|
||||
`TagNameGlob` rather than faulting.
|
||||
|
||||
These invariants are the real security surface of the Galaxy browse path; the
|
||||
SQL-injection framing does not apply to a constant-query layer.
|
||||
|
||||
## Live LDAP
|
||||
|
||||
`DashboardLdapLiveTests` in `src/ZB.MOM.WW.MxGateway.IntegrationTests/` exercises
|
||||
`DashboardAuthenticator` against the live GLAuth directory. It is skipped unless
|
||||
`MXGATEWAY_RUN_LIVE_LDAP_TESTS=1` is set because it binds against the GLAuth
|
||||
service described in `glauth.md`.
|
||||
|
||||
The suite builds the authenticator with a default `GatewayOptions`, so
|
||||
`LdapOptions.RequiredGroup` keeps its `GwAdmin` default. `GwAdmin` is the
|
||||
gateway-specific dashboard-admin role and is **not** part of the five baseline
|
||||
GLAuth role groups — it must be provisioned before the LDAP live tests pass.
|
||||
`AuthenticateAsync_AdminInGwAdminGroup_Succeeds` fails (rather than skips) when
|
||||
GLAuth has only the baseline groups, so this is a hard prerequisite beyond "LDAP
|
||||
is up." See the "Adding a gw-specific group" section of `glauth.md` for the
|
||||
provisioning step that adds `GwAdmin` and grants it to `admin`.
|
||||
|
||||
The suite covers both the success path and the `DashboardAuthenticator` failure
|
||||
branches: `admin` in `GwAdmin` succeeds; `readonly` is denied for missing group;
|
||||
`admin` with a wrong password is rejected by the candidate bind without leaking
|
||||
the password into `FailureMessage`; an unknown username yields no candidate; and
|
||||
an unreachable LDAP server is absorbed into a failed result rather than throwing.
|
||||
|
||||
Run the LDAP live tests explicitly:
|
||||
|
||||
```bash
|
||||
$env:MXGATEWAY_RUN_LIVE_LDAP_TESTS = "1"
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.IntegrationTests/ZB.MOM.WW.MxGateway.IntegrationTests.csproj --filter FullyQualifiedName~DashboardLdapLiveTests
|
||||
```
|
||||
|
||||
## Client E2E Scripts
|
||||
|
||||
`scripts/discover-testmachine-tags.ps1` queries the ZB Galaxy Repository for the
|
||||
@@ -100,11 +253,75 @@ powershell -ExecutionPolicy Bypass -File scripts/discover-testmachine-tags.ps1 -
|
||||
```
|
||||
|
||||
`scripts/run-client-e2e-tests.ps1` drives the .NET, Go, Rust, Python, and Java
|
||||
client CLIs through a live gateway session. For each client it opens one
|
||||
session, registers, verifies `SubscribeBulk` and `UnsubscribeBulk` on a bounded
|
||||
tag subset, adds and advises every discovered test tag, reads a bounded event
|
||||
stream, then closes the session in a `finally` path. The script writes a JSON
|
||||
report under `artifacts/e2e/`.
|
||||
client CLIs through a live gateway session. The gateway and worker are assumed
|
||||
to be already running at `-Endpoint`; the script does not start or stop them.
|
||||
For each client it runs these phases, then closes the session in a `finally`
|
||||
path and writes a JSON report under `artifacts/e2e/`:
|
||||
|
||||
1. **Session + register** — opens one session and registers.
|
||||
2. **Bulk** — verifies `SubscribeBulk` / `UnsubscribeBulk` on a bounded tag
|
||||
subset (skip with `-SkipBulk`).
|
||||
3. **Add-item / advise** — adds and advises every discovered test tag. The
|
||||
loop has no `StreamEvents` consumer attached, so advised tags accumulate
|
||||
MXAccess change events in the worker event channel
|
||||
(`MxGateway:Events:QueueCapacity`); left unbounded it overflows under
|
||||
`FailFast` backpressure and faults the worker. Every `-DrainEveryTags`
|
||||
advised tags (default 15) the loop connects a short-lived `StreamEvents`
|
||||
drain so the gateway pumps that channel empty. `-DrainEveryTags 0` disables
|
||||
the drain.
|
||||
4. **Stream** — asserts a bounded event stream delivers at least one event
|
||||
(skip with `-SkipStream`).
|
||||
5. **Parity** — asserts MXAccess error paths are rejected rather than silently
|
||||
succeeding: an invalid item handle and an unknown session id (skip with
|
||||
`-SkipParity`).
|
||||
6. **Auth rejection** — asserts `open-session` is rejected when the API key is
|
||||
missing, and (when `-RejectScopeApiKeyEnv` names an insufficient-scope key)
|
||||
when the key lacks the required scope. Skip with `-SkipAuth`.
|
||||
7. **Write round-trip** — *opt-in (`-VerifyWrite`).* Runs right after
|
||||
`register`: adds and advises a configurable writable attribute
|
||||
(`-WriteAttribute`, default `TestChangingInt`), writes a per-client
|
||||
sentinel value, then streams events and asserts an `OnWriteComplete` event
|
||||
for that item is observed — proof the write round-tripped through the
|
||||
gateway, worker, and MXAccess provider. The written value being echoed back
|
||||
in an `OnDataChange` is recorded best-effort (`echoObserved`): a
|
||||
provider-driven attribute such as `TestChangingInt` accepts the write but
|
||||
immediately overwrites it, so no data-change carries the value back. The
|
||||
Rust `stream-events` CLI emits full per-event JSON (`family`, `itemHandle`,
|
||||
`value`) so all five clients apply the same checks.
|
||||
|
||||
It is opt-in because it mutates live tag state. The phase fails fast if the
|
||||
write command is rejected — e.g. against a gateway whose worker predates
|
||||
write support (`MxAccessCommandExecutor` returning `InvalidRequest` for
|
||||
`Write`/`Write2`/`WriteSecured`/`WriteSecured2`).
|
||||
8. **Alarm feed + acknowledge** — *opt-in (`-VerifyAlarms`).* Runs after the
|
||||
stream phase. Exercises the two session-less alarm subcommands against the
|
||||
gateway's central alarm monitor: `stream-alarms` reads a bounded slice of
|
||||
the feed (`-AlarmStreamMax`, default 1 — the feed's first message always
|
||||
arrives immediately, whereas later ones depend on live transitions) and
|
||||
asserts at least one `AlarmFeedMessage`; `acknowledge-alarm` acknowledges
|
||||
`-AlarmReference` (default `Galaxy!TestArea.TestMachine_001.TestAlarm001`)
|
||||
and asserts the RPC round-trips. The native ack outcome is not asserted —
|
||||
it depends on whether that alarm is currently active.
|
||||
|
||||
It is opt-in because it depends on the gateway's central alarm monitor
|
||||
being enabled (`MxGateway:Alarms:Enabled`) and a live alarm provider.
|
||||
|
||||
Each client CLI is driven through one long-lived `batch` process. Every CLI
|
||||
exposes a `batch` subcommand: a process that reads one command line from stdin,
|
||||
runs it through the normal subcommand dispatch, writes the JSON result, then a
|
||||
line containing exactly `__MXGW_BATCH_EOR__`. The harness launches one such
|
||||
process per client and pings the ~250 operations of the flow through it, so the
|
||||
process — and, for the JVM, the runtime — cold-start is paid once per client
|
||||
instead of once per operation. A command that fails inside the batch process
|
||||
writes its `{"error":...}` envelope and the loop continues; the harness treats
|
||||
that envelope as the operation failure (used by the parity and auth phases).
|
||||
|
||||
Before the per-client phases run, the script builds the .NET CLI
|
||||
(`dotnet build`) and installs the Java CLI (`gradle :mxgateway-cli:installDist`)
|
||||
once, so the `batch` process launches straight from the compiled exe / the
|
||||
installed launcher. The Go, Rust, and Python batch processes are launched via
|
||||
`go run` / `cargo run` / `python -m`, which compile-or-start once when that
|
||||
single per-client process starts.
|
||||
|
||||
Build the gateway and worker, start the gateway, and provide a valid API key
|
||||
before running the client e2e script:
|
||||
@@ -121,40 +338,58 @@ powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -Clien
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -BulkTagCount 10
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -SkipStream
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -SkipBulk
|
||||
# Write round-trip (opt-in): point at a writable scalar attribute and its
|
||||
# value type.
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -VerifyWrite -WriteAttribute TestChangingInt -WriteType int32
|
||||
# Alarm feed + acknowledge (opt-in): needs MxGateway:Alarms:Enabled on the gateway.
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -VerifyAlarms -AlarmReference "Galaxy!TestArea.TestMachine_001.TestAlarm001"
|
||||
# Auth rejection: also assert an insufficient-scope key is denied.
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -RejectScopeApiKeyEnv MXGATEWAY_READONLY_API_KEY
|
||||
# Run all five clients concurrently as isolated child processes.
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -Parallel
|
||||
# Validate the flow offline (prints commands, contacts no gateway).
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -DryRun
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -Endpoint localhost:5000 -ApiKeyEnv MXGATEWAY_API_KEY
|
||||
```
|
||||
|
||||
When `-VerifyWrite` is enabled, the write round-trip fails loudly if the write
|
||||
command is rejected, if `-WriteAttribute` does not name a writable scalar
|
||||
attribute, or if no `OnWriteComplete` event is observed for the written item
|
||||
within `-WriteEchoMaxEvents` (default 200) streamed events. Raise
|
||||
`-WriteEchoMaxEvents` if the gateway's per-session event backlog is large
|
||||
enough to push `OnWriteComplete` past that bound.
|
||||
|
||||
## Focused Commands
|
||||
|
||||
Run the cross-language smoke matrix tests after changing the documented client
|
||||
smoke command list:
|
||||
|
||||
```bash
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~CrossLanguageSmokeMatrixTests
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj --filter FullyQualifiedName~CrossLanguageSmokeMatrixTests
|
||||
```
|
||||
|
||||
Run the parity fixture matrix tests after changing the integration parity
|
||||
scenario list:
|
||||
|
||||
```bash
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~ParityFixtureMatrixTests
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj --filter FullyQualifiedName~ParityFixtureMatrixTests
|
||||
```
|
||||
|
||||
Run the fake worker tests after changing gateway worker IPC, session startup, or
|
||||
event streaming behavior:
|
||||
|
||||
```bash
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~FakeWorkerHarnessTests
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~SessionWorkerClientFactoryFakeWorkerTests
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~GatewayEndToEndFakeWorkerSmokeTests
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~WorkerClientTests
|
||||
dotnet test src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj -p:Platform=x86 --filter FullyQualifiedName~WorkerPipeSessionTests
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj --filter FullyQualifiedName~FakeWorkerHarnessTests
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj --filter FullyQualifiedName~SessionWorkerClientFactoryFakeWorkerTests
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj --filter FullyQualifiedName~GatewayEndToEndFakeWorkerSmokeTests
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj --filter FullyQualifiedName~WorkerClientTests
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.Worker.Tests/ZB.MOM.WW.MxGateway.Worker.Tests.csproj -p:Platform=x86 --filter FullyQualifiedName~WorkerPipeSessionTests
|
||||
```
|
||||
|
||||
Run the gateway test project after shared gateway test infrastructure changes:
|
||||
|
||||
```bash
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
+12
-2
@@ -10,7 +10,7 @@ The layer is composed of four collaborators:
|
||||
|
||||
| Type | Lifetime | Role |
|
||||
|------|----------|------|
|
||||
| `MxAccessGatewayService` | scoped (gRPC) | Implements the four `MxAccessGateway` RPCs, performs exception mapping. |
|
||||
| `MxAccessGatewayService` | scoped (gRPC) | Implements the six `MxAccessGateway` RPCs, performs exception mapping. |
|
||||
| `MxAccessGrpcRequestValidator` | singleton | Rejects malformed requests before any session work runs. |
|
||||
| `MxAccessGrpcMapper` | singleton | Converts public proto types to internal `WorkerCommand`/`WorkerEvent` types and back. |
|
||||
| `IEventStreamService` (`EventStreamService`) | singleton | Owns the event stream pipeline, including bounded queue and backpressure handling. |
|
||||
@@ -29,7 +29,7 @@ A second gRPC service, `GalaxyRepositoryGrpcService`, is mapped alongside it. It
|
||||
|
||||
## RPC Handlers
|
||||
|
||||
`MxAccessGatewayService` derives from the generated `MxAccessGateway.MxAccessGatewayBase` and implements every RPC declared in `mxaccess_gateway.proto`. The proto contract itself is documented in [Contracts](./Contracts.md); this section covers only what the server-side handler does on top of that contract.
|
||||
`MxAccessGatewayService` derives from the generated `MxAccessGateway.MxAccessGatewayBase` and implements every RPC declared in `mxaccess_gateway.proto` — six in total: `OpenSession`, `CloseSession`, `Invoke`, `StreamEvents`, `AcknowledgeAlarm`, and `StreamAlarms`. The proto contract itself is documented in [Contracts](./Contracts.md); this section covers only what the server-side handler does on top of that contract.
|
||||
|
||||
Public gRPC send and receive message sizes are configured from
|
||||
`MxGateway:Protocol:MaxGrpcMessageBytes` (default 16 MiB). Official clients use
|
||||
@@ -86,6 +86,14 @@ Carrying the enqueue timestamp into the worker layer is what lets queue-wait tim
|
||||
|
||||
`StreamEvents` is a server-streaming RPC. The handler delegates the full pipeline to `IEventStreamService` and just forwards each `MxEvent` onto the response stream. Keeping the channel and producer/consumer machinery out of the handler means cancellation, exception mapping, and metric bookkeeping live in one place.
|
||||
|
||||
### `AcknowledgeAlarm`
|
||||
|
||||
`AcknowledgeAlarm` is a unary, **session-less** RPC that acknowledges a single alarm. The handler validates `alarm_full_reference` inline (it does not run through `MxAccessGrpcRequestValidator`) and delegates to `IGatewayAlarmService.AcknowledgeAsync`. The always-on `GatewayAlarmMonitor` routes the ack over its own gateway-managed worker session — clients no longer open a session to acknowledge an alarm. A reference that parses as a canonical GUID forwards to `AcknowledgeAlarmCommand`; a `Provider!Group.Tag` reference forwards to `AcknowledgeAlarmByNameCommand`.
|
||||
|
||||
### `StreamAlarms`
|
||||
|
||||
`StreamAlarms` is a server-streaming, **session-less** RPC that attaches to the gateway's central alarm feed. The handler delegates to `IGatewayAlarmService.StreamAsync`. The stream opens with one `AlarmFeedMessage` carrying an `active_alarm` per currently-active alarm (the ConditionRefresh snapshot), then a single `snapshot_complete`, then a `transition` for every subsequent raise / acknowledge / clear. It is served by the always-on `GatewayAlarmMonitor`, which owns a single gateway-managed worker session and fans out to every attached client — clients no longer open a session of their own. `alarm_filter_prefix`, when set, scopes the stream to a sub-tree.
|
||||
|
||||
## Validation Rules
|
||||
|
||||
`MxAccessGrpcRequestValidator` rejects requests with `StatusCode.InvalidArgument` before any session work happens. The rules are intentionally narrow — anything that requires session state (for example, "session does not exist") is left for `ISessionManager` so the validator can stay synchronous and side-effect free.
|
||||
@@ -96,6 +104,8 @@ Carrying the enqueue timestamp into the worker layer is what lets queue-wait tim
|
||||
| `CloseSession` | `session_id` must be non-empty. | `InvalidArgument` |
|
||||
| `StreamEvents` | `session_id` must be non-empty. | `InvalidArgument` |
|
||||
| `Invoke` | `session_id` non-empty, `command` present, `kind` not `Unspecified`, payload oneof must match `kind`. | `InvalidArgument` |
|
||||
| `AcknowledgeAlarm` | `alarm_full_reference` must be non-empty. Validated inline in the handler, not by `MxAccessGrpcRequestValidator`. | `InvalidArgument` |
|
||||
| `StreamAlarms` | No required fields — `alarm_filter_prefix` is optional. | — |
|
||||
|
||||
The payload-vs-kind check matters because the `MxCommand.payload` oneof is non-discriminated on the wire — a misaligned client could send `kind = Write` with a `Register` payload and silently confuse the worker. The validator turns that into a clear client error:
|
||||
|
||||
|
||||
@@ -64,9 +64,9 @@ Labels: `area:client-dotnet`, `type:infra`, `priority:p0`
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `clients/dotnet/MxGateway.Client`,
|
||||
- `clients/dotnet/MxGateway.Client.Cli`,
|
||||
- `clients/dotnet/MxGateway.Client.Tests`,
|
||||
- `clients/dotnet/ZB.MOM.WW.MxGateway.Client`,
|
||||
- `clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli`,
|
||||
- `clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests`,
|
||||
- optional integration test project,
|
||||
- generated protobuf setup.
|
||||
|
||||
|
||||
@@ -22,19 +22,19 @@ Labels: `area:gateway`, `type:infra`, `priority:p0`
|
||||
|
||||
Deliverables:
|
||||
|
||||
- create `src/MxGateway.sln`,
|
||||
- create `src/MxGateway.Contracts`,
|
||||
- create `src/MxGateway.Server`,
|
||||
- create `src/MxGateway.Tests`,
|
||||
- create `src/MxGateway.IntegrationTests`,
|
||||
- target `MxGateway.Server` to `net10.0`,
|
||||
- create `src/ZB.MOM.WW.MxGateway.slnx`,
|
||||
- create `src/ZB.MOM.WW.MxGateway.Contracts`,
|
||||
- create `src/ZB.MOM.WW.MxGateway.Server`,
|
||||
- create `src/ZB.MOM.WW.MxGateway.Tests`,
|
||||
- create `src/ZB.MOM.WW.MxGateway.IntegrationTests`,
|
||||
- target `ZB.MOM.WW.MxGateway.Server` to `net10.0`,
|
||||
- add shared C# build settings in `Directory.Build.props`,
|
||||
- add baseline tests.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- `dotnet build src/MxGateway.sln` succeeds,
|
||||
- `dotnet test src/MxGateway.sln` succeeds,
|
||||
- `dotnet build src/ZB.MOM.WW.MxGateway.slnx` succeeds,
|
||||
- `dotnet test src/ZB.MOM.WW.MxGateway.slnx` succeeds,
|
||||
- gateway project does not reference MXAccess COM.
|
||||
|
||||
### Issue: Define Protobuf Contracts
|
||||
@@ -43,8 +43,8 @@ Labels: `area:contracts`, `type:feature`, `priority:p0`
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `src/MxGateway.Contracts/Protos/mxaccess_gateway.proto`,
|
||||
- `src/MxGateway.Contracts/Protos/mxaccess_worker.proto`,
|
||||
- `src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto`,
|
||||
- `src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_worker.proto`,
|
||||
- `MxAccessGateway` service with `OpenSession`, `CloseSession`, `Invoke`, and
|
||||
`StreamEvents`,
|
||||
- `WorkerEnvelope` and worker IPC messages,
|
||||
|
||||
@@ -23,12 +23,12 @@ Labels: `area:worker`, `type:infra`, `priority:p0`
|
||||
|
||||
Deliverables:
|
||||
|
||||
- create `src/MxGateway.Worker`,
|
||||
- create `src/ZB.MOM.WW.MxGateway.Worker`,
|
||||
- target `.NET Framework 4.8`,
|
||||
- platform target `x86`,
|
||||
- reference generated worker contracts,
|
||||
- reference `ArchestrA.MXAccess.dll`,
|
||||
- create `src/MxGateway.Worker.Tests`,
|
||||
- create `src/ZB.MOM.WW.MxGateway.Worker.Tests`,
|
||||
- document MSBuild command from `docs/ToolchainLinks.md`.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ The metrics subsystem exposes counters, histograms, and observable gauges that d
|
||||
|
||||
## Overview
|
||||
|
||||
`GatewayMetrics` is a singleton (registered in `GatewayApplication.cs`) that owns a single `Meter` named `MxGateway.Server` and a set of synchronised counters, histograms, and observable gauges. Subsystems call typed mutator methods (`SessionOpened`, `CommandFailed`, `EventReceived`, etc.) rather than touching the `Meter` directly, which keeps the OpenTelemetry instrument names and tag conventions in one place. A `lock (_syncRoot)` block guards the scalar fields used by `GetSnapshot`, while per-event maps use `ConcurrentDictionary<string, long>` so the hot event path avoids the lock.
|
||||
`GatewayMetrics` is a singleton (registered in `GatewayApplication.cs`) that owns a single `Meter` named `ZB.MOM.WW.MxGateway.Server` and a set of synchronised counters, histograms, and observable gauges. Subsystems call typed mutator methods (`SessionOpened`, `CommandFailed`, `EventReceived`, etc.) rather than touching the `Meter` directly, which keeps the OpenTelemetry instrument names and tag conventions in one place. A `lock (_syncRoot)` block guards the scalar fields used by `GetSnapshot`, while per-event maps use `ConcurrentDictionary<string, long>` so the hot event path avoids the lock.
|
||||
|
||||
## Meter and OpenTelemetry Compatibility
|
||||
|
||||
@@ -13,7 +13,7 @@ The meter name is exposed as a constant so that hosting code can register it wit
|
||||
```csharp
|
||||
public sealed class GatewayMetrics : IDisposable
|
||||
{
|
||||
public const string MeterName = "MxGateway.Server";
|
||||
public const string MeterName = "ZB.MOM.WW.MxGateway.Server";
|
||||
|
||||
public GatewayMetrics()
|
||||
{
|
||||
|
||||
@@ -33,23 +33,23 @@ project targets .NET Framework 4.8, but the SDK resolver comes from the .NET SDK
|
||||
installation:
|
||||
|
||||
```powershell
|
||||
dotnet msbuild src\MxGateway.Worker\MxGateway.Worker.csproj /restore /p:Configuration=Debug /p:Platform=x86
|
||||
dotnet msbuild src\ZB.MOM.WW.MxGateway.Worker\ZB.MOM.WW.MxGateway.Worker.csproj /restore /p:Configuration=Debug /p:Platform=x86
|
||||
```
|
||||
|
||||
`docs/ToolchainLinks.md` records the Visual Studio MSBuild executable for
|
||||
classic .NET Framework and COM interop builds:
|
||||
|
||||
```powershell
|
||||
& "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe" src\MxGateway.Worker\MxGateway.Worker.csproj /p:Configuration=Debug /p:Platform=x86
|
||||
& "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe" src\ZB.MOM.WW.MxGateway.Worker\ZB.MOM.WW.MxGateway.Worker.csproj /p:Configuration=Debug /p:Platform=x86
|
||||
```
|
||||
|
||||
Run the worker tests with the same platform target:
|
||||
|
||||
```powershell
|
||||
dotnet test src\MxGateway.Worker.Tests\MxGateway.Worker.Tests.csproj -p:Platform=x86
|
||||
dotnet test src\ZB.MOM.WW.MxGateway.Worker.Tests\ZB.MOM.WW.MxGateway.Worker.Tests.csproj -p:Platform=x86
|
||||
```
|
||||
|
||||
The only MXAccess interop reference belongs in `MxGateway.Worker`. Gateway and
|
||||
The only MXAccess interop reference belongs in `ZB.MOM.WW.MxGateway.Worker`. Gateway and
|
||||
test projects may reference the worker project for metadata and scaffold tests,
|
||||
but they must not reference `ArchestrA.MXAccess.dll` directly.
|
||||
|
||||
@@ -132,7 +132,7 @@ credential, or API key values before the message is written.
|
||||
## Internal Components
|
||||
|
||||
```text
|
||||
MxGateway.Worker
|
||||
ZB.MOM.WW.MxGateway.Worker
|
||||
Program
|
||||
Bootstrap
|
||||
WorkerOptions
|
||||
@@ -251,7 +251,7 @@ The loop should update a heartbeat timestamp after:
|
||||
- processing an MXAccess event.
|
||||
|
||||
`StaRuntime` implements this runtime boundary in the worker. It starts one
|
||||
background thread named `MxGateway.Worker.STA`, sets it to `ApartmentState.STA`,
|
||||
background thread named `ZB.MOM.WW.MxGateway.Worker.STA`, sets it to `ApartmentState.STA`,
|
||||
initializes COM through `StaComApartmentInitializer`, and runs
|
||||
`StaMessagePump`. Commands are scheduled through `InvokeAsync`; the command
|
||||
queue signals an `AutoResetEvent` so `MsgWaitForMultipleObjectsEx` can wake the
|
||||
@@ -655,12 +655,39 @@ the event queue implementation owns those counters.
|
||||
|
||||
The STA watchdog currently emits a `WorkerFault` with
|
||||
`WorkerFaultCategory.StaHung` when `LastStaActivityUtc` is older than
|
||||
`WorkerPipeSessionOptions.HeartbeatGrace`. The fault includes the current
|
||||
command correlation id when a command is active. Command duration and high event
|
||||
queue depth remain observable through heartbeat fields until dedicated
|
||||
thresholds own those warnings. The worker reports stale STA activity, but the
|
||||
gateway owns the final kill decision through its existing heartbeat and worker
|
||||
lifecycle policy.
|
||||
`WorkerPipeSessionOptions.HeartbeatGrace` **and no command is in flight**.
|
||||
`StaRuntime.ProcessQueuedCommands` calls `MarkActivity()` only immediately
|
||||
before and after each work item, so a synchronously long-running STA command
|
||||
(for example a `ReadBulk` waiting `timeout_ms` for the first `OnDataChange`)
|
||||
legitimately freezes `LastStaActivityUtc` for the duration of the wait while
|
||||
the worker is healthy. The watchdog is therefore suppressed while the
|
||||
heartbeat snapshot's `CurrentCommandCorrelationId` is non-empty: the worker is
|
||||
busy executing a command, not hung, and the heartbeat already surfaces the
|
||||
in-flight correlation id so the gateway can apply its own per-command timeout
|
||||
if it considers the command too slow. The fault still fires on a truly hung
|
||||
STA — no command in flight and no activity for longer than `HeartbeatGrace` —
|
||||
which is the only case the watchdog can usefully distinguish from a slow
|
||||
command. Command duration and high event queue depth remain observable through
|
||||
heartbeat fields until dedicated thresholds own those warnings. The worker
|
||||
reports stale STA activity, but the gateway owns the final kill decision
|
||||
through its existing heartbeat and worker lifecycle policy.
|
||||
|
||||
The in-flight-command suppression itself is bounded by
|
||||
`WorkerPipeSessionOptions.HeartbeatStuckCeiling` (default 75 seconds = 5 ×
|
||||
`HeartbeatGrace`). The motivating case for the suppression is a legitimately
|
||||
slow synchronous command — but a genuinely stuck COM call (for example
|
||||
against a dead MXAccess provider whose cross-apartment marshaler is
|
||||
permanently blocked, or a write completion that never fires) leaves
|
||||
`CurrentCommandCorrelationId` non-empty indefinitely. Without an upper bound
|
||||
the worker-side `StaHung` watchdog would be permanently defeated for that
|
||||
session and only the gateway's per-command timeout would catch the hang —
|
||||
losing the worker-originated diagnostic (`StaHung` fault category, the
|
||||
stale-by interval) from the gateway audit trail. Once `LastStaActivityUtc`
|
||||
has been stale for longer than `HeartbeatStuckCeiling`, the watchdog fires
|
||||
`StaHung` regardless of whether a command is in flight, on the assumption
|
||||
that no legitimate STA command should run that long without periodically
|
||||
refreshing activity. Deployments that legitimately run very long bulk
|
||||
operations should raise the ceiling rather than disable it.
|
||||
|
||||
## Shutdown
|
||||
|
||||
@@ -807,7 +834,7 @@ tests. `AddItem` uses `TestChildObject.TestInt` by default and accepts an
|
||||
override through `MXGATEWAY_LIVE_MXACCESS_ITEM`; `AddItem2` uses the captured
|
||||
parity fixture shape `AddItem2("TestInt", "TestChildObject")`.
|
||||
|
||||
`WorkerLiveMxAccessSmokeTests` in `src/MxGateway.IntegrationTests/` uses the
|
||||
`WorkerLiveMxAccessSmokeTests` in `src/ZB.MOM.WW.MxGateway.IntegrationTests/` uses the
|
||||
same opt-in variable for the gateway-to-worker live smoke. It launches the x86
|
||||
worker through `WorkerProcessLauncher`, opens a gateway session, runs
|
||||
`Register`, `AddItem`, and `Advise`, waits for one `OnDataChange`, and closes
|
||||
|
||||
@@ -88,7 +88,7 @@ into a transport failure when the worker captured HRESULT or status details.
|
||||
Run the parity fixture matrix tests after changing the matrix:
|
||||
|
||||
```bash
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~ParityFixtureMatrixTests
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj --filter FullyQualifiedName~ParityFixtureMatrixTests
|
||||
```
|
||||
|
||||
Live MXAccess execution remains opt-in. The matrix defines which scenarios to
|
||||
|
||||
+10
-3
@@ -16,7 +16,7 @@ All four interfaces (`ISessionManager`, `ISessionRegistry`, `ISessionWorkerClien
|
||||
|
||||
The session id is an opaque string in the form `session-{guid:N}` and the per-session pipe name is `mxaccess-gateway-{ProcessId}-{SessionId}`. Encoding the gateway PID into the pipe name avoids collisions when an old gateway process leaks pipes that the OS has not yet reclaimed.
|
||||
|
||||
`SessionState` itself is the protobuf-generated enum from `MxGateway.Contracts.Proto`, so it is shared between the gateway and clients on the wire.
|
||||
`SessionState` itself is the protobuf-generated enum from `ZB.MOM.WW.MxGateway.Contracts.Proto`, so it is shared between the gateway and clients on the wire.
|
||||
|
||||
```csharp
|
||||
public void TransitionTo(SessionState nextState)
|
||||
@@ -33,12 +33,19 @@ public void TransitionTo(SessionState nextState)
|
||||
return;
|
||||
}
|
||||
|
||||
if (_state is SessionState.Closing
|
||||
&& nextState is not SessionState.Closed
|
||||
&& nextState is not SessionState.Faulted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_state = nextState;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Closed` is terminal and `Faulted` only allows a transition to `Closed`. This guards against late callbacks (worker exit, heartbeat timeout) re-animating a session that is already torn down.
|
||||
`Closed` is terminal, `Faulted` only allows a transition to `Closed`, and `Closing` only allows a transition to `Closed` or `Faulted`. This guards against late callbacks (worker exit, heartbeat timeout) re-animating a session that is already tearing down or torn down — once `CloseAsync` has set `Closing` under `_syncRoot`, no `TransitionTo(Ready)` from another thread can walk the session back to `Ready`. Both close-related writes (`Closing` and `Closed`) go through `_syncRoot` exactly like every other state write; `_closeLock` only serializes concurrent close attempts.
|
||||
|
||||
### SessionManager (ISessionManager)
|
||||
|
||||
@@ -184,7 +191,7 @@ Sessions open with `MxGateway:Sessions:DefaultLeaseSeconds` (default 1800) added
|
||||
|
||||
### Close
|
||||
|
||||
`GatewaySession.CloseAsync` is serialized by a per-session `SemaphoreSlim` (`_closeLock`). It transitions to `Closing`, asks the worker client to shut down within `ShutdownTimeout`, and on success transitions to `Closed`. If `WorkerClient.ShutdownAsync` throws, the session falls back to `IWorkerClient.Kill` (forced close):
|
||||
`GatewaySession.CloseAsync` is serialized by a per-session `SemaphoreSlim` (`_closeLock`) so only one close runs at a time, but every read/write of `_state` still passes through `_syncRoot` (via `TryBeginClose` and `MarkClosed`). The close path therefore obeys the same lock discipline as `TransitionTo` / `MarkFaulted`: it transitions to `Closing`, asks the worker client to shut down within `ShutdownTimeout`, and on success transitions to `Closed`. `DisposeAsync` waits on `_closeLock` once before disposing the semaphore so an in-flight close's `Release()` cannot race against the dispose. If `WorkerClient.ShutdownAsync` throws, the session falls back to `IWorkerClient.Kill` (forced close):
|
||||
|
||||
```csharp
|
||||
if (_workerClient is not null)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Worker Bootstrap
|
||||
|
||||
The bootstrap layer parses the command-line arguments and environment variables passed to the `MxGateway.Worker` process, validates them against the gateway contract, and produces either a populated `WorkerOptions` instance or a structured failure that maps to a `WorkerExitCode`.
|
||||
The bootstrap layer parses the command-line arguments and environment variables passed to the `ZB.MOM.WW.MxGateway.Worker` process, validates them against the gateway contract, and produces either a populated `WorkerOptions` instance or a structured failure that maps to a `WorkerExitCode`.
|
||||
|
||||
## Overview
|
||||
|
||||
The worker process is a short-lived child of the gateway. The gateway side of this contract lives in [WorkerProcessLauncher](./WorkerProcessLauncher.md). On the worker side, `Program.cs` is a single line that delegates to `WorkerApplication.Run(args)`:
|
||||
|
||||
```csharp
|
||||
using MxGateway.Worker;
|
||||
using ZB.MOM.WW.MxGateway.Worker;
|
||||
|
||||
return WorkerApplication.Run(args);
|
||||
```
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Worker Conversion Layer
|
||||
|
||||
The conversion layer in `MxGateway.Worker.Conversion` projects COM `VARIANT` payloads, `HRESULT` codes, and `MXSTATUS_PROXY` records into the protobuf wire types in `MxGateway.Contracts.Proto`. The design is parity-first: every projection preserves enough raw metadata that the original COM observation can be reconstructed downstream.
|
||||
The conversion layer in `ZB.MOM.WW.MxGateway.Worker.Conversion` projects COM `VARIANT` payloads, `HRESULT` codes, and `MXSTATUS_PROXY` records into the protobuf wire types in `ZB.MOM.WW.MxGateway.Contracts.Proto`. The design is parity-first: every projection preserves enough raw metadata that the original COM observation can be reconstructed downstream.
|
||||
|
||||
## Overview
|
||||
|
||||
`gateway.md` (sections "Value Model" and "Status Model") requires that the wire format use a value union capable of representing COM `VARIANT` values and arrays, that lossy conversions retain both the typed projection and raw diagnostic metadata, and that `MXSTATUS_PROXY` arrays never collapse to a single success flag. The types in `src/MxGateway.Worker/Conversion/` are the worker-side enforcement of those rules.
|
||||
`gateway.md` (sections "Value Model" and "Status Model") requires that the wire format use a value union capable of representing COM `VARIANT` values and arrays, that lossy conversions retain both the typed projection and raw diagnostic metadata, and that `MXSTATUS_PROXY` arrays never collapse to a single success flag. The types in `src/ZB.MOM.WW.MxGateway.Worker/Conversion/` are the worker-side enforcement of those rules.
|
||||
|
||||
The layer is split into three concerns:
|
||||
|
||||
|
||||
@@ -35,17 +35,22 @@ oversized frames, protocol version mismatches, and session mismatches.
|
||||
|
||||
## Verification
|
||||
|
||||
The frame protocol lives in `ZB.MOM.WW.MxGateway.Worker.Ipc` (`WorkerFrameReader`,
|
||||
`WorkerFrameWriter`, `WorkerFrameProtocolOptions`) and is covered by
|
||||
`src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerFrameProtocolTests.cs`. The worker is an
|
||||
x86 process, so build and test it with `-p:Platform=x86`.
|
||||
|
||||
Run the focused tests after changing the frame protocol:
|
||||
|
||||
```bash
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter WorkerFrameProtocolTests
|
||||
```powershell
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.Worker.Tests/ZB.MOM.WW.MxGateway.Worker.Tests.csproj -p:Platform=x86 --filter WorkerFrameProtocolTests
|
||||
```
|
||||
|
||||
Run the gateway build because the frame protocol is part of
|
||||
`MxGateway.Server`:
|
||||
Run the x86 worker build because the frame protocol is part of
|
||||
`ZB.MOM.WW.MxGateway.Worker`:
|
||||
|
||||
```bash
|
||||
dotnet build src/MxGateway.Server/MxGateway.Server.csproj
|
||||
```powershell
|
||||
dotnet build src/ZB.MOM.WW.MxGateway.Worker/ZB.MOM.WW.MxGateway.Worker.csproj -p:Platform=x86
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
@@ -60,13 +60,13 @@ optional pipe reservation, records a worker kill metric, and reports a
|
||||
Run the focused launcher tests after changing process launch behavior:
|
||||
|
||||
```bash
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter WorkerProcessLauncherTests
|
||||
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj --filter WorkerProcessLauncherTests
|
||||
```
|
||||
|
||||
Run the gateway build because the launcher is part of `MxGateway.Server`:
|
||||
Run the gateway build because the launcher is part of `ZB.MOM.WW.MxGateway.Server`:
|
||||
|
||||
```bash
|
||||
dotnet build src/MxGateway.Server/MxGateway.Server.csproj
|
||||
dotnet build src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ The worker STA runtime owns the dedicated single-threaded apartment thread that
|
||||
|
||||
## Why an STA Is Required
|
||||
|
||||
The installed MXAccess interop assembly declares an `Apartment` threading model (see `gateway.md` under "STA Worker Thread Model"). COM objects with that model must be created and called on a thread initialized as a single-threaded apartment, and any callbacks the object raises (event sink calls) are delivered through the thread's Windows message queue. A plain blocking queue is not sufficient: the STA loop must pump Windows messages so that the COM marshaler can deliver event invocations on the same thread that holds the object. Because of that constraint, every MXAccess operation in the worker is funneled through the types in `src/MxGateway.Worker/Sta/`.
|
||||
The installed MXAccess interop assembly declares an `Apartment` threading model (see `gateway.md` under "STA Worker Thread Model"). COM objects with that model must be created and called on a thread initialized as a single-threaded apartment, and any callbacks the object raises (event sink calls) are delivered through the thread's Windows message queue. A plain blocking queue is not sufficient: the STA loop must pump Windows messages so that the COM marshaler can deliver event invocations on the same thread that holds the object. Because of that constraint, every MXAccess operation in the worker is funneled through the types in `src/ZB.MOM.WW.MxGateway.Worker/Sta/`.
|
||||
|
||||
## Types
|
||||
|
||||
@@ -20,13 +20,13 @@ The installed MXAccess interop assembly declares an `Apartment` threading model
|
||||
|
||||
## STA Thread Initialization
|
||||
|
||||
`StaRuntime`'s constructor configures a background `Thread` named `MxGateway.Worker.STA` and forces it into `ApartmentState.STA` before the thread starts. `Start()` releases the thread and then blocks on `startedEvent` so callers observe a fully-initialized apartment (or a captured `startupException`) before the first `InvokeAsync` call:
|
||||
`StaRuntime`'s constructor configures a background `Thread` named `ZB.MOM.WW.MxGateway.Worker.STA` and forces it into `ApartmentState.STA` before the thread starts. `Start()` releases the thread and then blocks on `startedEvent` so callers observe a fully-initialized apartment (or a captured `startupException`) before the first `InvokeAsync` call:
|
||||
|
||||
```csharp
|
||||
staThread = new Thread(ThreadMain)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "MxGateway.Worker.STA"
|
||||
Name = "ZB.MOM.WW.MxGateway.Worker.STA"
|
||||
};
|
||||
staThread.SetApartmentState(ApartmentState.STA);
|
||||
```
|
||||
|
||||
+71
-12
@@ -104,8 +104,8 @@ Responsibilities:
|
||||
|
||||
The gateway must never instantiate or call MXAccess directly.
|
||||
|
||||
The gateway observability foundation lives in `MxGateway.Server.Diagnostics`
|
||||
and `MxGateway.Server.Metrics`. Structured logging scopes carry session,
|
||||
The gateway observability foundation lives in `ZB.MOM.WW.MxGateway.Server.Diagnostics`
|
||||
and `ZB.MOM.WW.MxGateway.Server.Metrics`. Structured logging scopes carry session,
|
||||
worker, correlation, command, and client identity fields with redaction applied
|
||||
before values enter log state. `GatewayMetrics` exposes counters, gauges, and
|
||||
histograms through .NET `Meter` and a snapshot API that dashboard services can
|
||||
@@ -113,13 +113,31 @@ project without binding to a metrics exporter.
|
||||
`DashboardSnapshotService` projects sessions, workers, metrics, faults, and
|
||||
effective configuration into immutable DTOs for read-only dashboard rendering.
|
||||
The Blazor Server dashboard renders those snapshots at `/dashboard`,
|
||||
`/dashboard/sessions`, `/dashboard/workers`, `/dashboard/events`, and
|
||||
`/dashboard/settings`. Components subscribe to
|
||||
`/dashboard/sessions`, `/dashboard/workers`, `/dashboard/events`,
|
||||
`/dashboard/galaxy`, and `/dashboard/settings`. Components subscribe to
|
||||
`IDashboardSnapshotService.WatchSnapshotsAsync()` and update on the configured
|
||||
snapshot interval without mutating session or worker state. The dashboard uses
|
||||
local Bootstrap CSS and JavaScript plus a small local stylesheet; it does not
|
||||
use a Blazor UI component library.
|
||||
|
||||
`/dashboard/browse` walks the `IGalaxyHierarchyCache` tree and reads subscribed
|
||||
tag values live through `IDashboardLiveDataService`, which owns one shared,
|
||||
lazily-opened gateway session for the whole dashboard. `/dashboard/alarms`
|
||||
reads the central alarm monitor's in-process cache directly. See
|
||||
`docs/GatewayDashboardDesign.md`.
|
||||
|
||||
The gateway runs an always-on central alarm monitor (`GatewayAlarmMonitor`):
|
||||
one gateway-owned worker session subscribes the configured AVEVA alarm
|
||||
provider, caches the active-alarm set (reconciled periodically against the
|
||||
worker's snapshot), and fans it out to every client through the session-less
|
||||
`StreamAlarms` RPC — the stream opens with the current active-alarm snapshot,
|
||||
then streams live transitions. `AcknowledgeAlarm` is session-less and routes
|
||||
through the monitor. Clients never open a worker session to see alarms, and
|
||||
alarm monitoring is independent of client lifecycle; the monitor re-opens its
|
||||
session if the worker faults. Gated by `MxGateway:Alarms:Enabled` — see
|
||||
`docs/DesignDecisions.md` for why this reverses the v1 single-subscriber rule
|
||||
for the alarm subsystem.
|
||||
|
||||
Dashboard routes use the same API-key verifier as gRPC. `/dashboard/login`
|
||||
accepts the API key in a form body, validates the configured `admin` scope,
|
||||
and issues an HTTP-only secure cookie for subsequent dashboard requests.
|
||||
@@ -283,6 +301,44 @@ Core commands:
|
||||
- `AuthenticateUser`
|
||||
- `ArchestrAUserToId`
|
||||
|
||||
Bulk variants (single gRPC round-trip carries the full list, the worker
|
||||
runs the per-item MXAccess calls sequentially on its STA, and the reply
|
||||
returns one result per requested entry — per-entry failures populate
|
||||
`was_successful = false` + `error_message` and never throw):
|
||||
|
||||
- `AddItemBulk` — `repeated string tag_addresses` → `BulkSubscribeReply`.
|
||||
- `AdviseItemBulk` — `repeated int32 item_handles` → `BulkSubscribeReply`.
|
||||
- `RemoveItemBulk` — `repeated int32 item_handles` → `BulkSubscribeReply`.
|
||||
- `UnAdviseItemBulk` — `repeated int32 item_handles` → `BulkSubscribeReply`.
|
||||
- `SubscribeBulk` — `repeated string tag_addresses` (AddItem + Advise per
|
||||
entry, with cleanup on Advise failure) → `BulkSubscribeReply`.
|
||||
- `UnsubscribeBulk` — `repeated int32 item_handles` (UnAdvise + RemoveItem
|
||||
per entry, with independent error tracking) → `BulkSubscribeReply`.
|
||||
- `WriteBulk` — `repeated WriteBulkEntry` (each `{item_handle, value, user_id}`)
|
||||
→ `BulkWriteReply` (`repeated BulkWriteResult`). Required scope: `invoke:write`.
|
||||
- `Write2Bulk` — `repeated Write2BulkEntry` (each adds `timestamp_value`) →
|
||||
`BulkWriteReply`. Required scope: `invoke:write`.
|
||||
- `WriteSecuredBulk` — `repeated WriteSecuredBulkEntry` (each
|
||||
`{item_handle, current_user_id, verifier_user_id, value}`) → `BulkWriteReply`.
|
||||
Required scope: `invoke:secure`. Same redaction rules as single-item
|
||||
`WriteSecured`: per-entry `value` must never reach logs unless an explicit
|
||||
redacted value-logging path is enabled.
|
||||
- `WriteSecured2Bulk` — `repeated WriteSecured2BulkEntry` (each adds
|
||||
`timestamp_value`) → `BulkWriteReply`. Required scope: `invoke:secure`.
|
||||
- `ReadBulk` — `repeated string tag_addresses` + `uint32 timeout_ms` →
|
||||
`BulkReadReply` (`repeated BulkReadResult`). MXAccess COM has no
|
||||
synchronous `Read`; the worker satisfies this command by returning the
|
||||
last cached `OnDataChange` payload when the requested tag is already
|
||||
advised (`was_cached = true`, no subscription side-effects), or by
|
||||
taking a full `AddItem` + `Advise` + wait-for-first-OnDataChange +
|
||||
`UnAdvise` + `RemoveItem` snapshot lifecycle when no live subscription
|
||||
exists (`was_cached = false`). Per-tag timeouts surface as
|
||||
`was_successful = false` rather than throwing. The cache lives on the
|
||||
worker's `MxAccessValueCache`, populated by `MxAccessBaseEventSink` on
|
||||
every `OnDataChange` after the event clears the outbound queue.
|
||||
Required scope: `invoke:read`. `timeout_ms == 0` uses the worker's
|
||||
default (1000 ms).
|
||||
|
||||
Optional diagnostics:
|
||||
|
||||
- `Ping`
|
||||
@@ -579,8 +635,11 @@ Policy:
|
||||
- command exceptions return structured command fault with HRESULT if known,
|
||||
- stale sessions are closed by lease timeout,
|
||||
- stuck workers are killed by process id,
|
||||
- gateway restart should not attempt to reattach old workers unless explicitly
|
||||
designed; first version should terminate orphaned workers on startup.
|
||||
- gateway restart does not reattach old workers; `OrphanWorkerCleanupHostedService`
|
||||
runs `OrphanWorkerTerminator` once on startup — before the server accepts
|
||||
sessions — to kill leftover `ZB.MOM.WW.MxGateway.Worker.exe` processes (matched by the
|
||||
configured worker executable path, or by image name when the x64 gateway cannot
|
||||
introspect the x86 worker's module) left behind by a previous unclean run.
|
||||
|
||||
Because each client owns one worker, a crash or leak affects only that session.
|
||||
|
||||
@@ -667,36 +726,36 @@ Optimizations after parity:
|
||||
Suggested additions:
|
||||
|
||||
```text
|
||||
src/MxGateway.Contracts/
|
||||
src/ZB.MOM.WW.MxGateway.Contracts/
|
||||
Protos/
|
||||
mxaccess_gateway.proto
|
||||
mxaccess_worker.proto
|
||||
Generated/
|
||||
|
||||
src/MxGateway.Server/
|
||||
src/ZB.MOM.WW.MxGateway.Server/
|
||||
Program.cs
|
||||
Sessions/
|
||||
Workers/
|
||||
Grpc/
|
||||
Metrics/
|
||||
|
||||
src/MxGateway.Worker/
|
||||
src/ZB.MOM.WW.MxGateway.Worker/
|
||||
Program.cs
|
||||
Ipc/
|
||||
Sta/
|
||||
MxAccess/
|
||||
Conversion/
|
||||
|
||||
src/MxGateway.Tests/
|
||||
src/ZB.MOM.WW.MxGateway.Tests/
|
||||
contract tests
|
||||
gateway session tests
|
||||
fake worker tests
|
||||
|
||||
src/MxGateway.Worker.Tests/
|
||||
src/ZB.MOM.WW.MxGateway.Worker.Tests/
|
||||
value/status conversion tests
|
||||
STA queue tests
|
||||
|
||||
src/MxGateway.IntegrationTests/
|
||||
src/ZB.MOM.WW.MxGateway.IntegrationTests/
|
||||
optional live MXAccess tests
|
||||
```
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$protoRoot = Join-Path $repoRoot "src/MxGateway.Contracts/Protos"
|
||||
$protoRoot = Join-Path $repoRoot "src/ZB.MOM.WW.MxGateway.Contracts/Protos"
|
||||
$manifestPath = Join-Path $repoRoot "clients/proto/proto-inputs.json"
|
||||
$descriptorPath = Join-Path $repoRoot "clients/proto/descriptors/mxaccessgw-client-v1.protoset"
|
||||
|
||||
|
||||
+1194
-149
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$testProject = Join-Path $repoRoot "src/MxGateway.Tests/MxGateway.Tests.csproj"
|
||||
$testProject = Join-Path $repoRoot "src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj"
|
||||
$arguments = @(
|
||||
"test",
|
||||
$testProject,
|
||||
|
||||
@@ -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,
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user