Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf20142634 | |||
| b995c174eb | |||
| ac2787f619 | |||
| d543679044 |
@@ -0,0 +1,481 @@
|
||||
# MXAccess Gateway Agent Guide
|
||||
|
||||
Repository: https://gitea.dohertylan.com/dohertj2/mxaccessgw
|
||||
|
||||
This project builds a gateway that gives modern clients full MXAccess parity
|
||||
without requiring those clients to load MXAccess COM, run as x86, or own an STA
|
||||
message pump. Treat the installed MXAccess COM component as the compatibility
|
||||
baseline.
|
||||
|
||||
Toolchain paths, versions, and external analysis locations are recorded in
|
||||
`docs/toolchain-links.md`. Use that file before searching for compilers,
|
||||
runtimes, protobuf tools, MXAccess notes, or Galaxy Repository SQL notes.
|
||||
|
||||
Implementation planning is recorded in `docs/implementation-plan-index.md`.
|
||||
Follow the order there unless the user explicitly reprioritizes: gateway first,
|
||||
MXAccess worker instance second, clients third.
|
||||
|
||||
## Core Contract
|
||||
|
||||
Preserve MXAccess behavior first:
|
||||
|
||||
- public MXAccess command semantics,
|
||||
- native MXAccess event families,
|
||||
- STA/message-pump delivery behavior,
|
||||
- installed-provider quirks,
|
||||
- HRESULT/status/value marshaling,
|
||||
- per-client isolation.
|
||||
|
||||
Do not simplify, normalize, or "fix" MXAccess behavior unless an explicit
|
||||
non-parity mode is being implemented and tested. `MxAsbClient` and managed NMX
|
||||
are future acceleration paths only; they do not define the parity contract.
|
||||
|
||||
## Architecture
|
||||
|
||||
The intended split is:
|
||||
|
||||
```text
|
||||
client
|
||||
-> gRPC over TCP
|
||||
-> .NET 10 x64 gateway
|
||||
-> session manager
|
||||
-> per-session .NET Framework 4.8 x86 worker process
|
||||
-> dedicated STA thread
|
||||
-> MXAccess COM instance
|
||||
-> Windows/COM message pump
|
||||
-> command queue
|
||||
-> event sink
|
||||
```
|
||||
|
||||
The gateway must never instantiate or call MXAccess directly. All MXAccess COM
|
||||
interaction belongs in the worker process on its dedicated STA thread.
|
||||
|
||||
The worker must not host public gRPC. Gateway-to-worker communication should use
|
||||
a small local IPC protocol, with named pipes and protobuf-framed messages as the
|
||||
default design.
|
||||
|
||||
## Runtime Targets
|
||||
|
||||
- Gateway: .NET 10, C#, x64 preferred, ASP.NET Core gRPC.
|
||||
- Worker: .NET Framework 4.8, C#, x86 by default.
|
||||
- Worker IPC: one bidirectional named pipe per worker.
|
||||
- Worker process model: one external client session maps to one worker by
|
||||
default.
|
||||
|
||||
## Style Guides
|
||||
|
||||
Follow the project documentation guide and the language guide for every changed
|
||||
area:
|
||||
|
||||
| Area | Style guide |
|
||||
|------|-------------|
|
||||
| Documentation | `StyleGuide.md` |
|
||||
| Gateway, worker, .NET client, and C# tests | `docs/style-guides/CSharpStyleGuide.md` |
|
||||
| Public gRPC and worker IPC contracts | `docs/style-guides/ProtobufStyleGuide.md` |
|
||||
| Go client | `docs/style-guides/GoStyleGuide.md` |
|
||||
| Rust client | `docs/style-guides/RustStyleGuide.md` |
|
||||
| Python client | `docs/style-guides/PythonStyleGuide.md` |
|
||||
| Java client | `docs/style-guides/JavaStyleGuide.md` |
|
||||
|
||||
When a change crosses languages, apply every affected style guide. Generated
|
||||
code follows its generator output; do not hand-edit it to match handwritten
|
||||
style.
|
||||
|
||||
## Expected Layout
|
||||
|
||||
Prefer this structure unless there is a strong reason to adjust it:
|
||||
|
||||
```text
|
||||
src/MxGateway.Contracts/
|
||||
Protos/
|
||||
mxaccess_gateway.proto
|
||||
mxaccess_worker.proto
|
||||
Generated/
|
||||
|
||||
src/MxGateway.Server/
|
||||
Program.cs
|
||||
Sessions/
|
||||
Workers/
|
||||
Grpc/
|
||||
Dashboard/
|
||||
Metrics/
|
||||
|
||||
src/MxGateway.Worker/
|
||||
Program.cs
|
||||
Ipc/
|
||||
Sta/
|
||||
MxAccess/
|
||||
Conversion/
|
||||
|
||||
src/MxGateway.Tests/
|
||||
contract tests
|
||||
gateway session tests
|
||||
fake worker tests
|
||||
|
||||
src/MxGateway.Worker.Tests/
|
||||
value/status conversion tests
|
||||
STA queue tests
|
||||
|
||||
src/MxGateway.IntegrationTests/
|
||||
optional live MXAccess tests
|
||||
|
||||
clients/dotnet/
|
||||
.NET 10 C# client library, test CLI, and tests
|
||||
|
||||
clients/go/
|
||||
Go client module, test CLI, and tests
|
||||
|
||||
clients/rust/
|
||||
Rust client crate, test CLI, and tests
|
||||
|
||||
clients/python/
|
||||
Python client package, test CLI, and tests
|
||||
|
||||
clients/java/
|
||||
Java client library, test CLI, and tests
|
||||
```
|
||||
|
||||
The contracts project may multi-target, or the `.proto` files may be shared as
|
||||
source inputs to both gateway and worker builds.
|
||||
|
||||
## Public API Shape
|
||||
|
||||
The external API should be session-oriented. Initial rollout should prefer
|
||||
unary `OpenSession`, `CloseSession`, and `Invoke`, plus server-streaming
|
||||
`StreamEvents`. Add a bidirectional `Session` stream after the command and event
|
||||
model is stable.
|
||||
|
||||
Do not compress MXAccess into generic verbs too early. Use a command enum with
|
||||
method-specific payloads so parity can be tested method by method.
|
||||
|
||||
Core MXAccess commands to represent:
|
||||
|
||||
- `Register`
|
||||
- `Unregister`
|
||||
- `AddItem`
|
||||
- `AddItem2`
|
||||
- `RemoveItem`
|
||||
- `Advise`
|
||||
- `UnAdvise`
|
||||
- `AdviseSupervisory`
|
||||
- `AddBufferedItem`
|
||||
- `SetBufferedUpdateInterval`
|
||||
- `Suspend`
|
||||
- `Activate`
|
||||
- `Write`
|
||||
- `Write2`
|
||||
- `WriteSecured`
|
||||
- `WriteSecured2`
|
||||
- `AuthenticateUser`
|
||||
- `ArchestrAUserToId`
|
||||
|
||||
Diagnostics may include `Ping`, `GetSessionState`, `GetWorkerInfo`,
|
||||
`DrainEvents`, and `ShutdownWorker`.
|
||||
|
||||
## Event Requirements
|
||||
|
||||
Represent every public MXAccess event family:
|
||||
|
||||
- `OnDataChange`
|
||||
- `OnWriteComplete`
|
||||
- `OperationComplete`
|
||||
- `OnBufferedDataChange`
|
||||
|
||||
Preserve per-worker event order. The gateway must not reorder events emitted by
|
||||
the same MXAccess instance.
|
||||
|
||||
Event DTOs should carry event family, session id, server handle, item handle,
|
||||
value, quality, timestamp, `MXSTATUS_PROXY[]` equivalent, raw HRESULT/status
|
||||
fields when available, event sequence, worker timestamp, and gateway receive
|
||||
timestamp.
|
||||
|
||||
## Value And Status Rules
|
||||
|
||||
Use a protobuf value union that can represent COM `VARIANT` values and arrays.
|
||||
When a value cannot be losslessly converted, preserve both the best typed
|
||||
projection and enough raw diagnostic metadata to reproduce the case.
|
||||
|
||||
Represent `MXSTATUS_PROXY` explicitly. Do not collapse status arrays into a
|
||||
single success flag.
|
||||
|
||||
Command replies should include protocol status, COM HRESULT if available,
|
||||
MXAccess return values, method-specific out parameters, and status arrays where
|
||||
the MXAccess method emits them.
|
||||
|
||||
## Galaxy Repository SQL Discovery
|
||||
|
||||
Galaxy tags, hierarchy, and attribute details can be queried from the AVEVA /
|
||||
Wonderware System Platform Galaxy Repository SQL Server database. Use this as a
|
||||
discovery and metadata path only; runtime MXAccess parity still belongs to the
|
||||
MXAccess-backed worker unless an explicit non-parity backend is being designed.
|
||||
|
||||
Full notes, schema details, screenshots, and query examples are in:
|
||||
|
||||
```text
|
||||
C:\Users\dohertj2\Desktop\lmxopcua\gr
|
||||
```
|
||||
|
||||
Important files in that notes directory:
|
||||
|
||||
- `connectioninfo.md` - SQL Server connection details and `sqlcmd` usage.
|
||||
- `layout.md` - hierarchy vs `tag_name` relationship.
|
||||
- `build_layout_plan.md` - extraction plan for hierarchy and attributes.
|
||||
- `schema.md` and `ddl/` - Galaxy Repository schema reference.
|
||||
- `queries/hierarchy.sql` - deployed object hierarchy.
|
||||
- `queries/attributes.sql` - user-defined dynamic attributes.
|
||||
- `queries/attributes_extended.sql` - system plus user-defined attributes.
|
||||
- `queries/change_detection.sql` - deployment-change polling via
|
||||
`galaxy.time_of_last_deploy`.
|
||||
|
||||
Current documented connection is SQL Server `localhost`, database `ZB`, Windows
|
||||
Auth. Example:
|
||||
|
||||
```powershell
|
||||
sqlcmd -S localhost -d ZB -E -Q "SELECT time_of_last_deploy FROM galaxy;"
|
||||
```
|
||||
|
||||
Key tables from the notes are `gobject`, `template_definition`,
|
||||
`dynamic_attribute`, `attribute_definition`, `primitive_instance`, and
|
||||
`galaxy`. The hierarchy uses contained names for human-readable browsing, while
|
||||
runtime tag references use globally unique `tag_name` values such as
|
||||
`<tag_name>.<AttributeName>`.
|
||||
|
||||
## MXAccess Analysis Source
|
||||
|
||||
Use the local MXAccess analysis project when answering questions about installed
|
||||
MXAccess classes, interfaces, fields, events, HRESULT/status behavior, value
|
||||
projection, captures, and parity gaps:
|
||||
|
||||
```text
|
||||
C:\Users\dohertj2\Desktop\mxaccess
|
||||
```
|
||||
|
||||
Primary files:
|
||||
|
||||
- `README.md` - overview of available analysis and capture artifacts.
|
||||
- `docs/MXAccess-Public-API.md` - COM class, ProgID, CLSID, method list,
|
||||
event signatures, `MxDataType`, `MxStatus`, and `MXSTATUS_PROXY`.
|
||||
- `docs/MXAccess-Reverse-Engineering.md` - installed runtime path and x86 COM
|
||||
constraints.
|
||||
- `docs/Current-Sprint-State.md` and `docs/DotNet10-Native-Library-Plan.md` -
|
||||
current parity gaps and managed native-client research status.
|
||||
- `src/MxTraceHarness/` - x86 MXAccess harness examples using the real COM
|
||||
interop assembly.
|
||||
- `captures/` and `analysis/` - observed native behavior and generated
|
||||
reverse-engineering artifacts.
|
||||
|
||||
Concrete MXAccess COM target from the analysis:
|
||||
|
||||
- class: `ArchestrA.MxAccess.LMXProxyServerClass`
|
||||
- CLSID: `{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}`
|
||||
- ProgID: `LMXProxy.LMXProxyServer.1`
|
||||
- version-independent ProgID: `LMXProxy.LMXProxyServer`
|
||||
- registered server: `C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll`
|
||||
- interop assembly:
|
||||
`C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`
|
||||
- threading model: `Apartment`
|
||||
|
||||
## Worker Rules
|
||||
|
||||
Each worker owns:
|
||||
|
||||
- one process,
|
||||
- one MXAccess session,
|
||||
- one dedicated STA thread,
|
||||
- one MXAccess COM object,
|
||||
- one inbound command queue,
|
||||
- one outbound event queue.
|
||||
|
||||
All MXAccess operations must run on the STA. A plain blocking queue is not
|
||||
enough for the STA; the STA loop must pump Windows/COM messages and service
|
||||
queued commands.
|
||||
|
||||
Do not block the STA on pipe writes, gRPC calls, or slow consumers. Event
|
||||
handlers should convert event args, enqueue outbound events, and return to
|
||||
pumping messages.
|
||||
|
||||
On graceful shutdown, reject new commands, optionally clean up active MXAccess
|
||||
handles, detach events, release the COM object, uninitialize COM, and exit. If
|
||||
graceful shutdown exceeds the configured timeout, the gateway may kill the
|
||||
worker.
|
||||
|
||||
## IPC Rules
|
||||
|
||||
Default pipe name shape:
|
||||
|
||||
```text
|
||||
mxaccess-gateway-{gatewayProcessId}-{sessionId}
|
||||
```
|
||||
|
||||
Frame messages as:
|
||||
|
||||
```text
|
||||
uint32 little-endian payload_length
|
||||
payload_length bytes protobuf WorkerEnvelope
|
||||
```
|
||||
|
||||
Every envelope should include protocol version, session id, monotonic sender
|
||||
sequence, correlation id, and a typed body. Protocol version mismatch should
|
||||
fail session creation.
|
||||
|
||||
Pipe security should be local-machine only, with ACLs restricted to the gateway
|
||||
identity and launched worker identity. Prefer a per-session nonce handshake.
|
||||
|
||||
## Gateway Rules
|
||||
|
||||
The gateway is responsible for:
|
||||
|
||||
- public TCP/gRPC API,
|
||||
- Blazor Server dashboard using Bootstrap CSS/JS only,
|
||||
- authn/authz when needed,
|
||||
- session creation and teardown,
|
||||
- worker launch and lifecycle management,
|
||||
- command routing,
|
||||
- event streaming,
|
||||
- leases, heartbeats, timeouts, and quotas,
|
||||
- worker kill/restart policy,
|
||||
- metrics and structured logs.
|
||||
|
||||
The gRPC layer should stay thin: validate request, find session, call the
|
||||
session worker client, map worker replies to public replies, and stream events.
|
||||
Keep MXAccess-specific translation logic testable outside the gRPC handlers.
|
||||
|
||||
Dashboard code should also stay thin and read-only for v1. Use a snapshot
|
||||
service over session/worker/metrics state; do not let Razor components mutate
|
||||
gateway sessions or workers directly. Do not use MudBlazor or other Blazor UI
|
||||
component libraries.
|
||||
|
||||
Gateway restart should not try to reattach old workers in the first version.
|
||||
Terminate orphaned workers on startup if that behavior is implemented.
|
||||
|
||||
## Command, Timeout, And Cancellation Semantics
|
||||
|
||||
Command lifecycle:
|
||||
|
||||
```text
|
||||
client gRPC command
|
||||
gateway validates session and payload
|
||||
gateway assigns correlation id
|
||||
gateway writes WorkerCommand to pipe
|
||||
worker queues command to STA
|
||||
STA executes MXAccess method
|
||||
worker captures return/out/status/HRESULT
|
||||
worker sends WorkerCommandReply
|
||||
gateway completes gRPC response
|
||||
```
|
||||
|
||||
Canceling a gRPC call should stop waiting in the gateway, but it cannot safely
|
||||
abort an in-flight COM call on the STA. Hard cancellation means killing the
|
||||
worker process.
|
||||
|
||||
If a command wedges the STA beyond a configured grace period, the gateway should
|
||||
kill the worker and fail the session.
|
||||
|
||||
## Backpressure Policy
|
||||
|
||||
Worker outbound events must use a bounded queue. For parity testing, prefer
|
||||
fail-fast behavior over silent drops. Production coalescing or drop policies
|
||||
must be explicit and observable.
|
||||
|
||||
The gateway should preserve per-session event order, apply backpressure from
|
||||
slow gRPC streams, and disconnect or coalesce only according to an explicit
|
||||
policy.
|
||||
|
||||
## Security And Logging
|
||||
|
||||
Use TLS for remote gRPC when crossing machine boundaries. Authentication may be
|
||||
Windows auth, mTLS, or a deployment-specific token.
|
||||
|
||||
Commands that write, authenticate users, or alter runtime state need explicit
|
||||
authorization design.
|
||||
|
||||
Never log passwords or raw credential values for `AuthenticateUser`,
|
||||
`WriteSecured`, or related secured operations. Do not log full values by
|
||||
default; make value logging opt-in and redacted.
|
||||
|
||||
## Testing Expectations
|
||||
|
||||
Use focused tests for:
|
||||
|
||||
- contract/protobuf compatibility,
|
||||
- gateway session state and worker lifecycle,
|
||||
- gateway behavior with a fake worker,
|
||||
- worker value/status conversion,
|
||||
- STA queue and message-pump behavior.
|
||||
|
||||
Live MXAccess integration tests are optional but should be isolated because they
|
||||
depend on installed COM components and provider behavior.
|
||||
|
||||
Parity tests should compare direct MXAccess behavior against the gateway:
|
||||
|
||||
- return values,
|
||||
- HRESULTs and exceptions,
|
||||
- event sequence,
|
||||
- value projection,
|
||||
- quality/status arrays,
|
||||
- invalid handle behavior,
|
||||
- cross-server handle behavior,
|
||||
- cleanup behavior.
|
||||
|
||||
Known important parity areas:
|
||||
|
||||
- `WriteSecured` may fail before a value-bearing NMX body is emitted.
|
||||
- `WriteSecured2` can succeed in observed native paths.
|
||||
- `OperationComplete` is distinct from write completion.
|
||||
- `OnBufferedDataChange` has a distinct public event shape.
|
||||
- Invalid handles and cross-server handles have specific exception/status
|
||||
behavior.
|
||||
- STA message pumping is required for event delivery.
|
||||
|
||||
## Source Update Workflow
|
||||
|
||||
When source code changes, build the affected component before handing work
|
||||
back. If the change crosses component boundaries, build each affected component
|
||||
instead of relying on a single top-level build.
|
||||
|
||||
Use the native build and test command for each changed area:
|
||||
|
||||
| Changed area | Required verification |
|
||||
|--------------|-----------------------|
|
||||
| Contracts or `.proto` files | regenerate generated code, then build gateway, worker, and every generated client touched by the contract |
|
||||
| Gateway server, sessions, workers, gRPC, dashboard, or metrics | build the .NET 10 gateway project and run affected gateway or fake-worker tests |
|
||||
| Worker IPC, STA, MXAccess, or conversion code | build the .NET Framework 4.8 x86 worker project and run affected worker tests |
|
||||
| Shared test infrastructure | run every test suite that consumes the changed helpers |
|
||||
| .NET client | build the .NET client library, CLI, and tests |
|
||||
| Go client | run Go formatting, build, and tests for the Go module |
|
||||
| Rust client | run Rust formatting, build or check, and tests for the Rust crate |
|
||||
| Python client | run Python formatting or linting if configured, package/build checks, and tests |
|
||||
| Java client | build the Java client library, CLI, and tests |
|
||||
| Integration tests | run them only when the required MXAccess COM component, provider state, and external services are available; otherwise document why they were skipped |
|
||||
|
||||
Update affected documentation in the same change as the source update. This
|
||||
includes `gateway.md`, component design docs under `docs/`, client docs, API
|
||||
contract notes, test instructions, and operational guidance. Documentation must
|
||||
follow `StyleGuide.md`: write technical present-tense prose, explain the reason
|
||||
for non-obvious choices, use exact code names, specify languages on code
|
||||
blocks, use relative links for internal docs, and avoid stale temporary notes.
|
||||
Source code and contract changes must also follow the relevant language guide
|
||||
from the Style Guides section.
|
||||
|
||||
Do not leave documentation describing old behavior after changing public APIs,
|
||||
contracts, configuration, build steps, security behavior, event shapes, value
|
||||
conversion, status mapping, lifecycle rules, or client semantics.
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
Build the smallest end-to-end slice first:
|
||||
|
||||
1. .NET 10 gateway starts.
|
||||
2. Client calls `OpenSession`.
|
||||
3. Gateway launches .NET Framework 4.8 x86 worker.
|
||||
4. Worker creates STA and MXAccess COM object.
|
||||
5. Client calls `Register`.
|
||||
6. Client calls `AddItem`.
|
||||
7. Client calls `Advise`.
|
||||
8. Worker forwards one `OnDataChange` event to the gateway.
|
||||
9. Gateway streams the event to the client.
|
||||
10. Client calls `CloseSession`.
|
||||
11. Gateway shuts down the worker.
|
||||
|
||||
That slice proves the high-risk requirements: process isolation, STA ownership,
|
||||
message pumping, command routing, and event streaming.
|
||||
@@ -1,125 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
`mxaccessgw` is the MXAccess Gateway: a gRPC service that gives modern (.NET, Go, Rust, Python, Java) clients full MXAccess parity without forcing them to load 32-bit MXAccess COM, run x86, or own an STA message pump.
|
||||
|
||||
The architecture is a two-process design — read `gateway.md` before making structural changes:
|
||||
|
||||
- **Gateway** (`src/MxGateway.Server`, .NET 10, x64): ASP.NET Core gRPC server. Owns the public API, sessions, auth, the Blazor dashboard, and the Galaxy Repository SQL browse RPCs. **Never instantiates MXAccess COM directly.**
|
||||
- **Worker** (`src/MxGateway.Worker`, .NET Framework 4.8, **x86**): one process per session. Owns one MXAccess COM instance on a dedicated STA, pumps Windows messages, and converts COM events to protobuf.
|
||||
- **IPC**: gateway↔worker uses one bidirectional named pipe per worker (`mxaccess-gateway-{gatewayPid}-{sessionId}`) with length-prefixed `WorkerEnvelope` protobuf frames. Gateway hosts the pipe server and launches the worker. **gRPC is not used inside the worker** — .NET Framework 4.8 doesn't have a first-class gRPC stack.
|
||||
- **Contracts** (`src/MxGateway.Contracts`): multi-targets `net10.0;net48` and owns the `.proto` files (`mxaccess_gateway.proto`, `mxaccess_worker.proto`, `galaxy_repository.proto`). All other projects consume the generated types from here. Do not hand-edit anything under `Generated/`.
|
||||
|
||||
The worker must do all MXAccess COM calls on its dedicated STA thread, and the STA loop must pump Windows messages (`MsgWaitForMultipleObjectsEx` + `PeekMessage`/`DispatchMessage`) so MXAccess events deliver. A plain blocking queue on an STA is not enough.
|
||||
|
||||
## Build, Test, Run
|
||||
|
||||
```powershell
|
||||
# Full solution build (gateway, worker, contracts, tests)
|
||||
dotnet build src/MxGateway.sln
|
||||
|
||||
# Worker must be built x86 — the gateway looks for MxGateway.Worker.exe under bin\x86
|
||||
dotnet build src/MxGateway.Worker/MxGateway.Worker.csproj -p:Platform=x86
|
||||
|
||||
# Gateway tests (no MXAccess required — uses FakeWorkerHarness)
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj
|
||||
dotnet test src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj -p:Platform=x86
|
||||
|
||||
# Run gateway locally (defaults bound under MxGateway:* in src/MxGateway.Server/appsettings.json)
|
||||
dotnet run --project src/MxGateway.Server/MxGateway.Server.csproj
|
||||
|
||||
# API-key admin CLI (same exe, "apikey" subcommand)
|
||||
dotnet run --project src/MxGateway.Server/MxGateway.Server.csproj -- apikey create --display-name "dev" --scopes session:open,session:close,invoke:read,invoke:write,invoke:secure,events:read,metadata:read,admin
|
||||
```
|
||||
|
||||
Single test by name (xUnit `--filter`):
|
||||
|
||||
```powershell
|
||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~GatewayEndToEndFakeWorkerSmokeTests
|
||||
```
|
||||
|
||||
Live MXAccess integration tests are **opt-in** because they need installed MXAccess COM and live provider state:
|
||||
|
||||
```powershell
|
||||
$env:MXGATEWAY_RUN_LIVE_MXACCESS_TESTS = "1"
|
||||
dotnet test src/MxGateway.IntegrationTests/MxGateway.IntegrationTests.csproj --filter FullyQualifiedName~WorkerLiveMxAccessSmokeTests
|
||||
```
|
||||
|
||||
Live LDAP tests use `MXGATEWAY_RUN_LIVE_LDAP_TESTS=1`. See `docs/GatewayTesting.md` for the full opt-in matrix and `LiveMxAccessFactAttribute` / `LiveLdapFactAttribute` for the gating logic.
|
||||
|
||||
## Clients
|
||||
|
||||
Each language client is in `clients/<lang>/` with its own README. They all consume the shared `.proto` files in `src/MxGateway.Contracts/Protos`:
|
||||
|
||||
- `clients/dotnet`: `dotnet build clients/dotnet/MxGateway.Client.sln`
|
||||
- `clients/python`: `python -m pip install -e ".[dev]"; python -m pytest`
|
||||
- `clients/rust`: `cargo test --workspace; cargo clippy --workspace --all-targets -- -D warnings`
|
||||
- `clients/java`: `gradle test` (Java 21)
|
||||
- Go client lives alongside as `mxgw-go` in the cross-language matrix
|
||||
|
||||
End-to-end matrix runner (needs running gateway + worker + valid API key):
|
||||
|
||||
```powershell
|
||||
$env:MXGATEWAY_API_KEY = "<api-key>"
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1
|
||||
```
|
||||
|
||||
## Repository-Specific Conventions
|
||||
|
||||
- **Build properties** (`src/Directory.Build.props`) enforce `Nullable=enable`, `TreatWarningsAsErrors=true`, latest analyzers, and `EnforceCodeStyleInBuild=true`. New warnings break the build — fix them, don't suppress unless the suppression has a narrow reason.
|
||||
- **Style guides** in `docs/style-guides/` are authoritative. Follow `CSharpStyleGuide.md` for gateway/worker/.NET-client code: file-scoped namespaces, `sealed` by default, `Async` suffix on Task-returning methods, MXAccess-aligned names (`MxStatusProxy`, `ServerHandle`, `ItemHandle`, `HResult`).
|
||||
- **MXAccess parity is the contract.** Don't "fix" surprising MXAccess behavior (e.g., `WriteSecured` failing before a value-bearing NMX body, distinct `OperationComplete` semantics, invalid-handle exceptions) unless the client explicitly opts into a non-parity mode. The installed MXAccess COM component is the baseline.
|
||||
- **Don't synthesize events.** The gateway forwards only events the worker emits; it never invents `OperationComplete` from write completion or command replies.
|
||||
- **One worker per session, one event subscriber per session** (v1). Multi-subscriber fan-out and reconnectable sessions are explicitly out of scope — see `docs/DesignDecisions.md`.
|
||||
- **Gateway restart does not reattach orphan workers.** The first version terminates orphaned workers on startup; do not design code paths that assume reattachment.
|
||||
- **No Blazor UI component libraries.** Dashboard uses local Bootstrap CSS/JS only — do not introduce MudBlazor, Radzen, FluentUI, etc.
|
||||
- **Don't log secrets or full tag values by default.** API keys, passwords, `WriteSecured` payloads, and `AuthenticateUser` credentials must never reach logs. Value logging is opt-in and redacted.
|
||||
- **Generated code** under `src/MxGateway.Contracts/Generated/`, `clients/*/generated*/`, `clients/python/src/mxgateway/generated/`, etc., is build output. Don't hand-edit. To regenerate, build the contracts project (`dotnet build src/MxGateway.Contracts/MxGateway.Contracts.csproj`) or run the per-client generation step in that client's README.
|
||||
- **Documentation style** (`StyleGuide.md`): PascalCase filenames, no marketing language, present tense, explain *why* not *what*.
|
||||
- **Update docs in the same change as the source.** When public APIs, contracts, configuration, build steps, security behavior, event shapes, value conversion, status mapping, or lifecycle rules change, the affected docs (`gateway.md`, `docs/`, client READMEs, design docs) must change in the same commit. Don't leave stale prose describing old behavior.
|
||||
|
||||
## Source Update Workflow
|
||||
|
||||
When source code changes, build and test the affected component before reporting work done. If the change crosses component boundaries, build each affected component — don't rely on a single top-level build:
|
||||
|
||||
| Changed area | Required verification |
|
||||
|---|---|
|
||||
| Contracts or `.proto` files | regenerate generated code, then build gateway, worker, and every generated client touched by the contract |
|
||||
| Gateway server, sessions, workers, gRPC, dashboard, metrics | `dotnet build src/MxGateway.Server` and run affected gateway / fake-worker tests |
|
||||
| Worker IPC, STA, MXAccess, conversion | `dotnet build src/MxGateway.Worker -p:Platform=x86` and run worker tests |
|
||||
| .NET client | `dotnet build clients/dotnet/MxGateway.Client.sln` and run its tests |
|
||||
| Go client | `gofmt`, `go build ./...`, `go test ./...` from `clients/go` |
|
||||
| Rust client | `cargo fmt`, `cargo check --workspace`, `cargo test --workspace`, `cargo clippy --all-targets -- -D warnings` from `clients/rust` |
|
||||
| Python client | `python -m pytest` from `clients/python` |
|
||||
| Java client | `gradle test` from `clients/java` |
|
||||
| Integration tests | run only when MXAccess COM, provider state, and external services are available; otherwise document why skipped |
|
||||
|
||||
## Design Sources To Consult Before Non-Trivial Changes
|
||||
|
||||
- `gateway.md` — top-level architecture, command/event surface, IPC envelope, STA thread model, fault handling.
|
||||
- `glauth.md` — local LDAP server (GLAuth on `localhost:3893`, base DN `dc=lmxopcua,dc=local`) used for dev authn. Pre-provisioned users (`admin/admin123`, `readonly/readonly123`, etc.) and the role→capability mapping live there.
|
||||
- `docs/DesignDecisions.md` — v1 choices (MXAccess COM target `LMXProxyServerClass` from `C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`, API-key-in-SQLite auth, fail-fast event backpressure, etc.).
|
||||
- `docs/GatewayProcessDesign.md`, `docs/MxAccessWorkerInstanceDesign.md`, `docs/WorkerFrameProtocol.md`, `docs/WorkerProcessLauncher.md` — detailed component designs.
|
||||
- `docs/GatewayConfiguration.md` — full `MxGateway:*` options bound by `GatewayOptions` and validated at startup by `GatewayOptionsValidator`.
|
||||
- `docs/GatewayTesting.md` — fake worker harness, live MXAccess smoke, parity matrix, cross-language smoke matrix.
|
||||
- `docs/ToolchainLinks.md` — installed compiler/SDK paths on this dev box (.NET 10.0.201, Go 1.26.2, Rust 1.95, Python 3.12.10, Temurin 21, protoc 34.1, etc.).
|
||||
|
||||
External analysis sources referenced by design docs:
|
||||
|
||||
- `C:\Users\dohertj2\Desktop\mxaccess` — MXAccess analysis project. Key files: `docs/MXAccess-Public-API.md` (COM class, ProgID, CLSID, method list, event signatures, `MxDataType`, `MxStatus`, `MXSTATUS_PROXY`), `docs/MXAccess-Reverse-Engineering.md` (installed runtime path, x86 COM constraints), `docs/Current-Sprint-State.md` (parity gaps), `src/MxTraceHarness/` (x86 harness using the real COM interop), `captures/` and `analysis/` (observed native behavior).
|
||||
- `C:\Users\dohertj2\Desktop\lmxopcua\gr` — Galaxy Repository (`ZB` SQL DB) notes. Key files: `connectioninfo.md`, `layout.md`, `schema.md`, `queries/hierarchy.sql`, `queries/attributes.sql`, `queries/attributes_extended.sql`, `queries/change_detection.sql`. Connection is SQL Server `localhost`, database `ZB`, Windows Auth.
|
||||
|
||||
## Authentication
|
||||
|
||||
Gateway gRPC clients authenticate with an API key in metadata: `authorization: Bearer mxgw_<key-id>_<secret>`. Keys are stored hashed (with a peppered SHA) in a gateway-owned SQLite DB (default `C:\ProgramData\MxGateway\gateway-auth.db`). Scopes (`session:open`, `session:close`, `invoke:read`, `invoke:write`, `invoke:secure`, `events:read`, `metadata:read`, `admin`) gate specific RPCs; missing → `Unauthenticated`, insufficient → `PermissionDenied`. The `apikey` subcommand on the server exe manages keys; see `src/MxGateway.Server/Security/Authentication/`.
|
||||
|
||||
Dashboard auth uses the same verifier but exchanges the API key for an HTTP-only secure cookie at `/dashboard/login`. `Dashboard:AllowAnonymousLocalhost` bypasses cookie auth on loopback when explicitly enabled.
|
||||
|
||||
## Process / Platform Notes
|
||||
|
||||
- Working tree is on Windows (`C:\Users\dohertj2\Desktop\mxaccessgw`). PowerShell is the native shell for tooling commands; bash is fine for git/grep/find.
|
||||
- The worker reference to `ArchestrA.MXAccess.dll` uses an absolute `HintPath` to `C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`. The worker only builds where MXAccess is installed (this dev box).
|
||||
- The repo is not a git repository at the top level — there's no `.git` directory in the working tree.
|
||||
@@ -1,140 +0,0 @@
|
||||
# Code Review Process
|
||||
|
||||
This document describes how to perform a comprehensive, per-module code review of
|
||||
the `mxaccessgw` codebase and how to track findings to resolution.
|
||||
|
||||
A **module** is one buildable project under `src/` (e.g. `src/MxGateway.Worker`)
|
||||
or one language client under `clients/` (e.g. `clients/rust`). Each module has
|
||||
its own folder under `code-reviews/` containing a single `findings.md`.
|
||||
|
||||
## 1. Before you start
|
||||
|
||||
1. Pick the module to review. Its folder is `code-reviews/<Module>/`:
|
||||
- For a `src/` project, `<Module>` is the project name with the `MxGateway.`
|
||||
prefix stripped — `src/MxGateway.Server` is reviewed in `code-reviews/Server/`.
|
||||
- For a language client, `<Module>` is `Client.<Lang>` — `clients/rust` is
|
||||
reviewed in `code-reviews/Client.Rust/`.
|
||||
2. Identify the design context for the module:
|
||||
- `gateway.md` — top-level architecture, command/event surface, IPC envelope,
|
||||
STA thread model, fault handling.
|
||||
- The relevant component design docs under `docs/` (e.g.
|
||||
`docs/MxAccessWorkerInstanceDesign.md`, `docs/GatewayProcessDesign.md`,
|
||||
`docs/Sessions.md`, `docs/Authentication.md`, `docs/GalaxyRepository.md`).
|
||||
- `docs/DesignDecisions.md` for the v1 design choices.
|
||||
- The **Repository-Specific Conventions** and **Process / Platform Notes** in
|
||||
`CLAUDE.md`.
|
||||
3. Record the exact commit being reviewed: `git rev-parse --short HEAD`. Every
|
||||
review is a snapshot — a finding only means something relative to a known
|
||||
commit.
|
||||
4. Open `code-reviews/<Module>/findings.md` and fill in the header table
|
||||
(reviewer, date, commit SHA, status).
|
||||
|
||||
## 2. Review checklist
|
||||
|
||||
Work through **every** category below for the module. A comprehensive review
|
||||
means the checklist is completed even where it produces no findings — record
|
||||
"No issues found" for a category rather than leaving it ambiguous.
|
||||
|
||||
1. **Correctness & logic bugs** — off-by-one, null handling, incorrect
|
||||
conditionals, misuse of APIs, broken edge cases.
|
||||
2. **mxaccessgw conventions** — the rules in `CLAUDE.md` and the style guides
|
||||
under `docs/style-guides/`: the gateway never instantiates MXAccess COM
|
||||
directly; all MXAccess COM calls run on the worker's dedicated STA thread and
|
||||
the STA loop pumps Windows messages; IPC uses one bidirectional named pipe per
|
||||
worker carrying length-prefixed `WorkerEnvelope` protobuf frames; MXAccess
|
||||
parity is the contract (don't "fix" surprising MXAccess behaviour, never
|
||||
synthesize events); one worker and one event subscriber per session; the
|
||||
gateway terminates orphan workers on startup and does not reattach; C# style
|
||||
(file-scoped namespaces, `sealed` by default, `Async` suffix, MXAccess-aligned
|
||||
names); no Blazor UI component libraries; no logging of secrets or full tag
|
||||
values; generated code is never hand-edited.
|
||||
3. **Concurrency & thread safety** — shared mutable state, STA affinity, race
|
||||
conditions, correct use of `async`/`await`, locking, disposal races.
|
||||
4. **Error handling & resilience** — exception paths, worker crash / reconnect
|
||||
handling, fail-fast event backpressure, transient vs permanent error
|
||||
classification, graceful degradation, correct gRPC status codes.
|
||||
5. **Security** — authentication/authorization checks, API-key scope enforcement,
|
||||
input validation, SQL injection in the Galaxy Repository RPCs, secret
|
||||
handling, the dashboard anonymous-localhost bypass, logging of sensitive data.
|
||||
6. **Performance & resource management** — `IDisposable` disposal, pipe / stream
|
||||
/ COM lifetimes, buffering and back-pressure, unnecessary allocations on hot
|
||||
paths, N+1 queries.
|
||||
7. **Design-document adherence** — does the code match `gateway.md`, the relevant
|
||||
`docs/` component designs, `docs/DesignDecisions.md`, and `CLAUDE.md`? Flag
|
||||
both code that drifts from the design and design docs that are now stale.
|
||||
8. **Code organization & conventions** — namespace hierarchy, project layout, the
|
||||
Options pattern, separation of concerns, additive-only contract evolution.
|
||||
9. **Testing coverage** — are the module's behaviours covered by tests
|
||||
(`src/MxGateway.Tests`, `src/MxGateway.Worker.Tests`,
|
||||
`src/MxGateway.IntegrationTests`)? Note untested critical paths and missing
|
||||
edge-case tests.
|
||||
10. **Documentation & comments** — XML doc accuracy, misleading or stale comments,
|
||||
undocumented non-obvious behaviour.
|
||||
|
||||
## 3. Recording findings
|
||||
|
||||
Add one entry per finding to the `## Findings` section of the module's
|
||||
`findings.md`, using the entry format in
|
||||
[`_template/findings.md`](code-reviews/_template/findings.md).
|
||||
|
||||
- **Finding ID** — `<Module>-NNN`, numbered sequentially within the module and
|
||||
never reused (e.g. `Worker-001`). IDs are permanent even after resolution.
|
||||
- **Severity:**
|
||||
- **Critical** — data loss, security breach, crash/deadlock, or outage.
|
||||
- **High** — incorrect behaviour with significant impact; no safe workaround.
|
||||
- **Medium** — incorrect or risky behaviour with limited impact or a workaround.
|
||||
- **Low** — minor issues, style, maintainability, documentation.
|
||||
- **Category** — one of the 10 checklist categories above.
|
||||
- **Location** — `file:line` (clickable), or a list of locations.
|
||||
- **Description** — what is wrong and why it matters.
|
||||
- **Recommendation** — concrete suggested fix.
|
||||
|
||||
After recording findings, update the module header table (status, open-finding
|
||||
count) and regenerate the base README (step 5).
|
||||
|
||||
## 4. Marking an item resolved
|
||||
|
||||
Findings are **never deleted** — they are an audit trail. To close one, change
|
||||
its **Status** and complete the **Resolution** field:
|
||||
|
||||
- `Open` — newly recorded, not yet addressed.
|
||||
- `In Progress` — a fix is actively being worked on.
|
||||
- `Resolved` — fixed. The Resolution field must state the fixing commit SHA, the
|
||||
date, and a one-line description of the fix.
|
||||
- `Won't Fix` — intentionally not fixed. The Resolution field must justify why.
|
||||
- `Deferred` — valid but postponed. The Resolution field must say what it is
|
||||
waiting on (e.g. a tracked issue or a later milestone).
|
||||
|
||||
`Resolved`, `Won't Fix`, and `Deferred` findings are all considered **closed**.
|
||||
`Open` and `In Progress` are **pending** and appear in the base README's Pending
|
||||
Findings table.
|
||||
|
||||
## 5. Updating the base README
|
||||
|
||||
`code-reviews/README.md` holds the single cross-module view (the Module Status
|
||||
table and the Pending / Closed Findings tables). It is **generated** from the
|
||||
per-module `findings.md` files — do not edit it by hand.
|
||||
|
||||
After any review or status change, regenerate it:
|
||||
|
||||
```
|
||||
python code-reviews/regen-readme.py
|
||||
```
|
||||
|
||||
`regen-readme.py --check` exits non-zero if `README.md` is stale, if a module
|
||||
header's `Open findings` count disagrees with its finding statuses, or if a
|
||||
finding carries an unrecognised Status value. The PowerShell wrapper
|
||||
`scripts/check-code-reviews-readme.ps1` runs that check and is the intended hook
|
||||
for CI or a pre-commit step.
|
||||
|
||||
> The repo's installed `python` is the real interpreter; the bare `python3`
|
||||
> alias resolves to the Windows Store stub and fails. Use `python`.
|
||||
|
||||
The per-module `findings.md` files are the source of truth; `README.md` is the
|
||||
aggregated index and must always agree with them — which the script guarantees.
|
||||
|
||||
## 6. Re-reviewing a module
|
||||
|
||||
Re-reviews append to the same `findings.md`. Update the header to the new commit
|
||||
and date, continue the finding numbering from the last used ID, and leave prior
|
||||
findings (including closed ones) in place as history.
|
||||
@@ -2,14 +2,11 @@ using System.Globalization;
|
||||
|
||||
namespace MxGateway.Client.Cli;
|
||||
|
||||
/// <summary>Parses command-line arguments into flags and named values.</summary>
|
||||
internal sealed class CliArguments
|
||||
{
|
||||
private readonly Dictionary<string, string> _values = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _flags = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Initializes a new instance by parsing the given command-line arguments.</summary>
|
||||
/// <param name="args">Unparsed command-line arguments; flags prefixed with '--' and values follow their flag.</param>
|
||||
public CliArguments(IEnumerable<string> args)
|
||||
{
|
||||
string? pendingName = null;
|
||||
@@ -42,15 +39,11 @@ internal sealed class CliArguments
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns whether the named flag was present in the arguments.</summary>
|
||||
/// <param name="name">The flag name (without '--' prefix).</param>
|
||||
public bool HasFlag(string name)
|
||||
{
|
||||
return _flags.Contains(name);
|
||||
}
|
||||
|
||||
/// <summary>Returns the value for a named argument, or <c>null</c> if absent.</summary>
|
||||
/// <param name="name">The argument name (without '--' prefix).</param>
|
||||
public string? GetOptional(string name)
|
||||
{
|
||||
return _values.TryGetValue(name, out string? value)
|
||||
@@ -58,8 +51,6 @@ internal sealed class CliArguments
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>Returns the value for a required named argument, or throws if absent.</summary>
|
||||
/// <param name="name">The argument name (without '--' prefix).</param>
|
||||
public string GetRequired(string name)
|
||||
{
|
||||
string? value = GetOptional(name);
|
||||
@@ -71,9 +62,6 @@ internal sealed class CliArguments
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>Parses and returns an int32 argument, or the default value if absent.</summary>
|
||||
/// <param name="name">The argument name (without '--' prefix).</param>
|
||||
/// <param name="defaultValue">The default value if the argument is absent; if <c>null</c>, the argument is required.</param>
|
||||
public int GetInt32(string name, int? defaultValue = null)
|
||||
{
|
||||
string? value = GetOptional(name);
|
||||
@@ -90,9 +78,6 @@ internal sealed class CliArguments
|
||||
return int.Parse(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>Parses and returns a uint32 argument, or the default value if absent.</summary>
|
||||
/// <param name="name">The argument name (without '--' prefix).</param>
|
||||
/// <param name="defaultValue">The default value if the argument is absent.</param>
|
||||
public uint GetUInt32(string name, uint defaultValue)
|
||||
{
|
||||
string? value = GetOptional(name);
|
||||
@@ -101,9 +86,6 @@ internal sealed class CliArguments
|
||||
: uint.Parse(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>Parses and returns a uint64 argument, or the default value if absent.</summary>
|
||||
/// <param name="name">The argument name (without '--' prefix).</param>
|
||||
/// <param name="defaultValue">The default value if the argument is absent.</param>
|
||||
public ulong GetUInt64(string name, ulong defaultValue)
|
||||
{
|
||||
string? value = GetOptional(name);
|
||||
@@ -112,9 +94,6 @@ internal sealed class CliArguments
|
||||
: ulong.Parse(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>Parses and returns a TimeSpan argument, or the default value if absent. Supports "ms", "s", and standard TimeSpan format.</summary>
|
||||
/// <param name="name">The argument name (without '--' prefix).</param>
|
||||
/// <param name="defaultValue">The default value if the argument is absent.</param>
|
||||
public TimeSpan GetDuration(string name, TimeSpan defaultValue)
|
||||
{
|
||||
string? value = GetOptional(name);
|
||||
|
||||
@@ -5,82 +5,34 @@ namespace MxGateway.Client.Cli;
|
||||
|
||||
public interface IMxGatewayCliClient : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Opens a new gateway session.
|
||||
/// </summary>
|
||||
/// <param name="request">Session open request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The session open reply.</returns>
|
||||
Task<OpenSessionReply> OpenSessionAsync(
|
||||
OpenSessionRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Closes an open gateway session.
|
||||
/// </summary>
|
||||
/// <param name="request">Session close request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The session close reply.</returns>
|
||||
Task<CloseSessionReply> CloseSessionAsync(
|
||||
CloseSessionRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes an MXAccess command on the session.
|
||||
/// </summary>
|
||||
/// <param name="request">The command request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The command reply.</returns>
|
||||
Task<MxCommandReply> InvokeAsync(
|
||||
MxCommandRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Streams events from the gateway session.
|
||||
/// </summary>
|
||||
/// <param name="request">The stream events request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>An async enumerable of events.</returns>
|
||||
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||
StreamEventsRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Tests connection to the Galaxy Repository.
|
||||
/// </summary>
|
||||
/// <param name="request">The connection test request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The connection test reply.</returns>
|
||||
Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
||||
TestConnectionRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last deployment time from the Galaxy Repository.
|
||||
/// </summary>
|
||||
/// <param name="request">The last deploy time request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The last deploy time reply.</returns>
|
||||
Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
||||
GetLastDeployTimeRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Discovers the Galaxy Repository hierarchy.
|
||||
/// </summary>
|
||||
/// <param name="request">The discover hierarchy request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The discover hierarchy reply.</returns>
|
||||
Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
||||
DiscoverHierarchyRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Watches for deployment events from the Galaxy Repository.
|
||||
/// </summary>
|
||||
/// <param name="request">The watch deploy events request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>An async enumerable of deployment events.</returns>
|
||||
IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
||||
WatchDeployEventsRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
@@ -9,10 +9,6 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
||||
private readonly MxGatewayClient _client;
|
||||
private readonly Lazy<GalaxyRepositoryClient> _galaxyClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MxGatewayCliClientAdapter"/> that bridges the CLI to the gateway client.
|
||||
/// </summary>
|
||||
/// <param name="client">The gateway client to adapt.</param>
|
||||
public MxGatewayCliClientAdapter(MxGatewayClient client)
|
||||
{
|
||||
_client = client;
|
||||
@@ -20,7 +16,6 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
||||
() => GalaxyRepositoryClient.Create(_client.Options));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<OpenSessionReply> OpenSessionAsync(
|
||||
OpenSessionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -28,7 +23,6 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
||||
return _client.OpenSessionRawAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<CloseSessionReply> CloseSessionAsync(
|
||||
CloseSessionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -36,7 +30,6 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
||||
return _client.CloseSessionRawAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<MxCommandReply> InvokeAsync(
|
||||
MxCommandRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -44,7 +37,6 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
||||
return _client.InvokeAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||
StreamEventsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -52,7 +44,6 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
||||
return _client.StreamEventsAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
||||
TestConnectionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -60,7 +51,6 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
||||
return _galaxyClient.Value.TestConnectionRawAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
||||
GetLastDeployTimeRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -68,7 +58,6 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
||||
return _galaxyClient.Value.GetLastDeployTimeRawAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
||||
DiscoverHierarchyRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -76,7 +65,6 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
||||
return _galaxyClient.Value.DiscoverHierarchyRawAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
||||
WatchDeployEventsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -84,7 +72,6 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
||||
return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_galaxyClient.IsValueCreated)
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
namespace MxGateway.Client.Cli;
|
||||
|
||||
/// <summary>Utility to redact API keys from error messages for safe output.</summary>
|
||||
internal static class MxGatewayCliSecretRedactor
|
||||
{
|
||||
/// <summary>Replaces occurrences of the API key in the value with a redacted placeholder.</summary>
|
||||
/// <param name="value">The message text to redact.</param>
|
||||
/// <param name="apiKey">The API key to remove; no redaction if null or empty.</param>
|
||||
public static string Redact(string value, string? apiKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(apiKey))
|
||||
|
||||
@@ -7,7 +7,6 @@ using MxGateway.Contracts.Proto.Galaxy;
|
||||
|
||||
namespace MxGateway.Client.Cli;
|
||||
|
||||
/// <summary>Command-line interface for the MXAccess Gateway client, supporting session and command operations.</summary>
|
||||
public static class MxGatewayClientCli
|
||||
{
|
||||
private const uint MaxAggregateEvents = 10_000;
|
||||
@@ -16,10 +15,6 @@ public static class MxGatewayClientCli
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
/// <summary>Runs the CLI synchronously with the given arguments, writing output and errors.</summary>
|
||||
/// <param name="args">Command-line arguments (command name followed by options).</param>
|
||||
/// <param name="standardOutput">TextWriter for command output.</param>
|
||||
/// <param name="standardError">TextWriter for error messages.</param>
|
||||
public static int Run(
|
||||
string[] args,
|
||||
TextWriter standardOutput,
|
||||
@@ -30,11 +25,6 @@ public static class MxGatewayClientCli
|
||||
.GetResult();
|
||||
}
|
||||
|
||||
/// <summary>Runs the CLI asynchronously with the given arguments, writing output and errors.</summary>
|
||||
/// <param name="args">Command-line arguments (command name followed by options).</param>
|
||||
/// <param name="standardOutput">TextWriter for command output.</param>
|
||||
/// <param name="standardError">TextWriter for error messages.</param>
|
||||
/// <param name="clientFactory">Optional factory to create the gateway client; defaults to MxGatewayClient.Create.</param>
|
||||
public static Task<int> RunAsync(
|
||||
string[] args,
|
||||
TextWriter standardOutput,
|
||||
@@ -122,10 +112,7 @@ public static class MxGatewayClientCli
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
// Redact the effective API key — whether it came from --api-key or from
|
||||
// the (documented default) --api-key-env environment variable — so a
|
||||
// transport error message that echoes the bearer token is never printed.
|
||||
string? apiKey = TryResolveApiKey(arguments);
|
||||
string? apiKey = arguments.GetOptional("api-key");
|
||||
string message = MxGatewayCliSecretRedactor.Redact(exception.Message, apiKey);
|
||||
|
||||
if (arguments.HasFlag("json"))
|
||||
@@ -170,27 +157,6 @@ public static class MxGatewayClientCli
|
||||
}
|
||||
|
||||
private static string ResolveApiKey(CliArguments arguments)
|
||||
{
|
||||
string? apiKey = TryResolveApiKey(arguments);
|
||||
if (!string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
string apiKeyEnvironmentName = arguments.GetOptional("api-key-env")
|
||||
?? "MXGATEWAY_API_KEY";
|
||||
|
||||
throw new ArgumentException(
|
||||
$"Gateway API key is required. Pass --api-key or set {apiKeyEnvironmentName}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective API key from <c>--api-key</c> or, failing that, the
|
||||
/// environment variable named by <c>--api-key-env</c> (default
|
||||
/// <c>MXGATEWAY_API_KEY</c>). Returns <see langword="null"/> when no key is
|
||||
/// configured; used for redaction where a missing key must not throw.
|
||||
/// </summary>
|
||||
private static string? TryResolveApiKey(CliArguments arguments)
|
||||
{
|
||||
string? apiKey = arguments.GetOptional("api-key");
|
||||
if (!string.IsNullOrWhiteSpace(apiKey))
|
||||
@@ -201,7 +167,14 @@ public static class MxGatewayClientCli
|
||||
string apiKeyEnvironmentName = arguments.GetOptional("api-key-env")
|
||||
?? "MXGATEWAY_API_KEY";
|
||||
|
||||
return Environment.GetEnvironmentVariable(apiKeyEnvironmentName);
|
||||
apiKey = Environment.GetEnvironmentVariable(apiKeyEnvironmentName);
|
||||
if (!string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
throw new ArgumentException(
|
||||
$"Gateway API key is required. Pass --api-key or set {apiKeyEnvironmentName}.");
|
||||
}
|
||||
|
||||
private static CancellationTokenSource CreateCancellation(CliArguments arguments, string command)
|
||||
|
||||
@@ -3,73 +3,32 @@ using MxGateway.Contracts.Proto.Galaxy;
|
||||
|
||||
namespace MxGateway.Client.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Fake Galaxy Repository client transport for testing.
|
||||
/// </summary>
|
||||
internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions options) : IGalaxyRepositoryClientTransport
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the gateway client options.
|
||||
/// </summary>
|
||||
public MxGatewayClientOptions Options { get; } = options;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw gRPC client; always null for the fake.
|
||||
/// </summary>
|
||||
public GalaxyRepository.GalaxyRepositoryClient? RawClient => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of TestConnection RPC calls made by the client.
|
||||
/// </summary>
|
||||
public List<(TestConnectionRequest Request, CallOptions CallOptions)> TestConnectionCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of GetLastDeployTime RPC calls made by the client.
|
||||
/// </summary>
|
||||
public List<(GetLastDeployTimeRequest Request, CallOptions CallOptions)> GetLastDeployTimeCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of DiscoverHierarchy RPC calls made by the client.
|
||||
/// </summary>
|
||||
public List<(DiscoverHierarchyRequest Request, CallOptions CallOptions)> DiscoverHierarchyCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reply to return from TestConnection; defaults to successful response.
|
||||
/// </summary>
|
||||
public TestConnectionReply TestConnectionReply { get; set; } = new() { Ok = true };
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reply to return from GetLastDeployTime; defaults to no deploy time present.
|
||||
/// </summary>
|
||||
public GetLastDeployTimeReply GetLastDeployTimeReply { get; set; } = new() { Present = false };
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reply to return from DiscoverHierarchy; defaults to empty response.
|
||||
/// </summary>
|
||||
public DiscoverHierarchyReply DiscoverHierarchyReply { get; set; } = new();
|
||||
|
||||
public Queue<DiscoverHierarchyReply> DiscoverHierarchyReplies { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the queue of exceptions to throw from TestConnection; dequeued in FIFO order.
|
||||
/// </summary>
|
||||
public Queue<Exception> TestConnectionExceptions { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the queue of exceptions to throw from GetLastDeployTime; dequeued in FIFO order.
|
||||
/// </summary>
|
||||
public Queue<Exception> GetLastDeployTimeExceptions { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the queue of exceptions to throw from DiscoverHierarchy; dequeued in FIFO order.
|
||||
/// </summary>
|
||||
public Queue<Exception> DiscoverHierarchyExceptions { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Records the request and either throws a queued exception or returns the configured reply.
|
||||
/// </summary>
|
||||
/// <param name="request">The TestConnectionRequest to process.</param>
|
||||
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||
public Task<TestConnectionReply> TestConnectionAsync(
|
||||
TestConnectionRequest request,
|
||||
CallOptions callOptions)
|
||||
@@ -83,11 +42,6 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
||||
return Task.FromResult(TestConnectionReply);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the request and either throws a queued exception or returns the configured reply.
|
||||
/// </summary>
|
||||
/// <param name="request">The GetLastDeployTimeRequest to process.</param>
|
||||
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||
public Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
||||
GetLastDeployTimeRequest request,
|
||||
CallOptions callOptions)
|
||||
@@ -101,11 +55,6 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
||||
return Task.FromResult(GetLastDeployTimeReply);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the request and either throws a queued exception or returns the configured reply.
|
||||
/// </summary>
|
||||
/// <param name="request">The DiscoverHierarchyRequest to process.</param>
|
||||
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||
public Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
||||
DiscoverHierarchyRequest request,
|
||||
CallOptions callOptions)
|
||||
@@ -122,19 +71,10 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
||||
: DiscoverHierarchyReply);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of WatchDeployEvents RPC calls made by the client.
|
||||
/// </summary>
|
||||
public List<(WatchDeployEventsRequest Request, CallOptions CallOptions)> WatchDeployEventsCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of events to stream from WatchDeployEvents.
|
||||
/// </summary>
|
||||
public List<DeployEvent> WatchDeployEvents { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the exception to throw from WatchDeployEvents, if any.
|
||||
/// </summary>
|
||||
public Exception? WatchDeployEventsException { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -143,11 +83,6 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
||||
/// </summary>
|
||||
public Func<CancellationToken, Task>? WatchDeployEventsBeforeYield { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Records the request and streams events, checking for queued exceptions and calling WatchDeployEventsBeforeYield before each event.
|
||||
/// </summary>
|
||||
/// <param name="request">The WatchDeployEventsRequest to process.</param>
|
||||
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||
WatchDeployEventsRequest request,
|
||||
CallOptions callOptions)
|
||||
|
||||
@@ -3,65 +3,23 @@ using MxGateway.Contracts.Proto;
|
||||
|
||||
namespace MxGateway.Client.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Fake implementation of IMxGatewayClientTransport for testing.
|
||||
/// </summary>
|
||||
internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMxGatewayClientTransport
|
||||
{
|
||||
private readonly Queue<MxCommandReply> _invokeReplies = new();
|
||||
private readonly List<MxEvent> _events = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the gateway client options.
|
||||
/// </summary>
|
||||
public MxGatewayClientOptions Options { get; } = options;
|
||||
|
||||
/// <summary>
|
||||
/// Gets null, since this is a test fake without a real gRPC client.
|
||||
/// </summary>
|
||||
public MxAccessGateway.MxAccessGatewayClient? RawClient => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of captured OpenSessionAsync calls.
|
||||
/// </summary>
|
||||
public List<(OpenSessionRequest Request, CallOptions CallOptions)> OpenSessionCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of captured CloseSessionAsync calls.
|
||||
/// </summary>
|
||||
public List<(CloseSessionRequest Request, CallOptions CallOptions)> CloseSessionCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of captured InvokeAsync calls.
|
||||
/// </summary>
|
||||
public List<(MxCommandRequest Request, CallOptions CallOptions)> InvokeCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of captured StreamEventsAsync calls.
|
||||
/// </summary>
|
||||
public List<(StreamEventsRequest Request, CallOptions CallOptions)> StreamEventsCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of captured AcknowledgeAlarmAsync calls.
|
||||
/// </summary>
|
||||
public List<(AcknowledgeAlarmRequest Request, CallOptions CallOptions)> AcknowledgeAlarmCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of captured QueryActiveAlarmsAsync calls.
|
||||
/// </summary>
|
||||
public List<(QueryActiveAlarmsRequest Request, CallOptions CallOptions)> QueryActiveAlarmsCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the queue of exceptions to throw from AcknowledgeAlarmAsync.
|
||||
/// </summary>
|
||||
public Queue<Exception> AcknowledgeAlarmExceptions { get; } = new();
|
||||
|
||||
private readonly Queue<AcknowledgeAlarmReply> _acknowledgeReplies = new();
|
||||
private readonly List<ActiveAlarmSnapshot> _activeAlarmSnapshots = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reply to return from OpenSessionAsync.
|
||||
/// </summary>
|
||||
public OpenSessionReply OpenSessionReply { get; set; } = new()
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
@@ -71,9 +29,6 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reply to return from CloseSessionAsync.
|
||||
/// </summary>
|
||||
public CloseSessionReply CloseSessionReply { get; set; } = new()
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
@@ -81,39 +36,12 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the queue of exceptions to throw from OpenSessionAsync.
|
||||
/// </summary>
|
||||
public Queue<Exception> OpenSessionExceptions { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the queue of exceptions to throw from CloseSessionAsync.
|
||||
/// </summary>
|
||||
public Queue<Exception> CloseSessionExceptions { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether thrown <see cref="RpcException"/>s are mapped
|
||||
/// to <see cref="MxGatewayException"/> the way the production gRPC transport does. Lets
|
||||
/// retry tests exercise the wrapped-exception predicate branch that runs in production.
|
||||
/// </summary>
|
||||
public bool MapTransportExceptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional hook awaited inside CloseSessionAsync after the call is
|
||||
/// recorded; lets tests pause a close mid-flight to observe concurrent dispose.
|
||||
/// </summary>
|
||||
public Func<Task>? CloseSessionHook { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the queue of exceptions to throw from InvokeAsync.
|
||||
/// </summary>
|
||||
public Queue<Exception> InvokeExceptions { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the OpenSessionAsync call is recorded and returns the configured reply.
|
||||
/// </summary>
|
||||
/// <param name="request">The OpenSessionRequest to process.</param>
|
||||
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||
public Task<OpenSessionReply> OpenSessionAsync(
|
||||
OpenSessionRequest request,
|
||||
CallOptions callOptions)
|
||||
@@ -121,41 +49,25 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
||||
OpenSessionCalls.Add((request, callOptions));
|
||||
if (OpenSessionExceptions.TryDequeue(out Exception? exception))
|
||||
{
|
||||
throw Translate(exception, callOptions);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
return Task.FromResult(OpenSessionReply);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the CloseSessionAsync call is recorded and returns the configured reply.
|
||||
/// </summary>
|
||||
/// <param name="request">The CloseSessionRequest to process.</param>
|
||||
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||
public async Task<CloseSessionReply> CloseSessionAsync(
|
||||
public Task<CloseSessionReply> CloseSessionAsync(
|
||||
CloseSessionRequest request,
|
||||
CallOptions callOptions)
|
||||
{
|
||||
CloseSessionCalls.Add((request, callOptions));
|
||||
|
||||
if (CloseSessionHook is not null)
|
||||
{
|
||||
await CloseSessionHook().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (CloseSessionExceptions.TryDequeue(out Exception? exception))
|
||||
{
|
||||
throw Translate(exception, callOptions);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
return CloseSessionReply;
|
||||
return Task.FromResult(CloseSessionReply);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the InvokeAsync call is recorded and returns the next enqueued reply.
|
||||
/// </summary>
|
||||
/// <param name="request">The MxCommandRequest to process.</param>
|
||||
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||
public Task<MxCommandReply> InvokeAsync(
|
||||
MxCommandRequest request,
|
||||
CallOptions callOptions)
|
||||
@@ -163,17 +75,12 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
||||
InvokeCalls.Add((request, callOptions));
|
||||
if (InvokeExceptions.TryDequeue(out Exception? exception))
|
||||
{
|
||||
throw Translate(exception, callOptions);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
return Task.FromResult(_invokeReplies.Dequeue());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the StreamEventsAsync call is recorded and yields all enqueued events.
|
||||
/// </summary>
|
||||
/// <param name="request">The StreamEventsRequest to process.</param>
|
||||
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||
StreamEventsRequest request,
|
||||
CallOptions callOptions)
|
||||
@@ -188,88 +95,13 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a reply to be returned from the next InvokeAsync call.
|
||||
/// </summary>
|
||||
/// <param name="reply">The reply to enqueue.</param>
|
||||
public void AddInvokeReply(MxCommandReply reply)
|
||||
{
|
||||
_invokeReplies.Enqueue(reply);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues an event to be yielded from StreamEventsAsync.
|
||||
/// </summary>
|
||||
/// <param name="gatewayEvent">The event to enqueue.</param>
|
||||
public void AddEvent(MxEvent gatewayEvent)
|
||||
{
|
||||
_events.Add(gatewayEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the acknowledge call and returns the next enqueued reply (or default).
|
||||
/// </summary>
|
||||
public Task<AcknowledgeAlarmReply> AcknowledgeAlarmAsync(
|
||||
AcknowledgeAlarmRequest request,
|
||||
CallOptions callOptions)
|
||||
{
|
||||
AcknowledgeAlarmCalls.Add((request, callOptions));
|
||||
if (AcknowledgeAlarmExceptions.TryDequeue(out Exception? exception))
|
||||
{
|
||||
throw exception;
|
||||
}
|
||||
|
||||
return Task.FromResult(_acknowledgeReplies.Count > 0
|
||||
? _acknowledgeReplies.Dequeue()
|
||||
: new AcknowledgeAlarmReply
|
||||
{
|
||||
SessionId = request.SessionId,
|
||||
CorrelationId = request.ClientCorrelationId,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
Status = new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok },
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the query call and yields each enqueued snapshot.
|
||||
/// </summary>
|
||||
public async IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync(
|
||||
QueryActiveAlarmsRequest request,
|
||||
CallOptions callOptions)
|
||||
{
|
||||
QueryActiveAlarmsCalls.Add((request, callOptions));
|
||||
|
||||
foreach (ActiveAlarmSnapshot snapshot in _activeAlarmSnapshots)
|
||||
{
|
||||
callOptions.CancellationToken.ThrowIfCancellationRequested();
|
||||
await Task.Yield();
|
||||
yield return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Enqueues an acknowledge reply.</summary>
|
||||
public void AddAcknowledgeReply(AcknowledgeAlarmReply reply)
|
||||
{
|
||||
_acknowledgeReplies.Enqueue(reply);
|
||||
}
|
||||
|
||||
/// <summary>Enqueues a snapshot to be yielded from QueryActiveAlarmsAsync.</summary>
|
||||
public void AddActiveAlarmSnapshot(ActiveAlarmSnapshot snapshot)
|
||||
{
|
||||
_activeAlarmSnapshots.Add(snapshot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a queued exception the way the production gRPC transport does when
|
||||
/// <see cref="MapTransportExceptions"/> is set; otherwise returns it unchanged.
|
||||
/// </summary>
|
||||
private Exception Translate(Exception exception, CallOptions callOptions)
|
||||
{
|
||||
if (MapTransportExceptions && exception is RpcException rpcException)
|
||||
{
|
||||
return RpcExceptionMapper.Map(rpcException, callOptions.CancellationToken);
|
||||
}
|
||||
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,6 @@ namespace MxGateway.Client.Tests;
|
||||
|
||||
public sealed class GalaxyRepositoryClientTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that TestConnectionAsync attaches the API key in request metadata and returns the Ok flag.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_AttachesApiKeyMetadataAndReturnsOkFlag()
|
||||
{
|
||||
@@ -24,9 +21,6 @@ public sealed class GalaxyRepositoryClientTests
|
||||
Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that TestConnectionAsync returns false when the server reports NotOk.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_ReturnsFalseWhenServerReportsNotOk()
|
||||
{
|
||||
@@ -39,9 +33,6 @@ public sealed class GalaxyRepositoryClientTests
|
||||
Assert.False(ok);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that GetLastDeployTimeAsync returns null when the server reports not present.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetLastDeployTimeAsync_ReturnsNullWhenNotPresent()
|
||||
{
|
||||
@@ -55,9 +46,6 @@ public sealed class GalaxyRepositoryClientTests
|
||||
Assert.Single(transport.GetLastDeployTimeCalls);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that GetLastDeployTimeAsync returns the timestamp when the server reports it present.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetLastDeployTimeAsync_ReturnsTimestampWhenPresent()
|
||||
{
|
||||
@@ -76,9 +64,6 @@ public sealed class GalaxyRepositoryClientTests
|
||||
Assert.Equal(expected, deployTime!.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that DiscoverHierarchyAsync returns the objects from the server reply.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
|
||||
{
|
||||
@@ -138,9 +123,6 @@ public sealed class GalaxyRepositoryClientTests
|
||||
Assert.Equal("DelmiaReceiver_001.DownloadPath", attribute.FullTagReference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that DiscoverHierarchyAsync propagates cancellation tokens to the transport.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DiscoverHierarchyAsync_PropagatesCancellationToTransport()
|
||||
{
|
||||
@@ -158,9 +140,6 @@ public sealed class GalaxyRepositoryClientTests
|
||||
Assert.False(call.CallOptions.CancellationToken.IsCancellationRequested);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that TestConnectionAsync retries on transient gRPC failures.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DiscoverHierarchyAsync_WithRepeatedPageToken_ThrowsProtocolError()
|
||||
{
|
||||
@@ -226,9 +205,6 @@ public sealed class GalaxyRepositoryClientTests
|
||||
Assert.Equal(2, transport.TestConnectionCalls.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that DiscoverHierarchyAsync retries on transient gRPC failures.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DiscoverHierarchyAsync_RetriesOnTransientGrpcFailure()
|
||||
{
|
||||
@@ -242,9 +218,6 @@ public sealed class GalaxyRepositoryClientTests
|
||||
Assert.Equal(2, transport.DiscoverHierarchyCalls.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WatchDeployEventsAsync delivers the bootstrap event.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WatchDeployEventsAsync_DeliversBootstrapEvent()
|
||||
{
|
||||
@@ -278,9 +251,6 @@ public sealed class GalaxyRepositoryClientTests
|
||||
Assert.Null(call.Request.LastSeenDeployTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WatchDeployEventsAsync delivers multiple events in order.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WatchDeployEventsAsync_DeliversMultipleEventsInOrder()
|
||||
{
|
||||
@@ -316,9 +286,6 @@ public sealed class GalaxyRepositoryClientTests
|
||||
Assert.Equal(t0, call.Request.LastSeenDeployTime!.ToDateTime());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WatchDeployEventsAsync stops iteration cleanly when cancelled.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WatchDeployEventsAsync_CancellationStopsIterationCleanly()
|
||||
{
|
||||
@@ -360,9 +327,6 @@ public sealed class GalaxyRepositoryClientTests
|
||||
Assert.Equal(1ul, received[0].Sequence);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WatchDeployEventsAsync throws ObjectDisposedException after the client is disposed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WatchDeployEventsAsync_ThrowsAfterDisposal()
|
||||
{
|
||||
@@ -375,9 +339,6 @@ public sealed class GalaxyRepositoryClientTests
|
||||
client.WatchDeployEventsAsync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that TestConnectionAsync throws ObjectDisposedException after the client is disposed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_ThrowsAfterDisposal()
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace MxGateway.Client.Tests;
|
||||
|
||||
public sealed class MxCommandReplyExtensionsTests
|
||||
{
|
||||
/// <summary>Verifies that successful replies pass both protocol and MxAccess success checks.</summary>
|
||||
[Fact]
|
||||
public void EnsureSuccess_WithRegisterFixture_ReturnsReply()
|
||||
{
|
||||
@@ -16,7 +15,6 @@ public sealed class MxCommandReplyExtensionsTests
|
||||
Assert.Same(reply, reply.EnsureMxAccessSuccess());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that MxAccess failures throw with preserved HResult and status details.</summary>
|
||||
[Fact]
|
||||
public void EnsureMxAccessSuccess_WithFailureFixture_PreservesHResultAndStatuses()
|
||||
{
|
||||
@@ -32,7 +30,6 @@ public sealed class MxCommandReplyExtensionsTests
|
||||
Assert.Contains("0x80040200", exception.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that session-not-found protocol failures throw the correct gateway exception.</summary>
|
||||
[Fact]
|
||||
public void EnsureProtocolSuccess_WithSessionFailure_ThrowsSessionException()
|
||||
{
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
using MxGateway.Contracts.Proto;
|
||||
|
||||
namespace MxGateway.Client.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// PR E.2 — pins the .NET SDK surface for the new alarm RPCs:
|
||||
/// <see cref="MxGatewayClient.AcknowledgeAlarmAsync"/> and
|
||||
/// <see cref="MxGatewayClient.QueryActiveAlarmsAsync"/>.
|
||||
/// </summary>
|
||||
public sealed class MxGatewayClientAlarmsTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AcknowledgeAlarmAsync_RecordsRequestShapeAndReturnsReply()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddAcknowledgeReply(new AcknowledgeAlarmReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
CorrelationId = "corr-1",
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
Status = new MxStatusProxy
|
||||
{
|
||||
Success = 1,
|
||||
Category = MxStatusCategory.Ok,
|
||||
DetectedBy = MxStatusSource.RespondingLmx,
|
||||
},
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
|
||||
AcknowledgeAlarmReply reply = await client.AcknowledgeAlarmAsync(new AcknowledgeAlarmRequest
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
ClientCorrelationId = "corr-1",
|
||||
AlarmFullReference = "Tank01.Level.HiHi",
|
||||
Comment = "investigating",
|
||||
OperatorUser = "alice",
|
||||
});
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||
Assert.Equal(MxStatusCategory.Ok, reply.Status.Category);
|
||||
|
||||
var call = Assert.Single(transport.AcknowledgeAlarmCalls);
|
||||
Assert.Equal("Tank01.Level.HiHi", call.Request.AlarmFullReference);
|
||||
Assert.Equal("investigating", call.Request.Comment);
|
||||
Assert.Equal("alice", call.Request.OperatorUser);
|
||||
Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcknowledgeAlarmAsync_HonorsCancellation()
|
||||
{
|
||||
// Acks are routed through the safe-unary retry pipeline (idempotent at the
|
||||
// MxAccess level), so the transport-side cancellation token is a linked one
|
||||
// rather than the caller's original. Verify cancellation by tripping the source
|
||||
// and asserting the call observes it.
|
||||
using CancellationTokenSource cancellation = new();
|
||||
cancellation.Cancel();
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
||||
client.AcknowledgeAlarmAsync(
|
||||
new AcknowledgeAlarmRequest
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
AlarmFullReference = "Tank01.Level.HiHi",
|
||||
Comment = string.Empty,
|
||||
OperatorUser = "alice",
|
||||
},
|
||||
cancellation.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcknowledgeAlarmAsync_MapsUnauthenticated_RpcException_ToTypedException()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AcknowledgeAlarmExceptions.Enqueue(
|
||||
new RpcException(new Status(StatusCode.Unauthenticated, "expired key")));
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
|
||||
// Note: the FakeGatewayTransport surfaces RpcException directly (it does not run
|
||||
// through GrpcMxGatewayClientTransport's mapping); the fake's contract here is to
|
||||
// pass the exception verbatim. RpcException → typed exception mapping is covered
|
||||
// in the GrpcMxGatewayClientTransport-level tests; the SDK-level test pins the
|
||||
// pass-through shape so a future migration to direct mapping won't silently
|
||||
// change observable behaviour.
|
||||
var ex = await Assert.ThrowsAsync<RpcException>(
|
||||
() => client.AcknowledgeAlarmAsync(new AcknowledgeAlarmRequest
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
AlarmFullReference = "Tank01.Level.HiHi",
|
||||
Comment = string.Empty,
|
||||
OperatorUser = "alice",
|
||||
}));
|
||||
Assert.Equal(StatusCode.Unauthenticated, ex.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryActiveAlarmsAsync_StreamsEnqueuedSnapshots()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddActiveAlarmSnapshot(MakeSnapshot("Tank01.Level.HiHi", AlarmConditionState.Active));
|
||||
transport.AddActiveAlarmSnapshot(MakeSnapshot("Tank02.Level.HiHi", AlarmConditionState.ActiveAcked));
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
|
||||
List<ActiveAlarmSnapshot> snapshots = [];
|
||||
await foreach (ActiveAlarmSnapshot snapshot in client.QueryActiveAlarmsAsync(new QueryActiveAlarmsRequest
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
}))
|
||||
{
|
||||
snapshots.Add(snapshot);
|
||||
}
|
||||
|
||||
Assert.Equal(2, snapshots.Count);
|
||||
Assert.Equal("Tank01.Level.HiHi", snapshots[0].AlarmFullReference);
|
||||
Assert.Equal(AlarmConditionState.Active, snapshots[0].CurrentState);
|
||||
Assert.Equal(AlarmConditionState.ActiveAcked, snapshots[1].CurrentState);
|
||||
Assert.Single(transport.QueryActiveAlarmsCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryActiveAlarmsAsync_PassesFilterPrefix()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
|
||||
await foreach (ActiveAlarmSnapshot _ in client.QueryActiveAlarmsAsync(new QueryActiveAlarmsRequest
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
AlarmFilterPrefix = "Tank01.",
|
||||
}))
|
||||
{
|
||||
// no snapshots enqueued; just verifying the request passes through
|
||||
}
|
||||
|
||||
var call = Assert.Single(transport.QueryActiveAlarmsCalls);
|
||||
Assert.Equal("Tank01.", call.Request.AlarmFilterPrefix);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryActiveAlarmsAsync_HonorsCancellationDuringEnumeration()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddActiveAlarmSnapshot(MakeSnapshot("Tank01.Level.HiHi", AlarmConditionState.Active));
|
||||
transport.AddActiveAlarmSnapshot(MakeSnapshot("Tank02.Level.HiHi", AlarmConditionState.Active));
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
|
||||
using CancellationTokenSource cancellation = new();
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(async () =>
|
||||
{
|
||||
await foreach (ActiveAlarmSnapshot _ in client.QueryActiveAlarmsAsync(
|
||||
new QueryActiveAlarmsRequest { SessionId = "session-fixture" },
|
||||
cancellation.Token))
|
||||
{
|
||||
cancellation.Cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static ActiveAlarmSnapshot MakeSnapshot(string fullReference, AlarmConditionState state)
|
||||
{
|
||||
return new ActiveAlarmSnapshot
|
||||
{
|
||||
AlarmFullReference = fullReference,
|
||||
SourceObjectReference = fullReference.Split('.')[0],
|
||||
AlarmTypeName = "AnalogLimitAlarm.HiHi",
|
||||
Severity = 750,
|
||||
CurrentState = state,
|
||||
Category = "Process",
|
||||
Description = "Tank high-high level",
|
||||
OriginalRaiseTimestamp = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc)),
|
||||
LastTransitionTimestamp = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 30, DateTimeKind.Utc)),
|
||||
};
|
||||
}
|
||||
|
||||
private static MxGatewayClient CreateClient(FakeGatewayTransport transport)
|
||||
{
|
||||
return new MxGatewayClient(transport.Options, transport);
|
||||
}
|
||||
|
||||
private static FakeGatewayTransport CreateTransport()
|
||||
{
|
||||
return new FakeGatewayTransport(new MxGatewayClientOptions
|
||||
{
|
||||
Endpoint = new Uri("http://localhost:5000"),
|
||||
ApiKey = "test-api-key",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,8 @@ using MxGateway.Contracts.Proto.Galaxy;
|
||||
|
||||
namespace MxGateway.Client.Tests;
|
||||
|
||||
/// <summary>Tests for the CLI command interface.</summary>
|
||||
public sealed class MxGatewayClientCliTests
|
||||
{
|
||||
/// <summary>Verifies that the version command prints compiled protocol versions.</summary>
|
||||
[Fact]
|
||||
public void Run_Version_PrintsCompiledProtocolVersions()
|
||||
{
|
||||
@@ -18,12 +16,11 @@ public sealed class MxGatewayClientCliTests
|
||||
var exitCode = MxGatewayClientCli.Run(["version"], output, error);
|
||||
|
||||
Assert.Equal(0, exitCode);
|
||||
Assert.Contains("gateway-protocol=3", output.ToString());
|
||||
Assert.Contains("gateway-protocol=2", output.ToString());
|
||||
Assert.Contains("worker-protocol=1", output.ToString());
|
||||
Assert.Equal(string.Empty, error.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the version command with --json flag prints JSON protocol versions.</summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_VersionJson_PrintsJsonProtocolVersions()
|
||||
{
|
||||
@@ -33,11 +30,10 @@ public sealed class MxGatewayClientCliTests
|
||||
int exitCode = await MxGatewayClientCli.RunAsync(["version", "--json"], output, error);
|
||||
|
||||
Assert.Equal(0, exitCode);
|
||||
Assert.Contains("\"gatewayProtocolVersion\":3", output.ToString());
|
||||
Assert.Contains("\"gatewayProtocolVersion\":2", output.ToString());
|
||||
Assert.Equal(string.Empty, error.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the write command builds a write request and prints JSON reply.</summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_Write_BuildsWriteCommandAndPrintsJsonReply()
|
||||
{
|
||||
@@ -82,7 +78,6 @@ public sealed class MxGatewayClientCliTests
|
||||
Assert.Equal(string.Empty, error.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that error output redacts sensitive API key values.</summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_ErrorOutput_RedactsApiKey()
|
||||
{
|
||||
@@ -106,44 +101,6 @@ public sealed class MxGatewayClientCliTests
|
||||
Assert.Contains("[redacted]", error.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that error output redacts the API key even when it was sourced from
|
||||
/// the <c>--api-key-env</c> environment variable rather than passed via
|
||||
/// <c>--api-key</c> — the documented default credential path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_ErrorOutput_RedactsApiKey_WhenSourcedFromEnvironmentVariable()
|
||||
{
|
||||
const string environmentVariableName = "MXGATEWAY_TEST_API_KEY_REDACT";
|
||||
using var output = new StringWriter();
|
||||
using var error = new StringWriter();
|
||||
|
||||
Environment.SetEnvironmentVariable(environmentVariableName, "env-secret-api-key");
|
||||
try
|
||||
{
|
||||
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||
[
|
||||
"open-session",
|
||||
"--endpoint",
|
||||
"http://localhost:5000",
|
||||
"--api-key-env",
|
||||
environmentVariableName,
|
||||
],
|
||||
output,
|
||||
error,
|
||||
_ => throw new InvalidOperationException("boom env-secret-api-key"));
|
||||
|
||||
Assert.Equal(1, exitCode);
|
||||
Assert.DoesNotContain("env-secret-api-key", error.ToString());
|
||||
Assert.Contains("[redacted]", error.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable(environmentVariableName, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that stream-events with max-events limit stops output in non-JSON format.</summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_StreamEvents_WithMaxEventsStopsNonJsonOutput()
|
||||
{
|
||||
@@ -185,7 +142,6 @@ public sealed class MxGatewayClientCliTests
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Verifies that smoke command closes opened session when a command fails.</summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_Smoke_WhenCommandFails_ClosesOpenedSession()
|
||||
{
|
||||
@@ -216,7 +172,6 @@ public sealed class MxGatewayClientCliTests
|
||||
Assert.Equal("session-fixture", closeRequest.SessionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that galaxy-test-connection command prints JSON reply.</summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_GalaxyTestConnection_PrintsJsonReply()
|
||||
{
|
||||
@@ -246,7 +201,6 @@ public sealed class MxGatewayClientCliTests
|
||||
Assert.Equal(string.Empty, error.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that galaxy-discover command prints hierarchy summary.</summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary()
|
||||
{
|
||||
@@ -316,7 +270,6 @@ public sealed class MxGatewayClientCliTests
|
||||
Assert.Equal(string.Empty, error.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that galaxy-watch command prints text output for deploy events.</summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents()
|
||||
{
|
||||
@@ -370,7 +323,6 @@ public sealed class MxGatewayClientCliTests
|
||||
Assert.Equal(string.Empty, error.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that galaxy-watch with --json emits one JSON object per event.</summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_GalaxyWatch_JsonEmitsOneObjectPerEvent()
|
||||
{
|
||||
@@ -405,31 +357,23 @@ public sealed class MxGatewayClientCliTests
|
||||
Assert.Contains("\"objectCount\": 99", text);
|
||||
}
|
||||
|
||||
/// <summary>Fake CLI client for testing.</summary>
|
||||
private sealed class FakeCliClient : IMxGatewayCliClient
|
||||
{
|
||||
/// <summary>Queue of invoke replies to return.</summary>
|
||||
public Queue<MxCommandReply> InvokeReplies { get; } = new();
|
||||
|
||||
/// <summary>List of received invoke requests.</summary>
|
||||
public List<MxCommandRequest> InvokeRequests { get; } = [];
|
||||
|
||||
/// <summary>List of received close session requests.</summary>
|
||||
public List<CloseSessionRequest> CloseSessionRequests { get; } = [];
|
||||
|
||||
/// <summary>List of events to yield when streaming.</summary>
|
||||
public List<MxEvent> Events { get; } = [];
|
||||
|
||||
/// <summary>Exception to throw on invoke, if any.</summary>
|
||||
public Exception? InvokeFailure { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<OpenSessionReply> OpenSessionAsync(
|
||||
OpenSessionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -443,7 +387,6 @@ public sealed class MxGatewayClientCliTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<CloseSessionReply> CloseSessionAsync(
|
||||
CloseSessionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -457,7 +400,6 @@ public sealed class MxGatewayClientCliTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<MxCommandReply> InvokeAsync(
|
||||
MxCommandRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -471,7 +413,6 @@ public sealed class MxGatewayClientCliTests
|
||||
return Task.FromResult(InvokeReplies.Dequeue());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||
StreamEventsRequest request,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
@@ -484,27 +425,20 @@ public sealed class MxGatewayClientCliTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Galaxy test connection reply to return.</summary>
|
||||
public TestConnectionReply GalaxyTestConnectionReply { get; set; } = new() { Ok = true };
|
||||
|
||||
/// <summary>Galaxy get last deploy time reply to return.</summary>
|
||||
public GetLastDeployTimeReply GalaxyGetLastDeployTimeReply { get; set; } = new() { Present = false };
|
||||
|
||||
/// <summary>Galaxy discover hierarchy reply to return.</summary>
|
||||
public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new();
|
||||
|
||||
public Queue<DiscoverHierarchyReply> GalaxyDiscoverHierarchyReplies { get; } = new();
|
||||
|
||||
/// <summary>List of received galaxy test connection requests.</summary>
|
||||
public List<TestConnectionRequest> GalaxyTestConnectionRequests { get; } = [];
|
||||
|
||||
/// <summary>List of received galaxy get last deploy time requests.</summary>
|
||||
public List<GetLastDeployTimeRequest> GalaxyGetLastDeployTimeRequests { get; } = [];
|
||||
|
||||
/// <summary>List of received galaxy discover hierarchy requests.</summary>
|
||||
public List<DiscoverHierarchyRequest> GalaxyDiscoverHierarchyRequests { get; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
||||
TestConnectionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -513,7 +447,6 @@ public sealed class MxGatewayClientCliTests
|
||||
return Task.FromResult(GalaxyTestConnectionReply);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
||||
GetLastDeployTimeRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -522,7 +455,6 @@ public sealed class MxGatewayClientCliTests
|
||||
return Task.FromResult(GalaxyGetLastDeployTimeReply);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
||||
DiscoverHierarchyRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -534,13 +466,10 @@ public sealed class MxGatewayClientCliTests
|
||||
: GalaxyDiscoverHierarchyReply);
|
||||
}
|
||||
|
||||
/// <summary>List of received galaxy watch deploy events requests.</summary>
|
||||
public List<WatchDeployEventsRequest> GalaxyWatchDeployEventsRequests { get; } = [];
|
||||
|
||||
/// <summary>List of deploy events to yield when watching.</summary>
|
||||
public List<DeployEvent> GalaxyDeployEvents { get; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
||||
WatchDeployEventsRequest request,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace MxGateway.Client.Tests;
|
||||
|
||||
public sealed class MxGatewayClientContractInfoTests
|
||||
{
|
||||
/// <summary>Verifies that the client's gateway protocol version matches the shared contract definition.</summary>
|
||||
[Fact]
|
||||
public void GatewayProtocolVersion_MatchesSharedContract()
|
||||
{
|
||||
@@ -13,7 +12,6 @@ public sealed class MxGatewayClientContractInfoTests
|
||||
MxGatewayClientContractInfo.GatewayProtocolVersion);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the client's worker protocol version matches the shared contract definition.</summary>
|
||||
[Fact]
|
||||
public void WorkerProtocolVersion_MatchesSharedContract()
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@ namespace MxGateway.Client.Tests;
|
||||
|
||||
public sealed class MxGatewayClientOptionsTests
|
||||
{
|
||||
/// <summary>Verifies that options with valid endpoint and API key pass validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_WithAbsoluteEndpointAndApiKey_Succeeds()
|
||||
{
|
||||
@@ -15,7 +14,6 @@ public sealed class MxGatewayClientOptionsTests
|
||||
options.Validate();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that empty API key causes validation to fail.</summary>
|
||||
[Fact]
|
||||
public void Validate_WithEmptyApiKey_Throws()
|
||||
{
|
||||
@@ -28,7 +26,6 @@ public sealed class MxGatewayClientOptionsTests
|
||||
Assert.Throws<ArgumentException>(options.Validate);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that invalid retry options cause validation to fail.</summary>
|
||||
[Fact]
|
||||
public void Validate_WithInvalidRetryOptions_Throws()
|
||||
{
|
||||
|
||||
@@ -3,10 +3,8 @@ using Grpc.Core;
|
||||
|
||||
namespace MxGateway.Client.Tests;
|
||||
|
||||
/// <summary>Tests for MxGatewaySession and client command behavior.</summary>
|
||||
public sealed class MxGatewayClientSessionTests
|
||||
{
|
||||
/// <summary>Verifies that open session attaches API key metadata and cancellation token.</summary>
|
||||
[Fact]
|
||||
public async Task OpenSessionRawAsync_AttachesApiKeyMetadataAndCancellation()
|
||||
{
|
||||
@@ -21,7 +19,6 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Equal(cancellation.Token, call.CallOptions.CancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that open session returns a session with the raw open reply.</summary>
|
||||
[Fact]
|
||||
public async Task OpenSessionAsync_ReturnsSessionWithRawOpenReply()
|
||||
{
|
||||
@@ -36,7 +33,6 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Equal(1234, session.OpenSessionReply.WorkerProcessId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that register builds a register command and returns server handle.</summary>
|
||||
[Fact]
|
||||
public async Task RegisterAsync_BuildsRegisterCommandAndReturnsServerHandle()
|
||||
{
|
||||
@@ -61,7 +57,6 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Equal("fixture-client", call.Request.Command.Register.ClientName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add item 2 builds a command with the specified context.</summary>
|
||||
[Fact]
|
||||
public async Task AddItem2Async_BuildsAddItem2CommandWithContext()
|
||||
{
|
||||
@@ -86,7 +81,6 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Equal("runtime", request.Command.AddItem2.ItemContext);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write raw builds a write command with the raw value.</summary>
|
||||
[Fact]
|
||||
public async Task WriteRawAsync_BuildsWriteCommandWithRawValue()
|
||||
{
|
||||
@@ -117,7 +111,6 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Equal(56, request.Command.Write.UserId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write 2 raw builds a write 2 command with value and timestamp.</summary>
|
||||
[Fact]
|
||||
public async Task Write2RawAsync_BuildsWrite2CommandWithValueAndTimestamp()
|
||||
{
|
||||
@@ -145,7 +138,6 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Equal(56, request.Command.Write2.UserId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscribe bulk builds one command and returns per-item results.</summary>
|
||||
[Fact]
|
||||
public async Task SubscribeBulkAsync_BuildsOneBulkCommandAndReturnsPerItemResults()
|
||||
{
|
||||
@@ -184,7 +176,6 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Equal(["Area001.Pump001.Speed"], request.Command.SubscribeBulk.TagAddresses);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that stream events yields events in the order received from the gateway.</summary>
|
||||
[Fact]
|
||||
public async Task StreamEventsAsync_YieldsEventsInGatewayOrder()
|
||||
{
|
||||
@@ -215,7 +206,6 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Equal("session-fixture", request.SessionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that close is explicit and idempotent.</summary>
|
||||
[Fact]
|
||||
public async Task CloseAsync_IsExplicitAndIdempotent()
|
||||
{
|
||||
@@ -231,53 +221,6 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Equal("session-fixture", call.Request.SessionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that disposing a session while other callers are concurrently inside
|
||||
/// <see cref="MxGatewaySession.CloseAsync"/> — one holding the close lock and one
|
||||
/// parked on it — never throws <see cref="ObjectDisposedException"/> into those
|
||||
/// callers. The close lock must outlive every pending close.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DisposeAsync_DoesNotRaceConcurrentCloseAsync()
|
||||
{
|
||||
for (int iteration = 0; iteration < 100; iteration++)
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
using SemaphoreSlim firstCloseEntered = new(0, 1);
|
||||
using SemaphoreSlim releaseFirstClose = new(0, 1);
|
||||
|
||||
// The first CloseAsync to reach the transport parks here while holding the
|
||||
// session's close lock; later callers queue on the lock behind it.
|
||||
transport.CloseSessionHook = async () =>
|
||||
{
|
||||
firstCloseEntered.Release();
|
||||
await releaseFirstClose.WaitAsync().ConfigureAwait(false);
|
||||
transport.CloseSessionHook = null;
|
||||
};
|
||||
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
// Holder enters CloseAsync, acquires the lock, and parks in the hook.
|
||||
Task holder = Task.Run(() => session.CloseAsync());
|
||||
await firstCloseEntered.WaitAsync();
|
||||
|
||||
// Waiter is parked on the close lock behind the holder.
|
||||
Task waiter = Task.Run(() => session.CloseAsync());
|
||||
|
||||
// DisposeAsync runs concurrently; it must wait out both callers before
|
||||
// disposing the close lock rather than tearing it down underneath them.
|
||||
Task dispose = session.DisposeAsync().AsTask();
|
||||
|
||||
releaseFirstClose.Release();
|
||||
|
||||
await holder;
|
||||
await waiter;
|
||||
await dispose;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that invoke retries safe diagnostic commands on transient RPC failure.</summary>
|
||||
[Fact]
|
||||
public async Task InvokeAsync_RetriesSafeDiagnosticCommandOnTransientGrpcFailure()
|
||||
{
|
||||
@@ -301,36 +244,6 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Equal(2, transport.InvokeCalls.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the retry pipeline still retries when the transport maps the raw
|
||||
/// <see cref="RpcException"/> to an <see cref="MxGatewayException"/> before it reaches
|
||||
/// the retry predicate — the wrapped-exception shape that production always produces.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task InvokeAsync_RetriesSafeDiagnosticCommand_WhenTransportMapsRpcException()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.MapTransportExceptions = true;
|
||||
transport.InvokeExceptions.Enqueue(CreateTransientRpcException());
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.Ping,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
await session.InvokeAsync(new MxCommandRequest
|
||||
{
|
||||
SessionId = session.SessionId,
|
||||
Command = new MxCommand { Kind = MxCommandKind.Ping, Ping = new PingCommand() },
|
||||
});
|
||||
|
||||
Assert.Equal(2, transport.InvokeCalls.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that open session does not retry on transient RPC failure.</summary>
|
||||
[Fact]
|
||||
public async Task OpenSessionAsync_DoesNotRetryTransientGrpcFailure()
|
||||
{
|
||||
@@ -343,7 +256,6 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Single(transport.OpenSessionCalls);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that invoke does not retry write commands on transient RPC failure.</summary>
|
||||
[Fact]
|
||||
public async Task InvokeAsync_DoesNotRetryWriteCommand()
|
||||
{
|
||||
@@ -358,7 +270,6 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Single(transport.InvokeCalls);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that invoke helpers pass cancellation token to the transport.</summary>
|
||||
[Fact]
|
||||
public async Task InvokeHelpers_PassCancellationTokenToTransport()
|
||||
{
|
||||
@@ -378,84 +289,6 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Equal(cancellation.Token, Assert.Single(transport.InvokeCalls).CallOptions.CancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a client-imposed <see cref="StatusCode.DeadlineExceeded"/> is not
|
||||
/// retried. The deadline budget is shared across the whole safe-unary operation, so
|
||||
/// an immediate retry would only fail again — the call must surface the failure.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task InvokeAsync_DoesNotRetrySafeDiagnosticCommand_OnDeadlineExceeded()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.InvokeExceptions.Enqueue(
|
||||
new RpcException(new Status(StatusCode.DeadlineExceeded, "deadline exceeded")));
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.Ping,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
await Assert.ThrowsAsync<RpcException>(async () => await session.InvokeAsync(
|
||||
new MxCommandRequest
|
||||
{
|
||||
SessionId = session.SessionId,
|
||||
Command = new MxCommand { Kind = MxCommandKind.Ping, Ping = new PingCommand() },
|
||||
}));
|
||||
|
||||
Assert.Single(transport.InvokeCalls);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a successful register reply missing the typed <c>register</c>
|
||||
/// payload throws a descriptive <see cref="MxGatewayException"/> rather than
|
||||
/// silently returning a zero server handle.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RegisterAsync_Throws_WhenSuccessfulReplyMissingPayload()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.Register,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
MxGatewayException exception = await Assert.ThrowsAsync<MxGatewayException>(
|
||||
async () => await session.RegisterAsync("client-name"));
|
||||
|
||||
Assert.Contains("register", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a successful add-item reply missing the typed <c>add_item</c>
|
||||
/// payload throws a descriptive <see cref="MxGatewayException"/> rather than
|
||||
/// silently returning a zero item handle.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddItemAsync_Throws_WhenSuccessfulReplyMissingPayload()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.AddItem,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
MxGatewayException exception = await Assert.ThrowsAsync<MxGatewayException>(
|
||||
async () => await session.AddItemAsync(1, "Area.Pump.Speed"));
|
||||
|
||||
Assert.Contains("add_item", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static MxGatewayClient CreateClient(FakeGatewayTransport transport)
|
||||
{
|
||||
return new MxGatewayClient(transport.Options, transport);
|
||||
|
||||
@@ -2,7 +2,6 @@ namespace MxGateway.Client.Tests;
|
||||
|
||||
public sealed class MxGatewayGeneratedContractTests
|
||||
{
|
||||
/// <summary>Verifies that the generated gRPC client can be instantiated from the client factory.</summary>
|
||||
[Fact]
|
||||
public async Task GeneratedGrpcClient_CanBeConstructedFromClientFactory()
|
||||
{
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace MxGateway.Client.Tests;
|
||||
|
||||
public sealed class MxStatusProxyExtensionsTests
|
||||
{
|
||||
/// <summary>Verifies that fixture statuses correctly project success and preserve raw integer fields.</summary>
|
||||
[Fact]
|
||||
public void FixtureStatuses_ProjectSuccessAndPreserveRawFields()
|
||||
{
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace MxGateway.Client.Tests;
|
||||
|
||||
public sealed class MxValueExtensionsTests
|
||||
{
|
||||
/// <summary>Verifies that scalar values are converted to correctly-typed MxValue protobuf messages.</summary>
|
||||
[Fact]
|
||||
public void ToMxValue_WithScalarValues_CreatesTypedProtobufValues()
|
||||
{
|
||||
@@ -19,7 +18,6 @@ public sealed class MxValueExtensionsTests
|
||||
Assert.Equal(MxValue.KindOneofCase.StringValue, "alpha".ToMxValue().KindCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that array values are converted to array-kind MxValue messages with correct element types and dimensions.</summary>
|
||||
[Fact]
|
||||
public void ToMxValue_WithArrays_CreatesTypedArrayProtobufValues()
|
||||
{
|
||||
@@ -31,7 +29,6 @@ public sealed class MxValueExtensionsTests
|
||||
Assert.Equal([2U], value.ArrayValue.Dimensions);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that fixture test cases project to expected MxValue kinds and preserve raw type metadata.</summary>
|
||||
[Fact]
|
||||
public void FixtureValues_ProjectExpectedKindsAndPreserveRawMetadata()
|
||||
{
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
using Grpc.Core;
|
||||
|
||||
namespace MxGateway.Client.Tests;
|
||||
|
||||
/// <summary>Tests for the shared gRPC-to-native exception mapping used by the transports.</summary>
|
||||
public sealed class RpcExceptionMapperTests
|
||||
{
|
||||
/// <summary>Verifies that an unauthenticated status maps to the authentication exception.</summary>
|
||||
[Fact]
|
||||
public void Map_UnauthenticatedStatus_ProducesAuthenticationException()
|
||||
{
|
||||
RpcException rpc = new(new Status(StatusCode.Unauthenticated, "no key"));
|
||||
|
||||
Exception mapped = RpcExceptionMapper.Map(rpc, CancellationToken.None);
|
||||
|
||||
MxGatewayAuthenticationException authentication =
|
||||
Assert.IsType<MxGatewayAuthenticationException>(mapped);
|
||||
Assert.Equal(StatusCode.Unauthenticated, authentication.StatusCode);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a permission-denied status maps to the authorization exception.</summary>
|
||||
[Fact]
|
||||
public void Map_PermissionDeniedStatus_ProducesAuthorizationException()
|
||||
{
|
||||
RpcException rpc = new(new Status(StatusCode.PermissionDenied, "missing scope"));
|
||||
|
||||
Exception mapped = RpcExceptionMapper.Map(rpc, CancellationToken.None);
|
||||
|
||||
MxGatewayAuthorizationException authorization =
|
||||
Assert.IsType<MxGatewayAuthorizationException>(mapped);
|
||||
Assert.Equal(StatusCode.PermissionDenied, authorization.StatusCode);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a cancelled status maps to OperationCanceledException.</summary>
|
||||
[Fact]
|
||||
public void Map_CancelledStatus_ProducesOperationCanceledException()
|
||||
{
|
||||
RpcException rpc = new(new Status(StatusCode.Cancelled, "cancelled"));
|
||||
|
||||
Exception mapped = RpcExceptionMapper.Map(rpc, CancellationToken.None);
|
||||
|
||||
Assert.IsType<OperationCanceledException>(mapped);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that non-auth statuses surface the originating gRPC status code on the
|
||||
/// mapped exception so callers can distinguish transient from permanent failures
|
||||
/// without reflecting into InnerException.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(StatusCode.NotFound)]
|
||||
[InlineData(StatusCode.InvalidArgument)]
|
||||
[InlineData(StatusCode.ResourceExhausted)]
|
||||
[InlineData(StatusCode.FailedPrecondition)]
|
||||
[InlineData(StatusCode.Unavailable)]
|
||||
[InlineData(StatusCode.Internal)]
|
||||
public void Map_NonAuthStatus_CarriesStatusCodeOnMxGatewayException(StatusCode statusCode)
|
||||
{
|
||||
RpcException rpc = new(new Status(statusCode, "boom"));
|
||||
|
||||
Exception mapped = RpcExceptionMapper.Map(rpc, CancellationToken.None);
|
||||
|
||||
MxGatewayException gatewayException = Assert.IsType<MxGatewayException>(mapped);
|
||||
Assert.Equal(statusCode, gatewayException.StatusCode);
|
||||
Assert.Same(rpc, gatewayException.InnerException);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an MxGatewayException built without a gRPC status reports a null StatusCode.</summary>
|
||||
[Fact]
|
||||
public void StatusCode_IsNull_WhenNoGrpcStatusProvided()
|
||||
{
|
||||
MxGatewayException gatewayException = new("plain failure");
|
||||
|
||||
Assert.Null(gatewayException.StatusCode);
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a Galaxy Repository client with custom transport and options.
|
||||
/// </summary>
|
||||
/// <param name="options">Client options.</param>
|
||||
/// <param name="transport">The underlying gRPC transport.</param>
|
||||
internal GalaxyRepositoryClient(
|
||||
MxGatewayClientOptions options,
|
||||
IGalaxyRepositoryClientTransport transport)
|
||||
@@ -57,23 +52,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
Options.LoggerFactory?.CreateLogger<GalaxyRepositoryClient>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client options used to configure timeouts, authentication, and retry policy.
|
||||
/// </summary>
|
||||
public MxGatewayClientOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The underlying generated gRPC client for advanced operations.
|
||||
/// </summary>
|
||||
public GalaxyRepository.GalaxyRepositoryClient RawClient =>
|
||||
_transport.RawClient
|
||||
?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Galaxy Repository client with the given options, establishing a new gRPC channel.
|
||||
/// </summary>
|
||||
/// <param name="options">Client options.</param>
|
||||
/// <returns>A new client instance.</returns>
|
||||
public static GalaxyRepositoryClient Create(MxGatewayClientOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
@@ -101,8 +85,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
/// Probes the Galaxy Repository database connection. Returns true when the
|
||||
/// gateway can reach the configured ZB SQL Server.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>True if connection is successful, false otherwise.</returns>
|
||||
public async Task<bool> TestConnectionAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
TestConnectionReply reply = await TestConnectionRawAsync(
|
||||
@@ -113,12 +95,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
return reply.Ok;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Probes the Galaxy Repository database connection without result wrapping.
|
||||
/// </summary>
|
||||
/// <param name="request">The test connection request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The raw server reply.</returns>
|
||||
public Task<TestConnectionReply> TestConnectionRawAsync(
|
||||
TestConnectionRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -135,8 +111,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
/// Returns the timestamp of the most recent Galaxy deployment, or
|
||||
/// <see langword="null"/> when no deployment has been recorded.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The deployment timestamp, or null if not recorded.</returns>
|
||||
public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
GetLastDeployTimeReply reply = await GetLastDeployTimeRawAsync(
|
||||
@@ -152,12 +126,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
return reply.TimeOfLastDeploy.ToDateTime();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the most recent Galaxy deployment timestamp without result wrapping.
|
||||
/// </summary>
|
||||
/// <param name="request">The last deploy-time request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The raw server reply.</returns>
|
||||
public Task<GetLastDeployTimeReply> GetLastDeployTimeRawAsync(
|
||||
GetLastDeployTimeRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -175,8 +143,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
/// includes its dynamic attributes so callers can determine which tag references
|
||||
/// they may subscribe to via the MxAccessGateway service.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The collection of Galaxy objects in the hierarchy.</returns>
|
||||
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DiscoverHierarchyAsync(new DiscoverHierarchyOptions(), cancellationToken).ConfigureAwait(false);
|
||||
@@ -256,12 +222,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the Galaxy object hierarchy without result wrapping.
|
||||
/// </summary>
|
||||
/// <param name="request">The discover-hierarchy request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The raw server reply.</returns>
|
||||
public Task<DiscoverHierarchyReply> DiscoverHierarchyRawAsync(
|
||||
DiscoverHierarchyRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -286,9 +246,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
/// at-least-once delivery beyond the per-subscriber buffer (gaps in
|
||||
/// <see cref="DeployEvent.Sequence"/> indicate dropped events).
|
||||
/// </remarks>
|
||||
/// <param name="lastSeenDeployTime">Optional timestamp to suppress the bootstrap event.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>An async enumerable of deploy events.</returns>
|
||||
public IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||
DateTimeOffset? lastSeenDeployTime = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -304,12 +261,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
return WatchDeployEventsRawAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to Galaxy deploy events without result wrapping.
|
||||
/// </summary>
|
||||
/// <param name="request">The watch-deploy-events request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>An async enumerable of raw deploy events.</returns>
|
||||
public IAsyncEnumerable<DeployEvent> WatchDeployEventsRawAsync(
|
||||
WatchDeployEventsRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -333,9 +284,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the gRPC channel and releases resources.
|
||||
/// </summary>
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed)
|
||||
@@ -348,32 +296,16 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates gRPC call options with the client's default timeout and API-key authorization.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The call options.</returns>
|
||||
internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
|
||||
{
|
||||
return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates gRPC call options for streaming RPCs with the stream timeout and API-key authorization.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The stream call options.</returns>
|
||||
internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken)
|
||||
{
|
||||
return CreateCallOptions(cancellationToken, Options.StreamTimeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates gRPC call options with the specified timeout and API-key authorization.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <param name="timeout">Optional timeout duration.</param>
|
||||
/// <returns>The call options.</returns>
|
||||
internal CallOptions CreateCallOptions(
|
||||
CancellationToken cancellationToken,
|
||||
TimeSpan? timeout)
|
||||
|
||||
@@ -3,27 +3,16 @@ using MxGateway.Contracts.Proto.Galaxy;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>
|
||||
/// gRPC implementation of IGalaxyRepositoryClientTransport.
|
||||
/// </summary>
|
||||
internal sealed class GrpcGalaxyRepositoryClientTransport(
|
||||
MxGatewayClientOptions options,
|
||||
GalaxyRepository.GalaxyRepositoryClient rawClient) : IGalaxyRepositoryClientTransport
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the gateway client options.
|
||||
/// </summary>
|
||||
public MxGatewayClientOptions Options { get; } = options;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying gRPC client.
|
||||
/// </summary>
|
||||
public GalaxyRepository.GalaxyRepositoryClient RawClient { get; } = rawClient;
|
||||
|
||||
/// <inheritdoc />
|
||||
GalaxyRepository.GalaxyRepositoryClient? IGalaxyRepositoryClientTransport.RawClient => RawClient;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TestConnectionReply> TestConnectionAsync(
|
||||
TestConnectionRequest request,
|
||||
CallOptions callOptions)
|
||||
@@ -36,11 +25,10 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
||||
}
|
||||
catch (RpcException exception)
|
||||
{
|
||||
throw RpcExceptionMapper.Map(exception, callOptions.CancellationToken);
|
||||
throw MapRpcException(exception, callOptions.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
||||
GetLastDeployTimeRequest request,
|
||||
CallOptions callOptions)
|
||||
@@ -53,11 +41,10 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
||||
}
|
||||
catch (RpcException exception)
|
||||
{
|
||||
throw RpcExceptionMapper.Map(exception, callOptions.CancellationToken);
|
||||
throw MapRpcException(exception, callOptions.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
||||
DiscoverHierarchyRequest request,
|
||||
CallOptions callOptions)
|
||||
@@ -70,11 +57,10 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
||||
}
|
||||
catch (RpcException exception)
|
||||
{
|
||||
throw RpcExceptionMapper.Map(exception, callOptions.CancellationToken);
|
||||
throw MapRpcException(exception, callOptions.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||
WatchDeployEventsRequest request,
|
||||
CallOptions callOptions,
|
||||
@@ -101,18 +87,41 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
||||
}
|
||||
catch (RpcException exception)
|
||||
{
|
||||
throw RpcExceptionMapper.Map(exception, effectiveCancellationToken);
|
||||
throw MapRpcException(exception, effectiveCancellationToken);
|
||||
}
|
||||
|
||||
yield return deployEvent;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IAsyncEnumerable<DeployEvent> IGalaxyRepositoryClientTransport.WatchDeployEventsAsync(
|
||||
WatchDeployEventsRequest request,
|
||||
CallOptions callOptions)
|
||||
{
|
||||
return WatchDeployEventsAsync(request, callOptions);
|
||||
}
|
||||
|
||||
private static Exception MapRpcException(
|
||||
RpcException exception,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested || exception.StatusCode == StatusCode.Cancelled)
|
||||
{
|
||||
return new OperationCanceledException(
|
||||
exception.Status.Detail,
|
||||
exception,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
return exception.StatusCode switch
|
||||
{
|
||||
StatusCode.Unauthenticated => new MxGatewayAuthenticationException(
|
||||
exception.Status.Detail,
|
||||
innerException: exception),
|
||||
StatusCode.PermissionDenied => new MxGatewayAuthorizationException(
|
||||
exception.Status.Detail,
|
||||
innerException: exception),
|
||||
_ => new MxGatewayException(exception.Status.Detail, exception),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,27 +3,16 @@ using MxGateway.Contracts.Proto;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>
|
||||
/// gRPC implementation of IMxGatewayClientTransport.
|
||||
/// </summary>
|
||||
internal sealed class GrpcMxGatewayClientTransport(
|
||||
MxGatewayClientOptions options,
|
||||
MxAccessGateway.MxAccessGatewayClient rawClient) : IMxGatewayClientTransport
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the gateway client options.
|
||||
/// </summary>
|
||||
public MxGatewayClientOptions Options { get; } = options;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying gRPC client.
|
||||
/// </summary>
|
||||
public MxAccessGateway.MxAccessGatewayClient RawClient { get; } = rawClient;
|
||||
|
||||
/// <inheritdoc />
|
||||
MxAccessGateway.MxAccessGatewayClient? IMxGatewayClientTransport.RawClient => RawClient;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<OpenSessionReply> OpenSessionAsync(
|
||||
OpenSessionRequest request,
|
||||
CallOptions callOptions)
|
||||
@@ -36,11 +25,10 @@ internal sealed class GrpcMxGatewayClientTransport(
|
||||
}
|
||||
catch (RpcException exception)
|
||||
{
|
||||
throw RpcExceptionMapper.Map(exception, callOptions.CancellationToken);
|
||||
throw MapRpcException(exception, callOptions.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CloseSessionReply> CloseSessionAsync(
|
||||
CloseSessionRequest request,
|
||||
CallOptions callOptions)
|
||||
@@ -53,11 +41,10 @@ internal sealed class GrpcMxGatewayClientTransport(
|
||||
}
|
||||
catch (RpcException exception)
|
||||
{
|
||||
throw RpcExceptionMapper.Map(exception, callOptions.CancellationToken);
|
||||
throw MapRpcException(exception, callOptions.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<MxCommandReply> InvokeAsync(
|
||||
MxCommandRequest request,
|
||||
CallOptions callOptions)
|
||||
@@ -70,11 +57,10 @@ internal sealed class GrpcMxGatewayClientTransport(
|
||||
}
|
||||
catch (RpcException exception)
|
||||
{
|
||||
throw RpcExceptionMapper.Map(exception, callOptions.CancellationToken);
|
||||
throw MapRpcException(exception, callOptions.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||
StreamEventsRequest request,
|
||||
CallOptions callOptions,
|
||||
@@ -101,14 +87,13 @@ internal sealed class GrpcMxGatewayClientTransport(
|
||||
}
|
||||
catch (RpcException exception)
|
||||
{
|
||||
throw RpcExceptionMapper.Map(exception, effectiveCancellationToken);
|
||||
throw MapRpcException(exception, effectiveCancellationToken);
|
||||
}
|
||||
|
||||
yield return gatewayEvent;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IAsyncEnumerable<MxEvent> IMxGatewayClientTransport.StreamEventsAsync(
|
||||
StreamEventsRequest request,
|
||||
CallOptions callOptions)
|
||||
@@ -116,62 +101,27 @@ internal sealed class GrpcMxGatewayClientTransport(
|
||||
return StreamEventsAsync(request, callOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<AcknowledgeAlarmReply> AcknowledgeAlarmAsync(
|
||||
AcknowledgeAlarmRequest request,
|
||||
CallOptions callOptions)
|
||||
private static Exception MapRpcException(
|
||||
RpcException exception,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
if (cancellationToken.IsCancellationRequested || exception.StatusCode == StatusCode.Cancelled)
|
||||
{
|
||||
return await RawClient.AcknowledgeAlarmAsync(request, callOptions)
|
||||
.ResponseAsync
|
||||
.ConfigureAwait(false);
|
||||
return new OperationCanceledException(
|
||||
exception.Status.Detail,
|
||||
exception,
|
||||
cancellationToken);
|
||||
}
|
||||
catch (RpcException exception)
|
||||
|
||||
return exception.StatusCode switch
|
||||
{
|
||||
throw RpcExceptionMapper.Map(exception, callOptions.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync(
|
||||
QueryActiveAlarmsRequest request,
|
||||
CallOptions callOptions,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
CancellationToken effectiveCancellationToken = cancellationToken.CanBeCanceled
|
||||
? cancellationToken
|
||||
: callOptions.CancellationToken;
|
||||
|
||||
using AsyncServerStreamingCall<ActiveAlarmSnapshot> call = RawClient.QueryActiveAlarms(request, callOptions);
|
||||
|
||||
IAsyncStreamReader<ActiveAlarmSnapshot> responseStream = call.ResponseStream;
|
||||
while (true)
|
||||
{
|
||||
ActiveAlarmSnapshot? snapshot;
|
||||
try
|
||||
{
|
||||
if (!await responseStream.MoveNext(effectiveCancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
snapshot = responseStream.Current;
|
||||
}
|
||||
catch (RpcException exception)
|
||||
{
|
||||
throw RpcExceptionMapper.Map(exception, effectiveCancellationToken);
|
||||
}
|
||||
|
||||
yield return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IAsyncEnumerable<ActiveAlarmSnapshot> IMxGatewayClientTransport.QueryActiveAlarmsAsync(
|
||||
QueryActiveAlarmsRequest request,
|
||||
CallOptions callOptions)
|
||||
{
|
||||
return QueryActiveAlarmsAsync(request, callOptions);
|
||||
StatusCode.Unauthenticated => new MxGatewayAuthenticationException(
|
||||
exception.Status.Detail,
|
||||
innerException: exception),
|
||||
StatusCode.PermissionDenied => new MxGatewayAuthorizationException(
|
||||
exception.Status.Detail,
|
||||
innerException: exception),
|
||||
_ => new MxGatewayException(exception.Status.Detail, exception),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,39 +3,24 @@ using MxGateway.Contracts.Proto.Galaxy;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>Transport layer for Galaxy Repository gRPC operations.</summary>
|
||||
internal interface IGalaxyRepositoryClientTransport
|
||||
{
|
||||
/// <summary>Gets the client options used to configure this transport.</summary>
|
||||
MxGatewayClientOptions Options { get; }
|
||||
|
||||
/// <summary>Gets the underlying gRPC client, or <c>null</c> if not yet initialized.</summary>
|
||||
GalaxyRepository.GalaxyRepositoryClient? RawClient { get; }
|
||||
|
||||
/// <summary>Tests the connection to the Galaxy Repository server.</summary>
|
||||
/// <param name="request">The test connection request.</param>
|
||||
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
|
||||
Task<TestConnectionReply> TestConnectionAsync(
|
||||
TestConnectionRequest request,
|
||||
CallOptions callOptions);
|
||||
|
||||
/// <summary>Gets the last deploy time from the Galaxy Repository server.</summary>
|
||||
/// <param name="request">The get last deploy time request.</param>
|
||||
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
|
||||
Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
||||
GetLastDeployTimeRequest request,
|
||||
CallOptions callOptions);
|
||||
|
||||
/// <summary>Discovers the object hierarchy in the Galaxy Repository.</summary>
|
||||
/// <param name="request">The discover hierarchy request.</param>
|
||||
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
|
||||
Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
||||
DiscoverHierarchyRequest request,
|
||||
CallOptions callOptions);
|
||||
|
||||
/// <summary>Watches for deployment events from the Galaxy Repository server.</summary>
|
||||
/// <param name="request">The watch deploy events request.</param>
|
||||
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
|
||||
IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||
WatchDeployEventsRequest request,
|
||||
CallOptions callOptions);
|
||||
|
||||
@@ -5,74 +5,23 @@ namespace MxGateway.Client;
|
||||
|
||||
internal interface IMxGatewayClientTransport
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the client configuration options.
|
||||
/// </summary>
|
||||
MxGatewayClientOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying gRPC client, if available.
|
||||
/// </summary>
|
||||
MxAccessGateway.MxAccessGatewayClient? RawClient { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens a new gateway session.
|
||||
/// </summary>
|
||||
/// <param name="request">Session open request.</param>
|
||||
/// <param name="callOptions">gRPC call options.</param>
|
||||
/// <returns>The session open reply.</returns>
|
||||
Task<OpenSessionReply> OpenSessionAsync(
|
||||
OpenSessionRequest request,
|
||||
CallOptions callOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Closes an open gateway session.
|
||||
/// </summary>
|
||||
/// <param name="request">Session close request.</param>
|
||||
/// <param name="callOptions">gRPC call options.</param>
|
||||
/// <returns>The session close reply.</returns>
|
||||
Task<CloseSessionReply> CloseSessionAsync(
|
||||
CloseSessionRequest request,
|
||||
CallOptions callOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes an MXAccess command on the session.
|
||||
/// </summary>
|
||||
/// <param name="request">The command request.</param>
|
||||
/// <param name="callOptions">gRPC call options.</param>
|
||||
/// <returns>The command reply.</returns>
|
||||
Task<MxCommandReply> InvokeAsync(
|
||||
MxCommandRequest request,
|
||||
CallOptions callOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Streams events from the session.
|
||||
/// </summary>
|
||||
/// <param name="request">The stream events request.</param>
|
||||
/// <param name="callOptions">gRPC call options.</param>
|
||||
/// <returns>An async enumerable of events.</returns>
|
||||
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||
StreamEventsRequest request,
|
||||
CallOptions callOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Acknowledges an active MXAccess alarm condition.
|
||||
/// </summary>
|
||||
/// <param name="request">The acknowledge request.</param>
|
||||
/// <param name="callOptions">gRPC call options.</param>
|
||||
/// <returns>The acknowledge reply with native MxStatus.</returns>
|
||||
Task<AcknowledgeAlarmReply> AcknowledgeAlarmAsync(
|
||||
AcknowledgeAlarmRequest request,
|
||||
CallOptions callOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Streams a snapshot of all alarms currently in Active or ActiveAcked state — the
|
||||
/// ConditionRefresh equivalent for the gateway.
|
||||
/// </summary>
|
||||
/// <param name="request">The query request, optionally scoped by alarm-reference prefix.</param>
|
||||
/// <param name="callOptions">gRPC call options.</param>
|
||||
/// <returns>An async enumerable of active-alarm snapshots.</returns>
|
||||
IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync(
|
||||
QueryActiveAlarmsRequest request,
|
||||
CallOptions callOptions);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,8 @@ using MxGateway.Contracts.Proto;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>Exception thrown when an MXAccess command fails with a non-zero HResult or failing status.</summary>
|
||||
public sealed class MxAccessException : MxGatewayCommandException
|
||||
{
|
||||
/// <summary>Initializes a new instance with the given message, reply, and optional inner exception.</summary>
|
||||
/// <param name="message">The error message describing the MXAccess failure.</param>
|
||||
/// <param name="reply">The MxCommandReply containing the failure details (statuses, HResult, etc.).</param>
|
||||
/// <param name="innerException">The underlying exception, if any.</param>
|
||||
public MxAccessException(
|
||||
string message,
|
||||
MxCommandReply reply,
|
||||
@@ -25,6 +20,5 @@ public sealed class MxAccessException : MxGatewayCommandException
|
||||
Reply = reply;
|
||||
}
|
||||
|
||||
/// <summary>Gets the underlying MxCommandReply containing full failure details.</summary>
|
||||
public MxCommandReply Reply { get; }
|
||||
}
|
||||
|
||||
@@ -2,11 +2,8 @@ using MxGateway.Contracts.Proto;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>Extension methods for checking MxCommandReply success conditions.</summary>
|
||||
public static class MxCommandReplyExtensions
|
||||
{
|
||||
/// <summary>Validates that the reply has a successful protocol status (Ok or MxAccessFailure), throwing a gateway exception if not.</summary>
|
||||
/// <param name="reply">The command reply to check.</param>
|
||||
public static MxCommandReply EnsureProtocolSuccess(this MxCommandReply reply)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reply);
|
||||
@@ -22,8 +19,6 @@ public static class MxCommandReplyExtensions
|
||||
throw CreateProtocolException(reply, code);
|
||||
}
|
||||
|
||||
/// <summary>Validates that the reply indicates MXAccess success (no HResult or status failures), throwing MxAccessException if not.</summary>
|
||||
/// <param name="reply">The command reply to check.</param>
|
||||
public static MxCommandReply EnsureMxAccessSuccess(this MxCommandReply reply)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reply);
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
using Grpc.Core;
|
||||
using MxGateway.Contracts.Proto;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>Exception thrown when an API key is invalid, expired, or malformed.</summary>
|
||||
public sealed class MxGatewayAuthenticationException : MxGatewayException
|
||||
{
|
||||
/// <summary>Initializes a new instance with the given details.</summary>
|
||||
/// <param name="message">The error message describing the authentication failure.</param>
|
||||
/// <param name="sessionId">The session ID, if available.</param>
|
||||
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
||||
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
||||
/// <param name="hResult">The HResult code, if available.</param>
|
||||
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
||||
/// <param name="innerException">The underlying exception, if any.</param>
|
||||
/// <param name="statusCode">The gRPC status code reported by the failed call, if available.</param>
|
||||
public MxGatewayAuthenticationException(
|
||||
string message,
|
||||
string? sessionId = null,
|
||||
@@ -22,8 +11,7 @@ public sealed class MxGatewayAuthenticationException : MxGatewayException
|
||||
ProtocolStatus? protocolStatus = null,
|
||||
int? hResult = null,
|
||||
IReadOnlyList<MxStatusProxy>? statuses = null,
|
||||
Exception? innerException = null,
|
||||
StatusCode? statusCode = null)
|
||||
Exception? innerException = null)
|
||||
: base(
|
||||
message,
|
||||
sessionId,
|
||||
@@ -31,8 +19,7 @@ public sealed class MxGatewayAuthenticationException : MxGatewayException
|
||||
protocolStatus,
|
||||
hResult,
|
||||
statuses ?? [],
|
||||
innerException,
|
||||
statusCode)
|
||||
innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
using Grpc.Core;
|
||||
using MxGateway.Contracts.Proto;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>Exception thrown when the API key lacks required scopes for an operation.</summary>
|
||||
public sealed class MxGatewayAuthorizationException : MxGatewayException
|
||||
{
|
||||
/// <summary>Initializes a new instance with the given details.</summary>
|
||||
/// <param name="message">The error message describing the authorization failure.</param>
|
||||
/// <param name="sessionId">The session ID, if available.</param>
|
||||
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
||||
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
||||
/// <param name="hResult">The HResult code, if available.</param>
|
||||
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
||||
/// <param name="innerException">The underlying exception, if any.</param>
|
||||
/// <param name="statusCode">The gRPC status code reported by the failed call, if available.</param>
|
||||
public MxGatewayAuthorizationException(
|
||||
string message,
|
||||
string? sessionId = null,
|
||||
@@ -22,8 +11,7 @@ public sealed class MxGatewayAuthorizationException : MxGatewayException
|
||||
ProtocolStatus? protocolStatus = null,
|
||||
int? hResult = null,
|
||||
IReadOnlyList<MxStatusProxy>? statuses = null,
|
||||
Exception? innerException = null,
|
||||
StatusCode? statusCode = null)
|
||||
Exception? innerException = null)
|
||||
: base(
|
||||
message,
|
||||
sessionId,
|
||||
@@ -31,8 +19,7 @@ public sealed class MxGatewayAuthorizationException : MxGatewayException
|
||||
protocolStatus,
|
||||
hResult,
|
||||
statuses ?? [],
|
||||
innerException,
|
||||
statusCode)
|
||||
innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,8 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
||||
private readonly GrpcChannel _channel;
|
||||
private readonly IMxGatewayClientTransport _transport;
|
||||
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
|
||||
private int _disposed;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MxGatewayClient"/> with given options and transport.
|
||||
/// </summary>
|
||||
/// <param name="options">Client configuration options.</param>
|
||||
/// <param name="transport">Transport implementation for gateway communication.</param>
|
||||
internal MxGatewayClient(
|
||||
MxGatewayClientOptions options,
|
||||
IMxGatewayClientTransport transport)
|
||||
@@ -51,23 +46,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
||||
Options.LoggerFactory?.CreateLogger<MxGatewayClient>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client configuration options.
|
||||
/// </summary>
|
||||
public MxGatewayClientOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying generated gRPC client.
|
||||
/// </summary>
|
||||
public MxAccessGateway.MxAccessGatewayClient RawClient =>
|
||||
_transport.RawClient
|
||||
?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new gateway client with the given options.
|
||||
/// </summary>
|
||||
/// <param name="options">Client configuration options.</param>
|
||||
/// <returns>A new gateway client instance.</returns>
|
||||
public static MxGatewayClient Create(MxGatewayClientOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
@@ -91,12 +75,6 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
||||
new MxAccessGateway.MxAccessGatewayClient(channel)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a new gateway session.
|
||||
/// </summary>
|
||||
/// <param name="request">Session open request; defaults to empty request if null.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A wrapped gateway session.</returns>
|
||||
public async Task<MxGatewaySession> OpenSessionAsync(
|
||||
OpenSessionRequest? request = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -109,12 +87,6 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
||||
return new MxGatewaySession(this, reply);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a new gateway session and returns the raw protobuf reply.
|
||||
/// </summary>
|
||||
/// <param name="request">Session open request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The raw gateway session open reply.</returns>
|
||||
public Task<OpenSessionReply> OpenSessionRawAsync(
|
||||
OpenSessionRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -125,12 +97,6 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
||||
return _transport.OpenSessionAsync(request, CreateCallOptions(cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes an open gateway session.
|
||||
/// </summary>
|
||||
/// <param name="request">Session close request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The session close reply.</returns>
|
||||
public Task<CloseSessionReply> CloseSessionRawAsync(
|
||||
CloseSessionRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -143,12 +109,6 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes an MXAccess command on the open session.
|
||||
/// </summary>
|
||||
/// <param name="request">The command request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The command reply.</returns>
|
||||
public Task<MxCommandReply> InvokeAsync(
|
||||
MxCommandRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -166,12 +126,6 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
||||
return _transport.InvokeAsync(request, CreateCallOptions(cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Streams events from the gateway session.
|
||||
/// </summary>
|
||||
/// <param name="request">The stream events request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>An async enumerable of events.</returns>
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||
StreamEventsRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -182,89 +136,28 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
||||
return _transport.StreamEventsAsync(request, CreateStreamCallOptions(cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Acknowledges an active MXAccess alarm condition through the gateway. The
|
||||
/// gateway authorizes <see cref="AcknowledgeAlarmRequest"/> against the API
|
||||
/// key's <c>admin</c> scope (there is no finer-grained alarm-ack sub-scope)
|
||||
/// and forwards the acknowledge to the worker's MXAccess session; the
|
||||
/// resulting <see cref="MxStatusProxy"/> is returned in the reply.
|
||||
/// </summary>
|
||||
/// <param name="request">The acknowledge request — alarm reference, comment, operator user.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The acknowledge reply with protocol + native MxStatus.</returns>
|
||||
public Task<AcknowledgeAlarmReply> AcknowledgeAlarmAsync(
|
||||
AcknowledgeAlarmRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ThrowIfDisposed();
|
||||
|
||||
return ExecuteSafeUnaryAsync(
|
||||
token => _transport.AcknowledgeAlarmAsync(request, CreateCallOptions(token)),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Streams a snapshot of all alarms currently Active or ActiveAcked — the gateway's
|
||||
/// ConditionRefresh equivalent. Used after reconnect to seed the local Part 9 state
|
||||
/// machine, or to reconcile alarms that may have been missed during a transport
|
||||
/// blip. Optionally scoped by alarm-reference prefix
|
||||
/// (<see cref="QueryActiveAlarmsRequest.AlarmFilterPrefix"/>) so a partial refresh
|
||||
/// can target an equipment sub-tree.
|
||||
/// </summary>
|
||||
/// <param name="request">The query request, optionally scoped by alarm-reference prefix.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the stream.</param>
|
||||
/// <returns>An async enumerable of active-alarm snapshots.</returns>
|
||||
public IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync(
|
||||
QueryActiveAlarmsRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ThrowIfDisposed();
|
||||
|
||||
return _transport.QueryActiveAlarmsAsync(request, CreateStreamCallOptions(cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the client and releases all resources.
|
||||
/// </summary>
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||
if (_disposed)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_channel?.Dispose();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates gRPC call options with default timeout and authorization.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the call.</param>
|
||||
/// <returns>Configured call options.</returns>
|
||||
internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
|
||||
{
|
||||
return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates gRPC call options for streaming with stream timeout and authorization.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the call.</param>
|
||||
/// <returns>Configured call options.</returns>
|
||||
internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken)
|
||||
{
|
||||
return CreateCallOptions(cancellationToken, Options.StreamTimeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates gRPC call options with specified timeout and authorization.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the call.</param>
|
||||
/// <param name="timeout">Optional timeout duration; null means no timeout.</param>
|
||||
/// <returns>Configured call options.</returns>
|
||||
internal CallOptions CreateCallOptions(
|
||||
CancellationToken cancellationToken,
|
||||
TimeSpan? timeout)
|
||||
@@ -335,6 +228,6 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this);
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,9 @@ namespace MxGateway.Client;
|
||||
/// </summary>
|
||||
public static class MxGatewayClientContractInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the gateway gRPC protocol version compiled into this client package.
|
||||
/// A client and gateway are wire-compatible only when this value matches the
|
||||
/// gateway's advertised gateway protocol version.
|
||||
/// </summary>
|
||||
public const uint GatewayProtocolVersion =
|
||||
GatewayContractInfo.GatewayProtocolVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the worker frame protocol version compiled into this client package.
|
||||
/// Exposed for diagnostics so callers can report the worker protocol the
|
||||
/// shared contracts were generated against.
|
||||
/// </summary>
|
||||
public const uint WorkerProtocolVersion =
|
||||
GatewayContractInfo.WorkerProtocolVersion;
|
||||
}
|
||||
|
||||
@@ -7,74 +7,28 @@ namespace MxGateway.Client;
|
||||
/// </summary>
|
||||
public sealed class MxGatewayClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the gateway endpoint URI (required).
|
||||
/// </summary>
|
||||
public required Uri Endpoint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the API key for gateway authentication (required).
|
||||
/// </summary>
|
||||
public required string ApiKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to use TLS for the gateway connection.
|
||||
/// </summary>
|
||||
public bool UseTls { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to a CA certificate file for custom certificate validation.
|
||||
/// </summary>
|
||||
public string? CaCertificatePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server name override for SNI during TLS handshake.
|
||||
/// </summary>
|
||||
public string? ServerNameOverride { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timeout for establishing connection to the gateway.
|
||||
/// </summary>
|
||||
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timeout budget for a unary gRPC operation. This is both the gRPC
|
||||
/// deadline stamped on each individual attempt and the overall budget for the
|
||||
/// whole safe-unary operation: for retryable calls the initial attempt, every
|
||||
/// retry, and the backoff delays between them all share this single budget.
|
||||
/// It is therefore an upper bound on the total wall-clock time a safe-unary
|
||||
/// call can take, not a fresh per-retry allowance.
|
||||
/// </summary>
|
||||
public TimeSpan DefaultCallTimeout { get; init; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional timeout for streaming gRPC calls.
|
||||
/// </summary>
|
||||
public TimeSpan? StreamTimeout { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum size, in bytes, of a single gRPC message the client will
|
||||
/// send or receive. Applied to both the send and receive limits of the
|
||||
/// underlying channel. Defaults to 16 MiB.
|
||||
/// </summary>
|
||||
public int MaxGrpcMessageBytes { get; init; } = 16 * 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retry configuration for safe unary calls.
|
||||
/// </summary>
|
||||
public MxGatewayClientRetryOptions Retry { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger factory for diagnostic logging.
|
||||
/// </summary>
|
||||
public ILoggerFactory? LoggerFactory { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates the client options for consistency and correctness.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentNullException">Endpoint is null.</exception>
|
||||
/// <exception cref="ArgumentException">Options are invalid or inconsistent.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Timeout values are not greater than zero.</exception>
|
||||
public void Validate()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(Endpoint);
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>Configuration for automatic retry behavior on transient gRPC call failures.</summary>
|
||||
public sealed class MxGatewayClientRetryOptions
|
||||
{
|
||||
/// <summary>Gets the maximum number of attempts (initial + retries); default is 2.</summary>
|
||||
public int MaxAttempts { get; init; } = 2;
|
||||
|
||||
/// <summary>Gets the initial delay between retry attempts; default is 200 milliseconds.</summary>
|
||||
public TimeSpan Delay { get; init; } = TimeSpan.FromMilliseconds(200);
|
||||
|
||||
/// <summary>Gets the maximum delay between retry attempts; default is 2 seconds.</summary>
|
||||
public TimeSpan MaxDelay { get; init; } = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>Gets a value indicating whether to add randomness to retry delays; default is true.</summary>
|
||||
public bool UseJitter { get; init; } = true;
|
||||
|
||||
/// <summary>Validates the retry options and throws if any constraint is violated.</summary>
|
||||
public void Validate()
|
||||
{
|
||||
if (MaxAttempts <= 0)
|
||||
|
||||
@@ -6,12 +6,8 @@ using Polly.Retry;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>Factory and helpers for exponential-backoff retry policies on transient gRPC failures.</summary>
|
||||
internal static class MxGatewayClientRetryPolicy
|
||||
{
|
||||
/// <summary>Creates a Polly ResiliencePipeline that retries transient gRPC failures with exponential backoff.</summary>
|
||||
/// <param name="options">Retry configuration (max attempts, delay bounds, jitter).</param>
|
||||
/// <param name="logger">Optional logger for retry diagnostics.</param>
|
||||
public static ResiliencePipeline Create(
|
||||
MxGatewayClientRetryOptions options,
|
||||
ILogger? logger)
|
||||
@@ -40,8 +36,6 @@ internal static class MxGatewayClientRetryPolicy
|
||||
.Build();
|
||||
}
|
||||
|
||||
/// <summary>Returns whether a command kind is eligible for automatic retry on transient failures.</summary>
|
||||
/// <param name="kind">The command kind to check.</param>
|
||||
public static bool IsRetryableCommand(MxCommandKind kind)
|
||||
{
|
||||
return kind is MxCommandKind.Ping
|
||||
@@ -61,13 +55,8 @@ internal static class MxGatewayClientRetryPolicy
|
||||
|
||||
private static bool IsTransientStatus(StatusCode statusCode)
|
||||
{
|
||||
// DeadlineExceeded is intentionally NOT treated as transient. The deadline
|
||||
// on every unary call is client-imposed (CreateCallOptions stamps the
|
||||
// DefaultCallTimeout budget), and that same budget is shared across the
|
||||
// initial attempt plus all retries plus backoff. A DeadlineExceeded means
|
||||
// the shared budget is exhausted, so an immediate retry would only fail
|
||||
// again — burning the remaining budget on a call that cannot succeed.
|
||||
return statusCode is StatusCode.Unavailable
|
||||
or StatusCode.DeadlineExceeded
|
||||
or StatusCode.ResourceExhausted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,8 @@ using MxGateway.Contracts.Proto;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>Exception thrown when a gateway command fails due to an unclassified protocol error.</summary>
|
||||
public class MxGatewayCommandException : MxGatewayException
|
||||
{
|
||||
/// <summary>Initializes a new instance with the given details.</summary>
|
||||
/// <param name="message">The error message describing the command failure.</param>
|
||||
/// <param name="sessionId">The session ID, if available.</param>
|
||||
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
||||
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
||||
/// <param name="hResult">The HResult code, if available.</param>
|
||||
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
||||
/// <param name="innerException">The underlying exception, if any.</param>
|
||||
public MxGatewayCommandException(
|
||||
string message,
|
||||
string? sessionId = null,
|
||||
|
||||
@@ -1,59 +1,21 @@
|
||||
using Grpc.Core;
|
||||
using MxGateway.Contracts.Proto;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a gateway RPC call fails or returns an error status.
|
||||
/// </summary>
|
||||
public class MxGatewayException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the MxGatewayException class with the specified message.
|
||||
/// </summary>
|
||||
/// <param name="message">Diagnostic message describing the failure.</param>
|
||||
public MxGatewayException(string message)
|
||||
: base(message)
|
||||
{
|
||||
Statuses = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the MxGatewayException class with the specified message and inner exception.
|
||||
/// </summary>
|
||||
/// <param name="message">Diagnostic message describing the failure.</param>
|
||||
/// <param name="innerException">Underlying exception that caused this failure.</param>
|
||||
public MxGatewayException(string message, Exception? innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
Statuses = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the MxGatewayException class carrying the originating
|
||||
/// gRPC status code so callers can distinguish transient from permanent failures.
|
||||
/// </summary>
|
||||
/// <param name="message">Diagnostic message describing the failure.</param>
|
||||
/// <param name="statusCode">The gRPC status code reported by the failed call.</param>
|
||||
/// <param name="innerException">Underlying exception that caused this failure.</param>
|
||||
public MxGatewayException(string message, StatusCode statusCode, Exception? innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
Statuses = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the MxGatewayException class with full diagnostic information.
|
||||
/// </summary>
|
||||
/// <param name="message">Diagnostic message describing the failure.</param>
|
||||
/// <param name="sessionId">Session ID associated with the exception, if available.</param>
|
||||
/// <param name="correlationId">Correlation ID associated with the exception, if available.</param>
|
||||
/// <param name="protocolStatus">Protocol-level status returned by the gateway, if available.</param>
|
||||
/// <param name="hResult">HRESULT code returned by the worker or MXAccess, if available.</param>
|
||||
/// <param name="statuses">List of MXAccess status codes returned by the operation.</param>
|
||||
/// <param name="innerException">Underlying exception that caused this failure.</param>
|
||||
/// <param name="statusCode">The gRPC status code reported by the failed call, if available.</param>
|
||||
public MxGatewayException(
|
||||
string message,
|
||||
string? sessionId,
|
||||
@@ -61,8 +23,7 @@ public class MxGatewayException : Exception
|
||||
ProtocolStatus? protocolStatus,
|
||||
int? hResult,
|
||||
IReadOnlyList<MxStatusProxy> statuses,
|
||||
Exception? innerException = null,
|
||||
StatusCode? statusCode = null)
|
||||
Exception? innerException = null)
|
||||
: base(message, innerException)
|
||||
{
|
||||
SessionId = sessionId;
|
||||
@@ -70,42 +31,15 @@ public class MxGatewayException : Exception
|
||||
ProtocolStatus = protocolStatus;
|
||||
HResultCode = hResult;
|
||||
Statuses = statuses;
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the session ID associated with the exception, if available.
|
||||
/// </summary>
|
||||
public string? SessionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the correlation ID associated with the exception, if available.
|
||||
/// </summary>
|
||||
public string? CorrelationId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the protocol-level status returned by the gateway, if available.
|
||||
/// </summary>
|
||||
public ProtocolStatus? ProtocolStatus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the HRESULT code returned by the worker or MXAccess, if available.
|
||||
/// </summary>
|
||||
public int? HResultCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of MXAccess status codes returned by the operation.
|
||||
/// </summary>
|
||||
public IReadOnlyList<MxStatusProxy> Statuses { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the gRPC status code reported by the failed call, if the failure originated
|
||||
/// from a gRPC <see cref="RpcException"/>. <see langword="null"/> when the exception
|
||||
/// was not produced from a gRPC status (for example, a protocol-level reply failure).
|
||||
/// Callers can inspect this to distinguish a transient outage
|
||||
/// (<see cref="Grpc.Core.StatusCode.Unavailable"/>) from a permanent error
|
||||
/// (<see cref="Grpc.Core.StatusCode.InvalidArgument"/>) without downcasting
|
||||
/// <see cref="Exception.InnerException"/>.
|
||||
/// </summary>
|
||||
public StatusCode? StatusCode { get; }
|
||||
}
|
||||
|
||||
@@ -9,16 +9,8 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
{
|
||||
private readonly MxGatewayClient _client;
|
||||
private readonly SemaphoreSlim _closeLock = new(1, 1);
|
||||
private readonly object _disposeGate = new();
|
||||
private CloseSessionReply? _closeReply;
|
||||
private int _activeCloseCount;
|
||||
private bool _closeLockDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new session backed by the given MXAccess gateway client.
|
||||
/// </summary>
|
||||
/// <param name="client">The gateway client used for commands and events.</param>
|
||||
/// <param name="openSessionReply">The server's session creation response.</param>
|
||||
internal MxGatewaySession(
|
||||
MxGatewayClient client,
|
||||
OpenSessionReply openSessionReply)
|
||||
@@ -27,21 +19,10 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
OpenSessionReply = openSessionReply ?? throw new ArgumentNullException(nameof(openSessionReply));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The session ID assigned by the gateway.
|
||||
/// </summary>
|
||||
public string SessionId => OpenSessionReply.SessionId;
|
||||
|
||||
/// <summary>
|
||||
/// The server's session creation response containing metadata.
|
||||
/// </summary>
|
||||
public OpenSessionReply OpenSessionReply { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Closes the session on the gateway. Idempotent.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The server's close-session reply.</returns>
|
||||
public async Task<CloseSessionReply> CloseAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_closeReply is not null)
|
||||
@@ -49,51 +30,26 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
return _closeReply;
|
||||
}
|
||||
|
||||
// Register as an in-flight closer under the dispose gate. DisposeAsync waits for
|
||||
// _activeCloseCount to drain before disposing the close lock, so the semaphore is
|
||||
// guaranteed to outlive every WaitAsync started here.
|
||||
lock (_disposeGate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_closeLockDisposed, this);
|
||||
_activeCloseCount++;
|
||||
}
|
||||
|
||||
await _closeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await _closeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
if (_closeReply is not null)
|
||||
{
|
||||
if (_closeReply is not null)
|
||||
{
|
||||
return _closeReply;
|
||||
}
|
||||
|
||||
_closeReply = await _client.CloseSessionRawAsync(
|
||||
new CloseSessionRequest { SessionId = SessionId },
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return _closeReply;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_closeLock.Release();
|
||||
}
|
||||
|
||||
_closeReply = await _client.CloseSessionRawAsync(
|
||||
new CloseSessionRequest { SessionId = SessionId },
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return _closeReply;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_disposeGate)
|
||||
{
|
||||
_activeCloseCount--;
|
||||
}
|
||||
_closeLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a client with the MXAccess session, returning a ServerHandle.
|
||||
/// </summary>
|
||||
/// <param name="clientName">Name to register.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The server handle assigned to the registered client.</returns>
|
||||
public async Task<int> RegisterAsync(
|
||||
string clientName,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -101,16 +57,9 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
MxCommandReply reply = await RegisterRawAsync(clientName, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||
return reply.Register?.ServerHandle
|
||||
?? throw CreateMissingPayloadException(reply, "register");
|
||||
return reply.Register?.ServerHandle ?? reply.ReturnValue.Int32Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a client with the MXAccess session without error checking.
|
||||
/// </summary>
|
||||
/// <param name="clientName">Name to register.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The raw server reply.</returns>
|
||||
public Task<MxCommandReply> RegisterRawAsync(
|
||||
string clientName,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -126,13 +75,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an item to the MXAccess session, returning an ItemHandle.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemDefinition">The item tag address.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The item handle assigned to the new item.</returns>
|
||||
public async Task<int> AddItemAsync(
|
||||
int serverHandle,
|
||||
string itemDefinition,
|
||||
@@ -144,17 +86,9 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||
return reply.AddItem?.ItemHandle
|
||||
?? throw CreateMissingPayloadException(reply, "add_item");
|
||||
return reply.AddItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an item to the MXAccess session without error checking.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemDefinition">The item tag address.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The raw server reply.</returns>
|
||||
public Task<MxCommandReply> AddItemRawAsync(
|
||||
int serverHandle,
|
||||
string itemDefinition,
|
||||
@@ -175,14 +109,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an item with context to the MXAccess session, returning an ItemHandle.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemDefinition">The item tag address.</param>
|
||||
/// <param name="itemContext">Additional context for the item.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The item handle assigned to the new item.</returns>
|
||||
public async Task<int> AddItem2Async(
|
||||
int serverHandle,
|
||||
string itemDefinition,
|
||||
@@ -196,18 +122,9 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||
return reply.AddItem2?.ItemHandle
|
||||
?? throw CreateMissingPayloadException(reply, "add_item2");
|
||||
return reply.AddItem2?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an item with context to the MXAccess session without error checking.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemDefinition">The item tag address.</param>
|
||||
/// <param name="itemContext">Additional context for the item.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The raw server reply.</returns>
|
||||
public Task<MxCommandReply> AddItem2RawAsync(
|
||||
int serverHandle,
|
||||
string itemDefinition,
|
||||
@@ -230,12 +147,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to events for an item (advises in MXAccess terminology).
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task AdviseAsync(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
@@ -246,13 +157,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to events for an item without error checking.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The raw server reply.</returns>
|
||||
public Task<MxCommandReply> AdviseRawAsync(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
@@ -271,12 +175,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from events for an item (unadvises in MXAccess terminology).
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task UnAdviseAsync(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
@@ -287,13 +185,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from events for an item without error checking.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The raw server reply.</returns>
|
||||
public Task<MxCommandReply> UnAdviseRawAsync(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
@@ -312,12 +203,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an item from the MXAccess session.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task RemoveItemAsync(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
@@ -328,13 +213,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an item from the MXAccess session without error checking.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The raw server reply.</returns>
|
||||
public Task<MxCommandReply> RemoveItemRawAsync(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
@@ -353,13 +231,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple items to the MXAccess session in a single command.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="tagAddresses">The item tag addresses to add.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Per-item subscription results.</returns>
|
||||
public async Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<string> tagAddresses,
|
||||
@@ -382,13 +253,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
return reply.AddItemBulk?.Results.ToArray() ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advises multiple items in a single command.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandles">The ItemHandles to advise.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Per-item subscription results.</returns>
|
||||
public async Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<int> itemHandles,
|
||||
@@ -411,13 +275,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
return reply.AdviseItemBulk?.Results.ToArray() ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes multiple items in a single command.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandles">The ItemHandles to remove.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Per-item subscription results.</returns>
|
||||
public async Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<int> itemHandles,
|
||||
@@ -440,13 +297,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
return reply.RemoveItemBulk?.Results.ToArray() ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unadvises multiple items in a single command.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandles">The ItemHandles to unadvise.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Per-item subscription results.</returns>
|
||||
public async Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<int> itemHandles,
|
||||
@@ -469,13 +319,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
return reply.UnAdviseItemBulk?.Results.ToArray() ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds and advises multiple items in a single command.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="tagAddresses">The item tag addresses to add and advise.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Per-item subscription results.</returns>
|
||||
public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<string> tagAddresses,
|
||||
@@ -498,13 +341,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
return reply.SubscribeBulk?.Results.ToArray() ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unadvises and removes multiple items in a single command.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandles">The ItemHandles to unsubscribe.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Per-item subscription results.</returns>
|
||||
public async Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(
|
||||
int serverHandle,
|
||||
IReadOnlyList<int> itemHandles,
|
||||
@@ -527,14 +363,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
return reply.UnsubscribeBulk?.Results.ToArray() ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a value to an item on the MXAccess server.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="userId">User ID context for the write.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task WriteAsync(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
@@ -547,15 +375,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a value to an item on the MXAccess server without error checking.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="userId">User ID context for the write.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The raw server reply.</returns>
|
||||
public Task<MxCommandReply> WriteRawAsync(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
@@ -580,15 +399,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a value and timestamp to an item on the MXAccess server.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="timestampValue">The timestamp to write with the value.</param>
|
||||
/// <param name="userId">User ID context for the write.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task Write2Async(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
@@ -608,16 +418,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a value and timestamp to an item on the MXAccess server without error checking.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="timestampValue">The timestamp to write with the value.</param>
|
||||
/// <param name="userId">User ID context for the write.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The raw server reply.</returns>
|
||||
public Task<MxCommandReply> Write2RawAsync(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
@@ -645,12 +445,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes an MXAccess command on this session.
|
||||
/// </summary>
|
||||
/// <param name="request">The command request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The raw server reply.</returns>
|
||||
public Task<MxCommandReply> InvokeAsync(
|
||||
MxCommandRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -659,12 +453,6 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
return _client.InvokeAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Streams events from the worker for this session, optionally starting after a given sequence number.
|
||||
/// </summary>
|
||||
/// <param name="afterWorkerSequence">The sequence number to stream from. Defaults to 0.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>An async enumerable of events.</returns>
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||
ulong afterWorkerSequence = 0,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -678,37 +466,9 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the session and releases resources.
|
||||
/// </summary>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
lock (_disposeGate)
|
||||
{
|
||||
if (_closeLockDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
|
||||
// Wait for every concurrent CloseAsync caller to leave the close lock before
|
||||
// disposing it; once _closeReply is set those callers return without awaiting.
|
||||
while (true)
|
||||
{
|
||||
lock (_disposeGate)
|
||||
{
|
||||
if (_activeCloseCount == 0)
|
||||
{
|
||||
_closeLockDisposed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Yield();
|
||||
}
|
||||
|
||||
_closeLock.Dispose();
|
||||
}
|
||||
|
||||
@@ -726,21 +486,4 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the exception thrown when a command reply passed protocol and
|
||||
/// MXAccess success checks but is missing the typed handle-bearing payload
|
||||
/// the command contract requires. Surfacing this as a clear error avoids
|
||||
/// silently handing a zero handle to the caller (it would otherwise fall
|
||||
/// through to <see cref="MxCommandReply.ReturnValue"/>, which is 0 when the
|
||||
/// reply carries no return value).
|
||||
/// </summary>
|
||||
private static MxGatewayException CreateMissingPayloadException(
|
||||
MxCommandReply reply,
|
||||
string expectedPayload)
|
||||
{
|
||||
return new MxGatewayException(
|
||||
$"Gateway reply for command kind={reply.Kind} reported success but is missing "
|
||||
+ $"the required '{expectedPayload}' payload; cannot resolve a handle. "
|
||||
+ $"session={reply.SessionId}; correlation={reply.CorrelationId}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,8 @@ using MxGateway.Contracts.Proto;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>Exception thrown when a session is not found, not ready, or invalid.</summary>
|
||||
public sealed class MxGatewaySessionException : MxGatewayException
|
||||
{
|
||||
/// <summary>Initializes a new instance with the given details.</summary>
|
||||
/// <param name="message">The error message describing the session failure.</param>
|
||||
/// <param name="sessionId">The session ID, if available.</param>
|
||||
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
||||
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
||||
/// <param name="hResult">The HResult code, if available.</param>
|
||||
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
||||
/// <param name="innerException">The underlying exception, if any.</param>
|
||||
public MxGatewaySessionException(
|
||||
string message,
|
||||
string? sessionId = null,
|
||||
|
||||
@@ -2,17 +2,8 @@ using MxGateway.Contracts.Proto;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>Exception thrown when the worker process is unavailable or fails to process a command.</summary>
|
||||
public sealed class MxGatewayWorkerException : MxGatewayException
|
||||
{
|
||||
/// <summary>Initializes a new instance with the given details.</summary>
|
||||
/// <param name="message">The error message describing the worker failure.</param>
|
||||
/// <param name="sessionId">The session ID, if available.</param>
|
||||
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
||||
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
||||
/// <param name="hResult">The HResult code, if available.</param>
|
||||
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
||||
/// <param name="innerException">The underlying exception, if any.</param>
|
||||
public MxGatewayWorkerException(
|
||||
string message,
|
||||
string? sessionId = null,
|
||||
|
||||
@@ -2,11 +2,8 @@ using MxGateway.Contracts.Proto;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>Extension methods for MxStatusProxy values.</summary>
|
||||
public static class MxStatusProxyExtensions
|
||||
{
|
||||
/// <summary>Returns whether the status indicates success (success flag set and category is Ok).</summary>
|
||||
/// <param name="status">The status to check.</param>
|
||||
public static bool IsSuccess(this MxStatusProxy status)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(status);
|
||||
@@ -15,8 +12,6 @@ public static class MxStatusProxyExtensions
|
||||
&& status.Category is MxStatusCategory.Ok;
|
||||
}
|
||||
|
||||
/// <summary>Returns a formatted summary of the status for diagnostic output.</summary>
|
||||
/// <param name="status">The status to summarize.</param>
|
||||
public static string ToDiagnosticSummary(this MxStatusProxy status)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(status);
|
||||
|
||||
@@ -10,10 +10,6 @@ namespace MxGateway.Client;
|
||||
/// </summary>
|
||||
public static class MxValueExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a boolean value to an MxValue with MxDataType.Boolean.
|
||||
/// </summary>
|
||||
/// <param name="value">Scalar boolean value to wrap.</param>
|
||||
public static MxValue ToMxValue(this bool value)
|
||||
{
|
||||
return new MxValue
|
||||
@@ -24,10 +20,6 @@ public static class MxValueExtensions
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a 32-bit integer value to an MxValue with MxDataType.Integer.
|
||||
/// </summary>
|
||||
/// <param name="value">32-bit integer value to wrap.</param>
|
||||
public static MxValue ToMxValue(this int value)
|
||||
{
|
||||
return new MxValue
|
||||
@@ -38,10 +30,6 @@ public static class MxValueExtensions
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a 64-bit integer value to an MxValue with MxDataType.Integer.
|
||||
/// </summary>
|
||||
/// <param name="value">64-bit integer value to wrap.</param>
|
||||
public static MxValue ToMxValue(this long value)
|
||||
{
|
||||
return new MxValue
|
||||
@@ -52,10 +40,6 @@ public static class MxValueExtensions
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a single-precision floating-point value to an MxValue with MxDataType.Float.
|
||||
/// </summary>
|
||||
/// <param name="value">Single-precision floating-point value to wrap.</param>
|
||||
public static MxValue ToMxValue(this float value)
|
||||
{
|
||||
return new MxValue
|
||||
@@ -66,10 +50,6 @@ public static class MxValueExtensions
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a double-precision floating-point value to an MxValue with MxDataType.Double.
|
||||
/// </summary>
|
||||
/// <param name="value">Double-precision floating-point value to wrap.</param>
|
||||
public static MxValue ToMxValue(this double value)
|
||||
{
|
||||
return new MxValue
|
||||
@@ -80,10 +60,6 @@ public static class MxValueExtensions
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string value to an MxValue with MxDataType.String.
|
||||
/// </summary>
|
||||
/// <param name="value">String value to wrap.</param>
|
||||
public static MxValue ToMxValue(this string value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
@@ -96,10 +72,6 @@ public static class MxValueExtensions
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a DateTimeOffset value to an MxValue with MxDataType.Time.
|
||||
/// </summary>
|
||||
/// <param name="value">DateTimeOffset value to wrap.</param>
|
||||
public static MxValue ToMxValue(this DateTimeOffset value)
|
||||
{
|
||||
return new MxValue
|
||||
@@ -110,10 +82,6 @@ public static class MxValueExtensions
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a DateTime value to an MxValue with MxDataType.Time.
|
||||
/// </summary>
|
||||
/// <param name="value">DateTime value to wrap.</param>
|
||||
public static MxValue ToMxValue(this DateTime value)
|
||||
{
|
||||
return new DateTimeOffset(
|
||||
@@ -123,10 +91,6 @@ public static class MxValueExtensions
|
||||
.ToMxValue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a boolean array to an MxValue with MxDataType.Boolean.
|
||||
/// </summary>
|
||||
/// <param name="values">Array of boolean values to wrap.</param>
|
||||
public static MxValue ToMxValue(this IReadOnlyList<bool> values)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(values);
|
||||
@@ -141,10 +105,6 @@ public static class MxValueExtensions
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a 32-bit integer array to an MxValue with MxDataType.Integer.
|
||||
/// </summary>
|
||||
/// <param name="values">Array of 32-bit integer values to wrap.</param>
|
||||
public static MxValue ToMxValue(this IReadOnlyList<int> values)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(values);
|
||||
@@ -159,10 +119,6 @@ public static class MxValueExtensions
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a 64-bit integer array to an MxValue with MxDataType.Integer.
|
||||
/// </summary>
|
||||
/// <param name="values">Array of 64-bit integer values to wrap.</param>
|
||||
public static MxValue ToMxValue(this IReadOnlyList<long> values)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(values);
|
||||
@@ -177,10 +133,6 @@ public static class MxValueExtensions
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a single-precision floating-point array to an MxValue with MxDataType.Float.
|
||||
/// </summary>
|
||||
/// <param name="values">Array of single-precision floating-point values to wrap.</param>
|
||||
public static MxValue ToMxValue(this IReadOnlyList<float> values)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(values);
|
||||
@@ -195,10 +147,6 @@ public static class MxValueExtensions
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a double-precision floating-point array to an MxValue with MxDataType.Double.
|
||||
/// </summary>
|
||||
/// <param name="values">Array of double-precision floating-point values to wrap.</param>
|
||||
public static MxValue ToMxValue(this IReadOnlyList<double> values)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(values);
|
||||
@@ -213,10 +161,6 @@ public static class MxValueExtensions
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string array to an MxValue with MxDataType.String.
|
||||
/// </summary>
|
||||
/// <param name="values">Array of string values to wrap.</param>
|
||||
public static MxValue ToMxValue(this IReadOnlyList<string> values)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(values);
|
||||
@@ -231,10 +175,6 @@ public static class MxValueExtensions
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a DateTimeOffset array to an MxValue with MxDataType.Time.
|
||||
/// </summary>
|
||||
/// <param name="values">Array of DateTimeOffset values to wrap.</param>
|
||||
public static MxValue ToMxValue(this IReadOnlyList<DateTimeOffset> values)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(values);
|
||||
@@ -249,10 +189,6 @@ public static class MxValueExtensions
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the projection kind (field name) of the given MxValue's current oneof value.
|
||||
/// </summary>
|
||||
/// <param name="value">The MxValue whose oneof projection kind is returned.</param>
|
||||
public static string GetProjectionKind(this MxValue value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
@@ -272,10 +208,6 @@ public static class MxValueExtensions
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an MxValue to a CLR object; returns the boxed value or null for null MxValues.
|
||||
/// </summary>
|
||||
/// <param name="value">The MxValue to convert.</param>
|
||||
public static object? ToClrValue(this MxValue value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
@@ -295,10 +227,6 @@ public static class MxValueExtensions
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an MxArray to a CLR array; returns null if the array does not have a known element type.
|
||||
/// </summary>
|
||||
/// <param name="array">The MxArray to convert.</param>
|
||||
public static object? ToClrArrayValue(this MxArray array)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(array);
|
||||
@@ -321,13 +249,6 @@ public static class MxValueExtensions
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an MxValue with MxDataType.Unknown from raw byte data, variant type, and diagnostic info.
|
||||
/// </summary>
|
||||
/// <param name="value">Raw byte data representing the value.</param>
|
||||
/// <param name="variantType">Variant type string (e.g., "VT_BSTR").</param>
|
||||
/// <param name="rawDiagnostic">Diagnostic string describing the raw value.</param>
|
||||
/// <param name="rawDataType">Optional MXAccess data type override.</param>
|
||||
public static MxValue ToRawMxValue(
|
||||
byte[] value,
|
||||
string variantType,
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
using Grpc.Core;
|
||||
|
||||
namespace MxGateway.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Maps low-level <see cref="RpcException"/>s raised by the gRPC stack to the client's
|
||||
/// native exception hierarchy. Shared by every gateway and Galaxy Repository transport
|
||||
/// so the gRPC-to-native translation has exactly one implementation.
|
||||
/// </summary>
|
||||
internal static class RpcExceptionMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Translates a <see cref="RpcException"/> into the most specific native exception type.
|
||||
/// </summary>
|
||||
/// <param name="exception">The gRPC exception to translate.</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// The cancellation token of the originating call; used to distinguish a caller-driven
|
||||
/// cancellation from a server-side <see cref="StatusCode.Cancelled"/> status.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// An <see cref="OperationCanceledException"/> when the call was cancelled, a typed
|
||||
/// authentication/authorization exception for auth statuses, or an
|
||||
/// <see cref="MxGatewayException"/> carrying the originating gRPC <see cref="StatusCode"/>.
|
||||
/// </returns>
|
||||
public static Exception Map(
|
||||
RpcException exception,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested || exception.StatusCode == StatusCode.Cancelled)
|
||||
{
|
||||
return new OperationCanceledException(
|
||||
exception.Status.Detail,
|
||||
exception,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
return exception.StatusCode switch
|
||||
{
|
||||
StatusCode.Unauthenticated => new MxGatewayAuthenticationException(
|
||||
exception.Status.Detail,
|
||||
statusCode: exception.StatusCode,
|
||||
innerException: exception),
|
||||
StatusCode.PermissionDenied => new MxGatewayAuthorizationException(
|
||||
exception.Status.Detail,
|
||||
statusCode: exception.StatusCode,
|
||||
innerException: exception),
|
||||
_ => new MxGatewayException(
|
||||
exception.Status.Detail,
|
||||
exception.StatusCode,
|
||||
exception),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -112,17 +112,6 @@ can keep the full `MxCommandReply`, HRESULT, and status array when MXAccess
|
||||
itself rejects a command. `MxAccessException.Reply` contains the raw generated
|
||||
reply.
|
||||
|
||||
When a gRPC call itself fails, the transport maps the underlying
|
||||
`RpcException` to a native exception: `Unauthenticated` becomes
|
||||
`MxGatewayAuthenticationException`, `PermissionDenied` becomes
|
||||
`MxGatewayAuthorizationException`, a cancelled call becomes
|
||||
`OperationCanceledException`, and every other status becomes a base
|
||||
`MxGatewayException`. `MxGatewayException.StatusCode` carries the originating
|
||||
gRPC `Grpc.Core.StatusCode` (non-null whenever the failure came from a gRPC
|
||||
status), so callers can distinguish a transient outage (`Unavailable`) from a
|
||||
permanent error (`InvalidArgument`, `NotFound`) without downcasting
|
||||
`InnerException`.
|
||||
|
||||
## CLI Usage
|
||||
|
||||
The test CLI supports deterministic JSON output for automation:
|
||||
@@ -142,8 +131,7 @@ dotnet run --project clients/dotnet/MxGateway.Client.Cli -- smoke --endpoint htt
|
||||
`smoke` opens a session, registers a client, adds one item, advises it,
|
||||
optionally writes a value when `--type` and `--value` are supplied, reads a
|
||||
bounded event stream, and closes the session in a `finally` block. CLI error
|
||||
output redacts the effective API key, whether it was supplied through
|
||||
`--api-key` or resolved from the `--api-key-env` environment variable.
|
||||
output redacts API keys supplied through `--api-key`.
|
||||
|
||||
## Galaxy Repository Browse
|
||||
|
||||
@@ -255,5 +243,5 @@ dotnet run --project clients/dotnet/MxGateway.Client.Cli -- smoke --endpoint $en
|
||||
## Related Documentation
|
||||
|
||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
||||
- [.NET Client Detailed Design](./DotnetClientDesign.md)
|
||||
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
||||
- [.NET Client Detailed Design](../../docs/clients-dotnet-csharp-design.md)
|
||||
|
||||
+5
-24
@@ -3,7 +3,7 @@
|
||||
The Go client module contains the generated MXAccess Gateway protobuf bindings,
|
||||
a small handwritten `mxgateway` package, and the `mxgw-go` test CLI scaffold.
|
||||
The module uses the shared proto inputs documented in
|
||||
`../../docs/ClientProtoGeneration.md` so gateway and client contracts stay in
|
||||
`../../docs/client-proto-generation.md` so gateway and client contracts stay in
|
||||
sync.
|
||||
|
||||
## Layout
|
||||
@@ -28,7 +28,7 @@ Run generation after the shared `.proto` files or the Go output path changes:
|
||||
./generate-proto.ps1
|
||||
```
|
||||
|
||||
The script uses the tool paths recorded in `../../docs/ToolchainLinks.md`.
|
||||
The script uses the tool paths recorded in `../../docs/toolchain-links.md`.
|
||||
|
||||
## Build And Test
|
||||
|
||||
@@ -79,30 +79,11 @@ client, err := mxgateway.Dial(ctx, mxgateway.Options{
|
||||
`AddItem`, `AddItem2`, `Advise`, `Write`, `Events`, and `Close`. Prefer
|
||||
`SubscribeEvents` or `SubscribeEventsAfter` for long-running streams because the
|
||||
returned subscription owns cancellation and exposes `Close` for deterministic
|
||||
goroutine cleanup. `Events` and `EventsAfter` are a compatibility shim with a
|
||||
bounded internal buffer: if the consumer drains too slowly the buffer fills,
|
||||
the underlying stream is cancelled, and a terminal `EventResult` carrying
|
||||
`ErrEventBufferOverflow` is delivered as the channel's last item before it
|
||||
closes — so a slow consumer can distinguish dropped events from a normal
|
||||
end-of-stream. `SubscribeEvents` blocks instead of dropping, so use it when no
|
||||
events may be lost. Raw protobuf messages remain available through the
|
||||
goroutine cleanup. Raw protobuf messages remain available through the
|
||||
`mxgateway` package aliases and the `Raw` helper methods. Typed errors support
|
||||
`errors.As` for `GatewayError`, `CommandError`, and `MxAccessError`; command
|
||||
errors preserve the raw reply.
|
||||
|
||||
`Dial` and `DialGalaxy` create the connection lazily (`grpc.NewClient`): a
|
||||
gateway that is briefly unavailable no longer turns into a hard error — the
|
||||
connection recovers once the gateway comes up. To keep fail-fast behavior,
|
||||
both run a readiness probe bounded by `DialTimeout` (default 10s, or the
|
||||
context deadline when sooner) and return a `*GatewayError` if the gateway
|
||||
cannot be reached in that window.
|
||||
|
||||
For retry, timeout, and auth handling, `GatewayError.Code()` exposes the
|
||||
wrapped gRPC `codes.Code`, and `mxgateway.IsTransient(err)` reports whether a
|
||||
failure (`Unavailable`, `DeadlineExceeded`, `ResourceExhausted`, `Aborted`)
|
||||
may succeed on retry — so callers do not have to unwrap the error and call
|
||||
`status.Code` themselves.
|
||||
|
||||
## Galaxy Repository browse
|
||||
|
||||
The `GalaxyRepository` service (proto package `galaxy_repository.v1`) is a
|
||||
@@ -228,5 +209,5 @@ go run ./cmd/mxgw-go smoke -endpoint $env:MXGATEWAY_ENDPOINT -plaintext -api-key
|
||||
## Related Documentation
|
||||
|
||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
||||
- [Go Client Detailed Design](./GoClientDesign.md)
|
||||
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
||||
- [Go Client Detailed Design](../../docs/clients-golang-design.md)
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
// Command mxgw-go is the reference Go CLI for the MXAccess Gateway.
|
||||
//
|
||||
// It exposes versioning, session lifecycle, command invocation, event
|
||||
// streaming, a smoke-test workflow, and Galaxy Repository browse subcommands
|
||||
// that exercise the same gRPC contract used by the mxgateway library.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -331,11 +326,6 @@ func runUnsubscribeBulk(ctx context.Context, args []string, stdout, stderr io.Wr
|
||||
return errors.New("session-id and item-handles are required")
|
||||
}
|
||||
|
||||
handles, err := parseInt32List(*itemHandles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, options, err := dialForCommand(ctx, common)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -343,7 +333,7 @@ func runUnsubscribeBulk(ctx context.Context, args []string, stdout, stderr io.Wr
|
||||
defer client.Close()
|
||||
|
||||
session := mxgateway.NewSessionForID(client, *sessionID)
|
||||
results, err := session.UnsubscribeBulk(ctx, int32(*serverHandle), handles)
|
||||
results, err := session.UnsubscribeBulk(ctx, int32(*serverHandle), parseInt32List(*itemHandles))
|
||||
return writeBulkOutput(stdout, *jsonOutput, "unsubscribe-bulk", options, results, err)
|
||||
}
|
||||
|
||||
@@ -519,7 +509,7 @@ func parseStringList(value string) []string {
|
||||
return items
|
||||
}
|
||||
|
||||
func parseInt32List(value string) ([]int32, error) {
|
||||
func parseInt32List(value string) []int32 {
|
||||
parts := strings.Split(value, ",")
|
||||
items := make([]int32, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
@@ -529,11 +519,11 @@ func parseInt32List(value string) ([]int32, error) {
|
||||
}
|
||||
parsed, err := strconv.ParseInt(item, 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid item handle %q: %w", item, err)
|
||||
panic(err)
|
||||
}
|
||||
items = append(items, int32(parsed))
|
||||
}
|
||||
return items, nil
|
||||
return items
|
||||
}
|
||||
|
||||
func bindCommonFlags(flags *flag.FlagSet) *commonOptions {
|
||||
|
||||
@@ -56,32 +56,3 @@ func TestParseValueBuildsTypedValue(t *testing.T) {
|
||||
t.Fatalf("int32 value = %d, want 123", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInt32ListParsesValidTokens(t *testing.T) {
|
||||
items, err := parseInt32List("1, 2 ,3")
|
||||
if err != nil {
|
||||
t.Fatalf("parseInt32List() error = %v", err)
|
||||
}
|
||||
want := []int32{1, 2, 3}
|
||||
if len(items) != len(want) {
|
||||
t.Fatalf("parseInt32List() = %v, want %v", items, want)
|
||||
}
|
||||
for i := range want {
|
||||
if items[i] != want[i] {
|
||||
t.Fatalf("parseInt32List()[%d] = %d, want %d", i, items[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInt32ListReturnsErrorOnMalformedToken(t *testing.T) {
|
||||
items, err := parseInt32List("1,foo")
|
||||
if err == nil {
|
||||
t.Fatalf("parseInt32List() error = nil, want a parse error; items = %v", items)
|
||||
}
|
||||
if items != nil {
|
||||
t.Fatalf("parseInt32List() items = %v, want nil on error", items)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "foo") {
|
||||
t.Fatalf("parseInt32List() error = %q, want it to name the bad token", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,13 @@ $protoc = 'C:\Users\dohertj2\AppData\Local\Microsoft\WinGet\Packages\Google.Prot
|
||||
$goPluginPath = 'C:\Users\dohertj2\go\bin'
|
||||
|
||||
if (-not (Test-Path $protoc)) {
|
||||
throw "protoc was not found at $protoc. See docs/ToolchainLinks.md."
|
||||
throw "protoc was not found at $protoc. See docs/toolchain-links.md."
|
||||
}
|
||||
|
||||
foreach ($pluginName in @('protoc-gen-go.exe', 'protoc-gen-go-grpc.exe')) {
|
||||
$pluginPath = Join-Path $goPluginPath $pluginName
|
||||
if (-not (Test-Path $pluginPath)) {
|
||||
throw "$pluginName was not found at $pluginPath. See docs/ToolchainLinks.md."
|
||||
throw "$pluginName was not found at $pluginPath. See docs/toolchain-links.md."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,12 +19,10 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
MxAccessGateway_OpenSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/OpenSession"
|
||||
MxAccessGateway_CloseSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/CloseSession"
|
||||
MxAccessGateway_Invoke_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/Invoke"
|
||||
MxAccessGateway_StreamEvents_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/StreamEvents"
|
||||
MxAccessGateway_AcknowledgeAlarm_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/AcknowledgeAlarm"
|
||||
MxAccessGateway_QueryActiveAlarms_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/QueryActiveAlarms"
|
||||
MxAccessGateway_OpenSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/OpenSession"
|
||||
MxAccessGateway_CloseSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/CloseSession"
|
||||
MxAccessGateway_Invoke_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/Invoke"
|
||||
MxAccessGateway_StreamEvents_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/StreamEvents"
|
||||
)
|
||||
|
||||
// MxAccessGatewayClient is the client API for MxAccessGateway service.
|
||||
@@ -37,8 +35,6 @@ type MxAccessGatewayClient interface {
|
||||
CloseSession(ctx context.Context, in *CloseSessionRequest, opts ...grpc.CallOption) (*CloseSessionReply, error)
|
||||
Invoke(ctx context.Context, in *MxCommandRequest, opts ...grpc.CallOption) (*MxCommandReply, error)
|
||||
StreamEvents(ctx context.Context, in *StreamEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[MxEvent], error)
|
||||
AcknowledgeAlarm(ctx context.Context, in *AcknowledgeAlarmRequest, opts ...grpc.CallOption) (*AcknowledgeAlarmReply, error)
|
||||
QueryActiveAlarms(ctx context.Context, in *QueryActiveAlarmsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ActiveAlarmSnapshot], error)
|
||||
}
|
||||
|
||||
type mxAccessGatewayClient struct {
|
||||
@@ -98,35 +94,6 @@ func (c *mxAccessGatewayClient) StreamEvents(ctx context.Context, in *StreamEven
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type MxAccessGateway_StreamEventsClient = grpc.ServerStreamingClient[MxEvent]
|
||||
|
||||
func (c *mxAccessGatewayClient) AcknowledgeAlarm(ctx context.Context, in *AcknowledgeAlarmRequest, opts ...grpc.CallOption) (*AcknowledgeAlarmReply, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AcknowledgeAlarmReply)
|
||||
err := c.cc.Invoke(ctx, MxAccessGateway_AcknowledgeAlarm_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *mxAccessGatewayClient) QueryActiveAlarms(ctx context.Context, in *QueryActiveAlarmsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ActiveAlarmSnapshot], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &MxAccessGateway_ServiceDesc.Streams[1], MxAccessGateway_QueryActiveAlarms_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[QueryActiveAlarmsRequest, ActiveAlarmSnapshot]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type MxAccessGateway_QueryActiveAlarmsClient = grpc.ServerStreamingClient[ActiveAlarmSnapshot]
|
||||
|
||||
// MxAccessGatewayServer is the server API for MxAccessGateway service.
|
||||
// All implementations must embed UnimplementedMxAccessGatewayServer
|
||||
// for forward compatibility.
|
||||
@@ -137,8 +104,6 @@ type MxAccessGatewayServer interface {
|
||||
CloseSession(context.Context, *CloseSessionRequest) (*CloseSessionReply, error)
|
||||
Invoke(context.Context, *MxCommandRequest) (*MxCommandReply, error)
|
||||
StreamEvents(*StreamEventsRequest, grpc.ServerStreamingServer[MxEvent]) error
|
||||
AcknowledgeAlarm(context.Context, *AcknowledgeAlarmRequest) (*AcknowledgeAlarmReply, error)
|
||||
QueryActiveAlarms(*QueryActiveAlarmsRequest, grpc.ServerStreamingServer[ActiveAlarmSnapshot]) error
|
||||
mustEmbedUnimplementedMxAccessGatewayServer()
|
||||
}
|
||||
|
||||
@@ -161,12 +126,6 @@ func (UnimplementedMxAccessGatewayServer) Invoke(context.Context, *MxCommandRequ
|
||||
func (UnimplementedMxAccessGatewayServer) StreamEvents(*StreamEventsRequest, grpc.ServerStreamingServer[MxEvent]) error {
|
||||
return status.Error(codes.Unimplemented, "method StreamEvents not implemented")
|
||||
}
|
||||
func (UnimplementedMxAccessGatewayServer) AcknowledgeAlarm(context.Context, *AcknowledgeAlarmRequest) (*AcknowledgeAlarmReply, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AcknowledgeAlarm not implemented")
|
||||
}
|
||||
func (UnimplementedMxAccessGatewayServer) QueryActiveAlarms(*QueryActiveAlarmsRequest, grpc.ServerStreamingServer[ActiveAlarmSnapshot]) error {
|
||||
return status.Error(codes.Unimplemented, "method QueryActiveAlarms not implemented")
|
||||
}
|
||||
func (UnimplementedMxAccessGatewayServer) mustEmbedUnimplementedMxAccessGatewayServer() {}
|
||||
func (UnimplementedMxAccessGatewayServer) testEmbeddedByValue() {}
|
||||
|
||||
@@ -253,35 +212,6 @@ func _MxAccessGateway_StreamEvents_Handler(srv interface{}, stream grpc.ServerSt
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type MxAccessGateway_StreamEventsServer = grpc.ServerStreamingServer[MxEvent]
|
||||
|
||||
func _MxAccessGateway_AcknowledgeAlarm_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AcknowledgeAlarmRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(MxAccessGatewayServer).AcknowledgeAlarm(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: MxAccessGateway_AcknowledgeAlarm_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(MxAccessGatewayServer).AcknowledgeAlarm(ctx, req.(*AcknowledgeAlarmRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _MxAccessGateway_QueryActiveAlarms_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(QueryActiveAlarmsRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(MxAccessGatewayServer).QueryActiveAlarms(m, &grpc.GenericServerStream[QueryActiveAlarmsRequest, ActiveAlarmSnapshot]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type MxAccessGateway_QueryActiveAlarmsServer = grpc.ServerStreamingServer[ActiveAlarmSnapshot]
|
||||
|
||||
// MxAccessGateway_ServiceDesc is the grpc.ServiceDesc for MxAccessGateway service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -301,10 +231,6 @@ var MxAccessGateway_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Invoke",
|
||||
Handler: _MxAccessGateway_Invoke_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AcknowledgeAlarm",
|
||||
Handler: _MxAccessGateway_AcknowledgeAlarm_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
@@ -312,11 +238,6 @@ var MxAccessGateway_ServiceDesc = grpc.ServiceDesc{
|
||||
Handler: _MxAccessGateway_StreamEvents_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "QueryActiveAlarms",
|
||||
Handler: _MxAccessGateway_QueryActiveAlarms_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "mxaccess_gateway.proto",
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package mxgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// AcknowledgeAlarm acknowledges an active MXAccess alarm condition through the
|
||||
// gateway. The gateway authenticates the request against the API key's
|
||||
// invoke:alarm-ack scope and forwards the acknowledge to the worker's MXAccess
|
||||
// session; the resulting native MxStatus is returned in the reply.
|
||||
//
|
||||
// Acks are idempotent — re-acking an already-acked condition is a no-op at
|
||||
// the MxAccess layer.
|
||||
func (c *Client) AcknowledgeAlarm(ctx context.Context, req *AcknowledgeAlarmRequest) (*AcknowledgeAlarmReply, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("mxgateway: acknowledge alarm request is required")
|
||||
}
|
||||
|
||||
callCtx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
reply, err := c.raw.AcknowledgeAlarm(callCtx, req)
|
||||
if err != nil {
|
||||
return nil, &GatewayError{Op: "acknowledge alarm", Err: err}
|
||||
}
|
||||
if err := EnsureProtocolSuccess("acknowledge alarm", reply.GetProtocolStatus(), nil); err != nil {
|
||||
return reply, err
|
||||
}
|
||||
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
// QueryActiveAlarms streams a snapshot of all alarms currently Active or
|
||||
// ActiveAcked — the gateway's ConditionRefresh equivalent. Used after reconnect
|
||||
// to seed local Part 9 state, or to reconcile alarms that may have been missed
|
||||
// during a transport blip.
|
||||
//
|
||||
// The returned stream is owned by the caller; cancel ctx to release it.
|
||||
// Optional alarm-reference prefix scoping (req.AlarmFilterPrefix) limits the
|
||||
// stream to a sub-tree.
|
||||
func (c *Client) QueryActiveAlarms(ctx context.Context, req *QueryActiveAlarmsRequest) (QueryActiveAlarmsClient, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("mxgateway: query active alarms request is required")
|
||||
}
|
||||
|
||||
stream, err := c.raw.QueryActiveAlarms(ctx, req)
|
||||
if err != nil {
|
||||
return nil, &GatewayError{Op: "query active alarms", Err: err}
|
||||
}
|
||||
|
||||
return stream, nil
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
package mxgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
)
|
||||
|
||||
// PR E.4 — pins the Go SDK surface for the new alarm RPCs:
|
||||
// AcknowledgeAlarm + QueryActiveAlarms.
|
||||
|
||||
func TestAcknowledgeAlarmSendsRequestAndReturnsReply(t *testing.T) {
|
||||
fake := &fakeGatewayWithAlarms{
|
||||
acknowledgeReply: &pb.AcknowledgeAlarmReply{
|
||||
SessionId: "session-1",
|
||||
CorrelationId: "corr-1",
|
||||
ProtocolStatus: &pb.ProtocolStatus{
|
||||
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
|
||||
},
|
||||
Status: &pb.MxStatusProxy{
|
||||
Success: 1,
|
||||
Category: pb.MxStatusCategory_MX_STATUS_CATEGORY_OK,
|
||||
},
|
||||
},
|
||||
}
|
||||
client, cleanup := newBufconnClientWithAlarms(t, fake)
|
||||
defer cleanup()
|
||||
|
||||
reply, err := client.AcknowledgeAlarm(context.Background(), &pb.AcknowledgeAlarmRequest{
|
||||
SessionId: "session-1",
|
||||
ClientCorrelationId: "corr-1",
|
||||
AlarmFullReference: "Tank01.Level.HiHi",
|
||||
Comment: "investigating",
|
||||
OperatorUser: "alice",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("AcknowledgeAlarm() error = %v", err)
|
||||
}
|
||||
if reply.GetProtocolStatus().GetCode() != pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK {
|
||||
t.Fatalf("protocol status = %v", reply.GetProtocolStatus().GetCode())
|
||||
}
|
||||
if got := fake.acknowledgeRequest.GetAlarmFullReference(); got != "Tank01.Level.HiHi" {
|
||||
t.Fatalf("captured alarm reference = %q", got)
|
||||
}
|
||||
if got := fake.acknowledgeRequest.GetComment(); got != "investigating" {
|
||||
t.Fatalf("captured comment = %q", got)
|
||||
}
|
||||
if got := fake.acknowledgeAuth; got != "Bearer test-api-key" {
|
||||
t.Fatalf("authorization metadata = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcknowledgeAlarmRejectsNilRequest(t *testing.T) {
|
||||
fake := &fakeGatewayWithAlarms{}
|
||||
client, cleanup := newBufconnClientWithAlarms(t, fake)
|
||||
defer cleanup()
|
||||
|
||||
_, err := client.AcknowledgeAlarm(context.Background(), nil)
|
||||
if err == nil || !errors.Is(err, errors.Unwrap(err)) && err.Error() != "mxgateway: acknowledge alarm request is required" {
|
||||
// Accept either: the helper returned the literal sentinel, or the
|
||||
// generic transport error — both prove nil was rejected.
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatalf("AcknowledgeAlarm(nil) returned no error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcknowledgeAlarmMapsUnauthenticated(t *testing.T) {
|
||||
fake := &fakeGatewayWithAlarms{
|
||||
acknowledgeError: status.Error(codes.Unauthenticated, "expired key"),
|
||||
}
|
||||
client, cleanup := newBufconnClientWithAlarms(t, fake)
|
||||
defer cleanup()
|
||||
|
||||
_, err := client.AcknowledgeAlarm(context.Background(), &pb.AcknowledgeAlarmRequest{
|
||||
SessionId: "session-1",
|
||||
AlarmFullReference: "Tank01.Level.HiHi",
|
||||
OperatorUser: "alice",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("AcknowledgeAlarm() returned no error on Unauthenticated")
|
||||
}
|
||||
var gwErr *GatewayError
|
||||
if !errors.As(err, &gwErr) {
|
||||
t.Fatalf("error %T does not unwrap to *GatewayError", err)
|
||||
}
|
||||
if got, _ := status.FromError(gwErr.Err); got.Code() != codes.Unauthenticated {
|
||||
t.Fatalf("inner status code = %v", got.Code())
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryActiveAlarmsStreamsSnapshots(t *testing.T) {
|
||||
fake := &fakeGatewayWithAlarms{
|
||||
activeSnapshots: []*pb.ActiveAlarmSnapshot{
|
||||
{
|
||||
AlarmFullReference: "Tank01.Level.HiHi",
|
||||
CurrentState: pb.AlarmConditionState_ALARM_CONDITION_STATE_ACTIVE,
|
||||
Severity: 750,
|
||||
},
|
||||
{
|
||||
AlarmFullReference: "Tank02.Level.HiHi",
|
||||
CurrentState: pb.AlarmConditionState_ALARM_CONDITION_STATE_ACTIVE_ACKED,
|
||||
Severity: 750,
|
||||
},
|
||||
},
|
||||
}
|
||||
client, cleanup := newBufconnClientWithAlarms(t, fake)
|
||||
defer cleanup()
|
||||
|
||||
stream, err := client.QueryActiveAlarms(context.Background(), &pb.QueryActiveAlarmsRequest{
|
||||
SessionId: "session-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("QueryActiveAlarms() error = %v", err)
|
||||
}
|
||||
|
||||
var received []*pb.ActiveAlarmSnapshot
|
||||
for {
|
||||
snap, err := stream.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("stream.Recv() error = %v", err)
|
||||
}
|
||||
received = append(received, snap)
|
||||
}
|
||||
if len(received) != 2 {
|
||||
t.Fatalf("snapshot count = %d, want 2", len(received))
|
||||
}
|
||||
if received[0].GetAlarmFullReference() != "Tank01.Level.HiHi" {
|
||||
t.Fatalf("snapshot[0] ref = %q", received[0].GetAlarmFullReference())
|
||||
}
|
||||
if received[1].GetCurrentState() != pb.AlarmConditionState_ALARM_CONDITION_STATE_ACTIVE_ACKED {
|
||||
t.Fatalf("snapshot[1] state = %v", received[1].GetCurrentState())
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryActiveAlarmsPassesFilterPrefix(t *testing.T) {
|
||||
fake := &fakeGatewayWithAlarms{}
|
||||
client, cleanup := newBufconnClientWithAlarms(t, fake)
|
||||
defer cleanup()
|
||||
|
||||
stream, err := client.QueryActiveAlarms(context.Background(), &pb.QueryActiveAlarmsRequest{
|
||||
SessionId: "session-1",
|
||||
AlarmFilterPrefix: "Tank01.",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("QueryActiveAlarms() error = %v", err)
|
||||
}
|
||||
for {
|
||||
_, err := stream.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("stream.Recv() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if got := fake.queryRequest.GetAlarmFilterPrefix(); got != "Tank01." {
|
||||
t.Fatalf("captured filter prefix = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeGatewayWithAlarms struct {
|
||||
pb.UnimplementedMxAccessGatewayServer
|
||||
|
||||
acknowledgeRequest *pb.AcknowledgeAlarmRequest
|
||||
acknowledgeReply *pb.AcknowledgeAlarmReply
|
||||
acknowledgeError error
|
||||
acknowledgeAuth string
|
||||
|
||||
queryRequest *pb.QueryActiveAlarmsRequest
|
||||
activeSnapshots []*pb.ActiveAlarmSnapshot
|
||||
}
|
||||
|
||||
func (s *fakeGatewayWithAlarms) AcknowledgeAlarm(ctx context.Context, req *pb.AcknowledgeAlarmRequest) (*pb.AcknowledgeAlarmReply, error) {
|
||||
s.acknowledgeRequest = req
|
||||
s.acknowledgeAuth = authorizationFromContext(ctx)
|
||||
if s.acknowledgeError != nil {
|
||||
return nil, s.acknowledgeError
|
||||
}
|
||||
if s.acknowledgeReply != nil {
|
||||
return s.acknowledgeReply, nil
|
||||
}
|
||||
return &pb.AcknowledgeAlarmReply{
|
||||
SessionId: req.GetSessionId(),
|
||||
ProtocolStatus: &pb.ProtocolStatus{
|
||||
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *fakeGatewayWithAlarms) QueryActiveAlarms(req *pb.QueryActiveAlarmsRequest, stream grpc.ServerStreamingServer[pb.ActiveAlarmSnapshot]) error {
|
||||
s.queryRequest = req
|
||||
for _, snap := range s.activeSnapshots {
|
||||
if err := stream.Send(snap); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newBufconnClientWithAlarms(t *testing.T, fake *fakeGatewayWithAlarms) (*Client, func()) {
|
||||
t.Helper()
|
||||
listener := bufconn.Listen(bufSize)
|
||||
server := grpc.NewServer()
|
||||
pb.RegisterMxAccessGatewayServer(server, fake)
|
||||
go func() {
|
||||
_ = server.Serve(listener)
|
||||
}()
|
||||
dialer := func(ctx context.Context, _ string) (net.Conn, error) {
|
||||
return listener.DialContext(ctx)
|
||||
}
|
||||
// grpc.NewClient defaults to the dns scheme; use passthrough so the
|
||||
// bufconn fake target reaches the context dialer unresolved.
|
||||
client, err := Dial(context.Background(), Options{
|
||||
Endpoint: "passthrough:///bufnet",
|
||||
APIKey: "test-api-key",
|
||||
Plaintext: true,
|
||||
DialOptions: []grpc.DialOption{grpc.WithContextDialer(dialer)},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Dial() error = %v", err)
|
||||
}
|
||||
return client, func() {
|
||||
client.Close()
|
||||
server.Stop()
|
||||
listener.Close()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,3 @@
|
||||
// Package mxgateway is the Go client for the MXAccess Gateway gRPC service.
|
||||
//
|
||||
// The package wraps the generated gRPC contract with session-oriented helpers
|
||||
// for invoking MXAccess commands, streaming events, and browsing the Galaxy
|
||||
// Repository. Authentication uses an API-key bearer token attached as gRPC
|
||||
// metadata on every call.
|
||||
//
|
||||
// Typical use opens a Client with Dial, opens a Session, invokes commands such
|
||||
// as Register, AddItem, Advise, and Write, and consumes events via
|
||||
// SubscribeEvents. Galaxy Repository browse RPCs are exposed through
|
||||
// GalaxyClient.
|
||||
package mxgateway
|
||||
|
||||
import (
|
||||
@@ -19,7 +8,6 @@ import (
|
||||
|
||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
@@ -28,6 +16,7 @@ import (
|
||||
const (
|
||||
defaultDialTimeout = 10 * time.Second
|
||||
defaultCallTimeout = 30 * time.Second
|
||||
defaultMaxGrpcMessageBytes = 16 * 1024 * 1024
|
||||
)
|
||||
|
||||
// Client owns a gateway gRPC connection and exposes session-oriented helpers.
|
||||
@@ -37,36 +26,22 @@ type Client struct {
|
||||
opts Options
|
||||
}
|
||||
|
||||
// Dial opens a gRPC connection to the gateway and configures auth metadata
|
||||
// and transport security.
|
||||
//
|
||||
// The connection is created lazily with grpc.NewClient: the channel is not
|
||||
// established until the first RPC (or the readiness probe below) needs it, so
|
||||
// a gateway that is briefly unavailable at Dial time no longer turns into a
|
||||
// hard error — the connection recovers when the gateway comes up. To preserve
|
||||
// fail-fast behavior, Dial then runs an explicit readiness probe bounded by
|
||||
// DialTimeout (default 10s, or ctx's deadline when sooner): it triggers the
|
||||
// initial connect and waits for the channel to reach Ready, returning a
|
||||
// *GatewayError if the gateway cannot be reached in that window. Cancelling
|
||||
// ctx aborts the probe.
|
||||
// Dial opens a gRPC connection to the gateway and configures auth metadata,
|
||||
// transport security, and blocking dial cancellation from ctx.
|
||||
func Dial(ctx context.Context, opts Options) (*Client, error) {
|
||||
conn, err := dial(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewClient(conn, opts), nil
|
||||
}
|
||||
|
||||
// dial builds the shared gRPC connection used by both Client and GalaxyClient:
|
||||
// it resolves transport credentials, assembles dial options, creates a lazy
|
||||
// connection with grpc.NewClient, and runs the DialTimeout-bounded readiness
|
||||
// probe so callers still fail fast when the gateway is unreachable.
|
||||
func dial(ctx context.Context, opts Options) (*grpc.ClientConn, error) {
|
||||
if opts.Endpoint == "" {
|
||||
return nil, errors.New("mxgateway: endpoint is required")
|
||||
}
|
||||
|
||||
dialCtx := ctx
|
||||
cancel := func() {}
|
||||
if opts.DialTimeout > 0 {
|
||||
dialCtx, cancel = context.WithTimeout(ctx, opts.DialTimeout)
|
||||
} else if _, ok := ctx.Deadline(); !ok {
|
||||
dialCtx, cancel = context.WithTimeout(ctx, defaultDialTimeout)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
transportCredentials, err := resolveTransportCredentials(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -76,46 +51,27 @@ func dial(ctx context.Context, opts Options) (*grpc.ClientConn, error) {
|
||||
grpc.WithTransportCredentials(transportCredentials),
|
||||
grpc.WithUnaryInterceptor(unaryAuthInterceptor(opts.APIKey)),
|
||||
grpc.WithStreamInterceptor(streamAuthInterceptor(opts.APIKey)),
|
||||
grpc.WithDefaultCallOptions(
|
||||
grpc.MaxCallRecvMsgSize(resolveMaxGrpcMessageBytes(opts)),
|
||||
grpc.MaxCallSendMsgSize(resolveMaxGrpcMessageBytes(opts)),
|
||||
),
|
||||
grpc.WithBlock(),
|
||||
}
|
||||
dialOptions = append(dialOptions, opts.DialOptions...)
|
||||
|
||||
conn, err := grpc.NewClient(opts.Endpoint, dialOptions...)
|
||||
conn, err := grpc.DialContext(dialCtx, opts.Endpoint, dialOptions...)
|
||||
if err != nil {
|
||||
return nil, &GatewayError{Op: "dial", Err: err}
|
||||
}
|
||||
|
||||
if err := waitForReady(ctx, conn, opts.DialTimeout); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, &GatewayError{Op: "dial", Err: err}
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
return NewClient(conn, opts), nil
|
||||
}
|
||||
|
||||
// waitForReady triggers the initial connect on conn and blocks until the
|
||||
// channel reaches connectivity.Ready, the timeout elapses, or ctx is
|
||||
// cancelled. The wait is bounded by dialTimeout when positive, otherwise by
|
||||
// ctx's existing deadline, otherwise by defaultDialTimeout.
|
||||
func waitForReady(ctx context.Context, conn *grpc.ClientConn, dialTimeout time.Duration) error {
|
||||
probeCtx := ctx
|
||||
cancel := func() {}
|
||||
if dialTimeout > 0 {
|
||||
probeCtx, cancel = context.WithTimeout(ctx, dialTimeout)
|
||||
} else if _, ok := ctx.Deadline(); !ok {
|
||||
probeCtx, cancel = context.WithTimeout(ctx, defaultDialTimeout)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
conn.Connect()
|
||||
for {
|
||||
state := conn.GetState()
|
||||
if state == connectivity.Ready {
|
||||
return nil
|
||||
}
|
||||
if !conn.WaitForStateChange(probeCtx, state) {
|
||||
return probeCtx.Err()
|
||||
}
|
||||
func resolveMaxGrpcMessageBytes(opts Options) int {
|
||||
if opts.MaxGrpcMessageBytes > 0 {
|
||||
return opts.MaxGrpcMessageBytes
|
||||
}
|
||||
return defaultMaxGrpcMessageBytes
|
||||
}
|
||||
|
||||
// NewClient wraps an existing gRPC connection. The caller owns closing conn
|
||||
@@ -233,15 +189,7 @@ func (c *Client) Close() error {
|
||||
}
|
||||
|
||||
func (c *Client) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return callContext(ctx, c.opts.CallTimeout)
|
||||
}
|
||||
|
||||
// callContext derives a per-RPC context from ctx, applying callTimeout: zero
|
||||
// uses defaultCallTimeout, a negative value disables the bound entirely, and a
|
||||
// caller-supplied deadline that is already sooner than the derived timeout is
|
||||
// kept as-is rather than being lengthened.
|
||||
func callContext(ctx context.Context, callTimeout time.Duration) (context.Context, context.CancelFunc) {
|
||||
timeout := callTimeout
|
||||
timeout := c.opts.CallTimeout
|
||||
if timeout == 0 {
|
||||
timeout = defaultCallTimeout
|
||||
}
|
||||
@@ -283,15 +231,10 @@ func resolveTransportCredentials(opts Options) (credentials.TransportCredentials
|
||||
|
||||
// OpenSessionOptions describes fields used to create an OpenSessionRequest.
|
||||
type OpenSessionOptions struct {
|
||||
// RequestedBackend selects the gateway worker backend (empty for default).
|
||||
RequestedBackend string
|
||||
// ClientSessionName is a human-readable name recorded on the session.
|
||||
ClientSessionName string
|
||||
// ClientCorrelationID echoes through gateway logs and replies for tracing.
|
||||
RequestedBackend string
|
||||
ClientSessionName string
|
||||
ClientCorrelationID string
|
||||
// CommandTimeout sets the per-command timeout the gateway forwards to the
|
||||
// worker; zero leaves the gateway default in place.
|
||||
CommandTimeout time.Duration
|
||||
CommandTimeout time.Duration
|
||||
}
|
||||
|
||||
// Request returns the raw protobuf OpenSessionRequest for these options.
|
||||
|
||||
@@ -117,7 +117,7 @@ func TestEventsAfterCancelsStreamWhenCompatibilityChannelIsAbandoned(t *testing.
|
||||
fake := &fakeGatewayServer{
|
||||
streamStarted: make(chan struct{}),
|
||||
streamDone: make(chan struct{}),
|
||||
streamEventCount: 256,
|
||||
streamEventCount: 64,
|
||||
}
|
||||
client, cleanup := newBufconnClient(t, fake)
|
||||
defer cleanup()
|
||||
@@ -135,25 +135,12 @@ func TestEventsAfterCancelsStreamWhenCompatibilityChannelIsAbandoned(t *testing.
|
||||
t.Fatal("compatibility event stream did not stop after result channel filled")
|
||||
}
|
||||
|
||||
// A slow consumer that abandons the buffer must still receive an explicit
|
||||
// terminal overflow error before the channel closes, so it can tell
|
||||
// "events dropped" apart from "stream ended normally".
|
||||
var sawOverflow bool
|
||||
for {
|
||||
select {
|
||||
case result, ok := <-events:
|
||||
case _, ok := <-events:
|
||||
if !ok {
|
||||
if !sawOverflow {
|
||||
t.Fatal("compatibility event channel closed without an ErrEventBufferOverflow result")
|
||||
}
|
||||
return
|
||||
}
|
||||
if result.Err != nil {
|
||||
if !errors.Is(result.Err, ErrEventBufferOverflow) {
|
||||
t.Fatalf("terminal result error = %v, want ErrEventBufferOverflow", result.Err)
|
||||
}
|
||||
sawOverflow = true
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("compatibility event channel did not close")
|
||||
}
|
||||
@@ -292,11 +279,8 @@ func newBufconnClient(t *testing.T, fake *fakeGatewayServer) (*Client, func()) {
|
||||
dialer := func(ctx context.Context, _ string) (net.Conn, error) {
|
||||
return listener.DialContext(ctx)
|
||||
}
|
||||
// grpc.NewClient defaults the target scheme to dns; the bufconn fake name
|
||||
// is not DNS-resolvable, so use the passthrough scheme to hand the target
|
||||
// straight to the context dialer.
|
||||
client, err := Dial(context.Background(), Options{
|
||||
Endpoint: "passthrough:///bufnet",
|
||||
Endpoint: "bufnet",
|
||||
APIKey: "test-api-key",
|
||||
Plaintext: true,
|
||||
DialOptions: []grpc.DialOption{
|
||||
|
||||
@@ -1,401 +0,0 @@
|
||||
package mxgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// --- Client.Go-008: resolveTransportCredentials precedence -----------------
|
||||
|
||||
// TestResolveTransportCredentialsPrecedence covers every branch of
|
||||
// resolveTransportCredentials, which previously only had the Plaintext path
|
||||
// exercised.
|
||||
func TestResolveTransportCredentialsPrecedence(t *testing.T) {
|
||||
custom := insecure.NewCredentials()
|
||||
|
||||
t.Run("TransportCredentialsWins", func(t *testing.T) {
|
||||
creds, err := resolveTransportCredentials(Options{
|
||||
TransportCredentials: custom,
|
||||
Plaintext: true, // must be ignored
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if creds != custom {
|
||||
t.Fatal("expected the explicit TransportCredentials to be returned as-is")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Plaintext", func(t *testing.T) {
|
||||
creds, err := resolveTransportCredentials(Options{Plaintext: true})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := creds.Info().SecurityProtocol; got != "insecure" {
|
||||
t.Fatalf("expected insecure credentials, got security protocol %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CACertFileMissingErrors", func(t *testing.T) {
|
||||
_, err := resolveTransportCredentials(Options{CACertFile: "does-not-exist.pem"})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for a missing CA cert file")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TLSConfigWithServerNameOverride", func(t *testing.T) {
|
||||
creds, err := resolveTransportCredentials(Options{
|
||||
TLSConfig: &tls.Config{MinVersion: tls.VersionTLS13},
|
||||
ServerNameOverride: "gateway.internal",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := creds.Info().ServerName; got != "gateway.internal" {
|
||||
t.Fatalf("expected ServerName override to be applied, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DefaultTLSFloor", func(t *testing.T) {
|
||||
creds, err := resolveTransportCredentials(Options{ServerNameOverride: "host"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := creds.Info().SecurityProtocol; got != "tls" {
|
||||
t.Fatalf("expected the default TLS credentials, got %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestResolveTransportCredentialsDoesNotMutateTLSConfig confirms the supplied
|
||||
// TLSConfig is cloned, not mutated, when ServerNameOverride is applied.
|
||||
func TestResolveTransportCredentialsDoesNotMutateTLSConfig(t *testing.T) {
|
||||
cfg := &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
if _, err := resolveTransportCredentials(Options{
|
||||
TLSConfig: cfg,
|
||||
ServerNameOverride: "override",
|
||||
}); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if cfg.ServerName != "" {
|
||||
t.Fatalf("resolveTransportCredentials mutated the caller's TLSConfig (ServerName=%q)", cfg.ServerName)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Client.Go-008: callContext deadline arithmetic ------------------------
|
||||
|
||||
// TestCallContextDeadlineArithmetic covers the shared callContext deadline
|
||||
// logic, including the negative-timeout disable case and the
|
||||
// caller-deadline-is-sooner case.
|
||||
func TestCallContextDeadlineArithmetic(t *testing.T) {
|
||||
t.Run("ZeroUsesDefault", func(t *testing.T) {
|
||||
ctx, cancel := callContext(context.Background(), 0)
|
||||
defer cancel()
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
t.Fatal("expected a deadline for the default timeout")
|
||||
}
|
||||
remaining := time.Until(deadline)
|
||||
if remaining <= 0 || remaining > defaultCallTimeout+time.Second {
|
||||
t.Fatalf("default deadline out of range: %v", remaining)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NegativeDisablesBound", func(t *testing.T) {
|
||||
base := context.Background()
|
||||
ctx, cancel := callContext(base, -1)
|
||||
defer cancel()
|
||||
if _, ok := ctx.Deadline(); ok {
|
||||
t.Fatal("a negative timeout must disable the deadline entirely")
|
||||
}
|
||||
if ctx != base {
|
||||
t.Fatal("a negative timeout must return the caller context unchanged")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PositiveAppliesTimeout", func(t *testing.T) {
|
||||
ctx, cancel := callContext(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
t.Fatal("expected a deadline")
|
||||
}
|
||||
remaining := time.Until(deadline)
|
||||
if remaining <= 0 || remaining > 5*time.Second+time.Second {
|
||||
t.Fatalf("deadline out of range: %v", remaining)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CallerDeadlineSoonerIsKept", func(t *testing.T) {
|
||||
base, baseCancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer baseCancel()
|
||||
ctx, cancel := callContext(base, 30*time.Second)
|
||||
defer cancel()
|
||||
if ctx != base {
|
||||
t.Fatal("a caller deadline sooner than the timeout must be kept as-is")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CallerDeadlineLaterIsShortened", func(t *testing.T) {
|
||||
base, baseCancel := context.WithTimeout(context.Background(), time.Hour)
|
||||
defer baseCancel()
|
||||
ctx, cancel := callContext(base, time.Second)
|
||||
defer cancel()
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
t.Fatal("expected a deadline")
|
||||
}
|
||||
if remaining := time.Until(deadline); remaining > 2*time.Second {
|
||||
t.Fatalf("expected the shorter timeout to win, got %v remaining", remaining)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- Client.Go-008: NativeValue / NativeArray edge branches ----------------
|
||||
|
||||
// TestNativeValueEdgeKinds covers the array, raw-bytes, null, and
|
||||
// nil-input branches of NativeValue.
|
||||
func TestNativeValueEdgeKinds(t *testing.T) {
|
||||
t.Run("NilInput", func(t *testing.T) {
|
||||
got, err := NativeValue(nil)
|
||||
if err != nil || got != nil {
|
||||
t.Fatalf("NativeValue(nil) = (%v, %v), want (nil, nil)", got, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ExplicitNull", func(t *testing.T) {
|
||||
got, err := NativeValue(&pb.MxValue{IsNull: true})
|
||||
if err != nil || got != nil {
|
||||
t.Fatalf("NativeValue(null) = (%v, %v), want (nil, nil)", got, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RawBytes", func(t *testing.T) {
|
||||
raw := []byte{0x01, 0x02, 0x03}
|
||||
got, err := NativeValue(&pb.MxValue{Kind: &pb.MxValue_RawValue{RawValue: raw}})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
gotBytes, ok := got.([]byte)
|
||||
if !ok || !reflect.DeepEqual(gotBytes, raw) {
|
||||
t.Fatalf("NativeValue raw = %v, want %v", got, raw)
|
||||
}
|
||||
// The result must be a copy, not aliasing the protobuf field.
|
||||
gotBytes[0] = 0xFF
|
||||
if raw[0] != 0x01 {
|
||||
t.Fatal("NativeValue raw result aliases the protobuf backing array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ArrayValue", func(t *testing.T) {
|
||||
value := &pb.MxValue{Kind: &pb.MxValue_ArrayValue{
|
||||
ArrayValue: &pb.MxArray{Values: &pb.MxArray_Int32Values{
|
||||
Int32Values: &pb.Int32Array{Values: []int32{7, 8}},
|
||||
}},
|
||||
}}
|
||||
got, err := NativeValue(value)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, []int32{7, 8}) {
|
||||
t.Fatalf("NativeValue array = %v, want [7 8]", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestNativeArrayEdgeKinds covers the nil, raw-bytes, timestamp-with-nil, and
|
||||
// unsupported-kind branches of NativeArray.
|
||||
func TestNativeArrayEdgeKinds(t *testing.T) {
|
||||
t.Run("NilInput", func(t *testing.T) {
|
||||
got, err := NativeArray(nil)
|
||||
if err != nil || got != nil {
|
||||
t.Fatalf("NativeArray(nil) = (%v, %v), want (nil, nil)", got, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RawValues", func(t *testing.T) {
|
||||
got, err := NativeArray(&pb.MxArray{Values: &pb.MxArray_RawValues{
|
||||
RawValues: &pb.RawArray{Values: [][]byte{{0x0A}, {0x0B}}},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
want := [][]byte{{0x0A}, {0x0B}}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("NativeArray raw = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TimestampWithNilEntry", func(t *testing.T) {
|
||||
got, err := NativeArray(&pb.MxArray{Values: &pb.MxArray_TimestampValues{
|
||||
TimestampValues: &pb.TimestampArray{Values: []*timestamppb.Timestamp{nil}},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
times, ok := got.([]time.Time)
|
||||
if !ok || len(times) != 1 || !times[0].IsZero() {
|
||||
t.Fatalf("NativeArray timestamp-with-nil = %v, want [zero-time]", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UnsupportedKind", func(t *testing.T) {
|
||||
// An MxArray with no oneof set hits the default branch.
|
||||
_, err := NativeArray(&pb.MxArray{})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for an MxArray with no values set")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unsupported array value kind") {
|
||||
t.Fatalf("unexpected error text: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestNativeValueUnsupportedKind covers the default branch of NativeValue.
|
||||
func TestNativeValueUnsupportedKind(t *testing.T) {
|
||||
// An MxValue with no oneof Kind set and IsNull false hits the default.
|
||||
_, err := NativeValue(&pb.MxValue{})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for an MxValue with no kind set")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unsupported value kind") {
|
||||
t.Fatalf("unexpected error text: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Client.Go-005: dial migration -----------------------------------------
|
||||
|
||||
// TestDialFailsFastWhenGatewayUnreachable confirms that after the migration to
|
||||
// grpc.NewClient the DialTimeout-bounded readiness probe still fails fast (and
|
||||
// wraps the failure in *GatewayError) when the gateway cannot be reached.
|
||||
func TestDialFailsFastWhenGatewayUnreachable(t *testing.T) {
|
||||
dialer := func(ctx context.Context, _ string) (net.Conn, error) {
|
||||
return nil, errors.New("connection refused")
|
||||
}
|
||||
start := time.Now()
|
||||
client, err := Dial(context.Background(), Options{
|
||||
Endpoint: "passthrough:///unreachable",
|
||||
APIKey: "k",
|
||||
Plaintext: true,
|
||||
DialTimeout: 500 * time.Millisecond,
|
||||
DialOptions: []grpc.DialOption{grpc.WithContextDialer(dialer)},
|
||||
})
|
||||
elapsed := time.Since(start)
|
||||
if err == nil {
|
||||
client.Close()
|
||||
t.Fatal("expected Dial to fail for an unreachable gateway")
|
||||
}
|
||||
var gwErr *GatewayError
|
||||
if !errors.As(err, &gwErr) || gwErr.Op != "dial" {
|
||||
t.Fatalf("expected a *GatewayError with Op=dial, got %#v", err)
|
||||
}
|
||||
if elapsed > 5*time.Second {
|
||||
t.Fatalf("Dial did not honor DialTimeout: took %v", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDialReadinessProbeReachesReady confirms the readiness probe succeeds
|
||||
// against a live (bufconn) gateway, i.e. the lazy grpc.NewClient connection is
|
||||
// driven to Ready before Dial returns.
|
||||
func TestDialReadinessProbeReachesReady(t *testing.T) {
|
||||
client, cleanup := newBufconnClient(t, &fakeGatewayServer{
|
||||
openReply: &pb.OpenSessionReply{},
|
||||
})
|
||||
defer cleanup()
|
||||
if client == nil {
|
||||
t.Fatal("expected a connected client")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Client.Go-006: error taxonomy ----------------------------------------
|
||||
|
||||
// TestGatewayErrorCode confirms GatewayError.Code surfaces the wrapped gRPC
|
||||
// status code without the caller unwrapping it.
|
||||
func TestGatewayErrorCode(t *testing.T) {
|
||||
var nilErr *GatewayError
|
||||
if got := nilErr.Code(); got != codes.OK {
|
||||
t.Fatalf("nil GatewayError.Code() = %v, want OK", got)
|
||||
}
|
||||
|
||||
gwErr := &GatewayError{Op: "invoke", Err: status.Error(codes.Unavailable, "down")}
|
||||
if got := gwErr.Code(); got != codes.Unavailable {
|
||||
t.Fatalf("GatewayError.Code() = %v, want Unavailable", got)
|
||||
}
|
||||
|
||||
plain := &GatewayError{Op: "dial", Err: errors.New("boom")}
|
||||
if got := plain.Code(); got != codes.Unknown {
|
||||
t.Fatalf("GatewayError.Code() for a non-status error = %v, want Unknown", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsTransient verifies the transient/permanent classification including
|
||||
// the unwrap-through-GatewayError path.
|
||||
func TestIsTransient(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{name: "nil", err: nil, want: false},
|
||||
{name: "unavailable wrapped", err: &GatewayError{Op: "invoke", Err: status.Error(codes.Unavailable, "x")}, want: true},
|
||||
{name: "deadline wrapped", err: &GatewayError{Op: "invoke", Err: status.Error(codes.DeadlineExceeded, "x")}, want: true},
|
||||
{name: "resource exhausted", err: &GatewayError{Err: status.Error(codes.ResourceExhausted, "x")}, want: true},
|
||||
{name: "unauthenticated permanent", err: &GatewayError{Err: status.Error(codes.Unauthenticated, "x")}, want: false},
|
||||
{name: "invalid argument permanent", err: &GatewayError{Err: status.Error(codes.InvalidArgument, "x")}, want: false},
|
||||
{name: "bare status unavailable", err: status.Error(codes.Unavailable, "x"), want: true},
|
||||
{name: "plain error", err: errors.New("nope"), want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsTransient(tt.err); got != tt.want {
|
||||
t.Fatalf("IsTransient(%v) = %v, want %v", tt.err, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Client.Go-007: correlation id fallback --------------------------------
|
||||
|
||||
// TestNewCorrelationIDUsesRandEntropy confirms the happy path yields a
|
||||
// 32-hex-character id.
|
||||
func TestNewCorrelationIDUsesRandEntropy(t *testing.T) {
|
||||
id := newCorrelationID()
|
||||
if len(id) != 32 {
|
||||
t.Fatalf("expected a 32-char hex id, got %q (len %d)", id, len(id))
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewCorrelationIDFallsBackOnRandFailure reproduces Client.Go-007: when
|
||||
// crypto/rand fails, newCorrelationID must not return an empty string but a
|
||||
// unique, non-empty fallback id so the command stays traceable.
|
||||
func TestNewCorrelationIDFallsBackOnRandFailure(t *testing.T) {
|
||||
original := randRead
|
||||
randRead = func([]byte) (int, error) { return 0, errors.New("entropy unavailable") }
|
||||
defer func() { randRead = original }()
|
||||
|
||||
first := newCorrelationID()
|
||||
second := newCorrelationID()
|
||||
|
||||
if first == "" || second == "" {
|
||||
t.Fatal("newCorrelationID returned an empty id on rand failure")
|
||||
}
|
||||
if !strings.HasPrefix(first, "fallback-") {
|
||||
t.Fatalf("expected a fallback- prefixed id, got %q", first)
|
||||
}
|
||||
if first == second {
|
||||
t.Fatalf("fallback correlation ids must be unique, got %q twice", first)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,17 @@
|
||||
package mxgateway
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// ErrEventBufferOverflow is the terminal error delivered on the compatibility
|
||||
// event channel returned by Session.Events / Session.EventsAfter when a slow
|
||||
// consumer lets the bounded result buffer fill. It signals that the stream was
|
||||
// cancelled and events were dropped, so a consumer can tell an overflow apart
|
||||
// from a normal end-of-stream. Use Session.SubscribeEvents to block instead of
|
||||
// dropping.
|
||||
var ErrEventBufferOverflow = errors.New("mxgateway: event buffer overflow; compatibility stream cancelled and events dropped")
|
||||
|
||||
// GatewayError wraps transport-level gRPC failures.
|
||||
type GatewayError struct {
|
||||
// Op names the operation that failed (for example "dial" or "invoke").
|
||||
Op string
|
||||
// Err is the underlying gRPC or transport error.
|
||||
Op string
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error returns the formatted gateway error message.
|
||||
func (e *GatewayError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
@@ -36,7 +22,6 @@ func (e *GatewayError) Error() string {
|
||||
return fmt.Sprintf("mxgateway: %s failed: %v", e.Op, e.Err)
|
||||
}
|
||||
|
||||
// Unwrap returns the wrapped transport error.
|
||||
func (e *GatewayError) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
@@ -44,57 +29,14 @@ func (e *GatewayError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// Code returns the gRPC status code of the wrapped transport error. It returns
|
||||
// codes.OK when the error is nil and codes.Unknown when the wrapped error does
|
||||
// not carry a gRPC status. Callers can use it to write retry, timeout, and
|
||||
// auth handling without manually unwrapping and re-parsing the error.
|
||||
func (e *GatewayError) Code() codes.Code {
|
||||
if e == nil || e.Err == nil {
|
||||
return codes.OK
|
||||
}
|
||||
return status.Code(e.Err)
|
||||
}
|
||||
|
||||
// IsTransient reports whether err is a transport failure that may succeed on
|
||||
// retry — for example a gateway that is briefly Unavailable or a call that
|
||||
// hit a DeadlineExceeded. Permanent failures (Unauthenticated, PermissionDenied,
|
||||
// InvalidArgument, NotFound, and similar) return false. It unwraps through
|
||||
// *GatewayError and any other error chain carrying a gRPC status, so callers
|
||||
// do not need to call status.Code themselves.
|
||||
func IsTransient(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
switch transientCode(err) {
|
||||
case codes.Unavailable, codes.DeadlineExceeded, codes.ResourceExhausted, codes.Aborted:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// transientCode extracts a gRPC status code from err, preferring a wrapped
|
||||
// *GatewayError's Code and otherwise falling back to status.Code on the chain.
|
||||
func transientCode(err error) codes.Code {
|
||||
var gatewayErr *GatewayError
|
||||
if errors.As(err, &gatewayErr) {
|
||||
return gatewayErr.Code()
|
||||
}
|
||||
return status.Code(err)
|
||||
}
|
||||
|
||||
// CommandError reports a non-OK gateway protocol status and keeps the raw
|
||||
// command reply when one exists.
|
||||
type CommandError struct {
|
||||
// Op names the gateway operation that produced the non-OK status.
|
||||
Op string
|
||||
// Status carries the gateway-reported protocol status.
|
||||
Op string
|
||||
Status *ProtocolStatus
|
||||
// Reply is the raw command reply, when one was returned alongside the status.
|
||||
Reply *MxCommandReply
|
||||
Reply *MxCommandReply
|
||||
}
|
||||
|
||||
// Error returns the formatted command error message.
|
||||
func (e *CommandError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
@@ -111,13 +53,10 @@ func (e *CommandError) Error() string {
|
||||
|
||||
// MxAccessError reports HRESULT or MXSTATUS_PROXY failures returned by MXAccess.
|
||||
type MxAccessError struct {
|
||||
// Command is the wrapped CommandError when the protocol status carried one.
|
||||
Command *CommandError
|
||||
// Reply is the raw MXAccess command reply that surfaced the failure.
|
||||
Reply *MxCommandReply
|
||||
Reply *MxCommandReply
|
||||
}
|
||||
|
||||
// Error returns the formatted MXAccess error message.
|
||||
func (e *MxAccessError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
@@ -134,13 +73,8 @@ func (e *MxAccessError) Error() string {
|
||||
return "mxgateway: MXAccess command failed"
|
||||
}
|
||||
|
||||
// Unwrap returns the wrapped CommandError, when one is present.
|
||||
//
|
||||
// When Command is nil (the HRESULT / MxStatusProxy path) it returns an
|
||||
// untyped nil rather than a typed-nil *CommandError, so errors.As does not
|
||||
// bind a nil pointer that a caller would then panic on.
|
||||
func (e *MxAccessError) Unwrap() error {
|
||||
if e == nil || e.Command == nil {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.Command
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package mxgateway
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestMxAccessErrorUnwrapHResultPathNoTypedNilCommandError reproduces
|
||||
// Client.Go-001: an MxAccessError built via the HRESULT / MxStatusProxy path
|
||||
// leaves Command nil. Unwrap must not hand back a typed-nil *CommandError,
|
||||
// because errors.As would then succeed while binding a nil pointer and a
|
||||
// caller dereferencing it would panic.
|
||||
func TestMxAccessErrorUnwrapHResultPathNoTypedNilCommandError(t *testing.T) {
|
||||
hresult := int32(-2147467259) // 0x80004005, a failing HRESULT.
|
||||
reply := &MxCommandReply{Hresult: &hresult}
|
||||
|
||||
err := EnsureMxAccessSuccess("invoke", reply)
|
||||
if err == nil {
|
||||
t.Fatal("expected MxAccessError for a failing HRESULT, got nil")
|
||||
}
|
||||
|
||||
var ce *CommandError
|
||||
if errors.As(err, &ce) {
|
||||
t.Fatalf("errors.As bound *CommandError from an HRESULT-only MxAccessError (ce=%v); "+
|
||||
"a caller dereferencing ce.Status would panic", ce)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMxAccessErrorUnwrapPopulatedCommand confirms the non-nil Command path
|
||||
// still unwraps to the wrapped *CommandError.
|
||||
func TestMxAccessErrorUnwrapPopulatedCommand(t *testing.T) {
|
||||
command := &CommandError{Op: "invoke"}
|
||||
err := &MxAccessError{Command: command}
|
||||
|
||||
var ce *CommandError
|
||||
if !errors.As(err, &ce) {
|
||||
t.Fatal("errors.As failed to bind the populated *CommandError")
|
||||
}
|
||||
if ce != command {
|
||||
t.Fatalf("errors.As bound an unexpected *CommandError: got %v want %v", ce, command)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package mxgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +14,8 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const discoverHierarchyPageSize = 5000
|
||||
|
||||
// RawGalaxyRepositoryClient is the generated gRPC client interface for the
|
||||
// Galaxy Repository service exposed for callers that need direct contract
|
||||
// access.
|
||||
@@ -19,26 +23,16 @@ type RawGalaxyRepositoryClient = pb.GalaxyRepositoryClient
|
||||
|
||||
// Generated protobuf aliases for Galaxy Repository messages.
|
||||
type (
|
||||
// TestConnectionRequest is the request for Galaxy Repository TestConnection.
|
||||
TestConnectionRequest = pb.TestConnectionRequest
|
||||
// TestConnectionReply is the reply for Galaxy Repository TestConnection.
|
||||
TestConnectionReply = pb.TestConnectionReply
|
||||
// GetLastDeployTimeRequest is the request for GetLastDeployTime.
|
||||
GetLastDeployTimeRequest = pb.GetLastDeployTimeRequest
|
||||
// GetLastDeployTimeReply is the reply for GetLastDeployTime.
|
||||
GetLastDeployTimeReply = pb.GetLastDeployTimeReply
|
||||
// DiscoverHierarchyRequest is the request for DiscoverHierarchy.
|
||||
DiscoverHierarchyRequest = pb.DiscoverHierarchyRequest
|
||||
// DiscoverHierarchyReply is the reply for DiscoverHierarchy.
|
||||
DiscoverHierarchyReply = pb.DiscoverHierarchyReply
|
||||
// GalaxyObject describes one Galaxy object with its dynamic attributes.
|
||||
GalaxyObject = pb.GalaxyObject
|
||||
// GalaxyAttribute describes one dynamic attribute on a GalaxyObject.
|
||||
GalaxyAttribute = pb.GalaxyAttribute
|
||||
// WatchDeployEventsRequest is the request for WatchDeployEvents.
|
||||
WatchDeployEventsRequest = pb.WatchDeployEventsRequest
|
||||
// DeployEvent is one Galaxy Repository deploy event.
|
||||
DeployEvent = pb.DeployEvent
|
||||
TestConnectionRequest = pb.TestConnectionRequest
|
||||
TestConnectionReply = pb.TestConnectionReply
|
||||
GetLastDeployTimeRequest = pb.GetLastDeployTimeRequest
|
||||
GetLastDeployTimeReply = pb.GetLastDeployTimeReply
|
||||
DiscoverHierarchyRequest = pb.DiscoverHierarchyRequest
|
||||
DiscoverHierarchyReply = pb.DiscoverHierarchyReply
|
||||
GalaxyObject = pb.GalaxyObject
|
||||
GalaxyAttribute = pb.GalaxyAttribute
|
||||
WatchDeployEventsRequest = pb.WatchDeployEventsRequest
|
||||
DeployEvent = pb.DeployEvent
|
||||
)
|
||||
|
||||
// RawDeployEventStream is the generated WatchDeployEvents client stream.
|
||||
@@ -55,13 +49,43 @@ type GalaxyClient struct {
|
||||
|
||||
// DialGalaxy opens a gRPC connection to the gateway for the Galaxy Repository
|
||||
// service. It applies the same authentication metadata, transport security,
|
||||
// lazy connection, and DialTimeout-bounded readiness probe as Dial.
|
||||
// and dial-timeout behavior as Dial.
|
||||
func DialGalaxy(ctx context.Context, opts Options) (*GalaxyClient, error) {
|
||||
conn, err := dial(ctx, opts)
|
||||
if opts.Endpoint == "" {
|
||||
return nil, errors.New("mxgateway: endpoint is required")
|
||||
}
|
||||
|
||||
dialCtx := ctx
|
||||
cancel := func() {}
|
||||
if opts.DialTimeout > 0 {
|
||||
dialCtx, cancel = context.WithTimeout(ctx, opts.DialTimeout)
|
||||
} else if _, ok := ctx.Deadline(); !ok {
|
||||
dialCtx, cancel = context.WithTimeout(ctx, defaultDialTimeout)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
transportCredentials, err := resolveTransportCredentials(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialOptions := []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(transportCredentials),
|
||||
grpc.WithUnaryInterceptor(unaryAuthInterceptor(opts.APIKey)),
|
||||
grpc.WithStreamInterceptor(streamAuthInterceptor(opts.APIKey)),
|
||||
grpc.WithDefaultCallOptions(
|
||||
grpc.MaxCallRecvMsgSize(resolveMaxGrpcMessageBytes(opts)),
|
||||
grpc.MaxCallSendMsgSize(resolveMaxGrpcMessageBytes(opts)),
|
||||
),
|
||||
grpc.WithBlock(),
|
||||
}
|
||||
dialOptions = append(dialOptions, opts.DialOptions...)
|
||||
|
||||
conn, err := grpc.DialContext(dialCtx, opts.Endpoint, dialOptions...)
|
||||
if err != nil {
|
||||
return nil, &GatewayError{Op: "dial", Err: err}
|
||||
}
|
||||
|
||||
return NewGalaxyClient(conn, opts), nil
|
||||
}
|
||||
|
||||
@@ -124,11 +148,28 @@ func (c *GalaxyClient) DiscoverHierarchy(ctx context.Context) ([]*GalaxyObject,
|
||||
callCtx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
reply, err := c.raw.DiscoverHierarchy(callCtx, &pb.DiscoverHierarchyRequest{})
|
||||
if err != nil {
|
||||
return nil, &GatewayError{Op: "galaxy discover hierarchy", Err: err}
|
||||
var objects []*GalaxyObject
|
||||
seenPageTokens := make(map[string]struct{})
|
||||
pageToken := ""
|
||||
for {
|
||||
reply, err := c.raw.DiscoverHierarchy(callCtx, &pb.DiscoverHierarchyRequest{
|
||||
PageSize: discoverHierarchyPageSize,
|
||||
PageToken: pageToken,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, &GatewayError{Op: "galaxy discover hierarchy", Err: err}
|
||||
}
|
||||
objects = append(objects, reply.GetObjects()...)
|
||||
pageToken = reply.GetNextPageToken()
|
||||
if pageToken == "" {
|
||||
break
|
||||
}
|
||||
if _, seen := seenPageTokens[pageToken]; seen {
|
||||
return nil, fmt.Errorf("mxgateway: galaxy discover hierarchy returned repeated page token %q", pageToken)
|
||||
}
|
||||
seenPageTokens[pageToken] = struct{}{}
|
||||
}
|
||||
return reply.GetObjects(), nil
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
// WatchDeployEventsRaw starts the generated WatchDeployEvents stream for callers
|
||||
@@ -212,5 +253,18 @@ func (c *GalaxyClient) Close() error {
|
||||
}
|
||||
|
||||
func (c *GalaxyClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return callContext(ctx, c.opts.CallTimeout)
|
||||
timeout := c.opts.CallTimeout
|
||||
if timeout == 0 {
|
||||
timeout = defaultCallTimeout
|
||||
}
|
||||
if timeout < 0 {
|
||||
return ctx, func() {}
|
||||
}
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
timeoutDeadline := time.Now().Add(timeout)
|
||||
if deadline.Before(timeoutDeadline) {
|
||||
return ctx, func() {}
|
||||
}
|
||||
}
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -55,8 +56,8 @@ func TestGalaxyGetLastDeployTimeReturnsTimestampWhenPresent(t *testing.T) {
|
||||
want := time.Date(2026, 4, 28, 12, 34, 56, 0, time.UTC)
|
||||
fake := &fakeGalaxyServer{
|
||||
deployReply: &pb.GetLastDeployTimeReply{
|
||||
Present: true,
|
||||
TimeOfLastDeploy: timestamppb.New(want),
|
||||
Present: true,
|
||||
TimeOfLastDeploy: timestamppb.New(want),
|
||||
},
|
||||
}
|
||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
||||
@@ -95,7 +96,9 @@ func TestGalaxyGetLastDeployTimeReturnsAbsentWhenTimestampNil(t *testing.T) {
|
||||
|
||||
func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
||||
fake := &fakeGalaxyServer{
|
||||
discoverReply: &pb.DiscoverHierarchyReply{
|
||||
discoverReplies: []*pb.DiscoverHierarchyReply{{
|
||||
NextPageToken: "page-2",
|
||||
TotalObjectCount: 2,
|
||||
Objects: []*pb.GalaxyObject{
|
||||
{
|
||||
GobjectId: 1,
|
||||
@@ -114,6 +117,10 @@ func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
TotalObjectCount: 2,
|
||||
Objects: []*pb.GalaxyObject{
|
||||
{
|
||||
GobjectId: 2,
|
||||
TagName: "TestMachine_002",
|
||||
@@ -121,7 +128,7 @@ func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
||||
ParentGobjectId: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
||||
defer cleanup()
|
||||
@@ -133,6 +140,15 @@ func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
||||
if len(objects) != 2 {
|
||||
t.Fatalf("len(objects) = %d, want 2", len(objects))
|
||||
}
|
||||
if len(fake.discoverRequests) != 2 {
|
||||
t.Fatalf("len(discoverRequests) = %d, want 2", len(fake.discoverRequests))
|
||||
}
|
||||
if fake.discoverRequests[0].GetPageSize() != 5000 || fake.discoverRequests[0].GetPageToken() != "" {
|
||||
t.Fatalf("first request = %+v", fake.discoverRequests[0])
|
||||
}
|
||||
if fake.discoverRequests[1].GetPageToken() != "page-2" {
|
||||
t.Fatalf("second page_token = %q, want page-2", fake.discoverRequests[1].GetPageToken())
|
||||
}
|
||||
if objects[0].GetTagName() != "TestMachine_001" {
|
||||
t.Fatalf("objects[0].TagName = %q", objects[0].GetTagName())
|
||||
}
|
||||
@@ -144,6 +160,25 @@ func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGalaxyDiscoverHierarchyRejectsRepeatedPageToken(t *testing.T) {
|
||||
fake := &fakeGalaxyServer{
|
||||
discoverReplies: []*pb.DiscoverHierarchyReply{
|
||||
{NextPageToken: "7:1"},
|
||||
{NextPageToken: "7:1"},
|
||||
},
|
||||
}
|
||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
||||
defer cleanup()
|
||||
|
||||
_, err := client.DiscoverHierarchy(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("DiscoverHierarchy() error = nil, want repeated token error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "repeated page token") {
|
||||
t.Fatalf("error = %v, want repeated page token", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGalaxyDialReturnsGatewayErrorOnRpcFailure(t *testing.T) {
|
||||
fake := &fakeGalaxyServer{failTest: true}
|
||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
||||
@@ -348,10 +383,8 @@ func newGalaxyBufconnClient(t *testing.T, fake *fakeGalaxyServer) (*GalaxyClient
|
||||
dialer := func(ctx context.Context, _ string) (net.Conn, error) {
|
||||
return listener.DialContext(ctx)
|
||||
}
|
||||
// grpc.NewClient defaults to the dns scheme; use passthrough so the
|
||||
// bufconn fake target reaches the context dialer unresolved.
|
||||
client, err := DialGalaxy(context.Background(), Options{
|
||||
Endpoint: "passthrough:///bufnet",
|
||||
Endpoint: "bufnet",
|
||||
APIKey: "test-api-key",
|
||||
Plaintext: true,
|
||||
DialOptions: []grpc.DialOption{
|
||||
@@ -377,6 +410,8 @@ type fakeGalaxyServer struct {
|
||||
failTest bool
|
||||
deployReply *pb.GetLastDeployTimeReply
|
||||
discoverReply *pb.DiscoverHierarchyReply
|
||||
discoverReplies []*pb.DiscoverHierarchyReply
|
||||
discoverRequests []*pb.DiscoverHierarchyRequest
|
||||
watchEvents []*pb.DeployEvent
|
||||
watchRequest *pb.WatchDeployEventsRequest
|
||||
watchSendInterval time.Duration
|
||||
@@ -402,6 +437,12 @@ func (s *fakeGalaxyServer) GetLastDeployTime(ctx context.Context, req *pb.GetLas
|
||||
}
|
||||
|
||||
func (s *fakeGalaxyServer) DiscoverHierarchy(ctx context.Context, req *pb.DiscoverHierarchyRequest) (*pb.DiscoverHierarchyReply, error) {
|
||||
s.discoverRequests = append(s.discoverRequests, req)
|
||||
if len(s.discoverReplies) > 0 {
|
||||
reply := s.discoverReplies[0]
|
||||
s.discoverReplies = s.discoverReplies[1:]
|
||||
return reply, nil
|
||||
}
|
||||
if s.discoverReply != nil {
|
||||
return s.discoverReply, nil
|
||||
}
|
||||
|
||||
@@ -11,29 +11,17 @@ import (
|
||||
|
||||
// Options configures gateway connections.
|
||||
type Options struct {
|
||||
// Endpoint is the gateway host:port address to dial.
|
||||
Endpoint string
|
||||
// APIKey is the bearer token attached to outgoing gRPC metadata.
|
||||
APIKey string
|
||||
// Plaintext disables TLS and uses insecure credentials when true.
|
||||
Plaintext bool
|
||||
// CACertFile points to a PEM file used to verify the gateway certificate.
|
||||
CACertFile string
|
||||
// ServerNameOverride overrides the TLS SNI/SAN name presented to the gateway.
|
||||
ServerNameOverride string
|
||||
// DialTimeout bounds the blocking Dial; zero applies a built-in default.
|
||||
DialTimeout time.Duration
|
||||
// CallTimeout bounds each unary RPC; zero applies a built-in default and
|
||||
// negative disables the bound entirely.
|
||||
CallTimeout time.Duration
|
||||
// TLSConfig supplies a custom TLS configuration; takes precedence over
|
||||
// CACertFile when TransportCredentials is unset.
|
||||
TLSConfig *tls.Config
|
||||
// TransportCredentials, when non-nil, overrides every other transport-level
|
||||
// option and is used as-is.
|
||||
Endpoint string
|
||||
APIKey string
|
||||
Plaintext bool
|
||||
CACertFile string
|
||||
ServerNameOverride string
|
||||
DialTimeout time.Duration
|
||||
CallTimeout time.Duration
|
||||
MaxGrpcMessageBytes int
|
||||
TLSConfig *tls.Config
|
||||
TransportCredentials credentials.TransportCredentials
|
||||
// DialOptions are appended to the gRPC dial options after the defaults.
|
||||
DialOptions []grpc.DialOption
|
||||
DialOptions []grpc.DialOption
|
||||
}
|
||||
|
||||
// RedactedAPIKey returns a display-safe representation of the configured API
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -20,10 +18,8 @@ const maxBulkItems = 1000
|
||||
|
||||
// EventResult carries either the next ordered event or a terminal stream error.
|
||||
type EventResult struct {
|
||||
// Event is the next event from the stream when Err is nil.
|
||||
Event *MxEvent
|
||||
// Err is the terminal stream error; when non-nil no further results follow.
|
||||
Err error
|
||||
Err error
|
||||
}
|
||||
|
||||
// EventSubscription owns a running gateway event stream.
|
||||
@@ -492,7 +488,7 @@ func ensureBulkSize(name string, length int) error {
|
||||
|
||||
func sendEventResult(
|
||||
ctx context.Context,
|
||||
results chan EventResult,
|
||||
results chan<- EventResult,
|
||||
result EventResult,
|
||||
cancelWhenBufferFull bool,
|
||||
cancel context.CancelFunc,
|
||||
@@ -504,12 +500,7 @@ func sendEventResult(
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
default:
|
||||
// The bounded compatibility buffer is full. Cancel the stream and
|
||||
// deliver an explicit terminal overflow error so a slow consumer
|
||||
// can tell dropped events apart from a normal end-of-stream,
|
||||
// rather than seeing the channel close silently.
|
||||
cancel()
|
||||
deliverTerminalResult(results, EventResult{Err: ErrEventBufferOverflow})
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -522,25 +513,6 @@ func sendEventResult(
|
||||
}
|
||||
}
|
||||
|
||||
// deliverTerminalResult places result on a full buffered channel by evicting
|
||||
// one of the oldest buffered events to make room. The caller closes results
|
||||
// afterwards, so the terminal result becomes the consumer's last item.
|
||||
func deliverTerminalResult(results chan EventResult, result EventResult) {
|
||||
for {
|
||||
select {
|
||||
case results <- result:
|
||||
return
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case <-results:
|
||||
default:
|
||||
// Another receiver drained the channel between the send and
|
||||
// receive attempts; retry the send.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) invokeCommand(ctx context.Context, command *MxCommand) (*MxCommandReply, error) {
|
||||
return s.client.Invoke(ctx, &pb.MxCommandRequest{
|
||||
SessionId: s.ID(),
|
||||
@@ -549,25 +521,10 @@ func (s *Session) invokeCommand(ctx context.Context, command *MxCommand) (*MxCom
|
||||
})
|
||||
}
|
||||
|
||||
// correlationIDCounter backs the deterministic fallback id used when
|
||||
// crypto/rand is unavailable, so every command still carries a unique,
|
||||
// traceable correlation id.
|
||||
var correlationIDCounter atomic.Uint64
|
||||
|
||||
// randRead is the entropy source for newCorrelationID. It is a package
|
||||
// variable solely so tests can simulate a crypto/rand failure.
|
||||
var randRead = rand.Read
|
||||
|
||||
// newCorrelationID returns a unique correlation id for an MxCommandRequest.
|
||||
// It prefers 16 bytes of crypto/rand entropy; if rand.Read fails (rare) it
|
||||
// falls back to a "fallback-" prefixed id built from the current time and a
|
||||
// process-wide monotonic counter rather than returning an empty string, which
|
||||
// would leave the command untraceable in gateway logs.
|
||||
func newCorrelationID() string {
|
||||
var buffer [16]byte
|
||||
if _, err := randRead(buffer[:]); err != nil {
|
||||
return fmt.Sprintf("fallback-%x-%x",
|
||||
time.Now().UnixNano(), correlationIDCounter.Add(1))
|
||||
if _, err := rand.Read(buffer[:]); err != nil {
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString(buffer[:])
|
||||
}
|
||||
|
||||
+58
-148
@@ -12,167 +12,77 @@ type RawEventStream = pb.MxAccessGateway_StreamEventsClient
|
||||
// Generated protobuf aliases keep raw contract access available from the public
|
||||
// mxgateway package while generated code remains under internal/generated.
|
||||
type (
|
||||
// OpenSessionRequest is the gateway OpenSession request message.
|
||||
OpenSessionRequest = pb.OpenSessionRequest
|
||||
// OpenSessionReply is the gateway OpenSession reply message.
|
||||
OpenSessionReply = pb.OpenSessionReply
|
||||
// CloseSessionRequest is the gateway CloseSession request message.
|
||||
CloseSessionRequest = pb.CloseSessionRequest
|
||||
// CloseSessionReply is the gateway CloseSession reply message.
|
||||
CloseSessionReply = pb.CloseSessionReply
|
||||
// StreamEventsRequest is the gateway StreamEvents request message.
|
||||
StreamEventsRequest = pb.StreamEventsRequest
|
||||
// MxCommandRequest carries one MXAccess command for Invoke.
|
||||
MxCommandRequest = pb.MxCommandRequest
|
||||
// MxCommandReply is the reply to an MXAccess command Invoke.
|
||||
MxCommandReply = pb.MxCommandReply
|
||||
// MxCommand is the discriminated union of MXAccess command payloads.
|
||||
MxCommand = pb.MxCommand
|
||||
// MxEvent is one ordered event delivered on a session event stream.
|
||||
MxEvent = pb.MxEvent
|
||||
// MxValue is the protobuf representation of an MXAccess value.
|
||||
MxValue = pb.MxValue
|
||||
// Value is an alias for MxValue retained for symmetry with other clients.
|
||||
Value = pb.MxValue
|
||||
// MxArray is the protobuf representation of an MXAccess array value.
|
||||
MxArray = pb.MxArray
|
||||
// MxStatusProxy mirrors the MXAccess MXSTATUS_PROXY structure.
|
||||
MxStatusProxy = pb.MxStatusProxy
|
||||
// ProtocolStatus is the gateway-level status carried on every reply.
|
||||
ProtocolStatus = pb.ProtocolStatus
|
||||
// RegisterCommand is the payload of an MXAccess Register command.
|
||||
RegisterCommand = pb.RegisterCommand
|
||||
// UnregisterCommand is the payload of an MXAccess Unregister command.
|
||||
UnregisterCommand = pb.UnregisterCommand
|
||||
// AddItemCommand is the payload of an MXAccess AddItem command.
|
||||
AddItemCommand = pb.AddItemCommand
|
||||
// AddItem2Command is the payload of an MXAccess AddItem2 command.
|
||||
AddItem2Command = pb.AddItem2Command
|
||||
// RemoveItemCommand is the payload of an MXAccess RemoveItem command.
|
||||
RemoveItemCommand = pb.RemoveItemCommand
|
||||
// AdviseCommand is the payload of an MXAccess Advise command.
|
||||
AdviseCommand = pb.AdviseCommand
|
||||
// UnAdviseCommand is the payload of an MXAccess UnAdvise command.
|
||||
UnAdviseCommand = pb.UnAdviseCommand
|
||||
// AddItemBulkCommand is the payload of an AddItem bulk command.
|
||||
AddItemBulkCommand = pb.AddItemBulkCommand
|
||||
// AdviseItemBulkCommand is the payload of an Advise bulk command.
|
||||
AdviseItemBulkCommand = pb.AdviseItemBulkCommand
|
||||
// RemoveItemBulkCommand is the payload of a RemoveItem bulk command.
|
||||
RemoveItemBulkCommand = pb.RemoveItemBulkCommand
|
||||
// UnAdviseItemBulkCommand is the payload of an UnAdvise bulk command.
|
||||
OpenSessionRequest = pb.OpenSessionRequest
|
||||
OpenSessionReply = pb.OpenSessionReply
|
||||
CloseSessionRequest = pb.CloseSessionRequest
|
||||
CloseSessionReply = pb.CloseSessionReply
|
||||
StreamEventsRequest = pb.StreamEventsRequest
|
||||
MxCommandRequest = pb.MxCommandRequest
|
||||
MxCommandReply = pb.MxCommandReply
|
||||
MxCommand = pb.MxCommand
|
||||
MxEvent = pb.MxEvent
|
||||
MxValue = pb.MxValue
|
||||
Value = pb.MxValue
|
||||
MxArray = pb.MxArray
|
||||
MxStatusProxy = pb.MxStatusProxy
|
||||
ProtocolStatus = pb.ProtocolStatus
|
||||
RegisterCommand = pb.RegisterCommand
|
||||
UnregisterCommand = pb.UnregisterCommand
|
||||
AddItemCommand = pb.AddItemCommand
|
||||
AddItem2Command = pb.AddItem2Command
|
||||
RemoveItemCommand = pb.RemoveItemCommand
|
||||
AdviseCommand = pb.AdviseCommand
|
||||
UnAdviseCommand = pb.UnAdviseCommand
|
||||
AddItemBulkCommand = pb.AddItemBulkCommand
|
||||
AdviseItemBulkCommand = pb.AdviseItemBulkCommand
|
||||
RemoveItemBulkCommand = pb.RemoveItemBulkCommand
|
||||
UnAdviseItemBulkCommand = pb.UnAdviseItemBulkCommand
|
||||
// SubscribeBulkCommand combines AddItem and Advise for a list of tags.
|
||||
SubscribeBulkCommand = pb.SubscribeBulkCommand
|
||||
// UnsubscribeBulkCommand combines UnAdvise and RemoveItem for a list of items.
|
||||
UnsubscribeBulkCommand = pb.UnsubscribeBulkCommand
|
||||
// WriteCommand is the payload of an MXAccess Write command.
|
||||
WriteCommand = pb.WriteCommand
|
||||
// Write2Command is the payload of an MXAccess Write2 command.
|
||||
Write2Command = pb.Write2Command
|
||||
// RegisterReply carries the ServerHandle returned by Register.
|
||||
RegisterReply = pb.RegisterReply
|
||||
// AddItemReply carries the ItemHandle returned by AddItem.
|
||||
AddItemReply = pb.AddItemReply
|
||||
// AddItem2Reply carries the ItemHandle returned by AddItem2.
|
||||
AddItem2Reply = pb.AddItem2Reply
|
||||
// SubscribeResult is one entry in a bulk command result list.
|
||||
SubscribeResult = pb.SubscribeResult
|
||||
// BulkSubscribeReply aggregates SubscribeResult entries for a bulk command.
|
||||
BulkSubscribeReply = pb.BulkSubscribeReply
|
||||
// AcknowledgeAlarmRequest is the gateway AcknowledgeAlarm request message.
|
||||
AcknowledgeAlarmRequest = pb.AcknowledgeAlarmRequest
|
||||
// AcknowledgeAlarmReply is the gateway AcknowledgeAlarm reply message.
|
||||
AcknowledgeAlarmReply = pb.AcknowledgeAlarmReply
|
||||
// QueryActiveAlarmsRequest is the gateway QueryActiveAlarms request message.
|
||||
QueryActiveAlarmsRequest = pb.QueryActiveAlarmsRequest
|
||||
// ActiveAlarmSnapshot is one row in a ConditionRefresh stream.
|
||||
ActiveAlarmSnapshot = pb.ActiveAlarmSnapshot
|
||||
// OnAlarmTransitionEvent is the body carried by alarm-transition MxEvents.
|
||||
OnAlarmTransitionEvent = pb.OnAlarmTransitionEvent
|
||||
SubscribeBulkCommand = pb.SubscribeBulkCommand
|
||||
UnsubscribeBulkCommand = pb.UnsubscribeBulkCommand
|
||||
WriteCommand = pb.WriteCommand
|
||||
Write2Command = pb.Write2Command
|
||||
RegisterReply = pb.RegisterReply
|
||||
AddItemReply = pb.AddItemReply
|
||||
AddItem2Reply = pb.AddItem2Reply
|
||||
SubscribeResult = pb.SubscribeResult
|
||||
BulkSubscribeReply = pb.BulkSubscribeReply
|
||||
)
|
||||
|
||||
// AlarmTransitionKind discriminates raise / acknowledge / clear / retrigger
|
||||
// transitions on an OnAlarmTransitionEvent.
|
||||
type AlarmTransitionKind = pb.AlarmTransitionKind
|
||||
|
||||
// AlarmConditionState reports the current state of an active alarm in a
|
||||
// ConditionRefresh snapshot.
|
||||
type AlarmConditionState = pb.AlarmConditionState
|
||||
|
||||
// QueryActiveAlarmsClient is the generated server-streaming client for the
|
||||
// QueryActiveAlarms RPC.
|
||||
type QueryActiveAlarmsClient = pb.MxAccessGateway_QueryActiveAlarmsClient
|
||||
|
||||
// Enumerations from the generated contract re-exported for client callers.
|
||||
type (
|
||||
// MxCommandKind discriminates which MXAccess command an MxCommand carries.
|
||||
MxCommandKind = pb.MxCommandKind
|
||||
// MxDataType is the MXAccess data type tag on values and arrays.
|
||||
MxDataType = pb.MxDataType
|
||||
// MxEventFamily groups MXAccess events by source category.
|
||||
MxEventFamily = pb.MxEventFamily
|
||||
// MxStatusCategory classifies MXSTATUS_PROXY entries.
|
||||
MxStatusCategory = pb.MxStatusCategory
|
||||
// MxStatusSource identifies the originator of a status entry.
|
||||
MxStatusSource = pb.MxStatusSource
|
||||
// ProtocolStatusCode enumerates gateway-level status codes.
|
||||
MxCommandKind = pb.MxCommandKind
|
||||
MxDataType = pb.MxDataType
|
||||
MxEventFamily = pb.MxEventFamily
|
||||
MxStatusCategory = pb.MxStatusCategory
|
||||
MxStatusSource = pb.MxStatusSource
|
||||
ProtocolStatusCode = pb.ProtocolStatusCode
|
||||
// SessionState enumerates gateway session lifecycle states.
|
||||
SessionState = pb.SessionState
|
||||
SessionState = pb.SessionState
|
||||
)
|
||||
|
||||
// MXAccess command kind, data type, and protocol status constants surfaced
|
||||
// from the generated contract.
|
||||
const (
|
||||
// CommandKindRegister selects the MXAccess Register command.
|
||||
CommandKindRegister = pb.MxCommandKind_MX_COMMAND_KIND_REGISTER
|
||||
// CommandKindUnregister selects the MXAccess Unregister command.
|
||||
CommandKindUnregister = pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER
|
||||
// CommandKindAddItem selects the MXAccess AddItem command.
|
||||
CommandKindAddItem = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM
|
||||
// CommandKindAddItem2 selects the MXAccess AddItem2 command.
|
||||
CommandKindAddItem2 = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2
|
||||
// CommandKindRemoveItem selects the MXAccess RemoveItem command.
|
||||
CommandKindRemoveItem = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM
|
||||
// CommandKindAdvise selects the MXAccess Advise command.
|
||||
CommandKindAdvise = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE
|
||||
// CommandKindUnAdvise selects the MXAccess UnAdvise command.
|
||||
CommandKindUnAdvise = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE
|
||||
// CommandKindAddItemBulk selects the AddItem bulk command.
|
||||
CommandKindAddItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM_BULK
|
||||
// CommandKindAdviseItemBulk selects the Advise bulk command.
|
||||
CommandKindAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_ITEM_BULK
|
||||
// CommandKindRemoveItemBulk selects the RemoveItem bulk command.
|
||||
CommandKindRemoveItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM_BULK
|
||||
// CommandKindUnAdviseItemBulk selects the UnAdvise bulk command.
|
||||
CommandKindRegister = pb.MxCommandKind_MX_COMMAND_KIND_REGISTER
|
||||
CommandKindUnregister = pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER
|
||||
CommandKindAddItem = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM
|
||||
CommandKindAddItem2 = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2
|
||||
CommandKindRemoveItem = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM
|
||||
CommandKindAdvise = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE
|
||||
CommandKindUnAdvise = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE
|
||||
CommandKindAddItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM_BULK
|
||||
CommandKindAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_ITEM_BULK
|
||||
CommandKindRemoveItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM_BULK
|
||||
CommandKindUnAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK
|
||||
// CommandKindSubscribeBulk selects the AddItem+Advise combined bulk command.
|
||||
CommandKindSubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK
|
||||
// CommandKindUnsubscribeBulk selects the UnAdvise+RemoveItem combined bulk command.
|
||||
CommandKindUnsubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_UNSUBSCRIBE_BULK
|
||||
// CommandKindWrite selects the MXAccess Write command.
|
||||
CommandKindWrite = pb.MxCommandKind_MX_COMMAND_KIND_WRITE
|
||||
// CommandKindWrite2 selects the MXAccess Write2 command.
|
||||
CommandKindWrite2 = pb.MxCommandKind_MX_COMMAND_KIND_WRITE2
|
||||
CommandKindSubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK
|
||||
CommandKindUnsubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_UNSUBSCRIBE_BULK
|
||||
CommandKindWrite = pb.MxCommandKind_MX_COMMAND_KIND_WRITE
|
||||
CommandKindWrite2 = pb.MxCommandKind_MX_COMMAND_KIND_WRITE2
|
||||
|
||||
// DataTypeUnknown denotes an unrecognized MXAccess data type.
|
||||
DataTypeUnknown = pb.MxDataType_MX_DATA_TYPE_UNKNOWN
|
||||
// DataTypeBoolean denotes an MXAccess Boolean value.
|
||||
DataTypeBoolean = pb.MxDataType_MX_DATA_TYPE_BOOLEAN
|
||||
// DataTypeInteger denotes an MXAccess Integer value.
|
||||
DataTypeInteger = pb.MxDataType_MX_DATA_TYPE_INTEGER
|
||||
// DataTypeFloat denotes an MXAccess Float (single precision) value.
|
||||
DataTypeFloat = pb.MxDataType_MX_DATA_TYPE_FLOAT
|
||||
// DataTypeDouble denotes an MXAccess Double (double precision) value.
|
||||
DataTypeDouble = pb.MxDataType_MX_DATA_TYPE_DOUBLE
|
||||
// DataTypeString denotes an MXAccess String value.
|
||||
DataTypeString = pb.MxDataType_MX_DATA_TYPE_STRING
|
||||
// DataTypeTime denotes an MXAccess timestamp value.
|
||||
DataTypeTime = pb.MxDataType_MX_DATA_TYPE_TIME
|
||||
DataTypeFloat = pb.MxDataType_MX_DATA_TYPE_FLOAT
|
||||
DataTypeDouble = pb.MxDataType_MX_DATA_TYPE_DOUBLE
|
||||
DataTypeString = pb.MxDataType_MX_DATA_TYPE_STRING
|
||||
DataTypeTime = pb.MxDataType_MX_DATA_TYPE_TIME
|
||||
|
||||
// ProtocolStatusOK indicates the gateway processed the request successfully.
|
||||
ProtocolStatusOK = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK
|
||||
// ProtocolStatusMxAccessFailure indicates the worker reported an MXAccess failure.
|
||||
ProtocolStatusOK = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK
|
||||
ProtocolStatusMxAccessFailure = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ const (
|
||||
|
||||
// GatewayProtocolVersion matches GatewayContractInfo.GatewayProtocolVersion
|
||||
// in the shared .NET contracts.
|
||||
GatewayProtocolVersion uint32 = 3
|
||||
GatewayProtocolVersion uint32 = 2
|
||||
|
||||
// WorkerProtocolVersion matches GatewayContractInfo.WorkerProtocolVersion
|
||||
// and is exposed for fake-worker and parity tests.
|
||||
|
||||
+3
-30
@@ -62,37 +62,10 @@ underlying protobuf messages. `MxGatewayCommandException` and
|
||||
`MxAccessException` preserve the raw `MxCommandReply` when the gateway returns a
|
||||
data-bearing MXAccess failure.
|
||||
|
||||
`openSession` verifies the gateway's reported `gateway_protocol_version` against
|
||||
the version this client was generated for and throws `MxGatewayException` on a
|
||||
mismatch, so an incompatible client fails fast with a clear message instead of
|
||||
issuing commands that fail downstream. A gateway that does not populate the
|
||||
field is accepted unchanged.
|
||||
|
||||
`MxGatewaySession` implements `AutoCloseable`. The try-with-resources `close()`
|
||||
performs a `CloseSession` network RPC but swallows (and logs) any failure of
|
||||
that RPC so a close-time error never replaces the exception a try-with-resources
|
||||
body is already propagating. Call `closeRaw()` explicitly when you need to
|
||||
observe the close result or handle a close-time failure.
|
||||
|
||||
`MxGatewayClient` and `GalaxyRepositoryClient` implement `AutoCloseable`. For a
|
||||
client that owns its channel (built with `connect`), the try-with-resources
|
||||
`close()` shuts the channel down and waits up to the configured connect timeout
|
||||
for termination, forcibly shutting it down on timeout, so in-flight calls and
|
||||
Netty event-loop threads are not left running after the block exits. If the
|
||||
calling thread is interrupted while waiting, the channel is forcibly shut down
|
||||
and the interrupt flag is restored. `closeAndAwaitTermination()` does the same
|
||||
but throws `InterruptedException` for callers that want a checked,
|
||||
blocking-aware shutdown. `close()` is a no-op for a caller-managed channel.
|
||||
|
||||
`MxEventStream` implements `Iterator<MxEvent>` and `AutoCloseable`. Closing it
|
||||
cancels the underlying gRPC stream. Canceling or timing out a Java client call
|
||||
only stops the client from waiting; it does not abort an in-flight MXAccess COM
|
||||
call on the worker STA. The event stream uses gRPC's default auto-inbound flow
|
||||
control with a fixed 16-element buffer and no client-side flow control: this is
|
||||
the gateway's documented fail-fast event-backpressure model, so a consumer that
|
||||
stalls long enough to fill the buffer triggers an overflow that cancels the
|
||||
subscription and surfaces an `MxGatewayException` from the next `next()` call.
|
||||
Drain events promptly and be prepared to resubscribe with a resume cursor.
|
||||
call on the worker STA.
|
||||
|
||||
## Galaxy Repository Browse
|
||||
|
||||
@@ -250,6 +223,6 @@ gradle :mxgateway-cli:run --args="smoke --endpoint $env:MXGATEWAY_ENDPOINT --pla
|
||||
## Related Documentation
|
||||
|
||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
||||
- [Java Client Detailed Design](./JavaClientDesign.md)
|
||||
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
||||
- [Java Client Detailed Design](../../docs/clients-java-design.md)
|
||||
- [Java Style Guide](../../docs/style-guides/JavaStyleGuide.md)
|
||||
|
||||
+11
-62
@@ -38,10 +38,6 @@ import picocli.CommandLine.Model.CommandSpec;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.Spec;
|
||||
|
||||
/**
|
||||
* Picocli entry point for the {@code mxgw-java} test CLI used by the
|
||||
* cross-language smoke matrix.
|
||||
*/
|
||||
@Command(
|
||||
name = "mxgw-java",
|
||||
mixinStandardHelpOptions = true,
|
||||
@@ -52,9 +48,6 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
@Spec
|
||||
private CommandSpec spec;
|
||||
|
||||
/**
|
||||
* Builds a CLI bound to the default gRPC client factory.
|
||||
*/
|
||||
public MxGatewayCli() {
|
||||
this(new GrpcMxGatewayCliClientFactory());
|
||||
}
|
||||
@@ -63,25 +56,11 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
this.clientFactory = clientFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process entry point.
|
||||
*
|
||||
* @param args command-line arguments
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
int exitCode = commandLine(new GrpcMxGatewayCliClientFactory()).execute(args);
|
||||
System.exit(exitCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test-friendly entry point that runs the CLI against the supplied
|
||||
* {@link PrintWriter} pair instead of the system streams.
|
||||
*
|
||||
* @param out writer that receives standard output
|
||||
* @param err writer that receives standard error
|
||||
* @param args command-line arguments
|
||||
* @return the picocli exit code
|
||||
*/
|
||||
public static int execute(PrintWriter out, PrintWriter err, String... args) {
|
||||
return execute(new GrpcMxGatewayCliClientFactory(), out, err, args);
|
||||
}
|
||||
@@ -301,9 +280,6 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Picocli subcommand that prints the client and protocol version numbers.
|
||||
*/
|
||||
@Command(name = "version", description = "Prints the Java client version.")
|
||||
public static final class VersionCommand implements Callable<Integer> {
|
||||
@Spec
|
||||
@@ -661,60 +637,33 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
@Option(names = "--timeout", defaultValue = "30s", description = "Per-call timeout.")
|
||||
String timeout;
|
||||
|
||||
/**
|
||||
* Returns this options object unchanged.
|
||||
*
|
||||
* <p>Retained as a no-op for call sites that read more naturally as
|
||||
* {@code common.resolved()}. Resolution of the API key and timeout is
|
||||
* computed lazily on demand by {@link #resolvedApiKey()} and
|
||||
* {@link #resolvedTimeout()}, so {@link #toClientOptions()} and
|
||||
* {@link #redactedJsonMap()} produce correct output regardless of
|
||||
* whether this method was ever called.
|
||||
*
|
||||
* @return this options object
|
||||
*/
|
||||
private String resolvedApiKey = "";
|
||||
private Duration resolvedTimeout = Duration.ofSeconds(30);
|
||||
|
||||
CommonOptions resolved() {
|
||||
resolvedApiKey = apiKey == null || apiKey.isBlank() ? System.getenv(apiKeyEnv) : apiKey;
|
||||
if (resolvedApiKey == null) {
|
||||
resolvedApiKey = "";
|
||||
}
|
||||
resolvedTimeout = parseDuration(timeout);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the effective API key: the explicit {@code --api-key} value
|
||||
* when non-blank, otherwise the value of the {@code --api-key-env}
|
||||
* environment variable, otherwise an empty string. Computed on each
|
||||
* call so there is no stale cached state.
|
||||
*
|
||||
* @return the resolved API key, never {@code null}
|
||||
*/
|
||||
String resolvedApiKey() {
|
||||
String resolved = apiKey == null || apiKey.isBlank() ? System.getenv(apiKeyEnv) : apiKey;
|
||||
return resolved == null ? "" : resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the effective per-call timeout from the {@code --timeout}
|
||||
* option. Computed on each call so there is no stale cached state.
|
||||
*
|
||||
* @return the resolved call timeout
|
||||
*/
|
||||
Duration resolvedTimeout() {
|
||||
return parseDuration(timeout);
|
||||
}
|
||||
|
||||
MxGatewayClientOptions toClientOptions() {
|
||||
return MxGatewayClientOptions.builder()
|
||||
.endpoint(endpoint)
|
||||
.apiKey(resolvedApiKey())
|
||||
.apiKey(resolvedApiKey)
|
||||
.plaintext(plaintext)
|
||||
.caCertificatePath(caFile)
|
||||
.serverNameOverride(serverNameOverride)
|
||||
.callTimeout(resolvedTimeout())
|
||||
.callTimeout(resolvedTimeout)
|
||||
.build();
|
||||
}
|
||||
|
||||
Map<String, Object> redactedJsonMap() {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
values.put("endpoint", endpoint);
|
||||
values.put("apiKey", MxGatewaySecrets.redactApiKey(resolvedApiKey()));
|
||||
values.put("apiKey", MxGatewaySecrets.redactApiKey(resolvedApiKey));
|
||||
values.put("apiKeyEnv", apiKeyEnv);
|
||||
values.put("plaintext", plaintext);
|
||||
values.put("caFile", caFile == null ? "" : caFile.toString());
|
||||
|
||||
+3
-5
@@ -32,7 +32,7 @@ final class MxGatewayCliTests {
|
||||
assertEquals(0, run.exitCode());
|
||||
assertEquals("", run.errors());
|
||||
assertTrue(run.output().contains("mxgateway-java 0.1.0"));
|
||||
assertTrue(run.output().contains("gatewayProtocolVersion=3"));
|
||||
assertTrue(run.output().contains("gatewayProtocolVersion=2"));
|
||||
assertTrue(run.output().contains("workerProtocolVersion=1"));
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ final class MxGatewayCliTests {
|
||||
|
||||
assertEquals(0, run.exitCode());
|
||||
assertTrue(run.output().contains("\"clientVersion\":\"0.1.0\""));
|
||||
assertTrue(run.output().contains("\"gatewayProtocolVersion\":3"));
|
||||
assertTrue(run.output().contains("\"gatewayProtocolVersion\":2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -62,10 +62,8 @@ final class MxGatewayCliTests {
|
||||
assertEquals(0, run.exitCode());
|
||||
assertTrue(run.output().contains("\"command\":\"open-session\""));
|
||||
assertTrue(run.output().contains("\"sessionId\":\"session-cli\""));
|
||||
// Only the non-secret mxgw_<key-id>_ prefix survives; the secret is fully masked.
|
||||
assertTrue(run.output().contains("mxgw_visible_***"));
|
||||
assertTrue(run.output().contains("mxgw***********cret"));
|
||||
assertFalse(run.output().contains("visible_secret"));
|
||||
assertFalse(run.output().contains("cret"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
-5
@@ -45,11 +45,6 @@ public final class DeployEventSubscription implements AutoCloseable {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the underlying gRPC call. Safe to invoke before the call has
|
||||
* started; cancellation is recorded and applied as soon as the stream
|
||||
* attaches.
|
||||
*/
|
||||
public void cancel() {
|
||||
cancelled.set(true);
|
||||
ClientCallStreamObserver<WatchDeployEventsRequest> stream = requestStream.get();
|
||||
|
||||
+106
-130
@@ -1,5 +1,8 @@
|
||||
package com.dohertylan.mxgateway.client;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import galaxy_repository.v1.GalaxyRepositoryGrpc;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply;
|
||||
@@ -14,6 +17,8 @@ import com.google.protobuf.Timestamp;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.ClientInterceptors;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.time.Instant;
|
||||
import java.util.Iterator;
|
||||
@@ -22,6 +27,7 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
/**
|
||||
* Thin wrapper around the generated {@link GalaxyRepositoryGrpc} stubs that
|
||||
@@ -48,12 +54,8 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a client over a caller-managed {@link Channel}. The caller owns
|
||||
* Construct a client over a caller-managed {@link Channel}. The caller owns
|
||||
* channel lifecycle; {@link #close()} is a no-op for this constructor.
|
||||
*
|
||||
* @param channel the gRPC channel to use for outbound calls
|
||||
* @param options the client options carrying the API key and timeouts
|
||||
* @throws NullPointerException if {@code options} is {@code null}
|
||||
*/
|
||||
public GalaxyRepositoryClient(Channel channel, MxGatewayClientOptions options) {
|
||||
this.ownedChannel = null;
|
||||
@@ -64,50 +66,25 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||
asyncStub = GalaxyRepositoryGrpc.newStub(intercepted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new client and owns its channel; {@link #close()} shuts the
|
||||
* channel down.
|
||||
*
|
||||
* @param options the client options carrying the endpoint and credentials
|
||||
* @return a connected client
|
||||
*/
|
||||
/** Build a new client and own its channel; close shuts the channel down. */
|
||||
public static GalaxyRepositoryClient connect(MxGatewayClientOptions options) {
|
||||
return new GalaxyRepositoryClient(
|
||||
MxGatewayChannels.createChannel(options, "failed to configure galaxy repository TLS"), options);
|
||||
return new GalaxyRepositoryClient(createChannel(options), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying blocking stub with the per-call deadline applied.
|
||||
*
|
||||
* @return the blocking stub
|
||||
*/
|
||||
public GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub rawBlockingStub() {
|
||||
return MxGatewayChannels.withDeadline(blockingStub, options);
|
||||
return withDeadline(blockingStub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying future stub with the per-call deadline applied.
|
||||
*
|
||||
* @return the future stub
|
||||
*/
|
||||
public GalaxyRepositoryGrpc.GalaxyRepositoryFutureStub rawFutureStub() {
|
||||
return MxGatewayChannels.withDeadline(futureStub, options);
|
||||
return withDeadline(futureStub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying async stub. Stream deadlines are applied per call.
|
||||
*
|
||||
* @return the async stub
|
||||
*/
|
||||
public GalaxyRepositoryGrpc.GalaxyRepositoryStub rawAsyncStub() {
|
||||
return asyncStub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the {@code TestConnection} RPC and returns the {@code ok} flag.
|
||||
*
|
||||
* @return {@code true} when the gateway reached the Galaxy Repository database
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* Invoke the {@code TestConnection} RPC and return the {@code ok} flag.
|
||||
*/
|
||||
public boolean testConnection() {
|
||||
try {
|
||||
@@ -121,25 +98,14 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@code TestConnection} asynchronously.
|
||||
*
|
||||
* @return a future completed with the {@code ok} flag, or completed
|
||||
* exceptionally with {@link MxGatewayException} on failure
|
||||
*/
|
||||
public CompletableFuture<Boolean> testConnectionAsync() {
|
||||
return MxGatewayChannels.toCompletable(
|
||||
rawFutureStub().testConnection(TestConnectionRequest.getDefaultInstance()),
|
||||
"galaxy test connection")
|
||||
return toCompletable(rawFutureStub().testConnection(TestConnectionRequest.getDefaultInstance()))
|
||||
.thenApply(TestConnectionReply::getOk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the {@code GetLastDeployTime} RPC.
|
||||
*
|
||||
* @return the time of the last deploy, or {@link Optional#empty()} when the
|
||||
* server reports {@code present=false}
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* Invoke the {@code GetLastDeployTime} RPC. Returns {@link Optional#empty()}
|
||||
* when the server reports {@code present=false}.
|
||||
*/
|
||||
public Optional<Instant> getLastDeployTime() {
|
||||
try {
|
||||
@@ -154,28 +120,15 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@code GetLastDeployTime} asynchronously.
|
||||
*
|
||||
* @return a future completed with the time of the last deploy, or
|
||||
* {@link Optional#empty()} when the server reports {@code present=false};
|
||||
* completed exceptionally with {@link MxGatewayException} on failure
|
||||
*/
|
||||
public CompletableFuture<Optional<Instant>> getLastDeployTimeAsync() {
|
||||
return MxGatewayChannels.toCompletable(
|
||||
rawFutureStub().getLastDeployTime(GetLastDeployTimeRequest.getDefaultInstance()),
|
||||
"galaxy get last deploy time")
|
||||
.thenApply(MxGatewayChannels.normalisingValidator(
|
||||
"galaxy get last deploy time", GalaxyRepositoryClient::mapDeployTime));
|
||||
return toCompletable(rawFutureStub().getLastDeployTime(GetLastDeployTimeRequest.getDefaultInstance()))
|
||||
.thenApply(GalaxyRepositoryClient::mapDeployTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the {@code DiscoverHierarchy} RPC and returns the generated
|
||||
* Invoke the {@code DiscoverHierarchy} RPC and return the generated
|
||||
* {@link GalaxyObject} messages directly. Callers can read every field of
|
||||
* the proto message without an extra DTO layer.
|
||||
*
|
||||
* @return the Galaxy object hierarchy
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public List<GalaxyObject> discoverHierarchy() {
|
||||
try {
|
||||
@@ -203,58 +156,42 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@code DiscoverHierarchy} asynchronously.
|
||||
*
|
||||
* @return a future completed with the Galaxy object hierarchy, or completed
|
||||
* exceptionally with {@link MxGatewayException} on failure
|
||||
*/
|
||||
public CompletableFuture<List<GalaxyObject>> discoverHierarchyAsync() {
|
||||
return discoverHierarchyPageAsync("", new java.util.ArrayList<>(), new java.util.HashSet<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to {@code WatchDeployEvents} via the async stub and consumes
|
||||
* Subscribe to {@code WatchDeployEvents} via the async stub and consume
|
||||
* results through a blocking iterator. Closing the returned stream cancels
|
||||
* the underlying gRPC call.
|
||||
*
|
||||
* @param lastSeenDeployTime optional. When non-{@code null}, the bootstrap
|
||||
* event is suppressed if the cached deploy time matches.
|
||||
* @return an iterator-style stream of deploy events
|
||||
* @param lastSeenDeployTime optional. When non-null, the bootstrap event is
|
||||
* suppressed if the cached deploy time matches.
|
||||
*/
|
||||
public DeployEventStream watchDeployEvents(Instant lastSeenDeployTime) {
|
||||
DeployEventStream stream = new DeployEventStream(16);
|
||||
MxGatewayChannels.withStreamDeadline(rawAsyncStub(), options)
|
||||
.watchDeployEvents(buildWatchRequest(lastSeenDeployTime), stream.observer());
|
||||
withStreamDeadline(rawAsyncStub()).watchDeployEvents(buildWatchRequest(lastSeenDeployTime), stream.observer());
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator-style alias for {@link #watchDeployEvents(Instant)} matching the
|
||||
* task-spec signature.
|
||||
*
|
||||
* @param lastSeenDeployTime optional cached deploy time for bootstrap suppression
|
||||
* @return an iterator over deploy events
|
||||
*/
|
||||
public Iterator<DeployEvent> watchDeployEventsIterator(Instant lastSeenDeployTime) {
|
||||
return watchDeployEvents(lastSeenDeployTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to {@code WatchDeployEvents} via the async stub, dispatching
|
||||
* Subscribe to {@code WatchDeployEvents} via the async stub, dispatching
|
||||
* each event to {@code observer}. The returned subscription is cancellable
|
||||
* and {@link AutoCloseable}.
|
||||
*
|
||||
* @param lastSeenDeployTime optional cached deploy time for bootstrap suppression
|
||||
* @param observer caller-supplied observer that receives events and completion
|
||||
* @return a cancellable subscription handle
|
||||
* @throws NullPointerException if {@code observer} is {@code null}
|
||||
*/
|
||||
public DeployEventSubscription watchDeployEventsAsync(
|
||||
Instant lastSeenDeployTime, StreamObserver<DeployEvent> observer) {
|
||||
Objects.requireNonNull(observer, "observer");
|
||||
DeployEventSubscription subscription = new DeployEventSubscription();
|
||||
MxGatewayChannels.withStreamDeadline(rawAsyncStub(), options)
|
||||
withStreamDeadline(rawAsyncStub())
|
||||
.watchDeployEvents(buildWatchRequest(lastSeenDeployTime), subscription.wrap(observer));
|
||||
return subscription;
|
||||
}
|
||||
@@ -270,41 +207,20 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts the owned channel down and awaits termination so try-with-resources
|
||||
* callers do not leave in-flight calls or Netty event-loop threads running
|
||||
* after the block exits.
|
||||
*
|
||||
* <p>Waits up to the configured connect timeout for graceful termination
|
||||
* and forcibly shuts the channel down on timeout. If the calling thread is
|
||||
* interrupted while waiting, the channel is forcibly shut down and the
|
||||
* thread's interrupt flag is restored. No-op for clients that do not own
|
||||
* their channel. For an explicitly checked, blocking-aware shutdown call
|
||||
* {@link #closeAndAwaitTermination()}.
|
||||
*/
|
||||
private <T extends io.grpc.stub.AbstractStub<T>> T withStreamDeadline(T stub) {
|
||||
if (options.streamTimeout() == null || options.streamTimeout().isNegative()) {
|
||||
return stub;
|
||||
}
|
||||
return stub.withDeadlineAfter(options.streamTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (ownedChannel == null) {
|
||||
return;
|
||||
}
|
||||
ownedChannel.shutdown();
|
||||
try {
|
||||
if (!ownedChannel.awaitTermination(options.connectTimeout().toMillis(), TimeUnit.MILLISECONDS)) {
|
||||
ownedChannel.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException error) {
|
||||
ownedChannel.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
if (ownedChannel != null) {
|
||||
ownedChannel.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts the owned channel down and waits up to the configured connect
|
||||
* timeout for termination, forcibly shutting it down on timeout. No-op
|
||||
* for clients that do not own their channel.
|
||||
*
|
||||
* @throws InterruptedException if the calling thread is interrupted while waiting
|
||||
*/
|
||||
public void closeAndAwaitTermination() throws InterruptedException {
|
||||
if (ownedChannel != null) {
|
||||
ownedChannel.shutdown();
|
||||
@@ -322,26 +238,86 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||
return Optional.of(Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos()));
|
||||
}
|
||||
|
||||
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
||||
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
||||
.maxInboundMessageSize(options.maxGrpcMessageBytes());
|
||||
if (!options.connectTimeout().isNegative()) {
|
||||
builder.withOption(
|
||||
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||
Math.toIntExact(options.connectTimeout().toMillis()));
|
||||
}
|
||||
if (options.plaintext()) {
|
||||
builder.usePlaintext();
|
||||
} else if (options.caCertificatePath() != null) {
|
||||
try {
|
||||
builder.sslContext(GrpcSslContexts.forClient()
|
||||
.trustManager(options.caCertificatePath().toFile())
|
||||
.build());
|
||||
} catch (SSLException error) {
|
||||
throw new MxGatewayException("failed to configure galaxy repository TLS", error);
|
||||
}
|
||||
} else {
|
||||
builder.useTransportSecurity();
|
||||
}
|
||||
if (!options.serverNameOverride().isBlank()) {
|
||||
builder.overrideAuthority(options.serverNameOverride());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private <T extends io.grpc.stub.AbstractStub<T>> T withDeadline(T stub) {
|
||||
if (options.callTimeout().isNegative()) {
|
||||
return stub;
|
||||
}
|
||||
return stub.withDeadlineAfter(options.callTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
private CompletableFuture<List<GalaxyObject>> discoverHierarchyPageAsync(
|
||||
String pageToken, java.util.ArrayList<GalaxyObject> objects, java.util.HashSet<String> seenPageTokens) {
|
||||
DiscoverHierarchyRequest request = DiscoverHierarchyRequest.newBuilder()
|
||||
.setPageSize(DISCOVER_HIERARCHY_PAGE_SIZE)
|
||||
.setPageToken(pageToken)
|
||||
.build();
|
||||
return MxGatewayChannels.toCompletable(rawFutureStub().discoverHierarchy(request), "galaxy discover hierarchy")
|
||||
.thenCompose(reply -> {
|
||||
objects.addAll(reply.getObjectsList());
|
||||
if (reply.getNextPageToken().isBlank()) {
|
||||
return CompletableFuture.completedFuture(objects);
|
||||
return toCompletable(rawFutureStub().discoverHierarchy(request)).thenCompose(reply -> {
|
||||
objects.addAll(reply.getObjectsList());
|
||||
if (reply.getNextPageToken().isBlank()) {
|
||||
return CompletableFuture.completedFuture(objects);
|
||||
}
|
||||
if (!seenPageTokens.add(reply.getNextPageToken())) {
|
||||
CompletableFuture<List<GalaxyObject>> failed = new CompletableFuture<>();
|
||||
failed.completeExceptionally(new MxGatewayException(
|
||||
"galaxy discover hierarchy returned repeated page token: " + reply.getNextPageToken()));
|
||||
return failed;
|
||||
}
|
||||
return discoverHierarchyPageAsync(reply.getNextPageToken(), objects, seenPageTokens);
|
||||
});
|
||||
}
|
||||
|
||||
private static <T> CompletableFuture<T> toCompletable(com.google.common.util.concurrent.ListenableFuture<T> source) {
|
||||
CompletableFuture<T> target = new CompletableFuture<>();
|
||||
Futures.addCallback(
|
||||
source,
|
||||
new FutureCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(T result) {
|
||||
target.complete(result);
|
||||
}
|
||||
if (!seenPageTokens.add(reply.getNextPageToken())) {
|
||||
CompletableFuture<List<GalaxyObject>> failed = new CompletableFuture<>();
|
||||
failed.completeExceptionally(new MxGatewayException(
|
||||
"galaxy discover hierarchy returned repeated page token: "
|
||||
+ reply.getNextPageToken()));
|
||||
return failed;
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable error) {
|
||||
if (error instanceof RuntimeException runtimeException) {
|
||||
target.completeExceptionally(MxGatewayErrors.fromGrpc("galaxy async call", runtimeException));
|
||||
return;
|
||||
}
|
||||
target.completeExceptionally(error);
|
||||
}
|
||||
return discoverHierarchyPageAsync(reply.getNextPageToken(), objects, seenPageTokens);
|
||||
});
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
target.whenComplete((ignoredResult, ignoredError) -> {
|
||||
if (target.isCancelled()) {
|
||||
source.cancel(true);
|
||||
}
|
||||
});
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
-18
@@ -3,29 +3,11 @@ package com.dohertylan.mxgateway.client;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
||||
|
||||
/**
|
||||
* Thrown when the worker reports an MXAccess COM-side failure. Distinguishes
|
||||
* MXAccess errors (non-zero {@code HResult} or unsuccessful {@code MxStatusProxy})
|
||||
* from other gateway protocol failures.
|
||||
*/
|
||||
public final class MxAccessException extends MxGatewayCommandException {
|
||||
/**
|
||||
* Creates a new MXAccess exception with an explicit protocol status.
|
||||
*
|
||||
* @param operation human-readable name of the failing operation
|
||||
* @param protocolStatus protocol status reported by the gateway
|
||||
* @param reply raw command reply containing the MXAccess failure detail
|
||||
*/
|
||||
public MxAccessException(String operation, ProtocolStatus protocolStatus, MxCommandReply reply) {
|
||||
super(operation, protocolStatus, reply);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MXAccess exception derived from a command reply.
|
||||
*
|
||||
* @param operation human-readable name of the failing operation
|
||||
* @param reply raw command reply; the protocol status is taken from this reply when present
|
||||
*/
|
||||
public MxAccessException(String operation, MxCommandReply reply) {
|
||||
super(operation, reply == null ? null : reply.getProtocolStatus(), reply);
|
||||
}
|
||||
|
||||
+8
-66
@@ -12,44 +12,12 @@ import java.util.concurrent.BlockingQueue;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
||||
|
||||
/**
|
||||
* Iterator-style adaptor over the gateway {@code StreamEvents} server-streaming
|
||||
* RPC.
|
||||
*
|
||||
* <p>Events arrive on a background gRPC thread and are buffered in a bounded
|
||||
* blocking queue; the iterator drains them on the calling thread. Closing the
|
||||
* stream cancels the underlying gRPC call. If the queue overflows the call is
|
||||
* cancelled and a follow-up call to {@link #next()} throws
|
||||
* {@link MxGatewayException}.
|
||||
*
|
||||
* <p><strong>Backpressure (fail-fast):</strong> this adaptor relies on gRPC's
|
||||
* default auto-inbound flow control — the async stub auto-requests messages, so
|
||||
* the gateway can push events faster than the consumer drains the bounded
|
||||
* 16-element buffer. There is intentionally <em>no</em> real client flow
|
||||
* control: a consumer that stalls long enough to let the buffer fill triggers
|
||||
* an immediate overflow that cancels the subscription and surfaces an
|
||||
* {@link MxGatewayException} on the next {@link #next()} call. This matches the
|
||||
* gateway's documented fail-fast event-backpressure design — a slow consumer
|
||||
* loses its subscription rather than silently dropping events. Consumers that
|
||||
* cannot keep up must drain {@link #next()} promptly (e.g. hand events to their
|
||||
* own larger queue) and be prepared to resubscribe with a resume cursor.
|
||||
*
|
||||
* <p><strong>Threading:</strong> the iterator methods ({@link #hasNext()} and
|
||||
* {@link #next()}) are <em>not</em> thread-safe and must be driven by a single
|
||||
* consumer thread. {@link #close()} may be called from any thread. Terminal
|
||||
* state transitions (queue overflow, server completion, and {@code close()})
|
||||
* are serialised so that the first terminal condition wins deterministically:
|
||||
* once an overflow exception has been observed it is never silently replaced
|
||||
* by an end-of-stream marker.
|
||||
*/
|
||||
public final class MxEventStream implements Iterator<MxEvent>, AutoCloseable {
|
||||
private static final Object END = new Object();
|
||||
|
||||
private final BlockingQueue<Object> queue;
|
||||
private final Object terminalLock = new Object();
|
||||
private volatile ClientCallStreamObserver<StreamEventsRequest> requestStream;
|
||||
private volatile boolean closed;
|
||||
private boolean terminated;
|
||||
private Object next;
|
||||
|
||||
MxEventStream(int capacity) {
|
||||
@@ -120,7 +88,7 @@ public final class MxEventStream implements Iterator<MxEvent>, AutoCloseable {
|
||||
if (stream != null) {
|
||||
stream.cancel("client cancelled event stream", null);
|
||||
}
|
||||
terminate(null);
|
||||
offer(END);
|
||||
}
|
||||
|
||||
private Object take() {
|
||||
@@ -137,7 +105,10 @@ public final class MxEventStream implements Iterator<MxEvent>, AutoCloseable {
|
||||
private void offer(Object value) {
|
||||
Objects.requireNonNull(value, "value");
|
||||
if (value == END) {
|
||||
terminate(null);
|
||||
if (!queue.offer(value)) {
|
||||
queue.clear();
|
||||
queue.offer(value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!queue.offer(value)) {
|
||||
@@ -145,38 +116,9 @@ public final class MxEventStream implements Iterator<MxEvent>, AutoCloseable {
|
||||
if (stream != null) {
|
||||
stream.cancel("client event stream queue overflowed", null);
|
||||
}
|
||||
terminate(new MxGatewayException("gateway stream events queue overflowed"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drives the single terminal transition. The first caller wins: a later
|
||||
* end-of-stream or {@code close()} cannot overwrite or discard an overflow
|
||||
* exception that has already been published to the consumer.
|
||||
*
|
||||
* @param fault the fault to surface to the consumer, or {@code null} for a
|
||||
* clean end-of-stream
|
||||
*/
|
||||
private void terminate(MxGatewayException fault) {
|
||||
synchronized (terminalLock) {
|
||||
if (terminated) {
|
||||
return;
|
||||
}
|
||||
terminated = true;
|
||||
if (fault != null) {
|
||||
// Make room for the fault marker; the consumer only needs the
|
||||
// terminal signal, queued data events are no longer relevant.
|
||||
queue.clear();
|
||||
queue.offer(fault);
|
||||
queue.offer(END);
|
||||
return;
|
||||
}
|
||||
// Clean end-of-stream: ensure the END marker is delivered even when
|
||||
// the queue is currently full of undrained data events.
|
||||
if (!queue.offer(END)) {
|
||||
queue.clear();
|
||||
queue.offer(END);
|
||||
}
|
||||
queue.clear();
|
||||
queue.offer(new MxGatewayException("gateway stream events queue overflowed"));
|
||||
queue.offer(END);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
package com.dohertylan.mxgateway.client;
|
||||
|
||||
import io.grpc.stub.ClientCallStreamObserver;
|
||||
import io.grpc.stub.ClientResponseObserver;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest;
|
||||
|
||||
/**
|
||||
* Cancellable handle returned by {@code queryActiveAlarms}.
|
||||
*
|
||||
* <p>Wraps a caller-supplied {@link StreamObserver} and exposes a
|
||||
* {@link #cancel()} entry point that aborts the underlying gRPC call. The
|
||||
* subscription also implements {@link AutoCloseable} so it can participate in
|
||||
* try-with-resources blocks.
|
||||
*/
|
||||
public final class MxGatewayActiveAlarmsSubscription implements AutoCloseable {
|
||||
private final AtomicReference<ClientCallStreamObserver<QueryActiveAlarmsRequest>> requestStream = new AtomicReference<>();
|
||||
private final AtomicBoolean cancelled = new AtomicBoolean();
|
||||
|
||||
ClientResponseObserver<QueryActiveAlarmsRequest, ActiveAlarmSnapshot> wrap(StreamObserver<ActiveAlarmSnapshot> observer) {
|
||||
return new ClientResponseObserver<>() {
|
||||
@Override
|
||||
public void beforeStart(ClientCallStreamObserver<QueryActiveAlarmsRequest> stream) {
|
||||
requestStream.set(stream);
|
||||
if (cancelled.get()) {
|
||||
stream.cancel("client cancelled active-alarms query", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(ActiveAlarmSnapshot value) {
|
||||
observer.onNext(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
observer.onError(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
observer.onCompleted();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the underlying gRPC call. Safe to invoke before the call has
|
||||
* started; cancellation is recorded and applied as soon as the stream
|
||||
* attaches.
|
||||
*/
|
||||
public void cancel() {
|
||||
cancelled.set(true);
|
||||
ClientCallStreamObserver<QueryActiveAlarmsRequest> stream = requestStream.get();
|
||||
if (stream != null) {
|
||||
stream.cancel("client cancelled active-alarms query", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
-10
@@ -8,22 +8,12 @@ import io.grpc.ForwardingClientCall;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.MethodDescriptor;
|
||||
|
||||
/**
|
||||
* gRPC client interceptor that attaches the {@code authorization: Bearer ...}
|
||||
* header carrying the gateway API key. A blank or {@code null} key disables
|
||||
* the interceptor so unauthenticated calls pass through unchanged.
|
||||
*/
|
||||
public final class MxGatewayAuthInterceptor implements ClientInterceptor {
|
||||
static final Metadata.Key<String> AUTHORIZATION_HEADER =
|
||||
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
|
||||
|
||||
private final String apiKey;
|
||||
|
||||
/**
|
||||
* Creates a new interceptor using the supplied API key.
|
||||
*
|
||||
* @param apiKey gateway API key; {@code null} or blank disables the interceptor
|
||||
*/
|
||||
public MxGatewayAuthInterceptor(String apiKey) {
|
||||
this.apiKey = apiKey == null ? "" : apiKey;
|
||||
}
|
||||
|
||||
-10
@@ -1,16 +1,6 @@
|
||||
package com.dohertylan.mxgateway.client;
|
||||
|
||||
/**
|
||||
* Thrown when the gateway rejects a call because the supplied API key is
|
||||
* missing, malformed, or unrecognised (gRPC {@code UNAUTHENTICATED}).
|
||||
*/
|
||||
public final class MxGatewayAuthenticationException extends MxGatewayException {
|
||||
/**
|
||||
* Creates a new authentication exception.
|
||||
*
|
||||
* @param message human-readable description of the failure
|
||||
* @param cause underlying gRPC error reported by the transport
|
||||
*/
|
||||
public MxGatewayAuthenticationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
-10
@@ -1,16 +1,6 @@
|
||||
package com.dohertylan.mxgateway.client;
|
||||
|
||||
/**
|
||||
* Thrown when the gateway accepts an API key but rejects a call because the
|
||||
* key lacks the required scope (gRPC {@code PERMISSION_DENIED}).
|
||||
*/
|
||||
public final class MxGatewayAuthorizationException extends MxGatewayException {
|
||||
/**
|
||||
* Creates a new authorization exception.
|
||||
*
|
||||
* @param message human-readable description of the failure
|
||||
* @param cause underlying gRPC error reported by the transport
|
||||
*/
|
||||
public MxGatewayAuthorizationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
package com.dohertylan.mxgateway.client;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
|
||||
import io.grpc.stub.AbstractStub;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
/**
|
||||
* Shared channel-builder and future-adaptor helpers used by both
|
||||
* {@link MxGatewayClient} and {@link GalaxyRepositoryClient}.
|
||||
*
|
||||
* <p>Extracted so transport construction, per-call deadlines, and the
|
||||
* {@link ListenableFuture}-to-{@link CompletableFuture} bridge live in one
|
||||
* place instead of being duplicated verbatim across the two clients.
|
||||
*/
|
||||
final class MxGatewayChannels {
|
||||
private MxGatewayChannels() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Netty managed channel from the supplied options, applying the
|
||||
* connect timeout, message-size limit, and the configured transport
|
||||
* security mode (plaintext, custom CA trust, or system trust).
|
||||
*
|
||||
* @param options the client options carrying endpoint and transport config
|
||||
* @param tlsErrorPrefix a human-readable prefix for the {@link MxGatewayException}
|
||||
* thrown when a custom CA certificate cannot be loaded
|
||||
* @return a new managed channel; the caller owns its lifecycle
|
||||
*/
|
||||
static ManagedChannel createChannel(MxGatewayClientOptions options, String tlsErrorPrefix) {
|
||||
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
||||
.maxInboundMessageSize(options.maxGrpcMessageBytes());
|
||||
if (!options.connectTimeout().isNegative()) {
|
||||
builder.withOption(
|
||||
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||
Math.toIntExact(options.connectTimeout().toMillis()));
|
||||
}
|
||||
if (options.plaintext()) {
|
||||
builder.usePlaintext();
|
||||
} else if (options.caCertificatePath() != null) {
|
||||
try {
|
||||
builder.sslContext(GrpcSslContexts.forClient()
|
||||
.trustManager(options.caCertificatePath().toFile())
|
||||
.build());
|
||||
} catch (SSLException | RuntimeException error) {
|
||||
// SSLException covers handshake-context failures; RuntimeException
|
||||
// (IllegalArgumentException wrapping CertificateException) covers a
|
||||
// missing or unreadable CA file. Either way callers see one typed
|
||||
// failure instead of a raw, unwrapped exception leaking out.
|
||||
throw new MxGatewayException(tlsErrorPrefix, error);
|
||||
}
|
||||
} else {
|
||||
builder.useTransportSecurity();
|
||||
}
|
||||
if (!options.serverNameOverride().isBlank()) {
|
||||
builder.overrideAuthority(options.serverNameOverride());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the configured per-call deadline to a unary stub.
|
||||
*
|
||||
* @param stub the stub to decorate
|
||||
* @param options the client options carrying the call timeout
|
||||
* @param <T> the concrete stub type
|
||||
* @return the stub with the call deadline applied, or the stub unchanged
|
||||
* when the call timeout is negative (disabled)
|
||||
*/
|
||||
static <T extends AbstractStub<T>> T withDeadline(T stub, MxGatewayClientOptions options) {
|
||||
if (options.callTimeout().isNegative()) {
|
||||
return stub;
|
||||
}
|
||||
return stub.withDeadlineAfter(options.callTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the configured streaming deadline to a streaming stub.
|
||||
*
|
||||
* @param stub the stub to decorate
|
||||
* @param options the client options carrying the stream timeout
|
||||
* @param <T> the concrete stub type
|
||||
* @return the stub with the stream deadline applied, or the stub unchanged
|
||||
* when the stream timeout is unset or negative (disabled)
|
||||
*/
|
||||
static <T extends AbstractStub<T>> T withStreamDeadline(T stub, MxGatewayClientOptions options) {
|
||||
if (options.streamTimeout() == null || options.streamTimeout().isNegative()) {
|
||||
return stub;
|
||||
}
|
||||
return stub.withDeadlineAfter(options.streamTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridges a Guava {@link ListenableFuture} to a {@link CompletableFuture},
|
||||
* normalising any failure through {@link MxGatewayErrors#fromGrpc} so the
|
||||
* async error surface matches the synchronous methods. Cancelling the
|
||||
* returned future cancels the source RPC.
|
||||
*
|
||||
* @param source the gRPC future-stub result
|
||||
* @param operation the operation name used in normalised error messages
|
||||
* @param <T> the reply type
|
||||
* @return a completable future mirroring the source
|
||||
*/
|
||||
static <T> CompletableFuture<T> toCompletable(ListenableFuture<T> source, String operation) {
|
||||
CompletableFuture<T> target = new CompletableFuture<>();
|
||||
Futures.addCallback(
|
||||
source,
|
||||
new FutureCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(T result) {
|
||||
target.complete(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable error) {
|
||||
if (error instanceof RuntimeException runtimeException) {
|
||||
target.completeExceptionally(MxGatewayErrors.fromGrpc(operation, runtimeException));
|
||||
return;
|
||||
}
|
||||
target.completeExceptionally(error);
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
target.whenComplete((ignoredResult, ignoredError) -> {
|
||||
if (target.isCancelled()) {
|
||||
source.cancel(true);
|
||||
}
|
||||
});
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts a reply-validating function for use inside {@code thenApply} so
|
||||
* any non-{@link MxGatewayException} {@link RuntimeException} it raises is
|
||||
* routed through {@link MxGatewayErrors#fromGrpc}. This keeps the async
|
||||
* error surface consistent with the synchronous methods, which normalise
|
||||
* failures with a {@code try/catch}.
|
||||
*
|
||||
* @param operation the operation name used in normalised error messages
|
||||
* @param validator the validating/transforming function applied to the reply
|
||||
* @param <T> the reply type
|
||||
* @param <R> the result type
|
||||
* @return a function suitable for {@link CompletableFuture#thenApply}
|
||||
*/
|
||||
static <T, R> Function<T, R> normalisingValidator(String operation, Function<T, R> validator) {
|
||||
return reply -> {
|
||||
try {
|
||||
return validator.apply(reply);
|
||||
} catch (MxGatewayException error) {
|
||||
throw error;
|
||||
} catch (RuntimeException error) {
|
||||
throw MxGatewayErrors.fromGrpc(operation, error);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+88
-237
@@ -1,17 +1,20 @@
|
||||
package com.dohertylan.mxgateway.client;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.protobuf.Duration;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.ClientInterceptors;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.net.ssl.SSLException;
|
||||
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||
@@ -20,18 +23,8 @@ import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
||||
|
||||
/**
|
||||
* Idiomatic Java wrapper around the generated {@code MxAccessGateway} gRPC
|
||||
* stubs.
|
||||
*
|
||||
* <p>Owns or borrows a {@link ManagedChannel}, attaches a
|
||||
* {@link MxGatewayAuthInterceptor} carrying the configured API key, and
|
||||
* exposes blocking, future, and async stub variants. Translates protocol
|
||||
* status failures into typed {@link MxGatewayException} subclasses.
|
||||
*/
|
||||
public final class MxGatewayClient implements AutoCloseable {
|
||||
private final ManagedChannel ownedChannel;
|
||||
private final MxGatewayClientOptions options;
|
||||
@@ -48,14 +41,6 @@ public final class MxGatewayClient implements AutoCloseable {
|
||||
asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a client over a caller-managed {@link Channel}. The caller
|
||||
* owns channel lifecycle; {@link #close()} is a no-op for this constructor.
|
||||
*
|
||||
* @param channel the gRPC channel to use for outbound calls
|
||||
* @param options the client options carrying the API key and timeouts
|
||||
* @throws NullPointerException if {@code options} is {@code null}
|
||||
*/
|
||||
public MxGatewayClient(Channel channel, MxGatewayClientOptions options) {
|
||||
this.ownedChannel = null;
|
||||
this.options = Objects.requireNonNull(options, "options");
|
||||
@@ -65,65 +50,27 @@ public final class MxGatewayClient implements AutoCloseable {
|
||||
asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new client and owns its channel; {@link #close()} shuts the
|
||||
* channel down.
|
||||
*
|
||||
* @param options the client options carrying the endpoint and credentials
|
||||
* @return a connected client
|
||||
*/
|
||||
public static MxGatewayClient connect(MxGatewayClientOptions options) {
|
||||
return new MxGatewayClient(
|
||||
MxGatewayChannels.createChannel(options, "failed to configure gateway TLS"), options);
|
||||
return new MxGatewayClient(createChannel(options), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying blocking stub with the per-call deadline applied.
|
||||
*
|
||||
* @return the blocking stub
|
||||
*/
|
||||
public MxAccessGatewayGrpc.MxAccessGatewayBlockingStub rawBlockingStub() {
|
||||
return MxGatewayChannels.withDeadline(blockingStub, options);
|
||||
return withDeadline(blockingStub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying future stub with the per-call deadline applied.
|
||||
*
|
||||
* @return the future stub
|
||||
*/
|
||||
public MxAccessGatewayGrpc.MxAccessGatewayFutureStub rawFutureStub() {
|
||||
return MxGatewayChannels.withDeadline(futureStub, options);
|
||||
return withDeadline(futureStub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying async stub. Stream deadlines are applied per call.
|
||||
*
|
||||
* @return the async stub
|
||||
*/
|
||||
public MxAccessGatewayGrpc.MxAccessGatewayStub rawAsyncStub() {
|
||||
return asyncStub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a gateway session and returns a typed handle for further commands.
|
||||
*
|
||||
* @param request the {@code OpenSessionRequest} to send
|
||||
* @return a session bound to the resulting {@code OpenSessionReply}
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public MxGatewaySession openSession(OpenSessionRequest request) {
|
||||
OpenSessionReply reply = openSessionRaw(request);
|
||||
return new MxGatewaySession(this, reply);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a gateway session using the configured call timeout for the
|
||||
* worker command timeout and a caller-supplied client session name.
|
||||
*
|
||||
* @param clientSessionName the human-readable session name reported by the gateway
|
||||
* @return a session bound to the resulting {@code OpenSessionReply}
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public MxGatewaySession openSession(String clientSessionName) {
|
||||
return openSession(OpenSessionRequest.newBuilder()
|
||||
.setClientSessionName(clientSessionName)
|
||||
@@ -134,18 +81,10 @@ public final class MxGatewayClient implements AutoCloseable {
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@code OpenSession} and returns the raw reply.
|
||||
*
|
||||
* @param request the {@code OpenSessionRequest} to send
|
||||
* @return the raw {@code OpenSessionReply}
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public OpenSessionReply openSessionRaw(OpenSessionRequest request) {
|
||||
try {
|
||||
OpenSessionReply reply = rawBlockingStub().openSession(request);
|
||||
MxGatewayErrors.ensureProtocolSuccess("open session", reply.getProtocolStatus(), null);
|
||||
ensureGatewayProtocolCompatible(reply);
|
||||
return reply;
|
||||
} catch (RuntimeException error) {
|
||||
if (error instanceof MxGatewayException) {
|
||||
@@ -155,50 +94,14 @@ public final class MxGatewayClient implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the gateway speaks the protocol version this client was
|
||||
* generated against. A gateway that leaves {@code gateway_protocol_version}
|
||||
* unset (value {@code 0}, e.g. an older gateway) is accepted unchanged.
|
||||
*
|
||||
* @param reply the {@code OpenSessionReply} returned by the gateway
|
||||
* @throws MxGatewayException if the gateway reports an incompatible protocol version
|
||||
*/
|
||||
private static void ensureGatewayProtocolCompatible(OpenSessionReply reply) {
|
||||
int gatewayVersion = reply.getGatewayProtocolVersion();
|
||||
int clientVersion = MxGatewayClientVersion.gatewayProtocolVersion();
|
||||
if (gatewayVersion != 0 && gatewayVersion != clientVersion) {
|
||||
throw new MxGatewayException("gateway protocol version mismatch: gateway reports "
|
||||
+ gatewayVersion + " but this client was built for " + clientVersion
|
||||
+ "; upgrade the client or gateway so the protocol versions match");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@code OpenSession} asynchronously.
|
||||
*
|
||||
* @param request the {@code OpenSessionRequest} to send
|
||||
* @return a future completed with the raw reply, or completed exceptionally
|
||||
* with {@link MxGatewayException} on failure
|
||||
*/
|
||||
public CompletableFuture<OpenSessionReply> openSessionAsync(OpenSessionRequest request) {
|
||||
CompletableFuture<OpenSessionReply> future =
|
||||
MxGatewayChannels.toCompletable(rawFutureStub().openSession(request), "open session");
|
||||
return future.thenApply(MxGatewayChannels.normalisingValidator("open session", reply -> {
|
||||
CompletableFuture<OpenSessionReply> future = toCompletable(rawFutureStub().openSession(request));
|
||||
return future.thenApply(reply -> {
|
||||
MxGatewayErrors.ensureProtocolSuccess("open session", reply.getProtocolStatus(), null);
|
||||
ensureGatewayProtocolCompatible(reply);
|
||||
return reply;
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the {@code Invoke} unary RPC and validates both the protocol
|
||||
* status and any MXAccess-side failure carried in the reply.
|
||||
*
|
||||
* @param request the {@code MxCommandRequest} to send
|
||||
* @return the raw command reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws MxAccessException when the worker reports an MXAccess COM-side failure
|
||||
*/
|
||||
public MxCommandReply invoke(MxCommandRequest request) {
|
||||
try {
|
||||
MxCommandReply reply = rawBlockingStub().invoke(request);
|
||||
@@ -213,31 +116,15 @@ public final class MxGatewayClient implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the {@code Invoke} RPC asynchronously.
|
||||
*
|
||||
* @param request the {@code MxCommandRequest} to send
|
||||
* @return a future completed with the raw reply, or completed exceptionally
|
||||
* with {@link MxGatewayException} (including {@link MxAccessException})
|
||||
* on failure
|
||||
*/
|
||||
public CompletableFuture<MxCommandReply> invokeAsync(MxCommandRequest request) {
|
||||
CompletableFuture<MxCommandReply> future =
|
||||
MxGatewayChannels.toCompletable(rawFutureStub().invoke(request), "invoke");
|
||||
return future.thenApply(MxGatewayChannels.normalisingValidator("invoke", reply -> {
|
||||
CompletableFuture<MxCommandReply> future = toCompletable(rawFutureStub().invoke(request));
|
||||
return future.thenApply(reply -> {
|
||||
MxGatewayErrors.ensureProtocolSuccess("invoke", reply.getProtocolStatus(), reply);
|
||||
MxGatewayErrors.ensureMxAccessSuccess("invoke", reply);
|
||||
return reply;
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the {@code CloseSession} unary RPC.
|
||||
*
|
||||
* @param request the {@code CloseSessionRequest} to send
|
||||
* @return the raw reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public CloseSessionReply closeSessionRaw(CloseSessionRequest request) {
|
||||
try {
|
||||
CloseSessionReply reply = rawBlockingStub().closeSession(request);
|
||||
@@ -251,131 +138,26 @@ public final class MxGatewayClient implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the {@code StreamEvents} server-streaming RPC and exposes
|
||||
* results as a blocking iterator. Closing the returned stream cancels the
|
||||
* underlying gRPC call.
|
||||
*
|
||||
* @param request the {@code StreamEventsRequest} carrying the session id and resume cursor
|
||||
* @return an iterator-style stream of events
|
||||
*/
|
||||
public MxEventStream streamEvents(StreamEventsRequest request) {
|
||||
MxEventStream stream = new MxEventStream(16);
|
||||
MxGatewayChannels.withStreamDeadline(rawAsyncStub(), options).streamEvents(request, stream.observer());
|
||||
withStreamDeadline(rawAsyncStub()).streamEvents(request, stream.observer());
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to {@code StreamEvents} and dispatches each event to the
|
||||
* supplied observer. The returned subscription is cancellable.
|
||||
*
|
||||
* @param request the {@code StreamEventsRequest} to send
|
||||
* @param observer caller-supplied observer that receives events and completion
|
||||
* @return a cancellable subscription handle
|
||||
*/
|
||||
public MxGatewayEventSubscription streamEventsAsync(
|
||||
StreamEventsRequest request, StreamObserver<MxEvent> observer) {
|
||||
MxGatewayEventSubscription subscription = new MxGatewayEventSubscription();
|
||||
MxGatewayChannels.withStreamDeadline(rawAsyncStub(), options)
|
||||
.streamEvents(request, subscription.wrap(observer));
|
||||
withStreamDeadline(rawAsyncStub()).streamEvents(request, subscription.wrap(observer));
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledges an active MXAccess alarm condition through the gateway.
|
||||
*
|
||||
* <p>The gateway authorizes this request against the API key's
|
||||
* {@code admin} scope (the gateway scope resolver maps alarm RPCs to the
|
||||
* default {@code admin} scope) and forwards the acknowledge to the
|
||||
* worker's MXAccess session; the resulting native MxStatus is returned
|
||||
* in the reply. Acks are idempotent at the MxAccess layer.
|
||||
*
|
||||
* @param request the {@code AcknowledgeAlarmRequest}
|
||||
* @return the raw acknowledge reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public AcknowledgeAlarmReply acknowledgeAlarm(AcknowledgeAlarmRequest request) {
|
||||
try {
|
||||
AcknowledgeAlarmReply reply = rawBlockingStub().acknowledgeAlarm(request);
|
||||
MxGatewayErrors.ensureProtocolSuccess("acknowledge alarm", reply.getProtocolStatus(), null);
|
||||
return reply;
|
||||
} catch (RuntimeException error) {
|
||||
if (error instanceof MxGatewayException) {
|
||||
throw error;
|
||||
}
|
||||
throw MxGatewayErrors.fromGrpc("acknowledge alarm", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledges an active MXAccess alarm condition asynchronously.
|
||||
*
|
||||
* @param request the {@code AcknowledgeAlarmRequest}
|
||||
* @return a future completed with the raw reply, or completed exceptionally
|
||||
* with {@link MxGatewayException} on failure
|
||||
*/
|
||||
public CompletableFuture<AcknowledgeAlarmReply> acknowledgeAlarmAsync(AcknowledgeAlarmRequest request) {
|
||||
CompletableFuture<AcknowledgeAlarmReply> future =
|
||||
MxGatewayChannels.toCompletable(rawFutureStub().acknowledgeAlarm(request), "acknowledge alarm");
|
||||
return future.thenApply(MxGatewayChannels.normalisingValidator("acknowledge alarm", reply -> {
|
||||
MxGatewayErrors.ensureProtocolSuccess("acknowledge alarm", reply.getProtocolStatus(), null);
|
||||
return reply;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams a snapshot of all alarms currently Active or ActiveAcked — the
|
||||
* gateway's ConditionRefresh equivalent. Used after reconnect to seed
|
||||
* local Part 9 state.
|
||||
*
|
||||
* @param request the {@code QueryActiveAlarmsRequest}, optionally scoped by
|
||||
* alarm-reference prefix
|
||||
* @param observer caller-supplied observer that receives snapshots and completion
|
||||
* @return a cancellable subscription handle
|
||||
*/
|
||||
public MxGatewayActiveAlarmsSubscription queryActiveAlarms(
|
||||
QueryActiveAlarmsRequest request, StreamObserver<ActiveAlarmSnapshot> observer) {
|
||||
MxGatewayActiveAlarmsSubscription subscription = new MxGatewayActiveAlarmsSubscription();
|
||||
MxGatewayChannels.withStreamDeadline(rawAsyncStub(), options)
|
||||
.queryActiveAlarms(request, subscription.wrap(observer));
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts the owned channel down and awaits termination so try-with-resources
|
||||
* callers do not leave in-flight calls or Netty event-loop threads running
|
||||
* after the block exits.
|
||||
*
|
||||
* <p>Waits up to the configured connect timeout for graceful termination
|
||||
* and forcibly shuts the channel down on timeout. If the calling thread is
|
||||
* interrupted while waiting, the channel is forcibly shut down and the
|
||||
* thread's interrupt flag is restored. No-op for clients that do not own
|
||||
* their channel. For an explicitly checked, blocking-aware shutdown call
|
||||
* {@link #closeAndAwaitTermination()}.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (ownedChannel == null) {
|
||||
return;
|
||||
}
|
||||
ownedChannel.shutdown();
|
||||
try {
|
||||
if (!ownedChannel.awaitTermination(options.connectTimeout().toMillis(), TimeUnit.MILLISECONDS)) {
|
||||
ownedChannel.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException error) {
|
||||
ownedChannel.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
if (ownedChannel != null) {
|
||||
ownedChannel.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts the owned channel down and waits up to the configured connect
|
||||
* timeout for termination, forcibly shutting it down on timeout. No-op
|
||||
* for clients that do not own their channel.
|
||||
*
|
||||
* @throws InterruptedException if the calling thread is interrupted while waiting
|
||||
*/
|
||||
public void closeAndAwaitTermination() throws InterruptedException {
|
||||
if (ownedChannel != null) {
|
||||
ownedChannel.shutdown();
|
||||
@@ -385,6 +167,75 @@ public final class MxGatewayClient implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
||||
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
||||
.maxInboundMessageSize(options.maxGrpcMessageBytes());
|
||||
if (!options.connectTimeout().isNegative()) {
|
||||
builder.withOption(
|
||||
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||
Math.toIntExact(options.connectTimeout().toMillis()));
|
||||
}
|
||||
if (options.plaintext()) {
|
||||
builder.usePlaintext();
|
||||
} else if (options.caCertificatePath() != null) {
|
||||
try {
|
||||
builder.sslContext(GrpcSslContexts.forClient()
|
||||
.trustManager(options.caCertificatePath().toFile())
|
||||
.build());
|
||||
} catch (SSLException error) {
|
||||
throw new MxGatewayException("failed to configure gateway TLS", error);
|
||||
}
|
||||
} else {
|
||||
builder.useTransportSecurity();
|
||||
}
|
||||
if (!options.serverNameOverride().isBlank()) {
|
||||
builder.overrideAuthority(options.serverNameOverride());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private <T extends io.grpc.stub.AbstractStub<T>> T withDeadline(T stub) {
|
||||
if (options.callTimeout().isNegative()) {
|
||||
return stub;
|
||||
}
|
||||
return stub.withDeadlineAfter(options.callTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
private <T extends io.grpc.stub.AbstractStub<T>> T withStreamDeadline(T stub) {
|
||||
if (options.streamTimeout() == null || options.streamTimeout().isNegative()) {
|
||||
return stub;
|
||||
}
|
||||
return stub.withDeadlineAfter(options.streamTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
private static <T> CompletableFuture<T> toCompletable(com.google.common.util.concurrent.ListenableFuture<T> source) {
|
||||
CompletableFuture<T> target = new CompletableFuture<>();
|
||||
Futures.addCallback(
|
||||
source,
|
||||
new FutureCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(T result) {
|
||||
target.complete(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable error) {
|
||||
if (error instanceof RuntimeException runtimeException) {
|
||||
target.completeExceptionally(MxGatewayErrors.fromGrpc("async call", runtimeException));
|
||||
return;
|
||||
}
|
||||
target.completeExceptionally(error);
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
target.whenComplete((ignoredResult, ignoredError) -> {
|
||||
if (target.isCancelled()) {
|
||||
source.cancel(true);
|
||||
}
|
||||
});
|
||||
return target;
|
||||
}
|
||||
|
||||
static ProtocolStatusCode okStatusCode() {
|
||||
return ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK;
|
||||
}
|
||||
|
||||
-118
@@ -4,13 +4,6 @@ import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Immutable configuration for {@link MxGatewayClient} and
|
||||
* {@link GalaxyRepositoryClient}.
|
||||
*
|
||||
* <p>Captures the gateway endpoint, API key, transport security selection and
|
||||
* call/stream timeouts. Instances are constructed via {@link #builder()}.
|
||||
*/
|
||||
public final class MxGatewayClientOptions {
|
||||
private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
|
||||
private static final Duration DEFAULT_CALL_TIMEOUT = Duration.ofSeconds(30);
|
||||
@@ -40,93 +33,42 @@ public final class MxGatewayClientOptions {
|
||||
: builder.maxGrpcMessageBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fresh builder with default timeouts and no endpoint set.
|
||||
*
|
||||
* @return a new {@link Builder}
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured gRPC target endpoint.
|
||||
*
|
||||
* @return the endpoint string in {@code host:port} or DNS-target form
|
||||
*/
|
||||
public String endpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured API key, or an empty string if none was supplied.
|
||||
*
|
||||
* @return the raw API key
|
||||
*/
|
||||
public String apiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the API key with the body redacted, safe to write to logs.
|
||||
*
|
||||
* @return the redacted form produced by {@link MxGatewaySecrets#redactApiKey(String)}
|
||||
*/
|
||||
public String redactedApiKey() {
|
||||
return MxGatewaySecrets.redactApiKey(apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the client is configured to use plaintext transport.
|
||||
*
|
||||
* @return {@code true} for plaintext, {@code false} for TLS
|
||||
*/
|
||||
public boolean plaintext() {
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured CA certificate file used to verify the gateway,
|
||||
* or {@code null} when the platform trust store is used.
|
||||
*
|
||||
* @return the CA certificate path, or {@code null}
|
||||
*/
|
||||
public Path caCertificatePath() {
|
||||
return caCertificatePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TLS server-name override, or an empty string when none was supplied.
|
||||
*
|
||||
* @return the server-name override
|
||||
*/
|
||||
public String serverNameOverride() {
|
||||
return serverNameOverride;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the channel connect timeout.
|
||||
*
|
||||
* @return the connect timeout duration
|
||||
*/
|
||||
public Duration connectTimeout() {
|
||||
return connectTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the per-call deadline applied to unary RPCs.
|
||||
*
|
||||
* @return the call timeout duration
|
||||
*/
|
||||
public Duration callTimeout() {
|
||||
return callTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deadline applied to server-streaming RPCs, or {@code null} when none is set.
|
||||
*
|
||||
* @return the stream timeout duration, or {@code null}
|
||||
*/
|
||||
public Duration streamTimeout() {
|
||||
return streamTimeout;
|
||||
}
|
||||
@@ -169,9 +111,6 @@ public final class MxGatewayClientOptions {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutable builder for {@link MxGatewayClientOptions}.
|
||||
*/
|
||||
public static final class Builder {
|
||||
private String endpoint;
|
||||
private String apiKey;
|
||||
@@ -186,92 +125,41 @@ public final class MxGatewayClientOptions {
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the gRPC target endpoint.
|
||||
*
|
||||
* @param value endpoint in {@code host:port} or DNS-target form; required
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder endpoint(String value) {
|
||||
endpoint = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the API key sent in the {@code authorization} header.
|
||||
*
|
||||
* @param value the API key, or {@code null}/blank to disable authentication
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder apiKey(String value) {
|
||||
apiKey = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects plaintext transport instead of TLS.
|
||||
*
|
||||
* @param value {@code true} for plaintext, {@code false} for TLS
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder plaintext(boolean value) {
|
||||
plaintext = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the CA certificate used to verify the gateway server.
|
||||
*
|
||||
* @param value path to a PEM-encoded CA certificate, or {@code null} to use the platform trust store
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder caCertificatePath(Path value) {
|
||||
caCertificatePath = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the TLS server name used during the handshake.
|
||||
*
|
||||
* @param value the override host name, or empty/{@code null} for none
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder serverNameOverride(String value) {
|
||||
serverNameOverride = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the channel connect timeout.
|
||||
*
|
||||
* @param value the connect timeout, must be non-{@code null}
|
||||
* @return this builder
|
||||
* @throws NullPointerException if {@code value} is {@code null}
|
||||
*/
|
||||
public Builder connectTimeout(Duration value) {
|
||||
connectTimeout = Objects.requireNonNull(value, "connectTimeout");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the per-call deadline applied to unary RPCs.
|
||||
*
|
||||
* @param value the call timeout, must be non-{@code null}
|
||||
* @return this builder
|
||||
* @throws NullPointerException if {@code value} is {@code null}
|
||||
*/
|
||||
public Builder callTimeout(Duration value) {
|
||||
callTimeout = Objects.requireNonNull(value, "callTimeout");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the deadline applied to server-streaming RPCs.
|
||||
*
|
||||
* @param value the stream timeout, must be non-{@code null}
|
||||
* @return this builder
|
||||
* @throws NullPointerException if {@code value} is {@code null}
|
||||
*/
|
||||
public Builder streamTimeout(Duration value) {
|
||||
streamTimeout = Objects.requireNonNull(value, "streamTimeout");
|
||||
return this;
|
||||
@@ -282,12 +170,6 @@ public final class MxGatewayClientOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an immutable {@link MxGatewayClientOptions} from the current state.
|
||||
*
|
||||
* @return a new options instance
|
||||
* @throws IllegalArgumentException if {@code endpoint} was not set or is blank
|
||||
*/
|
||||
public MxGatewayClientOptions build() {
|
||||
return new MxGatewayClientOptions(this);
|
||||
}
|
||||
|
||||
+1
-22
@@ -1,42 +1,21 @@
|
||||
package com.dohertylan.mxgateway.client;
|
||||
|
||||
/**
|
||||
* Reports the client and protocol version numbers compiled into this build.
|
||||
*
|
||||
* <p>Used by smoke-test tooling and the CLI to confirm that a gateway and
|
||||
* worker speak the same protocol version as the client.
|
||||
*/
|
||||
public final class MxGatewayClientVersion {
|
||||
private static final int GATEWAY_PROTOCOL_VERSION = 3;
|
||||
private static final int GATEWAY_PROTOCOL_VERSION = 2;
|
||||
private static final int WORKER_PROTOCOL_VERSION = 1;
|
||||
private static final String CLIENT_VERSION = "0.1.0";
|
||||
|
||||
private MxGatewayClientVersion() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable client release version.
|
||||
*
|
||||
* @return the client version string
|
||||
*/
|
||||
public static String clientVersion() {
|
||||
return CLIENT_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the gRPC gateway protocol version this client targets.
|
||||
*
|
||||
* @return the gateway protocol version
|
||||
*/
|
||||
public static int gatewayProtocolVersion() {
|
||||
return GATEWAY_PROTOCOL_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the worker IPC protocol version this client targets.
|
||||
*
|
||||
* @return the worker protocol version
|
||||
*/
|
||||
public static int workerProtocolVersion() {
|
||||
return WORKER_PROTOCOL_VERSION;
|
||||
}
|
||||
|
||||
-22
@@ -3,42 +3,20 @@ package com.dohertylan.mxgateway.client;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
||||
|
||||
/**
|
||||
* Thrown when the gateway accepts an MXAccess command but the command itself
|
||||
* fails at the protocol layer. Carries the original {@code MxCommandReply} and
|
||||
* {@code ProtocolStatus} so callers can inspect the failure detail.
|
||||
*/
|
||||
public class MxGatewayCommandException extends MxGatewayException {
|
||||
private final ProtocolStatus protocolStatus;
|
||||
private final MxCommandReply reply;
|
||||
|
||||
/**
|
||||
* Creates a new command exception.
|
||||
*
|
||||
* @param operation human-readable name of the failing operation
|
||||
* @param protocolStatus protocol status returned by the gateway
|
||||
* @param reply raw command reply, or {@code null} when the call failed before a reply was produced
|
||||
*/
|
||||
public MxGatewayCommandException(String operation, ProtocolStatus protocolStatus, MxCommandReply reply) {
|
||||
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
||||
this.protocolStatus = protocolStatus;
|
||||
this.reply = reply;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the gateway protocol status that triggered this exception.
|
||||
*
|
||||
* @return the protocol status, or {@code null} if none was supplied
|
||||
*/
|
||||
public ProtocolStatus protocolStatus() {
|
||||
return protocolStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw command reply associated with the failure.
|
||||
*
|
||||
* @return the command reply, or {@code null} if no reply was available
|
||||
*/
|
||||
public MxCommandReply reply() {
|
||||
return reply;
|
||||
}
|
||||
|
||||
-13
@@ -8,14 +8,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
||||
|
||||
/**
|
||||
* Cancellable handle returned by the async {@code streamEvents} variant.
|
||||
*
|
||||
* <p>Wraps a caller-supplied {@link StreamObserver} and exposes a
|
||||
* {@link #cancel()} entry point that aborts the underlying gRPC call. The
|
||||
* subscription also implements {@link AutoCloseable} so it can participate in
|
||||
* try-with-resources blocks.
|
||||
*/
|
||||
public final class MxGatewayEventSubscription implements AutoCloseable {
|
||||
private final AtomicReference<ClientCallStreamObserver<StreamEventsRequest>> requestStream = new AtomicReference<>();
|
||||
private final AtomicBoolean cancelled = new AtomicBoolean();
|
||||
@@ -47,11 +39,6 @@ public final class MxGatewayEventSubscription implements AutoCloseable {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the underlying gRPC call. Safe to invoke before the call has
|
||||
* started; cancellation is recorded and applied as soon as the stream
|
||||
* attaches.
|
||||
*/
|
||||
public void cancel() {
|
||||
cancelled.set(true);
|
||||
ClientCallStreamObserver<StreamEventsRequest> stream = requestStream.get();
|
||||
|
||||
-18
@@ -1,28 +1,10 @@
|
||||
package com.dohertylan.mxgateway.client;
|
||||
|
||||
/**
|
||||
* Base unchecked exception thrown by the MXAccess Gateway Java client.
|
||||
*
|
||||
* <p>All gateway-specific failures derive from this type so callers can catch a
|
||||
* single supertype regardless of whether the cause was a transport error,
|
||||
* protocol-level failure, or MXAccess-side problem.
|
||||
*/
|
||||
public class MxGatewayException extends RuntimeException {
|
||||
/**
|
||||
* Creates a new exception with the supplied message.
|
||||
*
|
||||
* @param message human-readable description of the failure
|
||||
*/
|
||||
public MxGatewayException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new exception with the supplied message and underlying cause.
|
||||
*
|
||||
* @param message human-readable description of the failure
|
||||
* @param cause underlying error that triggered the failure
|
||||
*/
|
||||
public MxGatewayException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
+5
-39
@@ -1,56 +1,22 @@
|
||||
package com.dohertylan.mxgateway.client;
|
||||
|
||||
/**
|
||||
* Helpers for redacting secrets such as gateway API keys from log output.
|
||||
*
|
||||
* <p>API keys must never reach logs in plaintext. The methods on this class
|
||||
* produce shortened, masked forms safe for diagnostic messages.
|
||||
*/
|
||||
public final class MxGatewaySecrets {
|
||||
private MxGatewaySecrets() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Redacts the secret portion of an API key, leaving only the non-secret
|
||||
* key identifier visible so the value remains comparable in logs.
|
||||
*
|
||||
* <p>A gateway API key has the form {@code mxgw_<key-id>_<secret>}. Only the
|
||||
* {@code mxgw_<key-id>_} prefix is non-secret; everything after the second
|
||||
* underscore is the secret and is masked entirely — no leading or
|
||||
* trailing characters of the secret are echoed. Tokens that do not match
|
||||
* the gateway shape are masked completely as {@code "<redacted>"}.
|
||||
*
|
||||
* @param apiKey the API key to redact, may be {@code null} or empty
|
||||
* @return an empty string for {@code null}/empty input, {@code "<redacted>"}
|
||||
* for non-gateway-shaped tokens, or {@code mxgw_<key-id>_***} with the
|
||||
* secret masked for gateway-shaped keys
|
||||
*/
|
||||
public static String redactApiKey(String apiKey) {
|
||||
if (apiKey == null || apiKey.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Gateway keys are mxgw_<key-id>_<secret>; keep only the non-secret prefix.
|
||||
if (apiKey.startsWith("mxgw_")) {
|
||||
int secretSeparator = apiKey.indexOf('_', "mxgw_".length());
|
||||
if (secretSeparator >= 0 && secretSeparator < apiKey.length() - 1) {
|
||||
return apiKey.substring(0, secretSeparator + 1) + "***";
|
||||
}
|
||||
if (apiKey.length() <= 8) {
|
||||
return "<redacted>";
|
||||
}
|
||||
|
||||
// Anything else is treated as wholly secret — reveal nothing.
|
||||
return "<redacted>";
|
||||
return apiKey.substring(0, 4)
|
||||
+ "*".repeat(apiKey.length() - 8)
|
||||
+ apiKey.substring(apiKey.length() - 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces gateway-style credential tokens (the {@code mxgw_} prefix and
|
||||
* any {@code Bearer} marker) inside a free-form string with a redaction
|
||||
* placeholder.
|
||||
*
|
||||
* @param value the string to scrub, may be {@code null}
|
||||
* @return an empty string for {@code null}, the original value when blank,
|
||||
* or the value with credential tokens replaced by {@code "<redacted>"}
|
||||
*/
|
||||
public static String redactCredentials(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return value == null ? "" : value;
|
||||
|
||||
+4
-274
@@ -30,17 +30,8 @@ import mxaccess_gateway.v1.MxaccessGateway.UnregisterCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.Write2Command;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.WriteCommand;
|
||||
|
||||
/**
|
||||
* Typed handle for a single MXAccess gateway session.
|
||||
*
|
||||
* <p>Wraps an {@link OpenSessionReply} together with the {@link MxGatewayClient}
|
||||
* that opened it and exposes the MXAccess command surface (Register, AddItem,
|
||||
* Advise, bulk subscribe variants, Write, event streaming, and close). Each
|
||||
* command request carries a freshly generated client correlation id.
|
||||
*/
|
||||
public final class MxGatewaySession implements AutoCloseable {
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
private static final System.Logger LOGGER = System.getLogger(MxGatewaySession.class.getName());
|
||||
|
||||
private final MxGatewayClient client;
|
||||
private final OpenSessionReply openReply;
|
||||
@@ -51,45 +42,19 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
this.openReply = Objects.requireNonNull(openReply, "openReply");
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a session handle for an existing gateway session id without
|
||||
* issuing an {@code OpenSession} call. Useful for CLI tools that operate
|
||||
* against a session opened in a separate invocation.
|
||||
*
|
||||
* @param client the gateway client used for further commands
|
||||
* @param sessionId the existing gateway session id
|
||||
* @return a session handle bound to the supplied id
|
||||
*/
|
||||
public static MxGatewaySession forSessionId(MxGatewayClient client, String sessionId) {
|
||||
return new MxGatewaySession(
|
||||
client, OpenSessionReply.newBuilder().setSessionId(sessionId).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the gateway-assigned session id.
|
||||
*
|
||||
* @return the session id
|
||||
*/
|
||||
public String sessionId() {
|
||||
return openReply.getSessionId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original {@link OpenSessionReply} that this session was opened with.
|
||||
*
|
||||
* @return the open-session reply
|
||||
*/
|
||||
public OpenSessionReply openReply() {
|
||||
return openReply;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a {@code CloseSession} RPC and caches the reply so subsequent calls
|
||||
* are idempotent.
|
||||
*
|
||||
* @return the raw close-session reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public synchronized CloseSessionReply closeRaw() {
|
||||
if (closeReply == null) {
|
||||
closeReply = client.closeSessionRaw(CloseSessionRequest.newBuilder()
|
||||
@@ -100,54 +65,19 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
return closeReply;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the session as part of try-with-resources.
|
||||
*
|
||||
* <p>This performs a {@code CloseSession} network RPC. Unlike
|
||||
* {@link #closeRaw()}, any failure of that RPC is swallowed (and recorded
|
||||
* as a suppressed exception when the JVM permits) rather than thrown: a
|
||||
* close-time transport or protocol failure must not replace the exception
|
||||
* that a try-with-resources body is already propagating. Callers that need
|
||||
* to observe the close result should call {@link #closeRaw()} explicitly.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
closeRaw();
|
||||
} catch (MxGatewayException error) {
|
||||
LOGGER.log(
|
||||
System.Logger.Level.WARNING,
|
||||
() -> "ignoring close-time failure for session " + sessionId(),
|
||||
error);
|
||||
}
|
||||
closeRaw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code Register} and returns the server handle.
|
||||
*
|
||||
* @param clientName the MXAccess client name to register
|
||||
* @return the {@code ServerHandle} returned by MXAccess
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public int register(String clientName) {
|
||||
MxCommandReply reply = registerRaw(clientName);
|
||||
if (reply.hasRegister()) {
|
||||
return reply.getRegister().getServerHandle();
|
||||
}
|
||||
if (reply.hasReturnValue()) {
|
||||
return reply.getReturnValue().getInt32Value();
|
||||
}
|
||||
throw new MxGatewayException(
|
||||
"gateway register reply carried neither a register payload nor a return value");
|
||||
return reply.getReturnValue().getInt32Value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code Register} and returns the raw reply.
|
||||
*
|
||||
* @param clientName the MXAccess client name to register
|
||||
* @return the raw command reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public MxCommandReply registerRaw(String clientName) {
|
||||
return invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_REGISTER)
|
||||
@@ -155,12 +85,6 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code Unregister}.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} returned by {@link #register(String)}
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public void unregister(int serverHandle) {
|
||||
invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_UNREGISTER)
|
||||
@@ -168,34 +92,14 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code AddItem} and returns the new item handle.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemDefinition the MXAccess item definition (tag reference)
|
||||
* @return the {@code ItemHandle} assigned by MXAccess
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public int addItem(int serverHandle, String itemDefinition) {
|
||||
MxCommandReply reply = addItemRaw(serverHandle, itemDefinition);
|
||||
if (reply.hasAddItem()) {
|
||||
return reply.getAddItem().getItemHandle();
|
||||
}
|
||||
if (reply.hasReturnValue()) {
|
||||
return reply.getReturnValue().getInt32Value();
|
||||
}
|
||||
throw new MxGatewayException(
|
||||
"gateway addItem reply carried neither an add-item payload nor a return value");
|
||||
return reply.getReturnValue().getInt32Value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code AddItem} and returns the raw reply.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemDefinition the MXAccess item definition
|
||||
* @return the raw command reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public MxCommandReply addItemRaw(int serverHandle, String itemDefinition) {
|
||||
return invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM)
|
||||
@@ -205,36 +109,14 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code AddItem2} and returns the new item handle.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemDefinition the MXAccess item definition
|
||||
* @param itemContext the MXAccess item context (e.g. galaxy/object scope)
|
||||
* @return the {@code ItemHandle} assigned by MXAccess
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public int addItem2(int serverHandle, String itemDefinition, String itemContext) {
|
||||
MxCommandReply reply = addItem2Raw(serverHandle, itemDefinition, itemContext);
|
||||
if (reply.hasAddItem2()) {
|
||||
return reply.getAddItem2().getItemHandle();
|
||||
}
|
||||
if (reply.hasReturnValue()) {
|
||||
return reply.getReturnValue().getInt32Value();
|
||||
}
|
||||
throw new MxGatewayException(
|
||||
"gateway addItem2 reply carried neither an add-item payload nor a return value");
|
||||
return reply.getReturnValue().getInt32Value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code AddItem2} and returns the raw reply.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemDefinition the MXAccess item definition
|
||||
* @param itemContext the MXAccess item context
|
||||
* @return the raw command reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public MxCommandReply addItem2Raw(int serverHandle, String itemDefinition, String itemContext) {
|
||||
return invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM2)
|
||||
@@ -245,25 +127,10 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code RemoveItem}.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to remove
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public void removeItem(int serverHandle, int itemHandle) {
|
||||
removeItemRaw(serverHandle, itemHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code RemoveItem} and returns the raw reply.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to remove
|
||||
* @return the raw command reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public MxCommandReply removeItemRaw(int serverHandle, int itemHandle) {
|
||||
return invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_REMOVE_ITEM)
|
||||
@@ -273,25 +140,10 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code Advise} so the item starts emitting data-change events.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to advise
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public void advise(int serverHandle, int itemHandle) {
|
||||
adviseRaw(serverHandle, itemHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code Advise} and returns the raw reply.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to advise
|
||||
* @return the raw command reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public MxCommandReply adviseRaw(int serverHandle, int itemHandle) {
|
||||
return invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE)
|
||||
@@ -301,25 +153,10 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code UnAdvise} so the item stops emitting data-change events.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to un-advise
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public void unAdvise(int serverHandle, int itemHandle) {
|
||||
unAdviseRaw(serverHandle, itemHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code UnAdvise} and returns the raw reply.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to un-advise
|
||||
* @return the raw command reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public MxCommandReply unAdviseRaw(int serverHandle, int itemHandle) {
|
||||
return invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_UN_ADVISE)
|
||||
@@ -329,15 +166,6 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the bulk {@code AddItem} variant.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the items
|
||||
* @param tagAddresses the MXAccess tag addresses to add
|
||||
* @return a per-tag {@link SubscribeResult} list
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws NullPointerException if {@code tagAddresses} is {@code null}
|
||||
*/
|
||||
public List<SubscribeResult> addItemBulk(int serverHandle, List<String> tagAddresses) {
|
||||
Objects.requireNonNull(tagAddresses, "tagAddresses");
|
||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||
@@ -349,15 +177,6 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
return reply.getAddItemBulk().getResultsList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the bulk {@code Advise} variant.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the items
|
||||
* @param itemHandles the {@code ItemHandle} list to advise
|
||||
* @return a per-item {@link SubscribeResult} list
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws NullPointerException if {@code itemHandles} is {@code null}
|
||||
*/
|
||||
public List<SubscribeResult> adviseItemBulk(int serverHandle, List<Integer> itemHandles) {
|
||||
Objects.requireNonNull(itemHandles, "itemHandles");
|
||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||
@@ -369,15 +188,6 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
return reply.getAdviseItemBulk().getResultsList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the bulk {@code RemoveItem} variant.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the items
|
||||
* @param itemHandles the {@code ItemHandle} list to remove
|
||||
* @return a per-item {@link SubscribeResult} list
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws NullPointerException if {@code itemHandles} is {@code null}
|
||||
*/
|
||||
public List<SubscribeResult> removeItemBulk(int serverHandle, List<Integer> itemHandles) {
|
||||
Objects.requireNonNull(itemHandles, "itemHandles");
|
||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||
@@ -389,15 +199,6 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
return reply.getRemoveItemBulk().getResultsList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the bulk {@code UnAdvise} variant.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the items
|
||||
* @param itemHandles the {@code ItemHandle} list to un-advise
|
||||
* @return a per-item {@link SubscribeResult} list
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws NullPointerException if {@code itemHandles} is {@code null}
|
||||
*/
|
||||
public List<SubscribeResult> unAdviseItemBulk(int serverHandle, List<Integer> itemHandles) {
|
||||
Objects.requireNonNull(itemHandles, "itemHandles");
|
||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||
@@ -409,16 +210,6 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
return reply.getUnAdviseItemBulk().getResultsList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the gateway {@code SubscribeBulk} convenience that combines
|
||||
* AddItem and Advise for the supplied tag addresses.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the items
|
||||
* @param tagAddresses the MXAccess tag addresses to subscribe
|
||||
* @return a per-tag {@link SubscribeResult} list
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws NullPointerException if {@code tagAddresses} is {@code null}
|
||||
*/
|
||||
public List<SubscribeResult> subscribeBulk(int serverHandle, List<String> tagAddresses) {
|
||||
Objects.requireNonNull(tagAddresses, "tagAddresses");
|
||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||
@@ -430,16 +221,6 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
return reply.getSubscribeBulk().getResultsList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the gateway {@code UnsubscribeBulk} convenience that combines
|
||||
* UnAdvise and RemoveItem for the supplied item handles.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the items
|
||||
* @param itemHandles the {@code ItemHandle} list to unsubscribe
|
||||
* @return a per-item {@link SubscribeResult} list
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws NullPointerException if {@code itemHandles} is {@code null}
|
||||
*/
|
||||
public List<SubscribeResult> unsubscribeBulk(int serverHandle, List<Integer> itemHandles) {
|
||||
Objects.requireNonNull(itemHandles, "itemHandles");
|
||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||
@@ -451,29 +232,10 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
return reply.getUnsubscribeBulk().getResultsList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code Write}.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to write
|
||||
* @param value the value to write
|
||||
* @param userId the MXAccess user id used for security checks
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public void write(int serverHandle, int itemHandle, MxValue value, int userId) {
|
||||
writeRaw(serverHandle, itemHandle, value, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code Write} and returns the raw reply.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to write
|
||||
* @param value the value to write
|
||||
* @param userId the MXAccess user id used for security checks
|
||||
* @return the raw command reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
|
||||
return invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE)
|
||||
@@ -485,16 +247,6 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code Write2}, which carries an explicit timestamp.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to write
|
||||
* @param value the value to write
|
||||
* @param timestampValue the timestamp value to associate with the write
|
||||
* @param userId the MXAccess user id used for security checks
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public void write2(int serverHandle, int itemHandle, MxValue value, MxValue timestampValue, int userId) {
|
||||
invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE2)
|
||||
@@ -507,24 +259,10 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to gateway events for this session starting from the
|
||||
* beginning of the worker event log.
|
||||
*
|
||||
* @return an iterator-style stream of events
|
||||
*/
|
||||
public MxEventStream streamEvents() {
|
||||
return streamEventsAfter(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to gateway events for this session starting after the
|
||||
* supplied worker sequence number.
|
||||
*
|
||||
* @param afterWorkerSequence the resume cursor; events with worker sequence
|
||||
* greater than this value are delivered
|
||||
* @return an iterator-style stream of events
|
||||
*/
|
||||
public MxEventStream streamEventsAfter(long afterWorkerSequence) {
|
||||
return client.streamEvents(StreamEventsRequest.newBuilder()
|
||||
.setSessionId(sessionId())
|
||||
@@ -532,14 +270,6 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a pre-built {@link MxCommand} for this session and returns the raw
|
||||
* reply, attaching a freshly generated client correlation id.
|
||||
*
|
||||
* @param command the command to send
|
||||
* @return the raw command reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public MxCommandReply invokeCommand(MxCommand command) {
|
||||
return client.invoke(MxCommandRequest.newBuilder()
|
||||
.setSessionId(sessionId())
|
||||
|
||||
-15
@@ -2,29 +2,14 @@ package com.dohertylan.mxgateway.client;
|
||||
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
||||
|
||||
/**
|
||||
* Thrown when the gateway reports a session-related protocol failure such as
|
||||
* {@code SESSION_NOT_FOUND} or {@code SESSION_NOT_READY}.
|
||||
*/
|
||||
public final class MxGatewaySessionException extends MxGatewayException {
|
||||
private final ProtocolStatus protocolStatus;
|
||||
|
||||
/**
|
||||
* Creates a new session exception from a protocol status.
|
||||
*
|
||||
* @param operation human-readable name of the failing operation
|
||||
* @param protocolStatus protocol status returned by the gateway
|
||||
*/
|
||||
public MxGatewaySessionException(String operation, ProtocolStatus protocolStatus) {
|
||||
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
||||
this.protocolStatus = protocolStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the gateway protocol status that triggered this exception.
|
||||
*
|
||||
* @return the protocol status, or {@code null} if none was supplied
|
||||
*/
|
||||
public ProtocolStatus protocolStatus() {
|
||||
return protocolStatus;
|
||||
}
|
||||
|
||||
-15
@@ -2,29 +2,14 @@ package com.dohertylan.mxgateway.client;
|
||||
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
||||
|
||||
/**
|
||||
* Thrown when the gateway reports a worker-side protocol failure such as
|
||||
* {@code WORKER_UNAVAILABLE} or {@code PROTOCOL_VIOLATION}.
|
||||
*/
|
||||
public final class MxGatewayWorkerException extends MxGatewayException {
|
||||
private final ProtocolStatus protocolStatus;
|
||||
|
||||
/**
|
||||
* Creates a new worker exception from a protocol status.
|
||||
*
|
||||
* @param operation human-readable name of the failing operation
|
||||
* @param protocolStatus protocol status returned by the gateway
|
||||
*/
|
||||
public MxGatewayWorkerException(String operation, ProtocolStatus protocolStatus) {
|
||||
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
||||
this.protocolStatus = protocolStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the gateway protocol status that triggered this exception.
|
||||
*
|
||||
* @return the protocol status, or {@code null} if none was supplied
|
||||
*/
|
||||
public ProtocolStatus protocolStatus() {
|
||||
return protocolStatus;
|
||||
}
|
||||
|
||||
-61
@@ -4,104 +4,43 @@ import mxaccess_gateway.v1.MxaccessGateway.MxStatusCategory;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusSource;
|
||||
|
||||
/**
|
||||
* Helpers for inspecting {@link MxStatusProxy} values returned by the gateway.
|
||||
*
|
||||
* <p>An {@code MxStatusProxy} mirrors the MXAccess COM {@code MXSTATUS_PROXY}
|
||||
* struct. The success flag uses the MXAccess convention where any non-zero
|
||||
* value indicates success.
|
||||
*/
|
||||
public final class MxStatuses {
|
||||
private MxStatuses() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the supplied status proxy reports success.
|
||||
*
|
||||
* @param status the status proxy, may be {@code null}
|
||||
* @return {@code true} if {@code status} is {@code null} or its success
|
||||
* flag is non-zero, {@code false} otherwise
|
||||
*/
|
||||
public static boolean succeeded(MxStatusProxy status) {
|
||||
return status == null || status.getSuccess() != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a raw {@link MxStatusProxy} in an accessor view that exposes its
|
||||
* fields with idiomatic Java getters.
|
||||
*
|
||||
* @param status the raw status proxy
|
||||
* @return a view backed by {@code status}
|
||||
*/
|
||||
public static MxStatusView view(MxStatusProxy status) {
|
||||
return new MxStatusView(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Idiomatic-Java accessor view over a raw {@link MxStatusProxy}.
|
||||
*
|
||||
* @param raw the underlying status proxy this view delegates to
|
||||
*/
|
||||
public record MxStatusView(MxStatusProxy raw) {
|
||||
/**
|
||||
* Returns the raw success flag (non-zero indicates success).
|
||||
*
|
||||
* @return the success flag value
|
||||
*/
|
||||
public int success() {
|
||||
return raw.getSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the high-level status category.
|
||||
*
|
||||
* @return the status category enum value
|
||||
*/
|
||||
public MxStatusCategory category() {
|
||||
return raw.getCategory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which subsystem detected the status.
|
||||
*
|
||||
* @return the detection source enum value
|
||||
*/
|
||||
public MxStatusSource detectedBy() {
|
||||
return raw.getDetectedBy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the detail code accompanying the status.
|
||||
*
|
||||
* @return the raw detail code
|
||||
*/
|
||||
public int detail() {
|
||||
return raw.getDetail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw, unmapped category code from MXAccess.
|
||||
*
|
||||
* @return the raw category integer
|
||||
*/
|
||||
public int rawCategory() {
|
||||
return raw.getRawCategory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw, unmapped detection-source code from MXAccess.
|
||||
*
|
||||
* @return the raw detection-source integer
|
||||
*/
|
||||
public int rawDetectedBy() {
|
||||
return raw.getRawDetectedBy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the diagnostic text supplied by MXAccess, if any.
|
||||
*
|
||||
* @return the diagnostic message, possibly empty
|
||||
*/
|
||||
public String diagnosticText() {
|
||||
return raw.getDiagnosticText();
|
||||
}
|
||||
|
||||
-84
@@ -17,24 +17,10 @@ import mxaccess_gateway.v1.MxaccessGateway.RawArray;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.StringArray;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.TimestampArray;
|
||||
|
||||
/**
|
||||
* Factory helpers for building {@link MxValue} and {@link MxArray} protobuf
|
||||
* messages and for converting them back into native Java types.
|
||||
*
|
||||
* <p>Each {@code *Value} factory sets the matching {@code MxDataType} and
|
||||
* COM {@code variant} type string so the worker can round-trip the value
|
||||
* through MXAccess without further coercion.
|
||||
*/
|
||||
public final class MxValues {
|
||||
private MxValues() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a boolean {@link MxValue}.
|
||||
*
|
||||
* @param value the boolean payload
|
||||
* @return a populated {@code MxValue}
|
||||
*/
|
||||
public static MxValue boolValue(boolean value) {
|
||||
return MxValue.newBuilder()
|
||||
.setDataType(MxDataType.MX_DATA_TYPE_BOOLEAN)
|
||||
@@ -43,12 +29,6 @@ public final class MxValues {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a 32-bit integer {@link MxValue}.
|
||||
*
|
||||
* @param value the int32 payload
|
||||
* @return a populated {@code MxValue}
|
||||
*/
|
||||
public static MxValue int32Value(int value) {
|
||||
return MxValue.newBuilder()
|
||||
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
||||
@@ -57,12 +37,6 @@ public final class MxValues {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a 64-bit integer {@link MxValue}.
|
||||
*
|
||||
* @param value the int64 payload
|
||||
* @return a populated {@code MxValue}
|
||||
*/
|
||||
public static MxValue int64Value(long value) {
|
||||
return MxValue.newBuilder()
|
||||
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
||||
@@ -71,12 +45,6 @@ public final class MxValues {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a 32-bit floating-point {@link MxValue}.
|
||||
*
|
||||
* @param value the float payload
|
||||
* @return a populated {@code MxValue}
|
||||
*/
|
||||
public static MxValue floatValue(float value) {
|
||||
return MxValue.newBuilder()
|
||||
.setDataType(MxDataType.MX_DATA_TYPE_FLOAT)
|
||||
@@ -85,12 +53,6 @@ public final class MxValues {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a 64-bit floating-point {@link MxValue}.
|
||||
*
|
||||
* @param value the double payload
|
||||
* @return a populated {@code MxValue}
|
||||
*/
|
||||
public static MxValue doubleValue(double value) {
|
||||
return MxValue.newBuilder()
|
||||
.setDataType(MxDataType.MX_DATA_TYPE_DOUBLE)
|
||||
@@ -99,12 +61,6 @@ public final class MxValues {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a string {@link MxValue}.
|
||||
*
|
||||
* @param value the string payload
|
||||
* @return a populated {@code MxValue}
|
||||
*/
|
||||
public static MxValue stringValue(String value) {
|
||||
return MxValue.newBuilder()
|
||||
.setDataType(MxDataType.MX_DATA_TYPE_STRING)
|
||||
@@ -113,12 +69,6 @@ public final class MxValues {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a timestamp {@link MxValue} from an {@link Instant}.
|
||||
*
|
||||
* @param value the instant to encode as MXAccess time
|
||||
* @return a populated {@code MxValue}
|
||||
*/
|
||||
public static MxValue timestampValue(Instant value) {
|
||||
return MxValue.newBuilder()
|
||||
.setDataType(MxDataType.MX_DATA_TYPE_TIME)
|
||||
@@ -130,14 +80,6 @@ public final class MxValues {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an {@link MxValue} back into a native Java value.
|
||||
*
|
||||
* @param value the MXAccess value, may be {@code null} or marked as null
|
||||
* @return the boxed primitive, {@code String}, {@link Instant}, byte array,
|
||||
* {@code List} of array elements, or {@code null} when the value
|
||||
* carries no payload
|
||||
*/
|
||||
public static Object nativeValue(MxValue value) {
|
||||
if (value == null || value.getIsNull()) {
|
||||
return null;
|
||||
@@ -157,14 +99,6 @@ public final class MxValues {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an {@link MxArray} into a native Java {@link List}.
|
||||
*
|
||||
* @param array the MXAccess array, may be {@code null}
|
||||
* @return a list of boxed primitives, strings, instants, or byte arrays;
|
||||
* an empty list when the array carries no elements;
|
||||
* {@code null} when {@code array} is {@code null}
|
||||
*/
|
||||
public static Object nativeArray(MxArray array) {
|
||||
if (array == null) {
|
||||
return null;
|
||||
@@ -183,12 +117,6 @@ public final class MxValues {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an {@link MxArray} of strings.
|
||||
*
|
||||
* @param values the string elements; the resulting array carries the size as a single dimension
|
||||
* @return a populated {@code MxArray}
|
||||
*/
|
||||
public static MxArray stringArray(List<String> values) {
|
||||
return MxArray.newBuilder()
|
||||
.setElementDataType(MxDataType.MX_DATA_TYPE_STRING)
|
||||
@@ -198,12 +126,6 @@ public final class MxValues {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an {@link MxArray} of 32-bit integers.
|
||||
*
|
||||
* @param values the int32 elements; the resulting array carries the size as a single dimension
|
||||
* @return a populated {@code MxArray}
|
||||
*/
|
||||
public static MxArray int32Array(List<Integer> values) {
|
||||
return MxArray.newBuilder()
|
||||
.setElementDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
||||
@@ -213,12 +135,6 @@ public final class MxValues {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a stable name for the {@link MxValue} kind, useful for logs.
|
||||
*
|
||||
* @param value the MXAccess value, may be {@code null}
|
||||
* @return the {@code KindCase} name, or {@code "KIND_NOT_SET"} when {@code value} is {@code null}
|
||||
*/
|
||||
public static String kindName(MxValue value) {
|
||||
return value == null ? "KIND_NOT_SET" : value.getKindCase().name();
|
||||
}
|
||||
|
||||
-503
@@ -1,503 +0,0 @@
|
||||
package com.dohertylan.mxgateway.client;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
import io.grpc.inprocess.InProcessServerBuilder;
|
||||
import io.grpc.stub.ClientCallStreamObserver;
|
||||
import io.grpc.stub.ClientResponseObserver;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AlarmConditionState;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Regression tests for the Low-severity Client.Java code-review findings
|
||||
* (Client.Java-006 through Client.Java-012). Covers the alarm RPC surface,
|
||||
* async streaming/subscription cancellation, queue overflow, and TLS-config
|
||||
* construction that Client.Java-007 reports as untested.
|
||||
*/
|
||||
final class MxGatewayLowFindingsTests {
|
||||
|
||||
// --- Client.Java-007: AcknowledgeAlarm RPC coverage ---
|
||||
|
||||
@Test
|
||||
void acknowledgeAlarmReturnsReplyAndSendsAuthMetadata() throws Exception {
|
||||
AtomicReference<String> authorization = new AtomicReference<>();
|
||||
AtomicReference<AcknowledgeAlarmRequest> seen = new AtomicReference<>();
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void acknowledgeAlarm(
|
||||
AcknowledgeAlarmRequest request, StreamObserver<AcknowledgeAlarmReply> responseObserver) {
|
||||
seen.set(request);
|
||||
responseObserver.onNext(AcknowledgeAlarmReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setProtocolStatus(ok())
|
||||
.setDiagnosticMessage("acked")
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (Harness harness = Harness.start(service, "mxgw_keyid_secret", authorization)) {
|
||||
AcknowledgeAlarmReply reply = harness.client().acknowledgeAlarm(AcknowledgeAlarmRequest.newBuilder()
|
||||
.setSessionId("s-1")
|
||||
.setAlarmFullReference("Area1.Pump.PV.HiHi")
|
||||
.setComment("operator note")
|
||||
.build());
|
||||
assertEquals("acked", reply.getDiagnosticMessage());
|
||||
assertEquals("Area1.Pump.PV.HiHi", seen.get().getAlarmFullReference());
|
||||
assertEquals("Bearer mxgw_keyid_secret", authorization.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void acknowledgeAlarmThrowsTypedExceptionOnProtocolFailure() throws Exception {
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void acknowledgeAlarm(
|
||||
AcknowledgeAlarmRequest request, StreamObserver<AcknowledgeAlarmReply> responseObserver) {
|
||||
responseObserver.onNext(AcknowledgeAlarmReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setProtocolStatus(ProtocolStatus.newBuilder()
|
||||
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_SESSION_NOT_FOUND))
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (Harness harness = Harness.start(service)) {
|
||||
assertThrows(
|
||||
MxGatewayException.class,
|
||||
() -> harness.client().acknowledgeAlarm(AcknowledgeAlarmRequest.newBuilder()
|
||||
.setSessionId("missing")
|
||||
.build()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void acknowledgeAlarmAsyncCompletesWithReply() throws Exception {
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void acknowledgeAlarm(
|
||||
AcknowledgeAlarmRequest request, StreamObserver<AcknowledgeAlarmReply> responseObserver) {
|
||||
responseObserver.onNext(AcknowledgeAlarmReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setProtocolStatus(ok())
|
||||
.setDiagnosticMessage("async-acked")
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (Harness harness = Harness.start(service)) {
|
||||
CompletableFuture<AcknowledgeAlarmReply> future = harness.client()
|
||||
.acknowledgeAlarmAsync(AcknowledgeAlarmRequest.newBuilder().setSessionId("s-2").build());
|
||||
assertEquals("async-acked", future.get(5, TimeUnit.SECONDS).getDiagnosticMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void acknowledgeAlarmAsyncFailsExceptionallyWithTypedException() throws Exception {
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void acknowledgeAlarm(
|
||||
AcknowledgeAlarmRequest request, StreamObserver<AcknowledgeAlarmReply> responseObserver) {
|
||||
responseObserver.onError(Status.UNAVAILABLE.withDescription("worker down").asRuntimeException());
|
||||
}
|
||||
};
|
||||
|
||||
try (Harness harness = Harness.start(service)) {
|
||||
CompletableFuture<AcknowledgeAlarmReply> future = harness.client()
|
||||
.acknowledgeAlarmAsync(AcknowledgeAlarmRequest.newBuilder().setSessionId("s-3").build());
|
||||
ExecutionException error = assertThrows(
|
||||
ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS));
|
||||
assertTrue(error.getCause() instanceof MxGatewayException, () -> String.valueOf(error.getCause()));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Client.Java-007: QueryActiveAlarms RPC + subscription coverage ---
|
||||
|
||||
@Test
|
||||
void queryActiveAlarmsDeliversSnapshotsToObserver() throws Exception {
|
||||
ActiveAlarmSnapshot snapshot = ActiveAlarmSnapshot.newBuilder()
|
||||
.setAlarmFullReference("Area1.Tank.Level.Hi")
|
||||
.setSeverity(800)
|
||||
.setCurrentState(AlarmConditionState.ALARM_CONDITION_STATE_ACTIVE)
|
||||
.build();
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void queryActiveAlarms(
|
||||
QueryActiveAlarmsRequest request, StreamObserver<ActiveAlarmSnapshot> responseObserver) {
|
||||
responseObserver.onNext(snapshot);
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (Harness harness = Harness.start(service)) {
|
||||
List<ActiveAlarmSnapshot> received = new ArrayList<>();
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
harness.client().queryActiveAlarms(
|
||||
QueryActiveAlarmsRequest.newBuilder().setSessionId("s-4").build(),
|
||||
new StreamObserver<>() {
|
||||
@Override
|
||||
public void onNext(ActiveAlarmSnapshot value) {
|
||||
received.add(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
done.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
done.countDown();
|
||||
}
|
||||
});
|
||||
assertTrue(done.await(5, TimeUnit.SECONDS), "stream should complete");
|
||||
assertEquals(1, received.size());
|
||||
assertEquals("Area1.Tank.Level.Hi", received.get(0).getAlarmFullReference());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeAlarmsSubscriptionCancelBeforeBeforeStartCancelsStream() {
|
||||
MxGatewayActiveAlarmsSubscription subscription = new MxGatewayActiveAlarmsSubscription();
|
||||
ClientResponseObserver<QueryActiveAlarmsRequest, ActiveAlarmSnapshot> observer =
|
||||
subscription.wrap(new StreamObserver<>() {
|
||||
@Override
|
||||
public void onNext(ActiveAlarmSnapshot value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
}
|
||||
});
|
||||
RecordingActiveAlarmsRequestStream requestStream = new RecordingActiveAlarmsRequestStream();
|
||||
|
||||
subscription.cancel();
|
||||
observer.beforeStart(requestStream);
|
||||
|
||||
assertTrue(requestStream.cancelled);
|
||||
assertEquals("client cancelled active-alarms query", requestStream.cancelMessage);
|
||||
}
|
||||
|
||||
// --- Client.Java-007: async streamEvents + subscription cancellation ---
|
||||
|
||||
@Test
|
||||
void streamEventsAsyncDeliversEventsToObserver() throws Exception {
|
||||
MxEvent event = MxEvent.newBuilder().setWorkerSequence(7).build();
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void streamEvents(StreamEventsRequest request, StreamObserver<MxEvent> responseObserver) {
|
||||
responseObserver.onNext(event);
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (Harness harness = Harness.start(service)) {
|
||||
List<MxEvent> received = new ArrayList<>();
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
harness.client().streamEventsAsync(
|
||||
StreamEventsRequest.newBuilder().setSessionId("s-5").build(),
|
||||
new StreamObserver<>() {
|
||||
@Override
|
||||
public void onNext(MxEvent value) {
|
||||
received.add(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
done.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
done.countDown();
|
||||
}
|
||||
});
|
||||
assertTrue(done.await(5, TimeUnit.SECONDS), "stream should complete");
|
||||
assertEquals(1, received.size());
|
||||
assertEquals(7, received.get(0).getWorkerSequence());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void eventSubscriptionCancelBeforeBeforeStartCancelsStream() {
|
||||
MxGatewayEventSubscription subscription = new MxGatewayEventSubscription();
|
||||
ClientResponseObserver<StreamEventsRequest, MxEvent> observer =
|
||||
subscription.wrap(new StreamObserver<>() {
|
||||
@Override
|
||||
public void onNext(MxEvent value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
}
|
||||
});
|
||||
RecordingEventsRequestStream requestStream = new RecordingEventsRequestStream();
|
||||
|
||||
subscription.cancel();
|
||||
observer.beforeStart(requestStream);
|
||||
|
||||
assertTrue(requestStream.cancelled);
|
||||
assertEquals("client cancelled event stream", requestStream.cancelMessage);
|
||||
}
|
||||
|
||||
// --- Client.Java-007 / Client.Java-011: MxEventStream queue overflow ---
|
||||
|
||||
@Test
|
||||
void eventStreamQueueOverflowSurfacesExceptionFromNext() {
|
||||
MxEventStream stream = new MxEventStream(2);
|
||||
ClientResponseObserver<StreamEventsRequest, MxEvent> observer = stream.observer();
|
||||
RecordingEventsRequestStream requestStream = new RecordingEventsRequestStream();
|
||||
observer.beforeStart(requestStream);
|
||||
|
||||
// Push far more events than the capacity-2 buffer can hold without draining.
|
||||
for (int i = 0; i < 16; i++) {
|
||||
observer.onNext(MxEvent.newBuilder().setWorkerSequence(i).build());
|
||||
}
|
||||
|
||||
// Overflow must cancel the gRPC call and surface as MxGatewayException.
|
||||
assertTrue(requestStream.cancelled, "overflow should cancel the underlying call");
|
||||
MxGatewayException error = assertThrows(MxGatewayException.class, () -> {
|
||||
while (stream.hasNext()) {
|
||||
stream.next();
|
||||
}
|
||||
});
|
||||
assertTrue(error.getMessage().contains("overflow"), error::getMessage);
|
||||
}
|
||||
|
||||
// --- Client.Java-007: TLS channel construction ---
|
||||
|
||||
@Test
|
||||
void connectWithMissingCaCertificateThrowsTypedTlsException() {
|
||||
MxGatewayClientOptions options = MxGatewayClientOptions.builder()
|
||||
.endpoint("localhost:5001")
|
||||
.apiKey("mxgw_id_secret")
|
||||
.plaintext(false)
|
||||
.caCertificatePath(Path.of("does-not-exist-" + UUID.randomUUID() + ".pem"))
|
||||
.build();
|
||||
|
||||
MxGatewayException error = assertThrows(MxGatewayException.class, () -> MxGatewayClient.connect(options));
|
||||
assertTrue(error.getMessage().contains("TLS"), error::getMessage);
|
||||
|
||||
MxGatewayException galaxyError =
|
||||
assertThrows(MxGatewayException.class, () -> GalaxyRepositoryClient.connect(options));
|
||||
assertTrue(galaxyError.getMessage().contains("TLS"), galaxyError::getMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectWithSystemTrustBuildsTlsChannelWithoutError() {
|
||||
// No CA path and plaintext=false exercises the useTransportSecurity() branch.
|
||||
MxGatewayClientOptions options = MxGatewayClientOptions.builder()
|
||||
.endpoint("localhost:5001")
|
||||
.apiKey("mxgw_id_secret")
|
||||
.plaintext(false)
|
||||
.build();
|
||||
|
||||
try (MxGatewayClient client = MxGatewayClient.connect(options)) {
|
||||
assertNotNull(client);
|
||||
}
|
||||
try (GalaxyRepositoryClient galaxy = GalaxyRepositoryClient.connect(options)) {
|
||||
assertNotNull(galaxy);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Client.Java-008: async error surface is normalised ---
|
||||
|
||||
@Test
|
||||
void openSessionAsyncNormalisesNonGatewayRuntimeExceptionFromValidator() {
|
||||
// ensureGatewayProtocolCompatible already throws MxGatewayException; this verifies
|
||||
// the normalisingValidator wrapper routes a stray RuntimeException through fromGrpc.
|
||||
CompletableFuture<String> source = new CompletableFuture<>();
|
||||
CompletableFuture<String> wrapped =
|
||||
source.thenApply(MxGatewayChannels.normalisingValidator("open session", reply -> {
|
||||
throw new IllegalStateException("malformed reply");
|
||||
}));
|
||||
source.complete("payload");
|
||||
|
||||
CompletionException error = assertThrows(CompletionException.class, wrapped::join);
|
||||
assertTrue(error.getCause() instanceof MxGatewayException, () -> String.valueOf(error.getCause()));
|
||||
}
|
||||
|
||||
private static ProtocolStatus ok() {
|
||||
return ProtocolStatus.newBuilder()
|
||||
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static class TestService extends MxAccessGatewayGrpc.MxAccessGatewayImplBase {
|
||||
}
|
||||
|
||||
private record Harness(Server server, ManagedChannel channel, MxGatewayClient client) implements AutoCloseable {
|
||||
static Harness start(MxAccessGatewayGrpc.MxAccessGatewayImplBase service) throws Exception {
|
||||
return start(service, "", new AtomicReference<>());
|
||||
}
|
||||
|
||||
static Harness start(
|
||||
MxAccessGatewayGrpc.MxAccessGatewayImplBase service,
|
||||
String apiKey,
|
||||
AtomicReference<String> authorization)
|
||||
throws Exception {
|
||||
String name = "mxgw-low-" + UUID.randomUUID();
|
||||
io.grpc.ServerInterceptor interceptor = new io.grpc.ServerInterceptor() {
|
||||
@Override
|
||||
public <ReqT, RespT> io.grpc.ServerCall.Listener<ReqT> interceptCall(
|
||||
io.grpc.ServerCall<ReqT, RespT> call,
|
||||
io.grpc.Metadata headers,
|
||||
io.grpc.ServerCallHandler<ReqT, RespT> next) {
|
||||
authorization.set(headers.get(MxGatewayAuthInterceptor.AUTHORIZATION_HEADER));
|
||||
return next.startCall(call, headers);
|
||||
}
|
||||
};
|
||||
Server server = InProcessServerBuilder.forName(name)
|
||||
.directExecutor()
|
||||
.addService(io.grpc.ServerInterceptors.intercept(service, interceptor))
|
||||
.build()
|
||||
.start();
|
||||
ManagedChannel channel = InProcessChannelBuilder.forName(name).directExecutor().build();
|
||||
MxGatewayClient client = new MxGatewayClient(
|
||||
channel,
|
||||
MxGatewayClientOptions.builder()
|
||||
.endpoint("in-process")
|
||||
.apiKey(apiKey)
|
||||
.plaintext(true)
|
||||
.callTimeout(Duration.ofSeconds(5))
|
||||
.streamTimeout(Duration.ofSeconds(5))
|
||||
.build());
|
||||
return new Harness(server, channel, client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
channel.shutdownNow();
|
||||
server.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingEventsRequestStream
|
||||
extends ClientCallStreamObserver<StreamEventsRequest> {
|
||||
private boolean cancelled;
|
||||
private String cancelMessage;
|
||||
|
||||
@Override
|
||||
public void cancel(String message, Throwable cause) {
|
||||
cancelled = true;
|
||||
cancelMessage = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnReadyHandler(Runnable onReadyHandler) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void request(int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMessageCompression(boolean enable) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableAutoInboundFlowControl() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(StreamEventsRequest value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingActiveAlarmsRequestStream
|
||||
extends ClientCallStreamObserver<QueryActiveAlarmsRequest> {
|
||||
private boolean cancelled;
|
||||
private String cancelMessage;
|
||||
|
||||
@Override
|
||||
public void cancel(String message, Throwable cause) {
|
||||
cancelled = true;
|
||||
cancelMessage = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnReadyHandler(Runnable onReadyHandler) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void request(int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMessageCompression(boolean enable) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableAutoInboundFlowControl() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(QueryActiveAlarmsRequest value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
}
|
||||
}
|
||||
}
|
||||
-394
@@ -1,394 +0,0 @@
|
||||
package com.dohertylan.mxgateway.client;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
import io.grpc.inprocess.InProcessServerBuilder;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.time.Duration;
|
||||
import java.util.UUID;
|
||||
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandKind;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Regression tests for the Medium-severity Client.Java code-review findings
|
||||
* (Client.Java-001 through Client.Java-005).
|
||||
*/
|
||||
final class MxGatewayMediumFindingsTests {
|
||||
|
||||
// --- Client.Java-001: redactApiKey must not leak trailing secret chars ---
|
||||
|
||||
@Test
|
||||
void redactApiKeyDoesNotLeakAnyCharacterOfTheSecret() {
|
||||
// mxgw_<key-id>_<secret> — the secret is the segment after the second underscore.
|
||||
String apiKey = "mxgw_keyid01_supersecretvalue";
|
||||
String redacted = MxGatewaySecrets.redactApiKey(apiKey);
|
||||
|
||||
// None of the secret characters may appear in the redacted output.
|
||||
assertFalse(redacted.contains("value"), () -> "redacted form leaked secret tail: " + redacted);
|
||||
assertFalse(redacted.endsWith("alue"), () -> "redacted form leaked trailing secret chars: " + redacted);
|
||||
assertFalse(redacted.contains("supersecret"), () -> "redacted form leaked secret: " + redacted);
|
||||
// The non-secret key-id prefix may stay so the value is still comparable in logs.
|
||||
assertTrue(redacted.startsWith("mxgw_keyid01_"), () -> "redacted form lost key-id prefix: " + redacted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void redactApiKeyForNonGatewayShapedKeyRevealsNothing() {
|
||||
String redacted = MxGatewaySecrets.redactApiKey("plain-opaque-token-1234");
|
||||
assertFalse(redacted.contains("1234"), () -> "redacted form leaked trailing chars: " + redacted);
|
||||
assertFalse(redacted.contains("plain-opaque-token"), () -> "redacted form leaked body: " + redacted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void redactApiKeyStillHandlesNullAndShortInput() {
|
||||
assertEquals("", MxGatewaySecrets.redactApiKey(null));
|
||||
assertEquals("", MxGatewaySecrets.redactApiKey(""));
|
||||
assertEquals("<redacted>", MxGatewaySecrets.redactApiKey("short"));
|
||||
}
|
||||
|
||||
// --- Client.Java-002: terminal-state transition must be deterministic ---
|
||||
|
||||
@Test
|
||||
void eventStreamOverflowExceptionSurvivesASubsequentClose() {
|
||||
// Deterministic reproduction of Client.Java-002: an overflow enqueues the
|
||||
// overflow exception, then a later close() must NOT discard it. The first
|
||||
// terminal condition (overflow) must win and stay observable by next().
|
||||
MxEventStream stream = new MxEventStream(2);
|
||||
io.grpc.stub.ClientResponseObserver<
|
||||
mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest,
|
||||
mxaccess_gateway.v1.MxaccessGateway.MxEvent>
|
||||
observer = stream.observer();
|
||||
observer.beforeStart(new NoopRequestStream());
|
||||
|
||||
// Force a queue overflow on a capacity-2 stream.
|
||||
for (int i = 0; i < 8; i++) {
|
||||
observer.onNext(testEvent(i));
|
||||
}
|
||||
|
||||
// A close() arriving after the overflow must not erase the overflow signal.
|
||||
stream.close();
|
||||
|
||||
MxGatewayException error = assertThrows(MxGatewayException.class, () -> {
|
||||
while (stream.hasNext()) {
|
||||
stream.next();
|
||||
}
|
||||
});
|
||||
assertTrue(error.getMessage().contains("overflow"), error::getMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void eventStreamConcurrentOverflowAndCloseAlwaysTerminate() throws Exception {
|
||||
// The terminal-state transition must be serialised: whatever the interleaving
|
||||
// of overflow and close, hasNext() always reaches a terminal state.
|
||||
for (int iteration = 0; iteration < 300; iteration++) {
|
||||
MxEventStream stream = new MxEventStream(2);
|
||||
io.grpc.stub.ClientResponseObserver<
|
||||
mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest,
|
||||
mxaccess_gateway.v1.MxaccessGateway.MxEvent>
|
||||
observer = stream.observer();
|
||||
observer.beforeStart(new NoopRequestStream());
|
||||
|
||||
Thread filler = new Thread(() -> {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
observer.onNext(testEvent(i));
|
||||
}
|
||||
});
|
||||
Thread closer = new Thread(stream::close);
|
||||
filler.start();
|
||||
closer.start();
|
||||
filler.join();
|
||||
closer.join();
|
||||
|
||||
try {
|
||||
while (stream.hasNext()) {
|
||||
stream.next();
|
||||
}
|
||||
} catch (MxGatewayException expected) {
|
||||
assertTrue(expected.getMessage().contains("overflow"), expected::getMessage);
|
||||
}
|
||||
assertFalse(stream.hasNext());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NoopRequestStream
|
||||
extends io.grpc.stub.ClientCallStreamObserver<mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest> {
|
||||
@Override
|
||||
public void cancel(String message, Throwable cause) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnReadyHandler(Runnable onReadyHandler) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void request(int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMessageCompression(boolean enable) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableAutoInboundFlowControl() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Client.Java-003: gateway protocol version mismatch must be rejected ---
|
||||
|
||||
@Test
|
||||
void openSessionRejectsIncompatibleGatewayProtocolVersion() throws Exception {
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void openSession(OpenSessionRequest request, StreamObserver<OpenSessionReply> responseObserver) {
|
||||
responseObserver.onNext(OpenSessionReply.newBuilder()
|
||||
.setSessionId("session-mismatch")
|
||||
.setGatewayProtocolVersion(MxGatewayClientVersion.gatewayProtocolVersion() + 1)
|
||||
.setProtocolStatus(ok())
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (Harness harness = Harness.start(service)) {
|
||||
MxGatewayException error = assertThrows(
|
||||
MxGatewayException.class,
|
||||
() -> harness.client().openSession("junit-session"));
|
||||
assertTrue(error.getMessage().contains("protocol version"), error::getMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void openSessionAcceptsMatchingOrUnsetGatewayProtocolVersion() throws Exception {
|
||||
TestService matching = new TestService() {
|
||||
@Override
|
||||
public void openSession(OpenSessionRequest request, StreamObserver<OpenSessionReply> responseObserver) {
|
||||
responseObserver.onNext(OpenSessionReply.newBuilder()
|
||||
.setSessionId("session-ok")
|
||||
.setGatewayProtocolVersion(MxGatewayClientVersion.gatewayProtocolVersion())
|
||||
.setProtocolStatus(ok())
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
try (Harness harness = Harness.start(matching)) {
|
||||
assertEquals("session-ok", harness.client().openSession("junit-session").sessionId());
|
||||
}
|
||||
|
||||
// A gateway that leaves the field unset (0) must not be rejected — older gateways
|
||||
// simply do not populate it.
|
||||
TestService unset = new TestService();
|
||||
try (Harness harness = Harness.start(unset)) {
|
||||
assertEquals("session-java", harness.client().openSession("junit-session").sessionId());
|
||||
}
|
||||
}
|
||||
|
||||
// --- Client.Java-004: missing typed payload AND missing return_value must throw ---
|
||||
|
||||
@Test
|
||||
void registerThrowsWhenReplyHasNeitherTypedPayloadNorReturnValue() throws Exception {
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
|
||||
// Reply with neither register payload nor return_value set.
|
||||
responseObserver.onNext(MxCommandReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setKind(request.getCommand().getKind())
|
||||
.setProtocolStatus(ok())
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (Harness harness = Harness.start(service)) {
|
||||
MxGatewaySession session = MxGatewaySession.forSessionId(harness.client(), "s");
|
||||
MxGatewayException error = assertThrows(
|
||||
MxGatewayException.class, () -> session.register("c"));
|
||||
assertTrue(error.getMessage().contains("register"), error::getMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void addItemThrowsWhenReplyHasNeitherTypedPayloadNorReturnValue() throws Exception {
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
|
||||
responseObserver.onNext(MxCommandReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setKind(request.getCommand().getKind())
|
||||
.setProtocolStatus(ok())
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (Harness harness = Harness.start(service)) {
|
||||
MxGatewaySession session = MxGatewaySession.forSessionId(harness.client(), "s");
|
||||
assertThrows(MxGatewayException.class, () -> session.addItem(1, "Tag"));
|
||||
assertThrows(MxGatewayException.class, () -> session.addItem2(1, "Tag", "ctx"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void addItemStillHonoursReturnValueFallback() throws Exception {
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
|
||||
responseObserver.onNext(MxCommandReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setKind(request.getCommand().getKind())
|
||||
.setProtocolStatus(ok())
|
||||
.setReturnValue(mxaccess_gateway.v1.MxaccessGateway.MxValue.newBuilder()
|
||||
.setInt32Value(99))
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (Harness harness = Harness.start(service)) {
|
||||
MxGatewaySession session = MxGatewaySession.forSessionId(harness.client(), "s");
|
||||
assertEquals(99, session.addItem(1, "Tag"));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Client.Java-005: close() must not mask the primary try-with-resources error ---
|
||||
|
||||
@Test
|
||||
void closeSuppressesCloseTimeFailureInsteadOfMaskingBodyException() throws Exception {
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void closeSession(CloseSessionRequest request, StreamObserver<CloseSessionReply> responseObserver) {
|
||||
responseObserver.onError(io.grpc.Status.UNAVAILABLE
|
||||
.withDescription("WORKER_UNAVAILABLE")
|
||||
.asRuntimeException());
|
||||
}
|
||||
};
|
||||
|
||||
try (Harness harness = Harness.start(service)) {
|
||||
IllegalStateException bodyError = assertThrows(IllegalStateException.class, () -> {
|
||||
try (MxGatewaySession session = MxGatewaySession.forSessionId(harness.client(), "s")) {
|
||||
throw new IllegalStateException("body failure");
|
||||
}
|
||||
});
|
||||
// The body exception must propagate; the close-time RPC failure must not replace it.
|
||||
assertEquals("body failure", bodyError.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeRawStillSurfacesCloseTimeFailureForCallersWhoWantIt() throws Exception {
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void closeSession(CloseSessionRequest request, StreamObserver<CloseSessionReply> responseObserver) {
|
||||
responseObserver.onError(io.grpc.Status.UNAVAILABLE
|
||||
.withDescription("WORKER_UNAVAILABLE")
|
||||
.asRuntimeException());
|
||||
}
|
||||
};
|
||||
|
||||
try (Harness harness = Harness.start(service)) {
|
||||
MxGatewaySession session = MxGatewaySession.forSessionId(harness.client(), "s");
|
||||
assertThrows(MxGatewayException.class, session::closeRaw);
|
||||
}
|
||||
}
|
||||
|
||||
private static mxaccess_gateway.v1.MxaccessGateway.MxEvent testEvent(int sequence) {
|
||||
return mxaccess_gateway.v1.MxaccessGateway.MxEvent.newBuilder()
|
||||
.setWorkerSequence(sequence)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static ProtocolStatus ok() {
|
||||
return ProtocolStatus.newBuilder()
|
||||
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static class TestService extends MxAccessGatewayGrpc.MxAccessGatewayImplBase {
|
||||
@Override
|
||||
public void openSession(OpenSessionRequest request, StreamObserver<OpenSessionReply> responseObserver) {
|
||||
responseObserver.onNext(OpenSessionReply.newBuilder()
|
||||
.setSessionId("session-java")
|
||||
.setProtocolStatus(ok())
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeSession(CloseSessionRequest request, StreamObserver<CloseSessionReply> responseObserver) {
|
||||
responseObserver.onNext(CloseSessionReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setProtocolStatus(ok())
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
|
||||
responseObserver.onNext(MxCommandReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_UNSPECIFIED)
|
||||
.setProtocolStatus(ok())
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
private record Harness(Server server, ManagedChannel channel, MxGatewayClient client) implements AutoCloseable {
|
||||
static Harness start(MxAccessGatewayGrpc.MxAccessGatewayImplBase service) throws Exception {
|
||||
String name = "mxgw-medium-" + UUID.randomUUID();
|
||||
Server server = InProcessServerBuilder.forName(name)
|
||||
.directExecutor()
|
||||
.addService(service)
|
||||
.build()
|
||||
.start();
|
||||
ManagedChannel channel = InProcessChannelBuilder.forName(name).directExecutor().build();
|
||||
MxGatewayClient client = new MxGatewayClient(
|
||||
channel,
|
||||
MxGatewayClientOptions.builder()
|
||||
.endpoint("in-process")
|
||||
.apiKey("")
|
||||
.plaintext(true)
|
||||
.callTimeout(Duration.ofSeconds(5))
|
||||
.build());
|
||||
return new Harness(server, channel, client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
channel.shutdownNow();
|
||||
server.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
-157
@@ -139,68 +139,6 @@ public final class MxAccessGatewayGrpc {
|
||||
return getStreamEventsMethod;
|
||||
}
|
||||
|
||||
private static volatile io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest,
|
||||
mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply> getAcknowledgeAlarmMethod;
|
||||
|
||||
@io.grpc.stub.annotations.RpcMethod(
|
||||
fullMethodName = SERVICE_NAME + '/' + "AcknowledgeAlarm",
|
||||
requestType = mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest.class,
|
||||
responseType = mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply.class,
|
||||
methodType = io.grpc.MethodDescriptor.MethodType.UNARY)
|
||||
public static io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest,
|
||||
mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply> getAcknowledgeAlarmMethod() {
|
||||
io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest, mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply> getAcknowledgeAlarmMethod;
|
||||
if ((getAcknowledgeAlarmMethod = MxAccessGatewayGrpc.getAcknowledgeAlarmMethod) == null) {
|
||||
synchronized (MxAccessGatewayGrpc.class) {
|
||||
if ((getAcknowledgeAlarmMethod = MxAccessGatewayGrpc.getAcknowledgeAlarmMethod) == null) {
|
||||
MxAccessGatewayGrpc.getAcknowledgeAlarmMethod = getAcknowledgeAlarmMethod =
|
||||
io.grpc.MethodDescriptor.<mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest, mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply>newBuilder()
|
||||
.setType(io.grpc.MethodDescriptor.MethodType.UNARY)
|
||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "AcknowledgeAlarm"))
|
||||
.setSampledToLocalTracing(true)
|
||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest.getDefaultInstance()))
|
||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply.getDefaultInstance()))
|
||||
.setSchemaDescriptor(new MxAccessGatewayMethodDescriptorSupplier("AcknowledgeAlarm"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return getAcknowledgeAlarmMethod;
|
||||
}
|
||||
|
||||
private static volatile io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest,
|
||||
mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot> getQueryActiveAlarmsMethod;
|
||||
|
||||
@io.grpc.stub.annotations.RpcMethod(
|
||||
fullMethodName = SERVICE_NAME + '/' + "QueryActiveAlarms",
|
||||
requestType = mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest.class,
|
||||
responseType = mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot.class,
|
||||
methodType = io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
|
||||
public static io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest,
|
||||
mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot> getQueryActiveAlarmsMethod() {
|
||||
io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest, mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot> getQueryActiveAlarmsMethod;
|
||||
if ((getQueryActiveAlarmsMethod = MxAccessGatewayGrpc.getQueryActiveAlarmsMethod) == null) {
|
||||
synchronized (MxAccessGatewayGrpc.class) {
|
||||
if ((getQueryActiveAlarmsMethod = MxAccessGatewayGrpc.getQueryActiveAlarmsMethod) == null) {
|
||||
MxAccessGatewayGrpc.getQueryActiveAlarmsMethod = getQueryActiveAlarmsMethod =
|
||||
io.grpc.MethodDescriptor.<mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest, mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot>newBuilder()
|
||||
.setType(io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
|
||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "QueryActiveAlarms"))
|
||||
.setSampledToLocalTracing(true)
|
||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest.getDefaultInstance()))
|
||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot.getDefaultInstance()))
|
||||
.setSchemaDescriptor(new MxAccessGatewayMethodDescriptorSupplier("QueryActiveAlarms"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return getQueryActiveAlarmsMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new async stub that supports all call types for the service
|
||||
*/
|
||||
@@ -294,20 +232,6 @@ public final class MxAccessGatewayGrpc {
|
||||
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.MxEvent> responseObserver) {
|
||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getStreamEventsMethod(), responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
default void acknowledgeAlarm(mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest request,
|
||||
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply> responseObserver) {
|
||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getAcknowledgeAlarmMethod(), responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
default void queryActiveAlarms(mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest request,
|
||||
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot> responseObserver) {
|
||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getQueryActiveAlarmsMethod(), responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -374,22 +298,6 @@ public final class MxAccessGatewayGrpc {
|
||||
io.grpc.stub.ClientCalls.asyncServerStreamingCall(
|
||||
getChannel().newCall(getStreamEventsMethod(), getCallOptions()), request, responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public void acknowledgeAlarm(mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest request,
|
||||
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply> responseObserver) {
|
||||
io.grpc.stub.ClientCalls.asyncUnaryCall(
|
||||
getChannel().newCall(getAcknowledgeAlarmMethod(), getCallOptions()), request, responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public void queryActiveAlarms(mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest request,
|
||||
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot> responseObserver) {
|
||||
io.grpc.stub.ClientCalls.asyncServerStreamingCall(
|
||||
getChannel().newCall(getQueryActiveAlarmsMethod(), getCallOptions()), request, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -440,22 +348,6 @@ public final class MxAccessGatewayGrpc {
|
||||
return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall(
|
||||
getChannel(), getStreamEventsMethod(), getCallOptions(), request);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply acknowledgeAlarm(mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest request) throws io.grpc.StatusException {
|
||||
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
|
||||
getChannel(), getAcknowledgeAlarmMethod(), getCallOptions(), request);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
@io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918")
|
||||
public io.grpc.stub.BlockingClientCall<?, mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot>
|
||||
queryActiveAlarms(mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest request) {
|
||||
return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall(
|
||||
getChannel(), getQueryActiveAlarmsMethod(), getCallOptions(), request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -505,21 +397,6 @@ public final class MxAccessGatewayGrpc {
|
||||
return io.grpc.stub.ClientCalls.blockingServerStreamingCall(
|
||||
getChannel(), getStreamEventsMethod(), getCallOptions(), request);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply acknowledgeAlarm(mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest request) {
|
||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||
getChannel(), getAcknowledgeAlarmMethod(), getCallOptions(), request);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public java.util.Iterator<mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot> queryActiveAlarms(
|
||||
mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest request) {
|
||||
return io.grpc.stub.ClientCalls.blockingServerStreamingCall(
|
||||
getChannel(), getQueryActiveAlarmsMethod(), getCallOptions(), request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -564,22 +441,12 @@ public final class MxAccessGatewayGrpc {
|
||||
return io.grpc.stub.ClientCalls.futureUnaryCall(
|
||||
getChannel().newCall(getInvokeMethod(), getCallOptions()), request);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public com.google.common.util.concurrent.ListenableFuture<mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply> acknowledgeAlarm(
|
||||
mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest request) {
|
||||
return io.grpc.stub.ClientCalls.futureUnaryCall(
|
||||
getChannel().newCall(getAcknowledgeAlarmMethod(), getCallOptions()), request);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int METHODID_OPEN_SESSION = 0;
|
||||
private static final int METHODID_CLOSE_SESSION = 1;
|
||||
private static final int METHODID_INVOKE = 2;
|
||||
private static final int METHODID_STREAM_EVENTS = 3;
|
||||
private static final int METHODID_ACKNOWLEDGE_ALARM = 4;
|
||||
private static final int METHODID_QUERY_ACTIVE_ALARMS = 5;
|
||||
|
||||
private static final class MethodHandlers<Req, Resp> implements
|
||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
||||
@@ -614,14 +481,6 @@ public final class MxAccessGatewayGrpc {
|
||||
serviceImpl.streamEvents((mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest) request,
|
||||
(io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.MxEvent>) responseObserver);
|
||||
break;
|
||||
case METHODID_ACKNOWLEDGE_ALARM:
|
||||
serviceImpl.acknowledgeAlarm((mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest) request,
|
||||
(io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply>) responseObserver);
|
||||
break;
|
||||
case METHODID_QUERY_ACTIVE_ALARMS:
|
||||
serviceImpl.queryActiveAlarms((mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest) request,
|
||||
(io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot>) responseObserver);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
@@ -668,20 +527,6 @@ public final class MxAccessGatewayGrpc {
|
||||
mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest,
|
||||
mxaccess_gateway.v1.MxaccessGateway.MxEvent>(
|
||||
service, METHODID_STREAM_EVENTS)))
|
||||
.addMethod(
|
||||
getAcknowledgeAlarmMethod(),
|
||||
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
||||
new MethodHandlers<
|
||||
mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest,
|
||||
mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply>(
|
||||
service, METHODID_ACKNOWLEDGE_ALARM)))
|
||||
.addMethod(
|
||||
getQueryActiveAlarmsMethod(),
|
||||
io.grpc.stub.ServerCalls.asyncServerStreamingCall(
|
||||
new MethodHandlers<
|
||||
mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest,
|
||||
mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot>(
|
||||
service, METHODID_QUERY_ACTIVE_ALARMS)))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -734,8 +579,6 @@ public final class MxAccessGatewayGrpc {
|
||||
.addMethod(getCloseSessionMethod())
|
||||
.addMethod(getInvokeMethod())
|
||||
.addMethod(getStreamEventsMethod())
|
||||
.addMethod(getAcknowledgeAlarmMethod())
|
||||
.addMethod(getQueryActiveAlarmsMethod())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+62
-62
@@ -1750,7 +1750,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
* <code>.google.protobuf.Timestamp time_of_last_deploy = 2;</code>
|
||||
*/
|
||||
private com.google.protobuf.SingleFieldBuilder<
|
||||
com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder>
|
||||
com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder>
|
||||
internalGetTimeOfLastDeployFieldBuilder() {
|
||||
if (timeOfLastDeployBuilder_ == null) {
|
||||
timeOfLastDeployBuilder_ = new com.google.protobuf.SingleFieldBuilder<
|
||||
@@ -2175,7 +2175,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
if (ref instanceof java.lang.String) {
|
||||
return (java.lang.String) ref;
|
||||
} else {
|
||||
com.google.protobuf.ByteString bs =
|
||||
com.google.protobuf.ByteString bs =
|
||||
(com.google.protobuf.ByteString) ref;
|
||||
java.lang.String s = bs.toStringUtf8();
|
||||
pageToken_ = s;
|
||||
@@ -2195,7 +2195,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getPageTokenBytes() {
|
||||
java.lang.Object ref = pageToken_;
|
||||
if (ref instanceof java.lang.String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
pageToken_ = b;
|
||||
@@ -2246,7 +2246,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
if (ref instanceof java.lang.String) {
|
||||
return (java.lang.String) ref;
|
||||
} else {
|
||||
com.google.protobuf.ByteString bs =
|
||||
com.google.protobuf.ByteString bs =
|
||||
(com.google.protobuf.ByteString) ref;
|
||||
java.lang.String s = bs.toStringUtf8();
|
||||
if (rootCase_ == 4) {
|
||||
@@ -2266,7 +2266,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
ref = root_;
|
||||
}
|
||||
if (ref instanceof java.lang.String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
if (rootCase_ == 4) {
|
||||
@@ -2298,7 +2298,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
if (ref instanceof java.lang.String) {
|
||||
return (java.lang.String) ref;
|
||||
} else {
|
||||
com.google.protobuf.ByteString bs =
|
||||
com.google.protobuf.ByteString bs =
|
||||
(com.google.protobuf.ByteString) ref;
|
||||
java.lang.String s = bs.toStringUtf8();
|
||||
if (rootCase_ == 5) {
|
||||
@@ -2318,7 +2318,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
ref = root_;
|
||||
}
|
||||
if (ref instanceof java.lang.String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
if (rootCase_ == 5) {
|
||||
@@ -2483,7 +2483,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
if (ref instanceof java.lang.String) {
|
||||
return (java.lang.String) ref;
|
||||
} else {
|
||||
com.google.protobuf.ByteString bs =
|
||||
com.google.protobuf.ByteString bs =
|
||||
(com.google.protobuf.ByteString) ref;
|
||||
java.lang.String s = bs.toStringUtf8();
|
||||
tagNameGlob_ = s;
|
||||
@@ -2503,7 +2503,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getTagNameGlobBytes() {
|
||||
java.lang.Object ref = tagNameGlob_;
|
||||
if (ref instanceof java.lang.String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
tagNameGlob_ = b;
|
||||
@@ -3328,7 +3328,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getPageTokenBytes() {
|
||||
java.lang.Object ref = pageToken_;
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
pageToken_ = b;
|
||||
@@ -3471,7 +3471,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
ref = root_;
|
||||
}
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
if (rootCase_ == 4) {
|
||||
@@ -3564,7 +3564,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
ref = root_;
|
||||
}
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
if (rootCase_ == 5) {
|
||||
@@ -3768,7 +3768,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
* <code>.google.protobuf.Int32Value max_depth = 6;</code>
|
||||
*/
|
||||
private com.google.protobuf.SingleFieldBuilder<
|
||||
com.google.protobuf.Int32Value, com.google.protobuf.Int32Value.Builder, com.google.protobuf.Int32ValueOrBuilder>
|
||||
com.google.protobuf.Int32Value, com.google.protobuf.Int32Value.Builder, com.google.protobuf.Int32ValueOrBuilder>
|
||||
internalGetMaxDepthFieldBuilder() {
|
||||
if (maxDepthBuilder_ == null) {
|
||||
maxDepthBuilder_ = new com.google.protobuf.SingleFieldBuilder<
|
||||
@@ -4073,7 +4073,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getTagNameGlobBytes() {
|
||||
java.lang.Object ref = tagNameGlob_;
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
tagNameGlob_ = b;
|
||||
@@ -4334,7 +4334,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
/**
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyObject objects = 1;</code>
|
||||
*/
|
||||
java.util.List<galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject>
|
||||
java.util.List<galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject>
|
||||
getObjectsList();
|
||||
/**
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyObject objects = 1;</code>
|
||||
@@ -4347,7 +4347,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
/**
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyObject objects = 1;</code>
|
||||
*/
|
||||
java.util.List<? extends galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder>
|
||||
java.util.List<? extends galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder>
|
||||
getObjectsOrBuilderList();
|
||||
/**
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyObject objects = 1;</code>
|
||||
@@ -4438,7 +4438,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyObject objects = 1;</code>
|
||||
*/
|
||||
@java.lang.Override
|
||||
public java.util.List<? extends galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder>
|
||||
public java.util.List<? extends galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder>
|
||||
getObjectsOrBuilderList() {
|
||||
return objects_;
|
||||
}
|
||||
@@ -4482,7 +4482,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
if (ref instanceof java.lang.String) {
|
||||
return (java.lang.String) ref;
|
||||
} else {
|
||||
com.google.protobuf.ByteString bs =
|
||||
com.google.protobuf.ByteString bs =
|
||||
(com.google.protobuf.ByteString) ref;
|
||||
java.lang.String s = bs.toStringUtf8();
|
||||
nextPageToken_ = s;
|
||||
@@ -4502,7 +4502,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getNextPageTokenBytes() {
|
||||
java.lang.Object ref = nextPageToken_;
|
||||
if (ref instanceof java.lang.String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
nextPageToken_ = b;
|
||||
@@ -4834,7 +4834,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
objectsBuilder_ = null;
|
||||
objects_ = other.objects_;
|
||||
bitField0_ = (bitField0_ & ~0x00000001);
|
||||
objectsBuilder_ =
|
||||
objectsBuilder_ =
|
||||
com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
|
||||
internalGetObjectsFieldBuilder() : null;
|
||||
} else {
|
||||
@@ -5111,7 +5111,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
/**
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyObject objects = 1;</code>
|
||||
*/
|
||||
public java.util.List<? extends galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder>
|
||||
public java.util.List<? extends galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder>
|
||||
getObjectsOrBuilderList() {
|
||||
if (objectsBuilder_ != null) {
|
||||
return objectsBuilder_.getMessageOrBuilderList();
|
||||
@@ -5137,12 +5137,12 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
/**
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyObject objects = 1;</code>
|
||||
*/
|
||||
public java.util.List<galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder>
|
||||
public java.util.List<galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder>
|
||||
getObjectsBuilderList() {
|
||||
return internalGetObjectsFieldBuilder().getBuilderList();
|
||||
}
|
||||
private com.google.protobuf.RepeatedFieldBuilder<
|
||||
galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder>
|
||||
galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder>
|
||||
internalGetObjectsFieldBuilder() {
|
||||
if (objectsBuilder_ == null) {
|
||||
objectsBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
|
||||
@@ -5189,7 +5189,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getNextPageTokenBytes() {
|
||||
java.lang.Object ref = nextPageToken_;
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
nextPageToken_ = b;
|
||||
@@ -5924,7 +5924,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
* <code>.google.protobuf.Timestamp last_seen_deploy_time = 1;</code>
|
||||
*/
|
||||
private com.google.protobuf.SingleFieldBuilder<
|
||||
com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder>
|
||||
com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder>
|
||||
internalGetLastSeenDeployTimeFieldBuilder() {
|
||||
if (lastSeenDeployTimeBuilder_ == null) {
|
||||
lastSeenDeployTimeBuilder_ = new com.google.protobuf.SingleFieldBuilder<
|
||||
@@ -6871,7 +6871,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
* <code>.google.protobuf.Timestamp observed_at = 2;</code>
|
||||
*/
|
||||
private com.google.protobuf.SingleFieldBuilder<
|
||||
com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder>
|
||||
com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder>
|
||||
internalGetObservedAtFieldBuilder() {
|
||||
if (observedAtBuilder_ == null) {
|
||||
observedAtBuilder_ = new com.google.protobuf.SingleFieldBuilder<
|
||||
@@ -7028,7 +7028,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
* <code>.google.protobuf.Timestamp time_of_last_deploy = 3;</code>
|
||||
*/
|
||||
private com.google.protobuf.SingleFieldBuilder<
|
||||
com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder>
|
||||
com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder>
|
||||
internalGetTimeOfLastDeployFieldBuilder() {
|
||||
if (timeOfLastDeployBuilder_ == null) {
|
||||
timeOfLastDeployBuilder_ = new com.google.protobuf.SingleFieldBuilder<
|
||||
@@ -7286,7 +7286,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
/**
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10;</code>
|
||||
*/
|
||||
java.util.List<galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute>
|
||||
java.util.List<galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute>
|
||||
getAttributesList();
|
||||
/**
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10;</code>
|
||||
@@ -7299,7 +7299,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
/**
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10;</code>
|
||||
*/
|
||||
java.util.List<? extends galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder>
|
||||
java.util.List<? extends galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder>
|
||||
getAttributesOrBuilderList();
|
||||
/**
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10;</code>
|
||||
@@ -7374,7 +7374,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
if (ref instanceof java.lang.String) {
|
||||
return (java.lang.String) ref;
|
||||
} else {
|
||||
com.google.protobuf.ByteString bs =
|
||||
com.google.protobuf.ByteString bs =
|
||||
(com.google.protobuf.ByteString) ref;
|
||||
java.lang.String s = bs.toStringUtf8();
|
||||
tagName_ = s;
|
||||
@@ -7390,7 +7390,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getTagNameBytes() {
|
||||
java.lang.Object ref = tagName_;
|
||||
if (ref instanceof java.lang.String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
tagName_ = b;
|
||||
@@ -7413,7 +7413,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
if (ref instanceof java.lang.String) {
|
||||
return (java.lang.String) ref;
|
||||
} else {
|
||||
com.google.protobuf.ByteString bs =
|
||||
com.google.protobuf.ByteString bs =
|
||||
(com.google.protobuf.ByteString) ref;
|
||||
java.lang.String s = bs.toStringUtf8();
|
||||
containedName_ = s;
|
||||
@@ -7429,7 +7429,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getContainedNameBytes() {
|
||||
java.lang.Object ref = containedName_;
|
||||
if (ref instanceof java.lang.String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
containedName_ = b;
|
||||
@@ -7452,7 +7452,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
if (ref instanceof java.lang.String) {
|
||||
return (java.lang.String) ref;
|
||||
} else {
|
||||
com.google.protobuf.ByteString bs =
|
||||
com.google.protobuf.ByteString bs =
|
||||
(com.google.protobuf.ByteString) ref;
|
||||
java.lang.String s = bs.toStringUtf8();
|
||||
browseName_ = s;
|
||||
@@ -7468,7 +7468,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getBrowseNameBytes() {
|
||||
java.lang.Object ref = browseName_;
|
||||
if (ref instanceof java.lang.String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
browseName_ = b;
|
||||
@@ -7573,7 +7573,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10;</code>
|
||||
*/
|
||||
@java.lang.Override
|
||||
public java.util.List<? extends galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder>
|
||||
public java.util.List<? extends galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder>
|
||||
getAttributesOrBuilderList() {
|
||||
return attributes_;
|
||||
}
|
||||
@@ -8059,7 +8059,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
attributesBuilder_ = null;
|
||||
attributes_ = other.attributes_;
|
||||
bitField0_ = (bitField0_ & ~0x00000200);
|
||||
attributesBuilder_ =
|
||||
attributesBuilder_ =
|
||||
com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
|
||||
internalGetAttributesFieldBuilder() : null;
|
||||
} else {
|
||||
@@ -8226,7 +8226,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getTagNameBytes() {
|
||||
java.lang.Object ref = tagName_;
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
tagName_ = b;
|
||||
@@ -8298,7 +8298,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getContainedNameBytes() {
|
||||
java.lang.Object ref = containedName_;
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
containedName_ = b;
|
||||
@@ -8370,7 +8370,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getBrowseNameBytes() {
|
||||
java.lang.Object ref = browseName_;
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
browseName_ = b;
|
||||
@@ -8851,7 +8851,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
/**
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10;</code>
|
||||
*/
|
||||
public java.util.List<? extends galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder>
|
||||
public java.util.List<? extends galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder>
|
||||
getAttributesOrBuilderList() {
|
||||
if (attributesBuilder_ != null) {
|
||||
return attributesBuilder_.getMessageOrBuilderList();
|
||||
@@ -8877,12 +8877,12 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
/**
|
||||
* <code>repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10;</code>
|
||||
*/
|
||||
public java.util.List<galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder>
|
||||
public java.util.List<galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder>
|
||||
getAttributesBuilderList() {
|
||||
return internalGetAttributesFieldBuilder().getBuilderList();
|
||||
}
|
||||
private com.google.protobuf.RepeatedFieldBuilder<
|
||||
galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder>
|
||||
galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder>
|
||||
internalGetAttributesFieldBuilder() {
|
||||
if (attributesBuilder_ == null) {
|
||||
attributesBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
|
||||
@@ -9088,7 +9088,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
if (ref instanceof java.lang.String) {
|
||||
return (java.lang.String) ref;
|
||||
} else {
|
||||
com.google.protobuf.ByteString bs =
|
||||
com.google.protobuf.ByteString bs =
|
||||
(com.google.protobuf.ByteString) ref;
|
||||
java.lang.String s = bs.toStringUtf8();
|
||||
attributeName_ = s;
|
||||
@@ -9104,7 +9104,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getAttributeNameBytes() {
|
||||
java.lang.Object ref = attributeName_;
|
||||
if (ref instanceof java.lang.String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
attributeName_ = b;
|
||||
@@ -9127,7 +9127,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
if (ref instanceof java.lang.String) {
|
||||
return (java.lang.String) ref;
|
||||
} else {
|
||||
com.google.protobuf.ByteString bs =
|
||||
com.google.protobuf.ByteString bs =
|
||||
(com.google.protobuf.ByteString) ref;
|
||||
java.lang.String s = bs.toStringUtf8();
|
||||
fullTagReference_ = s;
|
||||
@@ -9143,7 +9143,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getFullTagReferenceBytes() {
|
||||
java.lang.Object ref = fullTagReference_;
|
||||
if (ref instanceof java.lang.String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
fullTagReference_ = b;
|
||||
@@ -9177,7 +9177,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
if (ref instanceof java.lang.String) {
|
||||
return (java.lang.String) ref;
|
||||
} else {
|
||||
com.google.protobuf.ByteString bs =
|
||||
com.google.protobuf.ByteString bs =
|
||||
(com.google.protobuf.ByteString) ref;
|
||||
java.lang.String s = bs.toStringUtf8();
|
||||
dataTypeName_ = s;
|
||||
@@ -9193,7 +9193,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getDataTypeNameBytes() {
|
||||
java.lang.Object ref = dataTypeName_;
|
||||
if (ref instanceof java.lang.String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
dataTypeName_ = b;
|
||||
@@ -9835,7 +9835,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getAttributeNameBytes() {
|
||||
java.lang.Object ref = attributeName_;
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
attributeName_ = b;
|
||||
@@ -9907,7 +9907,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getFullTagReferenceBytes() {
|
||||
java.lang.Object ref = fullTagReference_;
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
fullTagReference_ = b;
|
||||
@@ -10011,7 +10011,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
getDataTypeNameBytes() {
|
||||
java.lang.Object ref = dataTypeName_;
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString b =
|
||||
com.google.protobuf.ByteString.copyFromUtf8(
|
||||
(java.lang.String) ref);
|
||||
dataTypeName_ = b;
|
||||
@@ -10335,52 +10335,52 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera
|
||||
|
||||
private static final com.google.protobuf.Descriptors.Descriptor
|
||||
internal_static_galaxy_repository_v1_TestConnectionRequest_descriptor;
|
||||
private static final
|
||||
private static final
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internal_static_galaxy_repository_v1_TestConnectionRequest_fieldAccessorTable;
|
||||
private static final com.google.protobuf.Descriptors.Descriptor
|
||||
internal_static_galaxy_repository_v1_TestConnectionReply_descriptor;
|
||||
private static final
|
||||
private static final
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internal_static_galaxy_repository_v1_TestConnectionReply_fieldAccessorTable;
|
||||
private static final com.google.protobuf.Descriptors.Descriptor
|
||||
internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_descriptor;
|
||||
private static final
|
||||
private static final
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_fieldAccessorTable;
|
||||
private static final com.google.protobuf.Descriptors.Descriptor
|
||||
internal_static_galaxy_repository_v1_GetLastDeployTimeReply_descriptor;
|
||||
private static final
|
||||
private static final
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internal_static_galaxy_repository_v1_GetLastDeployTimeReply_fieldAccessorTable;
|
||||
private static final com.google.protobuf.Descriptors.Descriptor
|
||||
internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_descriptor;
|
||||
private static final
|
||||
private static final
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_fieldAccessorTable;
|
||||
private static final com.google.protobuf.Descriptors.Descriptor
|
||||
internal_static_galaxy_repository_v1_DiscoverHierarchyReply_descriptor;
|
||||
private static final
|
||||
private static final
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internal_static_galaxy_repository_v1_DiscoverHierarchyReply_fieldAccessorTable;
|
||||
private static final com.google.protobuf.Descriptors.Descriptor
|
||||
internal_static_galaxy_repository_v1_WatchDeployEventsRequest_descriptor;
|
||||
private static final
|
||||
private static final
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internal_static_galaxy_repository_v1_WatchDeployEventsRequest_fieldAccessorTable;
|
||||
private static final com.google.protobuf.Descriptors.Descriptor
|
||||
internal_static_galaxy_repository_v1_DeployEvent_descriptor;
|
||||
private static final
|
||||
private static final
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internal_static_galaxy_repository_v1_DeployEvent_fieldAccessorTable;
|
||||
private static final com.google.protobuf.Descriptors.Descriptor
|
||||
internal_static_galaxy_repository_v1_GalaxyObject_descriptor;
|
||||
private static final
|
||||
private static final
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internal_static_galaxy_repository_v1_GalaxyObject_fieldAccessorTable;
|
||||
private static final com.google.protobuf.Descriptors.Descriptor
|
||||
internal_static_galaxy_repository_v1_GalaxyAttribute_descriptor;
|
||||
private static final
|
||||
private static final
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internal_static_galaxy_repository_v1_GalaxyAttribute_fieldAccessorTable;
|
||||
|
||||
|
||||
+334
-17753
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"fixtureSet": "mxaccess-gateway-client-behavior",
|
||||
"contractName": "mxaccess-gateway",
|
||||
"gatewayProtocolVersion": 3,
|
||||
"gatewayProtocolVersion": 2,
|
||||
"workerProtocolVersion": 1,
|
||||
"protoInputManifest": "clients/proto/proto-inputs.json",
|
||||
"fixtures": [
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
"backendName": "mxaccess-worker",
|
||||
"workerProcessId": 1234,
|
||||
"workerProtocolVersion": 1,
|
||||
"gatewayProtocolVersion": 3,
|
||||
"gatewayProtocolVersion": 2,
|
||||
"capabilities": [
|
||||
"unary-open-session",
|
||||
"unary-close-session",
|
||||
"unary-invoke",
|
||||
"server-stream-events",
|
||||
"unary-acknowledge-alarm",
|
||||
"server-stream-active-alarms"
|
||||
"server-stream-events"
|
||||
],
|
||||
"defaultCommandTimeout": "30s",
|
||||
"protocolStatus": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"fixtureSet": "mxaccess-gateway-parity-fixture-matrix",
|
||||
"contractName": "mxaccess-gateway",
|
||||
"gatewayProtocolVersion": 3,
|
||||
"gatewayProtocolVersion": 2,
|
||||
"workerProtocolVersion": 1,
|
||||
"sourceCaptureRoot": "C:/Users/dohertj2/Desktop/mxaccess/captures",
|
||||
"sourceDocs": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"contractName": "mxaccess-gateway",
|
||||
"gatewayProtocolVersion": 3,
|
||||
"gatewayProtocolVersion": 2,
|
||||
"workerProtocolVersion": 1,
|
||||
"protoRoot": "src/MxGateway.Contracts/Protos",
|
||||
"sourceFiles": [
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
The Python client package contains generated MXAccess Gateway protobuf
|
||||
bindings, the async `mxgateway` package, and the `mxgw-py` test CLI. The
|
||||
package uses the shared proto inputs documented in
|
||||
`../../docs/ClientProtoGeneration.md` so gateway and client contracts stay in
|
||||
`../../docs/client-proto-generation.md` so gateway and client contracts stay in
|
||||
sync.
|
||||
|
||||
## Layout
|
||||
@@ -31,7 +31,7 @@ changes:
|
||||
```
|
||||
|
||||
The script uses the Python tool path recorded in
|
||||
`../../docs/ToolchainLinks.md`.
|
||||
`../../docs/toolchain-links.md`.
|
||||
|
||||
## Build And Test
|
||||
|
||||
@@ -95,22 +95,6 @@ async with await GatewayClient.connect(
|
||||
events available for parity tests. `Session` helpers call the method-specific
|
||||
MXAccess commands and preserve raw replies on typed command exceptions.
|
||||
|
||||
`*_raw` methods (`GatewayClient.invoke_raw`, `Session.invoke_raw`) surface
|
||||
gateway protocol failures by raising the typed `MxGateway*` exceptions, but
|
||||
they deliberately do **not** run MXAccess-failure detection: an MXAccess
|
||||
HRESULT or `MxStatusProxy` status failure is left embedded in the returned
|
||||
reply and no `MxAccessError` is raised. `Session.invoke` adds that check on
|
||||
top. Parity-test callers using `invoke_raw` must inspect the reply's
|
||||
`protocol_status`, `hresult`, and `statuses` themselves. The non-raw `Session`
|
||||
helpers (`register`, `add_item`, `write`, the bulk methods, etc.) run the
|
||||
check and raise `MxAccessError`.
|
||||
|
||||
Value conversion (`to_mx_value`, used by `Session.write`/`write2` and the
|
||||
bulk helpers) rejects non-finite floats — `nan`, `inf`, and `-inf` raise
|
||||
`ValueError` rather than being forwarded to MXAccess, which has no defined
|
||||
wire representation for them. Python `bytes` values are an opaque
|
||||
`VT_RECORD` pass-through that MXAccess does not interpret.
|
||||
|
||||
Canceling a Python task cancels the client-side gRPC call or stream wait. It
|
||||
does not abort an in-flight MXAccess COM call inside the worker process.
|
||||
|
||||
@@ -147,25 +131,6 @@ The methods return native Python types (`bool`, `datetime | None`, and a
|
||||
into the hierarchy without learning the underlying stub class. The
|
||||
service requires the `metadata:read` scope on the API key.
|
||||
|
||||
`discover_hierarchy` buffers every object (with its full attribute list)
|
||||
into a single in-memory `list`. For a large Galaxy use `iter_hierarchy`
|
||||
instead — it is an async generator that fetches one page at a time and
|
||||
yields objects as they arrive, so peak memory stays bounded by a single
|
||||
page rather than the whole hierarchy:
|
||||
|
||||
```python
|
||||
async with await GalaxyRepositoryClient.connect(
|
||||
endpoint="localhost:5000",
|
||||
api_key="<gateway-api-key>",
|
||||
plaintext=True,
|
||||
) as galaxy:
|
||||
async for obj in galaxy.iter_hierarchy():
|
||||
print(obj.tag_name, obj.contained_name)
|
||||
```
|
||||
|
||||
Pages are fetched lazily: the next page is only requested once the
|
||||
caller has consumed every object from the current page.
|
||||
|
||||
### Watching deploy events
|
||||
|
||||
`GalaxyRepositoryClient.watch_deploy_events` opens a server-streaming
|
||||
@@ -254,5 +219,5 @@ mxgw-py smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGAT
|
||||
## Related Documentation
|
||||
|
||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
||||
- [Python Client Detailed Design](./PythonClientDesign.md)
|
||||
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
||||
- [Python Client Detailed Design](../../docs/clients-python-design.md)
|
||||
|
||||
@@ -7,7 +7,7 @@ $outputRoot = Join-Path $PSScriptRoot 'src\mxgateway\generated'
|
||||
$python = 'C:\Users\dohertj2\AppData\Local\Programs\Python\Python312\python.exe'
|
||||
|
||||
if (-not (Test-Path $python)) {
|
||||
throw "Python was not found at $python. See docs/ToolchainLinks.md."
|
||||
throw "Python was not found at $python. See docs/toolchain-links.md."
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $outputRoot -Force | Out-Null
|
||||
|
||||
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
name = "mxaccess-gateway-client"
|
||||
version = "0.1.0"
|
||||
description = "Async Python client for MXAccess Gateway."
|
||||
description = "Async Python client scaffold for MXAccess Gateway."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user