Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e00ee61cf0 | |||
| 271bf7edff | |||
| 3397e99783 | |||
| f598b3a647 | |||
| 509b0118d4 | |||
| 298836d2f3 | |||
| 96bea1d478 | |||
| a4ed605f74 | |||
| 4e02927f01 | |||
| 47b1fd422c | |||
| 9b21ca3554 | |||
| 01f5e6ad91 | |||
| 82eb0ad569 | |||
| f711a55be4 | |||
| f490ae2593 | |||
| 39f9fd8946 | |||
| bb7be14d1d | |||
| 8ac6642bf8 | |||
| 4e8928cf71 | |||
| f4423dfb6d | |||
| 3ff4969224 | |||
| 12881ca791 | |||
| 6e356da092 | |||
| a739fadb5f | |||
| 6b3c117d1e | |||
| c7d5b83390 | |||
| 1ac5bcafb2 | |||
| e7c2c546b5 | |||
| a14098468b | |||
| e030661c1b | |||
| 4e933802a7 | |||
| 6c3edf4516 | |||
| 9de2c0c43d | |||
| bc61598b44 | |||
| 335c952f00 | |||
| 3256733d24 | |||
| 4f0f03fca5 | |||
| 9ca200f814 | |||
| fe19c478c0 | |||
| d0bc78cd43 | |||
| 730fdc93e0 | |||
| 55470e3e09 | |||
| b4016e738c | |||
| 10004879f6 | |||
| 168bb9a39a | |||
| a7edc8f8bf | |||
| 0765eb4de3 | |||
| 26d0e2c471 | |||
| 65d83b1400 | |||
| 7b621e3f64 | |||
| 0f88a953d7 | |||
| ddad573b75 | |||
| 8d3352f2c6 | |||
| eed1e88a37 | |||
| 4731ab535c | |||
| 51a9dadf62 |
@@ -1,481 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# 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,invoke,event,metadata,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`, `invoke`, `event`, `metadata`, `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.
|
||||||
@@ -6,8 +6,8 @@ Provide an idiomatic .NET 10 C# client library for MXAccess Gateway, plus a test
|
|||||||
CLI and unit tests. This client is for modern .NET callers and must not load
|
CLI and unit tests. This client is for modern .NET callers and must not load
|
||||||
MXAccess COM.
|
MXAccess COM.
|
||||||
|
|
||||||
Follow the [C# Style Guide](./style-guides/CSharpStyleGuide.md) for
|
Follow the [C# Style Guide](../../docs/style-guides/CSharpStyleGuide.md) for
|
||||||
handwritten code and the [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md)
|
handwritten code and the [Protobuf Style Guide](../../docs/style-guides/ProtobufStyleGuide.md)
|
||||||
for generated contract inputs.
|
for generated contract inputs.
|
||||||
|
|
||||||
## Projects
|
## Projects
|
||||||
@@ -211,3 +211,10 @@ MXGATEWAY_TEST_ITEM=<item>
|
|||||||
|
|
||||||
Integration smoke should open, register, add, advise, stream for bounded time,
|
Integration smoke should open, register, add, advise, stream for bounded time,
|
||||||
and close.
|
and close.
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Client Libraries Detailed Design](../../docs/ClientLibrariesDesign.md)
|
||||||
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
||||||
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
|
- [C# Style Guide](../../docs/style-guides/CSharpStyleGuide.md)
|
||||||
@@ -2,11 +2,14 @@ using System.Globalization;
|
|||||||
|
|
||||||
namespace MxGateway.Client.Cli;
|
namespace MxGateway.Client.Cli;
|
||||||
|
|
||||||
|
/// <summary>Parses command-line arguments into flags and named values.</summary>
|
||||||
internal sealed class CliArguments
|
internal sealed class CliArguments
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, string> _values = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, string> _values = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly HashSet<string> _flags = 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)
|
public CliArguments(IEnumerable<string> args)
|
||||||
{
|
{
|
||||||
string? pendingName = null;
|
string? pendingName = null;
|
||||||
@@ -39,11 +42,15 @@ 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)
|
public bool HasFlag(string name)
|
||||||
{
|
{
|
||||||
return _flags.Contains(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)
|
public string? GetOptional(string name)
|
||||||
{
|
{
|
||||||
return _values.TryGetValue(name, out string? value)
|
return _values.TryGetValue(name, out string? value)
|
||||||
@@ -51,6 +58,8 @@ internal sealed class CliArguments
|
|||||||
: null;
|
: 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)
|
public string GetRequired(string name)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
@@ -62,6 +71,9 @@ internal sealed class CliArguments
|
|||||||
return value;
|
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)
|
public int GetInt32(string name, int? defaultValue = null)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
@@ -78,6 +90,9 @@ internal sealed class CliArguments
|
|||||||
return int.Parse(value, CultureInfo.InvariantCulture);
|
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)
|
public uint GetUInt32(string name, uint defaultValue)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
@@ -86,6 +101,9 @@ internal sealed class CliArguments
|
|||||||
: uint.Parse(value, CultureInfo.InvariantCulture);
|
: 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)
|
public ulong GetUInt64(string name, ulong defaultValue)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
@@ -94,6 +112,9 @@ internal sealed class CliArguments
|
|||||||
: ulong.Parse(value, CultureInfo.InvariantCulture);
|
: 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)
|
public TimeSpan GetDuration(string name, TimeSpan defaultValue)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
|
|||||||
@@ -5,34 +5,82 @@ namespace MxGateway.Client.Cli;
|
|||||||
|
|
||||||
public interface IMxGatewayCliClient : IAsyncDisposable
|
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(
|
Task<OpenSessionReply> OpenSessionAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CancellationToken cancellationToken);
|
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(
|
Task<CloseSessionReply> CloseSessionAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CancellationToken cancellationToken);
|
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(
|
Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CancellationToken cancellationToken);
|
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(
|
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CancellationToken cancellationToken);
|
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(
|
Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CancellationToken cancellationToken);
|
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(
|
Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CancellationToken cancellationToken);
|
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(
|
Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CancellationToken cancellationToken);
|
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(
|
IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
private readonly MxGatewayClient _client;
|
private readonly MxGatewayClient _client;
|
||||||
private readonly Lazy<GalaxyRepositoryClient> _galaxyClient;
|
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)
|
public MxGatewayCliClientAdapter(MxGatewayClient client)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
@@ -16,6 +20,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
() => GalaxyRepositoryClient.Create(_client.Options));
|
() => GalaxyRepositoryClient.Create(_client.Options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<OpenSessionReply> OpenSessionAsync(
|
public Task<OpenSessionReply> OpenSessionAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -23,6 +28,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _client.OpenSessionRawAsync(request, cancellationToken);
|
return _client.OpenSessionRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<CloseSessionReply> CloseSessionAsync(
|
public Task<CloseSessionReply> CloseSessionAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -30,6 +36,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _client.CloseSessionRawAsync(request, cancellationToken);
|
return _client.CloseSessionRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
public Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -37,6 +44,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _client.InvokeAsync(request, cancellationToken);
|
return _client.InvokeAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -44,6 +52,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _client.StreamEventsAsync(request, cancellationToken);
|
return _client.StreamEventsAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -51,6 +60,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _galaxyClient.Value.TestConnectionRawAsync(request, cancellationToken);
|
return _galaxyClient.Value.TestConnectionRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
public Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -58,6 +68,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _galaxyClient.Value.GetLastDeployTimeRawAsync(request, cancellationToken);
|
return _galaxyClient.Value.GetLastDeployTimeRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
public Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -65,6 +76,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _galaxyClient.Value.DiscoverHierarchyRawAsync(request, cancellationToken);
|
return _galaxyClient.Value.DiscoverHierarchyRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
public IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -72,6 +84,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken);
|
return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_galaxyClient.IsValueCreated)
|
if (_galaxyClient.IsValueCreated)
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
namespace MxGateway.Client.Cli;
|
namespace MxGateway.Client.Cli;
|
||||||
|
|
||||||
|
/// <summary>Utility to redact API keys from error messages for safe output.</summary>
|
||||||
internal static class MxGatewayCliSecretRedactor
|
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)
|
public static string Redact(string value, string? apiKey)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(apiKey))
|
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(apiKey))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using MxGateway.Contracts.Proto.Galaxy;
|
|||||||
|
|
||||||
namespace MxGateway.Client.Cli;
|
namespace MxGateway.Client.Cli;
|
||||||
|
|
||||||
|
/// <summary>Command-line interface for the MXAccess Gateway client, supporting session and command operations.</summary>
|
||||||
public static class MxGatewayClientCli
|
public static class MxGatewayClientCli
|
||||||
{
|
{
|
||||||
private const uint MaxAggregateEvents = 10_000;
|
private const uint MaxAggregateEvents = 10_000;
|
||||||
@@ -15,6 +16,10 @@ public static class MxGatewayClientCli
|
|||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
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(
|
public static int Run(
|
||||||
string[] args,
|
string[] args,
|
||||||
TextWriter standardOutput,
|
TextWriter standardOutput,
|
||||||
@@ -25,6 +30,11 @@ public static class MxGatewayClientCli
|
|||||||
.GetResult();
|
.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(
|
public static Task<int> RunAsync(
|
||||||
string[] args,
|
string[] args,
|
||||||
TextWriter standardOutput,
|
TextWriter standardOutput,
|
||||||
@@ -814,9 +824,7 @@ public static class MxGatewayClientCli
|
|||||||
TextWriter output,
|
TextWriter output,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
DiscoverHierarchyReply reply = await client.GalaxyDiscoverHierarchyAsync(
|
DiscoverHierarchyReply reply = await DiscoverAllGalaxyHierarchyAsync(client, cancellationToken)
|
||||||
new DiscoverHierarchyRequest(),
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (arguments.HasFlag("json"))
|
if (arguments.HasFlag("json"))
|
||||||
@@ -834,6 +842,39 @@ public static class MxGatewayClientCli
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<DiscoverHierarchyReply> DiscoverAllGalaxyHierarchyAsync(
|
||||||
|
IMxGatewayCliClient client,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
DiscoverHierarchyReply aggregate = new();
|
||||||
|
HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
|
||||||
|
string pageToken = string.Empty;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
DiscoverHierarchyReply page = await client.GalaxyDiscoverHierarchyAsync(
|
||||||
|
new DiscoverHierarchyRequest
|
||||||
|
{
|
||||||
|
PageSize = 5000,
|
||||||
|
PageToken = pageToken,
|
||||||
|
},
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
aggregate.Objects.Add(page.Objects);
|
||||||
|
aggregate.TotalObjectCount = page.TotalObjectCount;
|
||||||
|
pageToken = page.NextPageToken;
|
||||||
|
if (!string.IsNullOrWhiteSpace(pageToken)
|
||||||
|
&& !seenPageTokens.Add(pageToken))
|
||||||
|
{
|
||||||
|
throw new MxGatewayException(
|
||||||
|
$"Galaxy DiscoverHierarchy returned a repeated page token '{pageToken}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!string.IsNullOrWhiteSpace(pageToken));
|
||||||
|
|
||||||
|
return aggregate;
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<int> GalaxyWatchAsync(
|
private static async Task<int> GalaxyWatchAsync(
|
||||||
CliArguments arguments,
|
CliArguments arguments,
|
||||||
IMxGatewayCliClient client,
|
IMxGatewayCliClient client,
|
||||||
|
|||||||
@@ -3,30 +3,73 @@ using MxGateway.Contracts.Proto.Galaxy;
|
|||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
namespace MxGateway.Client.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fake Galaxy Repository client transport for testing.
|
||||||
|
/// </summary>
|
||||||
internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions options) : IGalaxyRepositoryClientTransport
|
internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions options) : IGalaxyRepositoryClientTransport
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the gateway client options.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientOptions Options { get; } = options;
|
public MxGatewayClientOptions Options { get; } = options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the raw gRPC client; always null for the fake.
|
||||||
|
/// </summary>
|
||||||
public GalaxyRepository.GalaxyRepositoryClient? RawClient => null;
|
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; } = [];
|
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; } = [];
|
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; } = [];
|
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 };
|
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 };
|
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 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();
|
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();
|
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();
|
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(
|
public Task<TestConnectionReply> TestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -40,6 +83,11 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
return Task.FromResult(TestConnectionReply);
|
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(
|
public Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -53,6 +101,11 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
return Task.FromResult(GetLastDeployTimeReply);
|
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(
|
public Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -63,13 +116,25 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(DiscoverHierarchyReply);
|
return Task.FromResult(
|
||||||
|
DiscoverHierarchyReplies.TryDequeue(out DiscoverHierarchyReply? reply)
|
||||||
|
? reply
|
||||||
|
: DiscoverHierarchyReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of WatchDeployEvents RPC calls made by the client.
|
||||||
|
/// </summary>
|
||||||
public List<(WatchDeployEventsRequest Request, CallOptions CallOptions)> WatchDeployEventsCalls { get; } = [];
|
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; } = [];
|
public List<DeployEvent> WatchDeployEvents { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the exception to throw from WatchDeployEvents, if any.
|
||||||
|
/// </summary>
|
||||||
public Exception? WatchDeployEventsException { get; set; }
|
public Exception? WatchDeployEventsException { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -78,6 +143,11 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<CancellationToken, Task>? WatchDeployEventsBeforeYield { get; set; }
|
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(
|
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
|
|||||||
@@ -3,23 +3,65 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
namespace MxGateway.Client.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fake implementation of IMxGatewayClientTransport for testing.
|
||||||
|
/// </summary>
|
||||||
internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMxGatewayClientTransport
|
internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMxGatewayClientTransport
|
||||||
{
|
{
|
||||||
private readonly Queue<MxCommandReply> _invokeReplies = new();
|
private readonly Queue<MxCommandReply> _invokeReplies = new();
|
||||||
private readonly List<MxEvent> _events = [];
|
private readonly List<MxEvent> _events = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the gateway client options.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientOptions Options { get; } = options;
|
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;
|
public MxAccessGateway.MxAccessGatewayClient? RawClient => null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of captured OpenSessionAsync calls.
|
||||||
|
/// </summary>
|
||||||
public List<(OpenSessionRequest Request, CallOptions CallOptions)> OpenSessionCalls { get; } = [];
|
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; } = [];
|
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; } = [];
|
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; } = [];
|
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()
|
public OpenSessionReply OpenSessionReply { get; set; } = new()
|
||||||
{
|
{
|
||||||
SessionId = "session-fixture",
|
SessionId = "session-fixture",
|
||||||
@@ -29,6 +71,9 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
|||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the reply to return from CloseSessionAsync.
|
||||||
|
/// </summary>
|
||||||
public CloseSessionReply CloseSessionReply { get; set; } = new()
|
public CloseSessionReply CloseSessionReply { get; set; } = new()
|
||||||
{
|
{
|
||||||
SessionId = "session-fixture",
|
SessionId = "session-fixture",
|
||||||
@@ -36,12 +81,26 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
|||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the queue of exceptions to throw from OpenSessionAsync.
|
||||||
|
/// </summary>
|
||||||
public Queue<Exception> OpenSessionExceptions { get; } = new();
|
public Queue<Exception> OpenSessionExceptions { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the queue of exceptions to throw from CloseSessionAsync.
|
||||||
|
/// </summary>
|
||||||
public Queue<Exception> CloseSessionExceptions { get; } = new();
|
public Queue<Exception> CloseSessionExceptions { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the queue of exceptions to throw from InvokeAsync.
|
||||||
|
/// </summary>
|
||||||
public Queue<Exception> InvokeExceptions { get; } = new();
|
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(
|
public Task<OpenSessionReply> OpenSessionAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -55,6 +114,11 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
|||||||
return Task.FromResult(OpenSessionReply);
|
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 Task<CloseSessionReply> CloseSessionAsync(
|
public Task<CloseSessionReply> CloseSessionAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -68,6 +132,11 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
|||||||
return Task.FromResult(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(
|
public Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -81,6 +150,11 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
|||||||
return Task.FromResult(_invokeReplies.Dequeue());
|
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(
|
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -95,13 +169,74 @@ 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)
|
public void AddInvokeReply(MxCommandReply reply)
|
||||||
{
|
{
|
||||||
_invokeReplies.Enqueue(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)
|
public void AddEvent(MxEvent gatewayEvent)
|
||||||
{
|
{
|
||||||
_events.Add(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class GalaxyRepositoryClientTests
|
public sealed class GalaxyRepositoryClientTests
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that TestConnectionAsync attaches the API key in request metadata and returns the Ok flag.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestConnectionAsync_AttachesApiKeyMetadataAndReturnsOkFlag()
|
public async Task TestConnectionAsync_AttachesApiKeyMetadataAndReturnsOkFlag()
|
||||||
{
|
{
|
||||||
@@ -21,6 +24,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization"));
|
Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that TestConnectionAsync returns false when the server reports NotOk.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestConnectionAsync_ReturnsFalseWhenServerReportsNotOk()
|
public async Task TestConnectionAsync_ReturnsFalseWhenServerReportsNotOk()
|
||||||
{
|
{
|
||||||
@@ -33,6 +39,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.False(ok);
|
Assert.False(ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that GetLastDeployTimeAsync returns null when the server reports not present.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetLastDeployTimeAsync_ReturnsNullWhenNotPresent()
|
public async Task GetLastDeployTimeAsync_ReturnsNullWhenNotPresent()
|
||||||
{
|
{
|
||||||
@@ -46,6 +55,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Single(transport.GetLastDeployTimeCalls);
|
Assert.Single(transport.GetLastDeployTimeCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that GetLastDeployTimeAsync returns the timestamp when the server reports it present.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetLastDeployTimeAsync_ReturnsTimestampWhenPresent()
|
public async Task GetLastDeployTimeAsync_ReturnsTimestampWhenPresent()
|
||||||
{
|
{
|
||||||
@@ -64,12 +76,17 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal(expected, deployTime!.Value);
|
Assert.Equal(expected, deployTime!.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that DiscoverHierarchyAsync returns the objects from the server reply.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
|
public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
|
||||||
{
|
{
|
||||||
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
||||||
transport.DiscoverHierarchyReply = new DiscoverHierarchyReply
|
transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||||
{
|
{
|
||||||
|
NextPageToken = "page-2",
|
||||||
|
TotalObjectCount = 2,
|
||||||
Objects =
|
Objects =
|
||||||
{
|
{
|
||||||
new GalaxyObject
|
new GalaxyObject
|
||||||
@@ -91,12 +108,29 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||||
|
{
|
||||||
|
TotalObjectCount = 2,
|
||||||
|
Objects =
|
||||||
|
{
|
||||||
|
new GalaxyObject
|
||||||
|
{
|
||||||
|
GobjectId = 13,
|
||||||
|
TagName = "DelmiaReceiver_002",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
await using GalaxyRepositoryClient client = CreateClient(transport);
|
await using GalaxyRepositoryClient client = CreateClient(transport);
|
||||||
|
|
||||||
IReadOnlyList<GalaxyObject> objects = await client.DiscoverHierarchyAsync();
|
IReadOnlyList<GalaxyObject> objects = await client.DiscoverHierarchyAsync();
|
||||||
|
|
||||||
GalaxyObject obj = Assert.Single(objects);
|
Assert.Equal(2, objects.Count);
|
||||||
|
Assert.Equal(2, transport.DiscoverHierarchyCalls.Count);
|
||||||
|
Assert.Equal(5000, transport.DiscoverHierarchyCalls[0].Request.PageSize);
|
||||||
|
Assert.Equal("", transport.DiscoverHierarchyCalls[0].Request.PageToken);
|
||||||
|
Assert.Equal("page-2", transport.DiscoverHierarchyCalls[1].Request.PageToken);
|
||||||
|
GalaxyObject obj = objects[0];
|
||||||
Assert.Equal(12, obj.GobjectId);
|
Assert.Equal(12, obj.GobjectId);
|
||||||
Assert.Equal("DelmiaReceiver_001", obj.TagName);
|
Assert.Equal("DelmiaReceiver_001", obj.TagName);
|
||||||
GalaxyAttribute attribute = Assert.Single(obj.Attributes);
|
GalaxyAttribute attribute = Assert.Single(obj.Attributes);
|
||||||
@@ -104,6 +138,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal("DelmiaReceiver_001.DownloadPath", attribute.FullTagReference);
|
Assert.Equal("DelmiaReceiver_001.DownloadPath", attribute.FullTagReference);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that DiscoverHierarchyAsync propagates cancellation tokens to the transport.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DiscoverHierarchyAsync_PropagatesCancellationToTransport()
|
public async Task DiscoverHierarchyAsync_PropagatesCancellationToTransport()
|
||||||
{
|
{
|
||||||
@@ -121,6 +158,60 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.False(call.CallOptions.CancellationToken.IsCancellationRequested);
|
Assert.False(call.CallOptions.CancellationToken.IsCancellationRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that TestConnectionAsync retries on transient gRPC failures.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task DiscoverHierarchyAsync_WithRepeatedPageToken_ThrowsProtocolError()
|
||||||
|
{
|
||||||
|
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
||||||
|
transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||||
|
{
|
||||||
|
NextPageToken = "7:1",
|
||||||
|
});
|
||||||
|
transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||||
|
{
|
||||||
|
NextPageToken = "7:1",
|
||||||
|
});
|
||||||
|
await using GalaxyRepositoryClient client = CreateClient(transport);
|
||||||
|
|
||||||
|
MxGatewayException exception = await Assert.ThrowsAsync<MxGatewayException>(
|
||||||
|
async () => await client.DiscoverHierarchyAsync());
|
||||||
|
|
||||||
|
Assert.Contains("repeated page token", exception.Message, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DiscoverHierarchyAsync_WithOptions_MapsTypedFilters()
|
||||||
|
{
|
||||||
|
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
||||||
|
await using GalaxyRepositoryClient client = CreateClient(transport);
|
||||||
|
|
||||||
|
await client.DiscoverHierarchyAsync(new DiscoverHierarchyOptions
|
||||||
|
{
|
||||||
|
RootContainedPath = "Area1/Line3",
|
||||||
|
MaxDepth = 2,
|
||||||
|
CategoryIds = [10, 13],
|
||||||
|
TemplateChainContains = ["Pump"],
|
||||||
|
TagNameGlob = "Pump_*",
|
||||||
|
IncludeAttributes = false,
|
||||||
|
AlarmBearingOnly = true,
|
||||||
|
HistorizedOnly = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
DiscoverHierarchyRequest request = Assert.Single(transport.DiscoverHierarchyCalls).Request;
|
||||||
|
Assert.Equal(DiscoverHierarchyRequest.RootOneofCase.RootContainedPath, request.RootCase);
|
||||||
|
Assert.Equal("Area1/Line3", request.RootContainedPath);
|
||||||
|
Assert.Equal(2, request.MaxDepth);
|
||||||
|
Assert.Equal([10, 13], request.CategoryIds);
|
||||||
|
Assert.Equal(["Pump"], request.TemplateChainContains);
|
||||||
|
Assert.Equal("Pump_*", request.TagNameGlob);
|
||||||
|
Assert.True(request.HasIncludeAttributes);
|
||||||
|
Assert.False(request.IncludeAttributes);
|
||||||
|
Assert.True(request.AlarmBearingOnly);
|
||||||
|
Assert.True(request.HistorizedOnly);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
|
public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
|
||||||
{
|
{
|
||||||
@@ -135,6 +226,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal(2, transport.TestConnectionCalls.Count);
|
Assert.Equal(2, transport.TestConnectionCalls.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that DiscoverHierarchyAsync retries on transient gRPC failures.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DiscoverHierarchyAsync_RetriesOnTransientGrpcFailure()
|
public async Task DiscoverHierarchyAsync_RetriesOnTransientGrpcFailure()
|
||||||
{
|
{
|
||||||
@@ -148,6 +242,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal(2, transport.DiscoverHierarchyCalls.Count);
|
Assert.Equal(2, transport.DiscoverHierarchyCalls.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that WatchDeployEventsAsync delivers the bootstrap event.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WatchDeployEventsAsync_DeliversBootstrapEvent()
|
public async Task WatchDeployEventsAsync_DeliversBootstrapEvent()
|
||||||
{
|
{
|
||||||
@@ -181,6 +278,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Null(call.Request.LastSeenDeployTime);
|
Assert.Null(call.Request.LastSeenDeployTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that WatchDeployEventsAsync delivers multiple events in order.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WatchDeployEventsAsync_DeliversMultipleEventsInOrder()
|
public async Task WatchDeployEventsAsync_DeliversMultipleEventsInOrder()
|
||||||
{
|
{
|
||||||
@@ -216,6 +316,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal(t0, call.Request.LastSeenDeployTime!.ToDateTime());
|
Assert.Equal(t0, call.Request.LastSeenDeployTime!.ToDateTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that WatchDeployEventsAsync stops iteration cleanly when cancelled.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WatchDeployEventsAsync_CancellationStopsIterationCleanly()
|
public async Task WatchDeployEventsAsync_CancellationStopsIterationCleanly()
|
||||||
{
|
{
|
||||||
@@ -257,6 +360,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal(1ul, received[0].Sequence);
|
Assert.Equal(1ul, received[0].Sequence);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that WatchDeployEventsAsync throws ObjectDisposedException after the client is disposed.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WatchDeployEventsAsync_ThrowsAfterDisposal()
|
public async Task WatchDeployEventsAsync_ThrowsAfterDisposal()
|
||||||
{
|
{
|
||||||
@@ -269,6 +375,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
client.WatchDeployEventsAsync());
|
client.WatchDeployEventsAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that TestConnectionAsync throws ObjectDisposedException after the client is disposed.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestConnectionAsync_ThrowsAfterDisposal()
|
public async Task TestConnectionAsync_ThrowsAfterDisposal()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class MxCommandReplyExtensionsTests
|
public sealed class MxCommandReplyExtensionsTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that successful replies pass both protocol and MxAccess success checks.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EnsureSuccess_WithRegisterFixture_ReturnsReply()
|
public void EnsureSuccess_WithRegisterFixture_ReturnsReply()
|
||||||
{
|
{
|
||||||
@@ -15,6 +16,7 @@ public sealed class MxCommandReplyExtensionsTests
|
|||||||
Assert.Same(reply, reply.EnsureMxAccessSuccess());
|
Assert.Same(reply, reply.EnsureMxAccessSuccess());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that MxAccess failures throw with preserved HResult and status details.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EnsureMxAccessSuccess_WithFailureFixture_PreservesHResultAndStatuses()
|
public void EnsureMxAccessSuccess_WithFailureFixture_PreservesHResultAndStatuses()
|
||||||
{
|
{
|
||||||
@@ -30,6 +32,7 @@ public sealed class MxCommandReplyExtensionsTests
|
|||||||
Assert.Contains("0x80040200", exception.Message);
|
Assert.Contains("0x80040200", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that session-not-found protocol failures throw the correct gateway exception.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EnsureProtocolSuccess_WithSessionFailure_ThrowsSessionException()
|
public void EnsureProtocolSuccess_WithSessionFailure_ThrowsSessionException()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
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,8 +5,10 @@ using MxGateway.Contracts.Proto.Galaxy;
|
|||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
namespace MxGateway.Client.Tests;
|
||||||
|
|
||||||
|
/// <summary>Tests for the CLI command interface.</summary>
|
||||||
public sealed class MxGatewayClientCliTests
|
public sealed class MxGatewayClientCliTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that the version command prints compiled protocol versions.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Run_Version_PrintsCompiledProtocolVersions()
|
public void Run_Version_PrintsCompiledProtocolVersions()
|
||||||
{
|
{
|
||||||
@@ -16,11 +18,12 @@ public sealed class MxGatewayClientCliTests
|
|||||||
var exitCode = MxGatewayClientCli.Run(["version"], output, error);
|
var exitCode = MxGatewayClientCli.Run(["version"], output, error);
|
||||||
|
|
||||||
Assert.Equal(0, exitCode);
|
Assert.Equal(0, exitCode);
|
||||||
Assert.Contains("gateway-protocol=1", output.ToString());
|
Assert.Contains("gateway-protocol=3", output.ToString());
|
||||||
Assert.Contains("worker-protocol=1", output.ToString());
|
Assert.Contains("worker-protocol=1", output.ToString());
|
||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that the version command with --json flag prints JSON protocol versions.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_VersionJson_PrintsJsonProtocolVersions()
|
public async Task RunAsync_VersionJson_PrintsJsonProtocolVersions()
|
||||||
{
|
{
|
||||||
@@ -30,10 +33,11 @@ public sealed class MxGatewayClientCliTests
|
|||||||
int exitCode = await MxGatewayClientCli.RunAsync(["version", "--json"], output, error);
|
int exitCode = await MxGatewayClientCli.RunAsync(["version", "--json"], output, error);
|
||||||
|
|
||||||
Assert.Equal(0, exitCode);
|
Assert.Equal(0, exitCode);
|
||||||
Assert.Contains("\"gatewayProtocolVersion\":1", output.ToString());
|
Assert.Contains("\"gatewayProtocolVersion\":3", output.ToString());
|
||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that the write command builds a write request and prints JSON reply.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_Write_BuildsWriteCommandAndPrintsJsonReply()
|
public async Task RunAsync_Write_BuildsWriteCommandAndPrintsJsonReply()
|
||||||
{
|
{
|
||||||
@@ -78,6 +82,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that error output redacts sensitive API key values.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_ErrorOutput_RedactsApiKey()
|
public async Task RunAsync_ErrorOutput_RedactsApiKey()
|
||||||
{
|
{
|
||||||
@@ -101,6 +106,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Contains("[redacted]", error.ToString());
|
Assert.Contains("[redacted]", error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that stream-events with max-events limit stops output in non-JSON format.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_StreamEvents_WithMaxEventsStopsNonJsonOutput()
|
public async Task RunAsync_StreamEvents_WithMaxEventsStopsNonJsonOutput()
|
||||||
{
|
{
|
||||||
@@ -142,6 +148,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Verifies that smoke command closes opened session when a command fails.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_Smoke_WhenCommandFails_ClosesOpenedSession()
|
public async Task RunAsync_Smoke_WhenCommandFails_ClosesOpenedSession()
|
||||||
{
|
{
|
||||||
@@ -172,6 +179,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Equal("session-fixture", closeRequest.SessionId);
|
Assert.Equal("session-fixture", closeRequest.SessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that galaxy-test-connection command prints JSON reply.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_GalaxyTestConnection_PrintsJsonReply()
|
public async Task RunAsync_GalaxyTestConnection_PrintsJsonReply()
|
||||||
{
|
{
|
||||||
@@ -201,14 +209,17 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that galaxy-discover command prints hierarchy summary.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary()
|
public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary()
|
||||||
{
|
{
|
||||||
using var output = new StringWriter();
|
using var output = new StringWriter();
|
||||||
using var error = new StringWriter();
|
using var error = new StringWriter();
|
||||||
FakeCliClient fakeClient = new();
|
FakeCliClient fakeClient = new();
|
||||||
fakeClient.GalaxyDiscoverHierarchyReply = new DiscoverHierarchyReply
|
fakeClient.GalaxyDiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||||
{
|
{
|
||||||
|
NextPageToken = "7:1",
|
||||||
|
TotalObjectCount = 2,
|
||||||
Objects =
|
Objects =
|
||||||
{
|
{
|
||||||
new GalaxyObject
|
new GalaxyObject
|
||||||
@@ -227,7 +238,21 @@ public sealed class MxGatewayClientCliTests
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
fakeClient.GalaxyDiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||||
|
{
|
||||||
|
TotalObjectCount = 2,
|
||||||
|
Objects =
|
||||||
|
{
|
||||||
|
new GalaxyObject
|
||||||
|
{
|
||||||
|
GobjectId = 8,
|
||||||
|
TagName = "DelmiaReceiver_002",
|
||||||
|
ContainedName = "DelmiaReceiver",
|
||||||
|
ParentGobjectId = 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
int exitCode = await MxGatewayClientCli.RunAsync(
|
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||||
[
|
[
|
||||||
@@ -242,14 +267,19 @@ public sealed class MxGatewayClientCliTests
|
|||||||
_ => fakeClient);
|
_ => fakeClient);
|
||||||
|
|
||||||
Assert.Equal(0, exitCode);
|
Assert.Equal(0, exitCode);
|
||||||
Assert.Single(fakeClient.GalaxyDiscoverHierarchyRequests);
|
Assert.Equal(2, fakeClient.GalaxyDiscoverHierarchyRequests.Count);
|
||||||
|
Assert.Equal(5000, fakeClient.GalaxyDiscoverHierarchyRequests[0].PageSize);
|
||||||
|
Assert.Equal("", fakeClient.GalaxyDiscoverHierarchyRequests[0].PageToken);
|
||||||
|
Assert.Equal("7:1", fakeClient.GalaxyDiscoverHierarchyRequests[1].PageToken);
|
||||||
string text = output.ToString();
|
string text = output.ToString();
|
||||||
Assert.Contains("objects=1", text);
|
Assert.Contains("objects=2", text);
|
||||||
Assert.Contains("DelmiaReceiver_001", text);
|
Assert.Contains("DelmiaReceiver_001", text);
|
||||||
|
Assert.Contains("DelmiaReceiver_002", text);
|
||||||
Assert.Contains("attributes=1", text);
|
Assert.Contains("attributes=1", text);
|
||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that galaxy-watch command prints text output for deploy events.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents()
|
public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents()
|
||||||
{
|
{
|
||||||
@@ -303,6 +333,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that galaxy-watch with --json emits one JSON object per event.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_GalaxyWatch_JsonEmitsOneObjectPerEvent()
|
public async Task RunAsync_GalaxyWatch_JsonEmitsOneObjectPerEvent()
|
||||||
{
|
{
|
||||||
@@ -337,23 +368,31 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Contains("\"objectCount\": 99", text);
|
Assert.Contains("\"objectCount\": 99", text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Fake CLI client for testing.</summary>
|
||||||
private sealed class FakeCliClient : IMxGatewayCliClient
|
private sealed class FakeCliClient : IMxGatewayCliClient
|
||||||
{
|
{
|
||||||
|
/// <summary>Queue of invoke replies to return.</summary>
|
||||||
public Queue<MxCommandReply> InvokeReplies { get; } = new();
|
public Queue<MxCommandReply> InvokeReplies { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>List of received invoke requests.</summary>
|
||||||
public List<MxCommandRequest> InvokeRequests { get; } = [];
|
public List<MxCommandRequest> InvokeRequests { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>List of received close session requests.</summary>
|
||||||
public List<CloseSessionRequest> CloseSessionRequests { get; } = [];
|
public List<CloseSessionRequest> CloseSessionRequests { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>List of events to yield when streaming.</summary>
|
||||||
public List<MxEvent> Events { get; } = [];
|
public List<MxEvent> Events { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>Exception to throw on invoke, if any.</summary>
|
||||||
public Exception? InvokeFailure { get; init; }
|
public Exception? InvokeFailure { get; init; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public ValueTask DisposeAsync()
|
public ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<OpenSessionReply> OpenSessionAsync(
|
public Task<OpenSessionReply> OpenSessionAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -367,6 +406,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<CloseSessionReply> CloseSessionAsync(
|
public Task<CloseSessionReply> CloseSessionAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -380,6 +420,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
public Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -393,6 +434,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
return Task.FromResult(InvokeReplies.Dequeue());
|
return Task.FromResult(InvokeReplies.Dequeue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
@@ -405,18 +447,27 @@ public sealed class MxGatewayClientCliTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Galaxy test connection reply to return.</summary>
|
||||||
public TestConnectionReply GalaxyTestConnectionReply { get; set; } = new() { Ok = true };
|
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 };
|
public GetLastDeployTimeReply GalaxyGetLastDeployTimeReply { get; set; } = new() { Present = false };
|
||||||
|
|
||||||
|
/// <summary>Galaxy discover hierarchy reply to return.</summary>
|
||||||
public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new();
|
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; } = [];
|
public List<TestConnectionRequest> GalaxyTestConnectionRequests { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>List of received galaxy get last deploy time requests.</summary>
|
||||||
public List<GetLastDeployTimeRequest> GalaxyGetLastDeployTimeRequests { get; } = [];
|
public List<GetLastDeployTimeRequest> GalaxyGetLastDeployTimeRequests { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>List of received galaxy discover hierarchy requests.</summary>
|
||||||
public List<DiscoverHierarchyRequest> GalaxyDiscoverHierarchyRequests { get; } = [];
|
public List<DiscoverHierarchyRequest> GalaxyDiscoverHierarchyRequests { get; } = [];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -425,6 +476,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
return Task.FromResult(GalaxyTestConnectionReply);
|
return Task.FromResult(GalaxyTestConnectionReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
public Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -433,18 +485,25 @@ public sealed class MxGatewayClientCliTests
|
|||||||
return Task.FromResult(GalaxyGetLastDeployTimeReply);
|
return Task.FromResult(GalaxyGetLastDeployTimeReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
public Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
GalaxyDiscoverHierarchyRequests.Add(request);
|
GalaxyDiscoverHierarchyRequests.Add(request);
|
||||||
return Task.FromResult(GalaxyDiscoverHierarchyReply);
|
return Task.FromResult(
|
||||||
|
GalaxyDiscoverHierarchyReplies.TryDequeue(out DiscoverHierarchyReply? reply)
|
||||||
|
? reply
|
||||||
|
: GalaxyDiscoverHierarchyReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>List of received galaxy watch deploy events requests.</summary>
|
||||||
public List<WatchDeployEventsRequest> GalaxyWatchDeployEventsRequests { get; } = [];
|
public List<WatchDeployEventsRequest> GalaxyWatchDeployEventsRequests { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>List of deploy events to yield when watching.</summary>
|
||||||
public List<DeployEvent> GalaxyDeployEvents { get; } = [];
|
public List<DeployEvent> GalaxyDeployEvents { get; } = [];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
public async IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class MxGatewayClientContractInfoTests
|
public sealed class MxGatewayClientContractInfoTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that the client's gateway protocol version matches the shared contract definition.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GatewayProtocolVersion_MatchesSharedContract()
|
public void GatewayProtocolVersion_MatchesSharedContract()
|
||||||
{
|
{
|
||||||
@@ -12,6 +13,7 @@ public sealed class MxGatewayClientContractInfoTests
|
|||||||
MxGatewayClientContractInfo.GatewayProtocolVersion);
|
MxGatewayClientContractInfo.GatewayProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that the client's worker protocol version matches the shared contract definition.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void WorkerProtocolVersion_MatchesSharedContract()
|
public void WorkerProtocolVersion_MatchesSharedContract()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class MxGatewayClientOptionsTests
|
public sealed class MxGatewayClientOptionsTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that options with valid endpoint and API key pass validation.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_WithAbsoluteEndpointAndApiKey_Succeeds()
|
public void Validate_WithAbsoluteEndpointAndApiKey_Succeeds()
|
||||||
{
|
{
|
||||||
@@ -14,6 +15,7 @@ public sealed class MxGatewayClientOptionsTests
|
|||||||
options.Validate();
|
options.Validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that empty API key causes validation to fail.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_WithEmptyApiKey_Throws()
|
public void Validate_WithEmptyApiKey_Throws()
|
||||||
{
|
{
|
||||||
@@ -26,6 +28,7 @@ public sealed class MxGatewayClientOptionsTests
|
|||||||
Assert.Throws<ArgumentException>(options.Validate);
|
Assert.Throws<ArgumentException>(options.Validate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that invalid retry options cause validation to fail.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_WithInvalidRetryOptions_Throws()
|
public void Validate_WithInvalidRetryOptions_Throws()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ using Grpc.Core;
|
|||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
namespace MxGateway.Client.Tests;
|
||||||
|
|
||||||
|
/// <summary>Tests for MxGatewaySession and client command behavior.</summary>
|
||||||
public sealed class MxGatewayClientSessionTests
|
public sealed class MxGatewayClientSessionTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that open session attaches API key metadata and cancellation token.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenSessionRawAsync_AttachesApiKeyMetadataAndCancellation()
|
public async Task OpenSessionRawAsync_AttachesApiKeyMetadataAndCancellation()
|
||||||
{
|
{
|
||||||
@@ -19,6 +21,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal(cancellation.Token, call.CallOptions.CancellationToken);
|
Assert.Equal(cancellation.Token, call.CallOptions.CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that open session returns a session with the raw open reply.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenSessionAsync_ReturnsSessionWithRawOpenReply()
|
public async Task OpenSessionAsync_ReturnsSessionWithRawOpenReply()
|
||||||
{
|
{
|
||||||
@@ -33,6 +36,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal(1234, session.OpenSessionReply.WorkerProcessId);
|
Assert.Equal(1234, session.OpenSessionReply.WorkerProcessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that register builds a register command and returns server handle.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RegisterAsync_BuildsRegisterCommandAndReturnsServerHandle()
|
public async Task RegisterAsync_BuildsRegisterCommandAndReturnsServerHandle()
|
||||||
{
|
{
|
||||||
@@ -57,6 +61,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal("fixture-client", call.Request.Command.Register.ClientName);
|
Assert.Equal("fixture-client", call.Request.Command.Register.ClientName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that add item 2 builds a command with the specified context.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddItem2Async_BuildsAddItem2CommandWithContext()
|
public async Task AddItem2Async_BuildsAddItem2CommandWithContext()
|
||||||
{
|
{
|
||||||
@@ -81,6 +86,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal("runtime", request.Command.AddItem2.ItemContext);
|
Assert.Equal("runtime", request.Command.AddItem2.ItemContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that write raw builds a write command with the raw value.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WriteRawAsync_BuildsWriteCommandWithRawValue()
|
public async Task WriteRawAsync_BuildsWriteCommandWithRawValue()
|
||||||
{
|
{
|
||||||
@@ -111,6 +117,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal(56, request.Command.Write.UserId);
|
Assert.Equal(56, request.Command.Write.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that write 2 raw builds a write 2 command with value and timestamp.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Write2RawAsync_BuildsWrite2CommandWithValueAndTimestamp()
|
public async Task Write2RawAsync_BuildsWrite2CommandWithValueAndTimestamp()
|
||||||
{
|
{
|
||||||
@@ -138,6 +145,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal(56, request.Command.Write2.UserId);
|
Assert.Equal(56, request.Command.Write2.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that subscribe bulk builds one command and returns per-item results.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SubscribeBulkAsync_BuildsOneBulkCommandAndReturnsPerItemResults()
|
public async Task SubscribeBulkAsync_BuildsOneBulkCommandAndReturnsPerItemResults()
|
||||||
{
|
{
|
||||||
@@ -176,6 +184,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal(["Area001.Pump001.Speed"], request.Command.SubscribeBulk.TagAddresses);
|
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]
|
[Fact]
|
||||||
public async Task StreamEventsAsync_YieldsEventsInGatewayOrder()
|
public async Task StreamEventsAsync_YieldsEventsInGatewayOrder()
|
||||||
{
|
{
|
||||||
@@ -206,6 +215,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal("session-fixture", request.SessionId);
|
Assert.Equal("session-fixture", request.SessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that close is explicit and idempotent.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CloseAsync_IsExplicitAndIdempotent()
|
public async Task CloseAsync_IsExplicitAndIdempotent()
|
||||||
{
|
{
|
||||||
@@ -221,6 +231,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal("session-fixture", call.Request.SessionId);
|
Assert.Equal("session-fixture", call.Request.SessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that invoke retries safe diagnostic commands on transient RPC failure.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task InvokeAsync_RetriesSafeDiagnosticCommandOnTransientGrpcFailure()
|
public async Task InvokeAsync_RetriesSafeDiagnosticCommandOnTransientGrpcFailure()
|
||||||
{
|
{
|
||||||
@@ -244,6 +255,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal(2, transport.InvokeCalls.Count);
|
Assert.Equal(2, transport.InvokeCalls.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that open session does not retry on transient RPC failure.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenSessionAsync_DoesNotRetryTransientGrpcFailure()
|
public async Task OpenSessionAsync_DoesNotRetryTransientGrpcFailure()
|
||||||
{
|
{
|
||||||
@@ -256,6 +268,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Single(transport.OpenSessionCalls);
|
Assert.Single(transport.OpenSessionCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that invoke does not retry write commands on transient RPC failure.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task InvokeAsync_DoesNotRetryWriteCommand()
|
public async Task InvokeAsync_DoesNotRetryWriteCommand()
|
||||||
{
|
{
|
||||||
@@ -270,6 +283,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Single(transport.InvokeCalls);
|
Assert.Single(transport.InvokeCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that invoke helpers pass cancellation token to the transport.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task InvokeHelpers_PassCancellationTokenToTransport()
|
public async Task InvokeHelpers_PassCancellationTokenToTransport()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class MxGatewayGeneratedContractTests
|
public sealed class MxGatewayGeneratedContractTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that the generated gRPC client can be instantiated from the client factory.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GeneratedGrpcClient_CanBeConstructedFromClientFactory()
|
public async Task GeneratedGrpcClient_CanBeConstructedFromClientFactory()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class MxStatusProxyExtensionsTests
|
public sealed class MxStatusProxyExtensionsTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that fixture statuses correctly project success and preserve raw integer fields.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FixtureStatuses_ProjectSuccessAndPreserveRawFields()
|
public void FixtureStatuses_ProjectSuccessAndPreserveRawFields()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class MxValueExtensionsTests
|
public sealed class MxValueExtensionsTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that scalar values are converted to correctly-typed MxValue protobuf messages.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToMxValue_WithScalarValues_CreatesTypedProtobufValues()
|
public void ToMxValue_WithScalarValues_CreatesTypedProtobufValues()
|
||||||
{
|
{
|
||||||
@@ -18,6 +19,7 @@ public sealed class MxValueExtensionsTests
|
|||||||
Assert.Equal(MxValue.KindOneofCase.StringValue, "alpha".ToMxValue().KindCase);
|
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]
|
[Fact]
|
||||||
public void ToMxValue_WithArrays_CreatesTypedArrayProtobufValues()
|
public void ToMxValue_WithArrays_CreatesTypedArrayProtobufValues()
|
||||||
{
|
{
|
||||||
@@ -29,6 +31,7 @@ public sealed class MxValueExtensionsTests
|
|||||||
Assert.Equal([2U], value.ArrayValue.Dimensions);
|
Assert.Equal([2U], value.ArrayValue.Dimensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that fixture test cases project to expected MxValue kinds and preserve raw type metadata.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FixtureValues_ProjectExpectedKindsAndPreserveRawMetadata()
|
public void FixtureValues_ProjectExpectedKindsAndPreserveRawMetadata()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
public sealed record DiscoverHierarchyOptions
|
||||||
|
{
|
||||||
|
public int? RootGobjectId { get; init; }
|
||||||
|
|
||||||
|
public string? RootTagName { get; init; }
|
||||||
|
|
||||||
|
public string? RootContainedPath { get; init; }
|
||||||
|
|
||||||
|
public int? MaxDepth { get; init; }
|
||||||
|
|
||||||
|
public IReadOnlyList<int> CategoryIds { get; init; } = Array.Empty<int>();
|
||||||
|
|
||||||
|
public IReadOnlyList<string> TemplateChainContains { get; init; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
public string? TagNameGlob { get; init; }
|
||||||
|
|
||||||
|
public bool? IncludeAttributes { get; init; }
|
||||||
|
|
||||||
|
public bool AlarmBearingOnly { get; init; }
|
||||||
|
|
||||||
|
public bool HistorizedOnly { get; init; }
|
||||||
|
}
|
||||||
@@ -18,11 +18,18 @@ namespace MxGateway.Client;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||||
{
|
{
|
||||||
|
private const int DiscoverHierarchyPageSize = 5000;
|
||||||
|
|
||||||
private readonly GrpcChannel? _channel;
|
private readonly GrpcChannel? _channel;
|
||||||
private readonly IGalaxyRepositoryClientTransport _transport;
|
private readonly IGalaxyRepositoryClientTransport _transport;
|
||||||
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
|
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
|
||||||
private bool _disposed;
|
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(
|
internal GalaxyRepositoryClient(
|
||||||
MxGatewayClientOptions options,
|
MxGatewayClientOptions options,
|
||||||
IGalaxyRepositoryClientTransport transport)
|
IGalaxyRepositoryClientTransport transport)
|
||||||
@@ -50,12 +57,23 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
Options.LoggerFactory?.CreateLogger<GalaxyRepositoryClient>());
|
Options.LoggerFactory?.CreateLogger<GalaxyRepositoryClient>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client options used to configure timeouts, authentication, and retry policy.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientOptions Options { get; }
|
public MxGatewayClientOptions Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The underlying generated gRPC client for advanced operations.
|
||||||
|
/// </summary>
|
||||||
public GalaxyRepository.GalaxyRepositoryClient RawClient =>
|
public GalaxyRepository.GalaxyRepositoryClient RawClient =>
|
||||||
_transport.RawClient
|
_transport.RawClient
|
||||||
?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");
|
?? 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)
|
public static GalaxyRepositoryClient Create(MxGatewayClientOptions options)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
@@ -68,6 +86,8 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
HttpHandler = handler,
|
HttpHandler = handler,
|
||||||
LoggerFactory = options.LoggerFactory,
|
LoggerFactory = options.LoggerFactory,
|
||||||
|
MaxReceiveMessageSize = options.MaxGrpcMessageBytes,
|
||||||
|
MaxSendMessageSize = options.MaxGrpcMessageBytes,
|
||||||
});
|
});
|
||||||
|
|
||||||
return new GalaxyRepositoryClient(
|
return new GalaxyRepositoryClient(
|
||||||
@@ -81,6 +101,8 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
/// Probes the Galaxy Repository database connection. Returns true when the
|
/// Probes the Galaxy Repository database connection. Returns true when the
|
||||||
/// gateway can reach the configured ZB SQL Server.
|
/// gateway can reach the configured ZB SQL Server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>True if connection is successful, false otherwise.</returns>
|
||||||
public async Task<bool> TestConnectionAsync(CancellationToken cancellationToken = default)
|
public async Task<bool> TestConnectionAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
TestConnectionReply reply = await TestConnectionRawAsync(
|
TestConnectionReply reply = await TestConnectionRawAsync(
|
||||||
@@ -91,6 +113,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
return reply.Ok;
|
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(
|
public Task<TestConnectionReply> TestConnectionRawAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -107,6 +135,8 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
/// Returns the timestamp of the most recent Galaxy deployment, or
|
/// Returns the timestamp of the most recent Galaxy deployment, or
|
||||||
/// <see langword="null"/> when no deployment has been recorded.
|
/// <see langword="null"/> when no deployment has been recorded.
|
||||||
/// </summary>
|
/// </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)
|
public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
GetLastDeployTimeReply reply = await GetLastDeployTimeRawAsync(
|
GetLastDeployTimeReply reply = await GetLastDeployTimeRawAsync(
|
||||||
@@ -122,6 +152,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
return reply.TimeOfLastDeploy.ToDateTime();
|
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(
|
public Task<GetLastDeployTimeReply> GetLastDeployTimeRawAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -139,16 +175,93 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
/// includes its dynamic attributes so callers can determine which tag references
|
/// includes its dynamic attributes so callers can determine which tag references
|
||||||
/// they may subscribe to via the MxAccessGateway service.
|
/// they may subscribe to via the MxAccessGateway service.
|
||||||
/// </summary>
|
/// </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)
|
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync(
|
return await DiscoverHierarchyAsync(new DiscoverHierarchyOptions(), cancellationToken).ConfigureAwait(false);
|
||||||
new DiscoverHierarchyRequest(),
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
return reply.Objects;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(
|
||||||
|
DiscoverHierarchyOptions options,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
List<GalaxyObject> objects = [];
|
||||||
|
HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
|
||||||
|
string pageToken = string.Empty;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
DiscoverHierarchyRequest request = CreateDiscoverHierarchyRequest(options);
|
||||||
|
request.PageSize = DiscoverHierarchyPageSize;
|
||||||
|
request.PageToken = pageToken;
|
||||||
|
DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync(
|
||||||
|
request,
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
objects.AddRange(reply.Objects);
|
||||||
|
pageToken = reply.NextPageToken;
|
||||||
|
if (!string.IsNullOrWhiteSpace(pageToken)
|
||||||
|
&& !seenPageTokens.Add(pageToken))
|
||||||
|
{
|
||||||
|
throw new MxGatewayException(
|
||||||
|
$"Galaxy DiscoverHierarchy returned a repeated page token '{pageToken}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!string.IsNullOrWhiteSpace(pageToken));
|
||||||
|
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DiscoverHierarchyRequest CreateDiscoverHierarchyRequest(DiscoverHierarchyOptions options)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
|
|
||||||
|
DiscoverHierarchyRequest request = new()
|
||||||
|
{
|
||||||
|
AlarmBearingOnly = options.AlarmBearingOnly,
|
||||||
|
HistorizedOnly = options.HistorizedOnly,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.RootGobjectId.HasValue)
|
||||||
|
{
|
||||||
|
request.RootGobjectId = options.RootGobjectId.Value;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(options.RootTagName))
|
||||||
|
{
|
||||||
|
request.RootTagName = options.RootTagName;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(options.RootContainedPath))
|
||||||
|
{
|
||||||
|
request.RootContainedPath = options.RootContainedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.MaxDepth.HasValue)
|
||||||
|
{
|
||||||
|
request.MaxDepth = options.MaxDepth.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.CategoryIds.Add(options.CategoryIds);
|
||||||
|
request.TemplateChainContains.Add(options.TemplateChainContains);
|
||||||
|
if (!string.IsNullOrWhiteSpace(options.TagNameGlob))
|
||||||
|
{
|
||||||
|
request.TagNameGlob = options.TagNameGlob;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.IncludeAttributes.HasValue)
|
||||||
|
{
|
||||||
|
request.IncludeAttributes = options.IncludeAttributes.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
public Task<DiscoverHierarchyReply> DiscoverHierarchyRawAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -173,6 +286,9 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
/// at-least-once delivery beyond the per-subscriber buffer (gaps in
|
/// at-least-once delivery beyond the per-subscriber buffer (gaps in
|
||||||
/// <see cref="DeployEvent.Sequence"/> indicate dropped events).
|
/// <see cref="DeployEvent.Sequence"/> indicate dropped events).
|
||||||
/// </remarks>
|
/// </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(
|
public IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||||
DateTimeOffset? lastSeenDeployTime = null,
|
DateTimeOffset? lastSeenDeployTime = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -188,6 +304,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
return WatchDeployEventsRawAsync(request, cancellationToken);
|
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(
|
public IAsyncEnumerable<DeployEvent> WatchDeployEventsRawAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -211,6 +333,9 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes the gRPC channel and releases resources.
|
||||||
|
/// </summary>
|
||||||
public ValueTask DisposeAsync()
|
public ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
@@ -223,16 +348,32 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
return ValueTask.CompletedTask;
|
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)
|
internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
|
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)
|
internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return CreateCallOptions(cancellationToken, Options.StreamTimeout);
|
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(
|
internal CallOptions CreateCallOptions(
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
TimeSpan? timeout)
|
TimeSpan? timeout)
|
||||||
|
|||||||
@@ -3,16 +3,27 @@ using MxGateway.Contracts.Proto.Galaxy;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// gRPC implementation of IGalaxyRepositoryClientTransport.
|
||||||
|
/// </summary>
|
||||||
internal sealed class GrpcGalaxyRepositoryClientTransport(
|
internal sealed class GrpcGalaxyRepositoryClientTransport(
|
||||||
MxGatewayClientOptions options,
|
MxGatewayClientOptions options,
|
||||||
GalaxyRepository.GalaxyRepositoryClient rawClient) : IGalaxyRepositoryClientTransport
|
GalaxyRepository.GalaxyRepositoryClient rawClient) : IGalaxyRepositoryClientTransport
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the gateway client options.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientOptions Options { get; } = options;
|
public MxGatewayClientOptions Options { get; } = options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the underlying gRPC client.
|
||||||
|
/// </summary>
|
||||||
public GalaxyRepository.GalaxyRepositoryClient RawClient { get; } = rawClient;
|
public GalaxyRepository.GalaxyRepositoryClient RawClient { get; } = rawClient;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
GalaxyRepository.GalaxyRepositoryClient? IGalaxyRepositoryClientTransport.RawClient => RawClient;
|
GalaxyRepository.GalaxyRepositoryClient? IGalaxyRepositoryClientTransport.RawClient => RawClient;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<TestConnectionReply> TestConnectionAsync(
|
public async Task<TestConnectionReply> TestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -29,6 +40,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
public async Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -45,6 +57,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
public async Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -61,6 +74,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CallOptions callOptions,
|
CallOptions callOptions,
|
||||||
@@ -94,6 +108,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
IAsyncEnumerable<DeployEvent> IGalaxyRepositoryClientTransport.WatchDeployEventsAsync(
|
IAsyncEnumerable<DeployEvent> IGalaxyRepositoryClientTransport.WatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
|
|||||||
@@ -3,16 +3,27 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// gRPC implementation of IMxGatewayClientTransport.
|
||||||
|
/// </summary>
|
||||||
internal sealed class GrpcMxGatewayClientTransport(
|
internal sealed class GrpcMxGatewayClientTransport(
|
||||||
MxGatewayClientOptions options,
|
MxGatewayClientOptions options,
|
||||||
MxAccessGateway.MxAccessGatewayClient rawClient) : IMxGatewayClientTransport
|
MxAccessGateway.MxAccessGatewayClient rawClient) : IMxGatewayClientTransport
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the gateway client options.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientOptions Options { get; } = options;
|
public MxGatewayClientOptions Options { get; } = options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the underlying gRPC client.
|
||||||
|
/// </summary>
|
||||||
public MxAccessGateway.MxAccessGatewayClient RawClient { get; } = rawClient;
|
public MxAccessGateway.MxAccessGatewayClient RawClient { get; } = rawClient;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
MxAccessGateway.MxAccessGatewayClient? IMxGatewayClientTransport.RawClient => RawClient;
|
MxAccessGateway.MxAccessGatewayClient? IMxGatewayClientTransport.RawClient => RawClient;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<OpenSessionReply> OpenSessionAsync(
|
public async Task<OpenSessionReply> OpenSessionAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -29,6 +40,7 @@ internal sealed class GrpcMxGatewayClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<CloseSessionReply> CloseSessionAsync(
|
public async Task<CloseSessionReply> CloseSessionAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -45,6 +57,7 @@ internal sealed class GrpcMxGatewayClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<MxCommandReply> InvokeAsync(
|
public async Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -61,6 +74,7 @@ internal sealed class GrpcMxGatewayClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CallOptions callOptions,
|
CallOptions callOptions,
|
||||||
@@ -94,6 +108,7 @@ internal sealed class GrpcMxGatewayClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
IAsyncEnumerable<MxEvent> IMxGatewayClientTransport.StreamEventsAsync(
|
IAsyncEnumerable<MxEvent> IMxGatewayClientTransport.StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -101,6 +116,65 @@ internal sealed class GrpcMxGatewayClientTransport(
|
|||||||
return StreamEventsAsync(request, callOptions);
|
return StreamEventsAsync(request, callOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<AcknowledgeAlarmReply> AcknowledgeAlarmAsync(
|
||||||
|
AcknowledgeAlarmRequest request,
|
||||||
|
CallOptions callOptions)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await RawClient.AcknowledgeAlarmAsync(request, callOptions)
|
||||||
|
.ResponseAsync
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (RpcException exception)
|
||||||
|
{
|
||||||
|
throw MapRpcException(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 MapRpcException(exception, effectiveCancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
IAsyncEnumerable<ActiveAlarmSnapshot> IMxGatewayClientTransport.QueryActiveAlarmsAsync(
|
||||||
|
QueryActiveAlarmsRequest request,
|
||||||
|
CallOptions callOptions)
|
||||||
|
{
|
||||||
|
return QueryActiveAlarmsAsync(request, callOptions);
|
||||||
|
}
|
||||||
|
|
||||||
private static Exception MapRpcException(
|
private static Exception MapRpcException(
|
||||||
RpcException exception,
|
RpcException exception,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -3,24 +3,39 @@ using MxGateway.Contracts.Proto.Galaxy;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Transport layer for Galaxy Repository gRPC operations.</summary>
|
||||||
internal interface IGalaxyRepositoryClientTransport
|
internal interface IGalaxyRepositoryClientTransport
|
||||||
{
|
{
|
||||||
|
/// <summary>Gets the client options used to configure this transport.</summary>
|
||||||
MxGatewayClientOptions Options { get; }
|
MxGatewayClientOptions Options { get; }
|
||||||
|
|
||||||
|
/// <summary>Gets the underlying gRPC client, or <c>null</c> if not yet initialized.</summary>
|
||||||
GalaxyRepository.GalaxyRepositoryClient? RawClient { get; }
|
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(
|
Task<TestConnectionReply> TestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CallOptions callOptions);
|
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(
|
Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CallOptions callOptions);
|
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(
|
Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CallOptions callOptions);
|
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(
|
IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CallOptions callOptions);
|
CallOptions callOptions);
|
||||||
|
|||||||
@@ -5,23 +5,74 @@ namespace MxGateway.Client;
|
|||||||
|
|
||||||
internal interface IMxGatewayClientTransport
|
internal interface IMxGatewayClientTransport
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the client configuration options.
|
||||||
|
/// </summary>
|
||||||
MxGatewayClientOptions Options { get; }
|
MxGatewayClientOptions Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the underlying gRPC client, if available.
|
||||||
|
/// </summary>
|
||||||
MxAccessGateway.MxAccessGatewayClient? RawClient { get; }
|
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(
|
Task<OpenSessionReply> OpenSessionAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CallOptions callOptions);
|
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(
|
Task<CloseSessionReply> CloseSessionAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CallOptions callOptions);
|
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(
|
Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CallOptions callOptions);
|
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(
|
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CallOptions callOptions);
|
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,8 +2,13 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
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
|
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(
|
public MxAccessException(
|
||||||
string message,
|
string message,
|
||||||
MxCommandReply reply,
|
MxCommandReply reply,
|
||||||
@@ -20,5 +25,6 @@ public sealed class MxAccessException : MxGatewayCommandException
|
|||||||
Reply = reply;
|
Reply = reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the underlying MxCommandReply containing full failure details.</summary>
|
||||||
public MxCommandReply Reply { get; }
|
public MxCommandReply Reply { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Extension methods for checking MxCommandReply success conditions.</summary>
|
||||||
public static class MxCommandReplyExtensions
|
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)
|
public static MxCommandReply EnsureProtocolSuccess(this MxCommandReply reply)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(reply);
|
ArgumentNullException.ThrowIfNull(reply);
|
||||||
@@ -19,6 +22,8 @@ public static class MxCommandReplyExtensions
|
|||||||
throw CreateProtocolException(reply, code);
|
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)
|
public static MxCommandReply EnsureMxAccessSuccess(this MxCommandReply reply)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(reply);
|
ArgumentNullException.ThrowIfNull(reply);
|
||||||
|
|||||||
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Exception thrown when an API key is invalid, expired, or malformed.</summary>
|
||||||
public sealed class MxGatewayAuthenticationException : MxGatewayException
|
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>
|
||||||
public MxGatewayAuthenticationException(
|
public MxGatewayAuthenticationException(
|
||||||
string message,
|
string message,
|
||||||
string? sessionId = null,
|
string? sessionId = null,
|
||||||
|
|||||||
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Exception thrown when the API key lacks required scopes for an operation.</summary>
|
||||||
public sealed class MxGatewayAuthorizationException : MxGatewayException
|
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>
|
||||||
public MxGatewayAuthorizationException(
|
public MxGatewayAuthorizationException(
|
||||||
string message,
|
string message,
|
||||||
string? sessionId = null,
|
string? sessionId = null,
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
|
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
|
||||||
private bool _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(
|
internal MxGatewayClient(
|
||||||
MxGatewayClientOptions options,
|
MxGatewayClientOptions options,
|
||||||
IMxGatewayClientTransport transport)
|
IMxGatewayClientTransport transport)
|
||||||
@@ -46,12 +51,23 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
Options.LoggerFactory?.CreateLogger<MxGatewayClient>());
|
Options.LoggerFactory?.CreateLogger<MxGatewayClient>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the client configuration options.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientOptions Options { get; }
|
public MxGatewayClientOptions Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the underlying generated gRPC client.
|
||||||
|
/// </summary>
|
||||||
public MxAccessGateway.MxAccessGatewayClient RawClient =>
|
public MxAccessGateway.MxAccessGatewayClient RawClient =>
|
||||||
_transport.RawClient
|
_transport.RawClient
|
||||||
?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");
|
?? 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)
|
public static MxGatewayClient Create(MxGatewayClientOptions options)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
@@ -64,6 +80,8 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
HttpHandler = handler,
|
HttpHandler = handler,
|
||||||
LoggerFactory = options.LoggerFactory,
|
LoggerFactory = options.LoggerFactory,
|
||||||
|
MaxReceiveMessageSize = options.MaxGrpcMessageBytes,
|
||||||
|
MaxSendMessageSize = options.MaxGrpcMessageBytes,
|
||||||
});
|
});
|
||||||
|
|
||||||
return new MxGatewayClient(
|
return new MxGatewayClient(
|
||||||
@@ -73,6 +91,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
new MxAccessGateway.MxAccessGatewayClient(channel)));
|
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(
|
public async Task<MxGatewaySession> OpenSessionAsync(
|
||||||
OpenSessionRequest? request = null,
|
OpenSessionRequest? request = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -85,6 +109,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
return new MxGatewaySession(this, reply);
|
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(
|
public Task<OpenSessionReply> OpenSessionRawAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -95,6 +125,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
return _transport.OpenSessionAsync(request, CreateCallOptions(cancellationToken));
|
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(
|
public Task<CloseSessionReply> CloseSessionRawAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -107,6 +143,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
cancellationToken);
|
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(
|
public Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -124,6 +166,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
return _transport.InvokeAsync(request, CreateCallOptions(cancellationToken));
|
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(
|
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -134,6 +182,51 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
return _transport.StreamEventsAsync(request, CreateStreamCallOptions(cancellationToken));
|
return _transport.StreamEventsAsync(request, CreateStreamCallOptions(cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acknowledges an active MXAccess alarm condition through the gateway. The
|
||||||
|
/// gateway authenticates the request against the API key's <c>invoke:alarm-ack</c>
|
||||||
|
/// 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()
|
public ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
@@ -146,16 +239,32 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
return ValueTask.CompletedTask;
|
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)
|
internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
|
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)
|
internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return CreateCallOptions(cancellationToken, Options.StreamTimeout);
|
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(
|
internal CallOptions CreateCallOptions(
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
TimeSpan? timeout)
|
TimeSpan? timeout)
|
||||||
|
|||||||
@@ -7,26 +7,64 @@ namespace MxGateway.Client;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class MxGatewayClientOptions
|
public sealed class MxGatewayClientOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the gateway endpoint URI (required).
|
||||||
|
/// </summary>
|
||||||
public required Uri Endpoint { get; init; }
|
public required Uri Endpoint { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the API key for gateway authentication (required).
|
||||||
|
/// </summary>
|
||||||
public required string ApiKey { get; init; }
|
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; }
|
public bool UseTls { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the path to a CA certificate file for custom certificate validation.
|
||||||
|
/// </summary>
|
||||||
public string? CaCertificatePath { get; init; }
|
public string? CaCertificatePath { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the server name override for SNI during TLS handshake.
|
||||||
|
/// </summary>
|
||||||
public string? ServerNameOverride { get; init; }
|
public string? ServerNameOverride { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the timeout for establishing connection to the gateway.
|
||||||
|
/// </summary>
|
||||||
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(10);
|
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default timeout for unary gRPC calls.
|
||||||
|
/// </summary>
|
||||||
public TimeSpan DefaultCallTimeout { get; init; } = TimeSpan.FromSeconds(30);
|
public TimeSpan DefaultCallTimeout { get; init; } = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the optional timeout for streaming gRPC calls.
|
||||||
|
/// </summary>
|
||||||
public TimeSpan? StreamTimeout { get; init; }
|
public TimeSpan? StreamTimeout { get; init; }
|
||||||
|
|
||||||
|
public int MaxGrpcMessageBytes { get; init; } = 16 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the retry configuration for safe unary calls.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientRetryOptions Retry { get; init; } = new();
|
public MxGatewayClientRetryOptions Retry { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the logger factory for diagnostic logging.
|
||||||
|
/// </summary>
|
||||||
public ILoggerFactory? LoggerFactory { get; init; }
|
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()
|
public void Validate()
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(Endpoint);
|
ArgumentNullException.ThrowIfNull(Endpoint);
|
||||||
@@ -66,6 +104,13 @@ public sealed class MxGatewayClientOptions
|
|||||||
"The stream timeout must be greater than zero when configured.");
|
"The stream timeout must be greater than zero when configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (MaxGrpcMessageBytes <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(MaxGrpcMessageBytes),
|
||||||
|
"The maximum gRPC message size must be greater than zero.");
|
||||||
|
}
|
||||||
|
|
||||||
if (UseTls && Endpoint.Scheme != Uri.UriSchemeHttps)
|
if (UseTls && Endpoint.Scheme != Uri.UriSchemeHttps)
|
||||||
{
|
{
|
||||||
throw new ArgumentException(
|
throw new ArgumentException(
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Configuration for automatic retry behavior on transient gRPC call failures.</summary>
|
||||||
public sealed class MxGatewayClientRetryOptions
|
public sealed class MxGatewayClientRetryOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>Gets the maximum number of attempts (initial + retries); default is 2.</summary>
|
||||||
public int MaxAttempts { get; init; } = 2;
|
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);
|
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);
|
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;
|
public bool UseJitter { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>Validates the retry options and throws if any constraint is violated.</summary>
|
||||||
public void Validate()
|
public void Validate()
|
||||||
{
|
{
|
||||||
if (MaxAttempts <= 0)
|
if (MaxAttempts <= 0)
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ using Polly.Retry;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Factory and helpers for exponential-backoff retry policies on transient gRPC failures.</summary>
|
||||||
internal static class MxGatewayClientRetryPolicy
|
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(
|
public static ResiliencePipeline Create(
|
||||||
MxGatewayClientRetryOptions options,
|
MxGatewayClientRetryOptions options,
|
||||||
ILogger? logger)
|
ILogger? logger)
|
||||||
@@ -36,6 +40,8 @@ internal static class MxGatewayClientRetryPolicy
|
|||||||
.Build();
|
.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)
|
public static bool IsRetryableCommand(MxCommandKind kind)
|
||||||
{
|
{
|
||||||
return kind is MxCommandKind.Ping
|
return kind is MxCommandKind.Ping
|
||||||
|
|||||||
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Exception thrown when a gateway command fails due to an unclassified protocol error.</summary>
|
||||||
public class MxGatewayCommandException : MxGatewayException
|
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(
|
public MxGatewayCommandException(
|
||||||
string message,
|
string message,
|
||||||
string? sessionId = null,
|
string? sessionId = null,
|
||||||
|
|||||||
@@ -2,20 +2,42 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when a gateway RPC call fails or returns an error status.
|
||||||
|
/// </summary>
|
||||||
public class MxGatewayException : Exception
|
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)
|
public MxGatewayException(string message)
|
||||||
: base(message)
|
: base(message)
|
||||||
{
|
{
|
||||||
Statuses = [];
|
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)
|
public MxGatewayException(string message, Exception? innerException)
|
||||||
: base(message, innerException)
|
: base(message, innerException)
|
||||||
{
|
{
|
||||||
Statuses = [];
|
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>
|
||||||
public MxGatewayException(
|
public MxGatewayException(
|
||||||
string message,
|
string message,
|
||||||
string? sessionId,
|
string? sessionId,
|
||||||
@@ -33,13 +55,28 @@ public class MxGatewayException : Exception
|
|||||||
Statuses = statuses;
|
Statuses = statuses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the session ID associated with the exception, if available.
|
||||||
|
/// </summary>
|
||||||
public string? SessionId { get; }
|
public string? SessionId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the correlation ID associated with the exception, if available.
|
||||||
|
/// </summary>
|
||||||
public string? CorrelationId { get; }
|
public string? CorrelationId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the protocol-level status returned by the gateway, if available.
|
||||||
|
/// </summary>
|
||||||
public ProtocolStatus? ProtocolStatus { get; }
|
public ProtocolStatus? ProtocolStatus { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the HRESULT code returned by the worker or MXAccess, if available.
|
||||||
|
/// </summary>
|
||||||
public int? HResultCode { get; }
|
public int? HResultCode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of MXAccess status codes returned by the operation.
|
||||||
|
/// </summary>
|
||||||
public IReadOnlyList<MxStatusProxy> Statuses { get; }
|
public IReadOnlyList<MxStatusProxy> Statuses { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
private readonly SemaphoreSlim _closeLock = new(1, 1);
|
private readonly SemaphoreSlim _closeLock = new(1, 1);
|
||||||
private CloseSessionReply? _closeReply;
|
private CloseSessionReply? _closeReply;
|
||||||
|
|
||||||
|
/// <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(
|
internal MxGatewaySession(
|
||||||
MxGatewayClient client,
|
MxGatewayClient client,
|
||||||
OpenSessionReply openSessionReply)
|
OpenSessionReply openSessionReply)
|
||||||
@@ -19,10 +24,21 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
OpenSessionReply = openSessionReply ?? throw new ArgumentNullException(nameof(openSessionReply));
|
OpenSessionReply = openSessionReply ?? throw new ArgumentNullException(nameof(openSessionReply));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The session ID assigned by the gateway.
|
||||||
|
/// </summary>
|
||||||
public string SessionId => OpenSessionReply.SessionId;
|
public string SessionId => OpenSessionReply.SessionId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The server's session creation response containing metadata.
|
||||||
|
/// </summary>
|
||||||
public OpenSessionReply OpenSessionReply { get; }
|
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)
|
public async Task<CloseSessionReply> CloseAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_closeReply is not null)
|
if (_closeReply is not null)
|
||||||
@@ -50,6 +66,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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(
|
public async Task<int> RegisterAsync(
|
||||||
string clientName,
|
string clientName,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -60,6 +82,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.Register?.ServerHandle ?? reply.ReturnValue.Int32Value;
|
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(
|
public Task<MxCommandReply> RegisterRawAsync(
|
||||||
string clientName,
|
string clientName,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -75,6 +103,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
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(
|
public async Task<int> AddItemAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
string itemDefinition,
|
string itemDefinition,
|
||||||
@@ -89,6 +124,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.AddItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
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(
|
public Task<MxCommandReply> AddItemRawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
string itemDefinition,
|
string itemDefinition,
|
||||||
@@ -109,6 +151,14 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
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(
|
public async Task<int> AddItem2Async(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
string itemDefinition,
|
string itemDefinition,
|
||||||
@@ -125,6 +175,14 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.AddItem2?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
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(
|
public Task<MxCommandReply> AddItem2RawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
string itemDefinition,
|
string itemDefinition,
|
||||||
@@ -147,6 +205,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
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(
|
public async Task AdviseAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -157,6 +221,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
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(
|
public Task<MxCommandReply> AdviseRawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -175,6 +246,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
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(
|
public async Task UnAdviseAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -185,6 +262,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
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(
|
public Task<MxCommandReply> UnAdviseRawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -203,6 +287,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
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(
|
public async Task RemoveItemAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -213,6 +303,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
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(
|
public Task<MxCommandReply> RemoveItemRawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -231,6 +328,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
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(
|
public async Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<string> tagAddresses,
|
IReadOnlyList<string> tagAddresses,
|
||||||
@@ -253,6 +357,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.AddItemBulk?.Results.ToArray() ?? [];
|
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(
|
public async Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<int> itemHandles,
|
IReadOnlyList<int> itemHandles,
|
||||||
@@ -275,6 +386,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.AdviseItemBulk?.Results.ToArray() ?? [];
|
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(
|
public async Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<int> itemHandles,
|
IReadOnlyList<int> itemHandles,
|
||||||
@@ -297,6 +415,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.RemoveItemBulk?.Results.ToArray() ?? [];
|
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(
|
public async Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<int> itemHandles,
|
IReadOnlyList<int> itemHandles,
|
||||||
@@ -319,6 +444,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.UnAdviseItemBulk?.Results.ToArray() ?? [];
|
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(
|
public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<string> tagAddresses,
|
IReadOnlyList<string> tagAddresses,
|
||||||
@@ -341,6 +473,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.SubscribeBulk?.Results.ToArray() ?? [];
|
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(
|
public async Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<int> itemHandles,
|
IReadOnlyList<int> itemHandles,
|
||||||
@@ -363,6 +502,14 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.UnsubscribeBulk?.Results.ToArray() ?? [];
|
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(
|
public async Task WriteAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -375,6 +522,15 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
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(
|
public Task<MxCommandReply> WriteRawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -399,6 +555,15 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
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(
|
public async Task Write2Async(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -418,6 +583,16 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
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(
|
public Task<MxCommandReply> Write2RawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -445,6 +620,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
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(
|
public Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -453,6 +634,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return _client.InvokeAsync(request, cancellationToken);
|
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(
|
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
ulong afterWorkerSequence = 0,
|
ulong afterWorkerSequence = 0,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -466,6 +653,9 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes the session and releases resources.
|
||||||
|
/// </summary>
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
await CloseAsync().ConfigureAwait(false);
|
await CloseAsync().ConfigureAwait(false);
|
||||||
|
|||||||
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Exception thrown when a session is not found, not ready, or invalid.</summary>
|
||||||
public sealed class MxGatewaySessionException : MxGatewayException
|
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(
|
public MxGatewaySessionException(
|
||||||
string message,
|
string message,
|
||||||
string? sessionId = null,
|
string? sessionId = null,
|
||||||
|
|||||||
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Exception thrown when the worker process is unavailable or fails to process a command.</summary>
|
||||||
public sealed class MxGatewayWorkerException : MxGatewayException
|
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(
|
public MxGatewayWorkerException(
|
||||||
string message,
|
string message,
|
||||||
string? sessionId = null,
|
string? sessionId = null,
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Extension methods for MxStatusProxy values.</summary>
|
||||||
public static class MxStatusProxyExtensions
|
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)
|
public static bool IsSuccess(this MxStatusProxy status)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(status);
|
ArgumentNullException.ThrowIfNull(status);
|
||||||
@@ -12,6 +15,8 @@ public static class MxStatusProxyExtensions
|
|||||||
&& status.Category is MxStatusCategory.Ok;
|
&& 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)
|
public static string ToDiagnosticSummary(this MxStatusProxy status)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(status);
|
ArgumentNullException.ThrowIfNull(status);
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ namespace MxGateway.Client;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class MxValueExtensions
|
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)
|
public static MxValue ToMxValue(this bool value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -20,6 +24,10 @@ 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)
|
public static MxValue ToMxValue(this int value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -30,6 +38,10 @@ 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)
|
public static MxValue ToMxValue(this long value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -40,6 +52,10 @@ 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)
|
public static MxValue ToMxValue(this float value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -50,6 +66,10 @@ 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)
|
public static MxValue ToMxValue(this double value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -60,6 +80,10 @@ 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)
|
public static MxValue ToMxValue(this string value)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
@@ -72,6 +96,10 @@ 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)
|
public static MxValue ToMxValue(this DateTimeOffset value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -82,6 +110,10 @@ 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)
|
public static MxValue ToMxValue(this DateTime value)
|
||||||
{
|
{
|
||||||
return new DateTimeOffset(
|
return new DateTimeOffset(
|
||||||
@@ -91,6 +123,10 @@ public static class MxValueExtensions
|
|||||||
.ToMxValue();
|
.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)
|
public static MxValue ToMxValue(this IReadOnlyList<bool> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -105,6 +141,10 @@ 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)
|
public static MxValue ToMxValue(this IReadOnlyList<int> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -119,6 +159,10 @@ 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)
|
public static MxValue ToMxValue(this IReadOnlyList<long> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -133,6 +177,10 @@ 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)
|
public static MxValue ToMxValue(this IReadOnlyList<float> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -147,6 +195,10 @@ 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)
|
public static MxValue ToMxValue(this IReadOnlyList<double> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -161,6 +213,10 @@ 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)
|
public static MxValue ToMxValue(this IReadOnlyList<string> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -175,6 +231,10 @@ 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)
|
public static MxValue ToMxValue(this IReadOnlyList<DateTimeOffset> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -189,6 +249,10 @@ 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)
|
public static string GetProjectionKind(this MxValue value)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
@@ -208,6 +272,10 @@ 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)
|
public static object? ToClrValue(this MxValue value)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
@@ -227,6 +295,10 @@ 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)
|
public static object? ToClrArrayValue(this MxArray array)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(array);
|
ArgumentNullException.ThrowIfNull(array);
|
||||||
@@ -249,6 +321,13 @@ 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(
|
public static MxValue ToRawMxValue(
|
||||||
byte[] value,
|
byte[] value,
|
||||||
string variantType,
|
string variantType,
|
||||||
|
|||||||
@@ -164,6 +164,19 @@ foreach (GalaxyObject galaxyObject in objects)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Use `DiscoverHierarchyOptions` to request a server-side slice without pulling
|
||||||
|
the full Galaxy:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
IReadOnlyList<GalaxyObject> pumps = await repository.DiscoverHierarchyAsync(
|
||||||
|
new DiscoverHierarchyOptions
|
||||||
|
{
|
||||||
|
RootContainedPath = "Area1/Line3",
|
||||||
|
TagNameGlob = "Pump_*",
|
||||||
|
IncludeAttributes = false,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
The CLI exposes the same operations:
|
The CLI exposes the same operations:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -230,5 +243,5 @@ dotnet run --project clients/dotnet/MxGateway.Client.Cli -- smoke --endpoint $en
|
|||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
||||||
- [.NET Client Detailed Design](../../docs/clients-dotnet-csharp-design.md)
|
- [.NET Client Detailed Design](./DotnetClientDesign.md)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ Provide an idiomatic Go client module for MXAccess Gateway, plus a test CLI and
|
|||||||
unit tests. The Go client should be suitable for services and command-line
|
unit tests. The Go client should be suitable for services and command-line
|
||||||
automation.
|
automation.
|
||||||
|
|
||||||
Follow the [Go Style Guide](./style-guides/GoStyleGuide.md) for handwritten
|
Follow the [Go Style Guide](../../docs/style-guides/GoStyleGuide.md) for handwritten
|
||||||
code and the [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md) for
|
code and the [Protobuf Style Guide](../../docs/style-guides/ProtobufStyleGuide.md) for
|
||||||
generated contract inputs.
|
generated contract inputs.
|
||||||
|
|
||||||
## Module Layout
|
## Module Layout
|
||||||
@@ -176,3 +176,10 @@ MXGATEWAY_INTEGRATION=1
|
|||||||
|
|
||||||
Integration test should run `OpenSession`, `Register`, `AddItem`, `Advise`,
|
Integration test should run `OpenSession`, `Register`, `AddItem`, `Advise`,
|
||||||
bounded `StreamEvents`, and `CloseSession`.
|
bounded `StreamEvents`, and `CloseSession`.
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Client Libraries Detailed Design](../../docs/ClientLibrariesDesign.md)
|
||||||
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
||||||
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
|
- [Go Style Guide](../../docs/style-guides/GoStyleGuide.md)
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
The Go client module contains the generated MXAccess Gateway protobuf bindings,
|
The Go client module contains the generated MXAccess Gateway protobuf bindings,
|
||||||
a small handwritten `mxgateway` package, and the `mxgw-go` test CLI scaffold.
|
a small handwritten `mxgateway` package, and the `mxgw-go` test CLI scaffold.
|
||||||
The module uses the shared proto inputs documented in
|
The module uses the shared proto inputs documented in
|
||||||
`../../docs/client-proto-generation.md` so gateway and client contracts stay in
|
`../../docs/ClientProtoGeneration.md` so gateway and client contracts stay in
|
||||||
sync.
|
sync.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
@@ -28,7 +28,7 @@ Run generation after the shared `.proto` files or the Go output path changes:
|
|||||||
./generate-proto.ps1
|
./generate-proto.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
The script uses the tool paths recorded in `../../docs/toolchain-links.md`.
|
The script uses the tool paths recorded in `../../docs/ToolchainLinks.md`.
|
||||||
|
|
||||||
## Build And Test
|
## Build And Test
|
||||||
|
|
||||||
@@ -209,5 +209,5 @@ go run ./cmd/mxgw-go smoke -endpoint $env:MXGATEWAY_ENDPOINT -plaintext -api-key
|
|||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
||||||
- [Go Client Detailed Design](../../docs/clients-golang-design.md)
|
- [Go Client Detailed Design](./GoClientDesign.md)
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
// 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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ $protoc = 'C:\Users\dohertj2\AppData\Local\Microsoft\WinGet\Packages\Google.Prot
|
|||||||
$goPluginPath = 'C:\Users\dohertj2\go\bin'
|
$goPluginPath = 'C:\Users\dohertj2\go\bin'
|
||||||
|
|
||||||
if (-not (Test-Path $protoc)) {
|
if (-not (Test-Path $protoc)) {
|
||||||
throw "protoc was not found at $protoc. See docs/toolchain-links.md."
|
throw "protoc was not found at $protoc. See docs/ToolchainLinks.md."
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($pluginName in @('protoc-gen-go.exe', 'protoc-gen-go-grpc.exe')) {
|
foreach ($pluginName in @('protoc-gen-go.exe', 'protoc-gen-go-grpc.exe')) {
|
||||||
$pluginPath = Join-Path $goPluginPath $pluginName
|
$pluginPath = Join-Path $goPluginPath $pluginName
|
||||||
if (-not (Test-Path $pluginPath)) {
|
if (-not (Test-Path $pluginPath)) {
|
||||||
throw "$pluginName was not found at $pluginPath. See docs/toolchain-links.md."
|
throw "$pluginName was not found at $pluginPath. See docs/ToolchainLinks.md."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
sync "sync"
|
sync "sync"
|
||||||
unsafe "unsafe"
|
unsafe "unsafe"
|
||||||
@@ -191,9 +192,38 @@ func (x *GetLastDeployTimeReply) GetTimeOfLastDeploy() *timestamppb.Timestamp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DiscoverHierarchyRequest struct {
|
type DiscoverHierarchyRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// Maximum number of objects to return. The server applies its default when
|
||||||
sizeCache protoimpl.SizeCache
|
// unset and rejects non-positive values.
|
||||||
|
PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
|
||||||
|
// Opaque token returned by a previous DiscoverHierarchy response.
|
||||||
|
PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"`
|
||||||
|
// Optional. When set, return only this object and its descendants.
|
||||||
|
// Empty = full hierarchy.
|
||||||
|
//
|
||||||
|
// Types that are valid to be assigned to Root:
|
||||||
|
//
|
||||||
|
// *DiscoverHierarchyRequest_RootGobjectId
|
||||||
|
// *DiscoverHierarchyRequest_RootTagName
|
||||||
|
// *DiscoverHierarchyRequest_RootContainedPath
|
||||||
|
Root isDiscoverHierarchyRequest_Root `protobuf_oneof:"root"`
|
||||||
|
// Optional. Cap on descendant depth from root. Zero returns only the root.
|
||||||
|
// Unset means unlimited depth.
|
||||||
|
MaxDepth *wrapperspb.Int32Value `protobuf:"bytes,6,opt,name=max_depth,json=maxDepth,proto3" json:"max_depth,omitempty"`
|
||||||
|
// Optional object category id filters.
|
||||||
|
CategoryIds []int32 `protobuf:"varint,7,rep,packed,name=category_ids,json=categoryIds,proto3" json:"category_ids,omitempty"`
|
||||||
|
// Optional case-insensitive substring filters against template names.
|
||||||
|
TemplateChainContains []string `protobuf:"bytes,8,rep,name=template_chain_contains,json=templateChainContains,proto3" json:"template_chain_contains,omitempty"`
|
||||||
|
// Optional anchored, case-insensitive glob over object tag_name.
|
||||||
|
TagNameGlob string `protobuf:"bytes,9,opt,name=tag_name_glob,json=tagNameGlob,proto3" json:"tag_name_glob,omitempty"`
|
||||||
|
// Optional. Unset or true includes attributes. False returns object skeletons.
|
||||||
|
IncludeAttributes *bool `protobuf:"varint,10,opt,name=include_attributes,json=includeAttributes,proto3,oneof" json:"include_attributes,omitempty"`
|
||||||
|
// Optional. Return only objects with at least one alarm-bearing attribute.
|
||||||
|
AlarmBearingOnly bool `protobuf:"varint,11,opt,name=alarm_bearing_only,json=alarmBearingOnly,proto3" json:"alarm_bearing_only,omitempty"`
|
||||||
|
// Optional. Return only objects with at least one historized attribute.
|
||||||
|
HistorizedOnly bool `protobuf:"varint,12,opt,name=historized_only,json=historizedOnly,proto3" json:"historized_only,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *DiscoverHierarchyRequest) Reset() {
|
func (x *DiscoverHierarchyRequest) Reset() {
|
||||||
@@ -226,11 +256,134 @@ func (*DiscoverHierarchyRequest) Descriptor() ([]byte, []int) {
|
|||||||
return file_galaxy_repository_proto_rawDescGZIP(), []int{4}
|
return file_galaxy_repository_proto_rawDescGZIP(), []int{4}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetPageSize() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.PageSize
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetPageToken() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.PageToken
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetRoot() isDiscoverHierarchyRequest_Root {
|
||||||
|
if x != nil {
|
||||||
|
return x.Root
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetRootGobjectId() int32 {
|
||||||
|
if x != nil {
|
||||||
|
if x, ok := x.Root.(*DiscoverHierarchyRequest_RootGobjectId); ok {
|
||||||
|
return x.RootGobjectId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetRootTagName() string {
|
||||||
|
if x != nil {
|
||||||
|
if x, ok := x.Root.(*DiscoverHierarchyRequest_RootTagName); ok {
|
||||||
|
return x.RootTagName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetRootContainedPath() string {
|
||||||
|
if x != nil {
|
||||||
|
if x, ok := x.Root.(*DiscoverHierarchyRequest_RootContainedPath); ok {
|
||||||
|
return x.RootContainedPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetMaxDepth() *wrapperspb.Int32Value {
|
||||||
|
if x != nil {
|
||||||
|
return x.MaxDepth
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetCategoryIds() []int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.CategoryIds
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetTemplateChainContains() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.TemplateChainContains
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetTagNameGlob() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.TagNameGlob
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetIncludeAttributes() bool {
|
||||||
|
if x != nil && x.IncludeAttributes != nil {
|
||||||
|
return *x.IncludeAttributes
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetAlarmBearingOnly() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.AlarmBearingOnly
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetHistorizedOnly() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.HistorizedOnly
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type isDiscoverHierarchyRequest_Root interface {
|
||||||
|
isDiscoverHierarchyRequest_Root()
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscoverHierarchyRequest_RootGobjectId struct {
|
||||||
|
RootGobjectId int32 `protobuf:"varint,3,opt,name=root_gobject_id,json=rootGobjectId,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscoverHierarchyRequest_RootTagName struct {
|
||||||
|
RootTagName string `protobuf:"bytes,4,opt,name=root_tag_name,json=rootTagName,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscoverHierarchyRequest_RootContainedPath struct {
|
||||||
|
RootContainedPath string `protobuf:"bytes,5,opt,name=root_contained_path,json=rootContainedPath,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*DiscoverHierarchyRequest_RootGobjectId) isDiscoverHierarchyRequest_Root() {}
|
||||||
|
|
||||||
|
func (*DiscoverHierarchyRequest_RootTagName) isDiscoverHierarchyRequest_Root() {}
|
||||||
|
|
||||||
|
func (*DiscoverHierarchyRequest_RootContainedPath) isDiscoverHierarchyRequest_Root() {}
|
||||||
|
|
||||||
type DiscoverHierarchyReply struct {
|
type DiscoverHierarchyReply struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Objects []*GalaxyObject `protobuf:"bytes,1,rep,name=objects,proto3" json:"objects,omitempty"`
|
Objects []*GalaxyObject `protobuf:"bytes,1,rep,name=objects,proto3" json:"objects,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// Non-empty when another page is available.
|
||||||
sizeCache protoimpl.SizeCache
|
NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"`
|
||||||
|
// Total number of objects in the cached hierarchy at the time of the call.
|
||||||
|
TotalObjectCount int32 `protobuf:"varint,3,opt,name=total_object_count,json=totalObjectCount,proto3" json:"total_object_count,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *DiscoverHierarchyReply) Reset() {
|
func (x *DiscoverHierarchyReply) Reset() {
|
||||||
@@ -270,6 +423,20 @@ func (x *DiscoverHierarchyReply) GetObjects() []*GalaxyObject {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyReply) GetNextPageToken() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.NextPageToken
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyReply) GetTotalObjectCount() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.TotalObjectCount
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type WatchDeployEventsRequest struct {
|
type WatchDeployEventsRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// Optional. When set, the bootstrap event is suppressed if the cached deploy
|
// Optional. When set, the bootstrap event is suppressed if the cached deploy
|
||||||
@@ -647,17 +814,35 @@ var File_galaxy_repository_proto protoreflect.FileDescriptor
|
|||||||
|
|
||||||
const file_galaxy_repository_proto_rawDesc = "" +
|
const file_galaxy_repository_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\x17galaxy_repository.proto\x12\x14galaxy_repository.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x17\n" +
|
"\x17galaxy_repository.proto\x12\x14galaxy_repository.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\x17\n" +
|
||||||
"\x15TestConnectionRequest\"%\n" +
|
"\x15TestConnectionRequest\"%\n" +
|
||||||
"\x13TestConnectionReply\x12\x0e\n" +
|
"\x13TestConnectionReply\x12\x0e\n" +
|
||||||
"\x02ok\x18\x01 \x01(\bR\x02ok\"\x1a\n" +
|
"\x02ok\x18\x01 \x01(\bR\x02ok\"\x1a\n" +
|
||||||
"\x18GetLastDeployTimeRequest\"}\n" +
|
"\x18GetLastDeployTimeRequest\"}\n" +
|
||||||
"\x16GetLastDeployTimeReply\x12\x18\n" +
|
"\x16GetLastDeployTimeReply\x12\x18\n" +
|
||||||
"\apresent\x18\x01 \x01(\bR\apresent\x12I\n" +
|
"\apresent\x18\x01 \x01(\bR\apresent\x12I\n" +
|
||||||
"\x13time_of_last_deploy\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x10timeOfLastDeploy\"\x1a\n" +
|
"\x13time_of_last_deploy\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x10timeOfLastDeploy\"\xbb\x04\n" +
|
||||||
"\x18DiscoverHierarchyRequest\"V\n" +
|
"\x18DiscoverHierarchyRequest\x12\x1b\n" +
|
||||||
|
"\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"page_token\x18\x02 \x01(\tR\tpageToken\x12(\n" +
|
||||||
|
"\x0froot_gobject_id\x18\x03 \x01(\x05H\x00R\rrootGobjectId\x12$\n" +
|
||||||
|
"\rroot_tag_name\x18\x04 \x01(\tH\x00R\vrootTagName\x120\n" +
|
||||||
|
"\x13root_contained_path\x18\x05 \x01(\tH\x00R\x11rootContainedPath\x128\n" +
|
||||||
|
"\tmax_depth\x18\x06 \x01(\v2\x1b.google.protobuf.Int32ValueR\bmaxDepth\x12!\n" +
|
||||||
|
"\fcategory_ids\x18\a \x03(\x05R\vcategoryIds\x126\n" +
|
||||||
|
"\x17template_chain_contains\x18\b \x03(\tR\x15templateChainContains\x12\"\n" +
|
||||||
|
"\rtag_name_glob\x18\t \x01(\tR\vtagNameGlob\x122\n" +
|
||||||
|
"\x12include_attributes\x18\n" +
|
||||||
|
" \x01(\bH\x01R\x11includeAttributes\x88\x01\x01\x12,\n" +
|
||||||
|
"\x12alarm_bearing_only\x18\v \x01(\bR\x10alarmBearingOnly\x12'\n" +
|
||||||
|
"\x0fhistorized_only\x18\f \x01(\bR\x0ehistorizedOnlyB\x06\n" +
|
||||||
|
"\x04rootB\x15\n" +
|
||||||
|
"\x13_include_attributes\"\xac\x01\n" +
|
||||||
"\x16DiscoverHierarchyReply\x12<\n" +
|
"\x16DiscoverHierarchyReply\x12<\n" +
|
||||||
"\aobjects\x18\x01 \x03(\v2\".galaxy_repository.v1.GalaxyObjectR\aobjects\"i\n" +
|
"\aobjects\x18\x01 \x03(\v2\".galaxy_repository.v1.GalaxyObjectR\aobjects\x12&\n" +
|
||||||
|
"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12,\n" +
|
||||||
|
"\x12total_object_count\x18\x03 \x01(\x05R\x10totalObjectCount\"i\n" +
|
||||||
"\x18WatchDeployEventsRequest\x12M\n" +
|
"\x18WatchDeployEventsRequest\x12M\n" +
|
||||||
"\x15last_seen_deploy_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x12lastSeenDeployTime\"\xbb\x02\n" +
|
"\x15last_seen_deploy_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x12lastSeenDeployTime\"\xbb\x02\n" +
|
||||||
"\vDeployEvent\x12\x1a\n" +
|
"\vDeployEvent\x12\x1a\n" +
|
||||||
@@ -730,27 +915,29 @@ var file_galaxy_repository_proto_goTypes = []any{
|
|||||||
(*GalaxyObject)(nil), // 8: galaxy_repository.v1.GalaxyObject
|
(*GalaxyObject)(nil), // 8: galaxy_repository.v1.GalaxyObject
|
||||||
(*GalaxyAttribute)(nil), // 9: galaxy_repository.v1.GalaxyAttribute
|
(*GalaxyAttribute)(nil), // 9: galaxy_repository.v1.GalaxyAttribute
|
||||||
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
|
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
|
||||||
|
(*wrapperspb.Int32Value)(nil), // 11: google.protobuf.Int32Value
|
||||||
}
|
}
|
||||||
var file_galaxy_repository_proto_depIdxs = []int32{
|
var file_galaxy_repository_proto_depIdxs = []int32{
|
||||||
10, // 0: galaxy_repository.v1.GetLastDeployTimeReply.time_of_last_deploy:type_name -> google.protobuf.Timestamp
|
10, // 0: galaxy_repository.v1.GetLastDeployTimeReply.time_of_last_deploy:type_name -> google.protobuf.Timestamp
|
||||||
8, // 1: galaxy_repository.v1.DiscoverHierarchyReply.objects:type_name -> galaxy_repository.v1.GalaxyObject
|
11, // 1: galaxy_repository.v1.DiscoverHierarchyRequest.max_depth:type_name -> google.protobuf.Int32Value
|
||||||
10, // 2: galaxy_repository.v1.WatchDeployEventsRequest.last_seen_deploy_time:type_name -> google.protobuf.Timestamp
|
8, // 2: galaxy_repository.v1.DiscoverHierarchyReply.objects:type_name -> galaxy_repository.v1.GalaxyObject
|
||||||
10, // 3: galaxy_repository.v1.DeployEvent.observed_at:type_name -> google.protobuf.Timestamp
|
10, // 3: galaxy_repository.v1.WatchDeployEventsRequest.last_seen_deploy_time:type_name -> google.protobuf.Timestamp
|
||||||
10, // 4: galaxy_repository.v1.DeployEvent.time_of_last_deploy:type_name -> google.protobuf.Timestamp
|
10, // 4: galaxy_repository.v1.DeployEvent.observed_at:type_name -> google.protobuf.Timestamp
|
||||||
9, // 5: galaxy_repository.v1.GalaxyObject.attributes:type_name -> galaxy_repository.v1.GalaxyAttribute
|
10, // 5: galaxy_repository.v1.DeployEvent.time_of_last_deploy:type_name -> google.protobuf.Timestamp
|
||||||
0, // 6: galaxy_repository.v1.GalaxyRepository.TestConnection:input_type -> galaxy_repository.v1.TestConnectionRequest
|
9, // 6: galaxy_repository.v1.GalaxyObject.attributes:type_name -> galaxy_repository.v1.GalaxyAttribute
|
||||||
2, // 7: galaxy_repository.v1.GalaxyRepository.GetLastDeployTime:input_type -> galaxy_repository.v1.GetLastDeployTimeRequest
|
0, // 7: galaxy_repository.v1.GalaxyRepository.TestConnection:input_type -> galaxy_repository.v1.TestConnectionRequest
|
||||||
4, // 8: galaxy_repository.v1.GalaxyRepository.DiscoverHierarchy:input_type -> galaxy_repository.v1.DiscoverHierarchyRequest
|
2, // 8: galaxy_repository.v1.GalaxyRepository.GetLastDeployTime:input_type -> galaxy_repository.v1.GetLastDeployTimeRequest
|
||||||
6, // 9: galaxy_repository.v1.GalaxyRepository.WatchDeployEvents:input_type -> galaxy_repository.v1.WatchDeployEventsRequest
|
4, // 9: galaxy_repository.v1.GalaxyRepository.DiscoverHierarchy:input_type -> galaxy_repository.v1.DiscoverHierarchyRequest
|
||||||
1, // 10: galaxy_repository.v1.GalaxyRepository.TestConnection:output_type -> galaxy_repository.v1.TestConnectionReply
|
6, // 10: galaxy_repository.v1.GalaxyRepository.WatchDeployEvents:input_type -> galaxy_repository.v1.WatchDeployEventsRequest
|
||||||
3, // 11: galaxy_repository.v1.GalaxyRepository.GetLastDeployTime:output_type -> galaxy_repository.v1.GetLastDeployTimeReply
|
1, // 11: galaxy_repository.v1.GalaxyRepository.TestConnection:output_type -> galaxy_repository.v1.TestConnectionReply
|
||||||
5, // 12: galaxy_repository.v1.GalaxyRepository.DiscoverHierarchy:output_type -> galaxy_repository.v1.DiscoverHierarchyReply
|
3, // 12: galaxy_repository.v1.GalaxyRepository.GetLastDeployTime:output_type -> galaxy_repository.v1.GetLastDeployTimeReply
|
||||||
7, // 13: galaxy_repository.v1.GalaxyRepository.WatchDeployEvents:output_type -> galaxy_repository.v1.DeployEvent
|
5, // 13: galaxy_repository.v1.GalaxyRepository.DiscoverHierarchy:output_type -> galaxy_repository.v1.DiscoverHierarchyReply
|
||||||
10, // [10:14] is the sub-list for method output_type
|
7, // 14: galaxy_repository.v1.GalaxyRepository.WatchDeployEvents:output_type -> galaxy_repository.v1.DeployEvent
|
||||||
6, // [6:10] is the sub-list for method input_type
|
11, // [11:15] is the sub-list for method output_type
|
||||||
6, // [6:6] is the sub-list for extension type_name
|
7, // [7:11] is the sub-list for method input_type
|
||||||
6, // [6:6] is the sub-list for extension extendee
|
7, // [7:7] is the sub-list for extension type_name
|
||||||
0, // [0:6] is the sub-list for field type_name
|
7, // [7:7] is the sub-list for extension extendee
|
||||||
|
0, // [0:7] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_galaxy_repository_proto_init() }
|
func init() { file_galaxy_repository_proto_init() }
|
||||||
@@ -758,6 +945,11 @@ func file_galaxy_repository_proto_init() {
|
|||||||
if File_galaxy_repository_proto != nil {
|
if File_galaxy_repository_proto != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
file_galaxy_repository_proto_msgTypes[4].OneofWrappers = []any{
|
||||||
|
(*DiscoverHierarchyRequest_RootGobjectId)(nil),
|
||||||
|
(*DiscoverHierarchyRequest_RootTagName)(nil),
|
||||||
|
(*DiscoverHierarchyRequest_RootContainedPath)(nil),
|
||||||
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -19,10 +19,12 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MxAccessGateway_OpenSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/OpenSession"
|
MxAccessGateway_OpenSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/OpenSession"
|
||||||
MxAccessGateway_CloseSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/CloseSession"
|
MxAccessGateway_CloseSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/CloseSession"
|
||||||
MxAccessGateway_Invoke_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/Invoke"
|
MxAccessGateway_Invoke_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/Invoke"
|
||||||
MxAccessGateway_StreamEvents_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/StreamEvents"
|
MxAccessGateway_StreamEvents_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/StreamEvents"
|
||||||
|
MxAccessGateway_AcknowledgeAlarm_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/AcknowledgeAlarm"
|
||||||
|
MxAccessGateway_QueryActiveAlarms_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/QueryActiveAlarms"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MxAccessGatewayClient is the client API for MxAccessGateway service.
|
// MxAccessGatewayClient is the client API for MxAccessGateway service.
|
||||||
@@ -35,6 +37,8 @@ type MxAccessGatewayClient interface {
|
|||||||
CloseSession(ctx context.Context, in *CloseSessionRequest, opts ...grpc.CallOption) (*CloseSessionReply, error)
|
CloseSession(ctx context.Context, in *CloseSessionRequest, opts ...grpc.CallOption) (*CloseSessionReply, error)
|
||||||
Invoke(ctx context.Context, in *MxCommandRequest, opts ...grpc.CallOption) (*MxCommandReply, 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)
|
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 {
|
type mxAccessGatewayClient struct {
|
||||||
@@ -94,6 +98,35 @@ 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.
|
// 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]
|
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.
|
// MxAccessGatewayServer is the server API for MxAccessGateway service.
|
||||||
// All implementations must embed UnimplementedMxAccessGatewayServer
|
// All implementations must embed UnimplementedMxAccessGatewayServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@@ -104,6 +137,8 @@ type MxAccessGatewayServer interface {
|
|||||||
CloseSession(context.Context, *CloseSessionRequest) (*CloseSessionReply, error)
|
CloseSession(context.Context, *CloseSessionRequest) (*CloseSessionReply, error)
|
||||||
Invoke(context.Context, *MxCommandRequest) (*MxCommandReply, error)
|
Invoke(context.Context, *MxCommandRequest) (*MxCommandReply, error)
|
||||||
StreamEvents(*StreamEventsRequest, grpc.ServerStreamingServer[MxEvent]) error
|
StreamEvents(*StreamEventsRequest, grpc.ServerStreamingServer[MxEvent]) error
|
||||||
|
AcknowledgeAlarm(context.Context, *AcknowledgeAlarmRequest) (*AcknowledgeAlarmReply, error)
|
||||||
|
QueryActiveAlarms(*QueryActiveAlarmsRequest, grpc.ServerStreamingServer[ActiveAlarmSnapshot]) error
|
||||||
mustEmbedUnimplementedMxAccessGatewayServer()
|
mustEmbedUnimplementedMxAccessGatewayServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +161,12 @@ func (UnimplementedMxAccessGatewayServer) Invoke(context.Context, *MxCommandRequ
|
|||||||
func (UnimplementedMxAccessGatewayServer) StreamEvents(*StreamEventsRequest, grpc.ServerStreamingServer[MxEvent]) error {
|
func (UnimplementedMxAccessGatewayServer) StreamEvents(*StreamEventsRequest, grpc.ServerStreamingServer[MxEvent]) error {
|
||||||
return status.Error(codes.Unimplemented, "method StreamEvents not implemented")
|
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) mustEmbedUnimplementedMxAccessGatewayServer() {}
|
||||||
func (UnimplementedMxAccessGatewayServer) testEmbeddedByValue() {}
|
func (UnimplementedMxAccessGatewayServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -212,6 +253,35 @@ 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.
|
// 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]
|
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.
|
// MxAccessGateway_ServiceDesc is the grpc.ServiceDesc for MxAccessGateway service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -231,6 +301,10 @@ var MxAccessGateway_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "Invoke",
|
MethodName: "Invoke",
|
||||||
Handler: _MxAccessGateway_Invoke_Handler,
|
Handler: _MxAccessGateway_Invoke_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "AcknowledgeAlarm",
|
||||||
|
Handler: _MxAccessGateway_AcknowledgeAlarm_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{
|
Streams: []grpc.StreamDesc{
|
||||||
{
|
{
|
||||||
@@ -238,6 +312,11 @@ var MxAccessGateway_ServiceDesc = grpc.ServiceDesc{
|
|||||||
Handler: _MxAccessGateway_StreamEvents_Handler,
|
Handler: _MxAccessGateway_StreamEvents_Handler,
|
||||||
ServerStreams: true,
|
ServerStreams: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
StreamName: "QueryActiveAlarms",
|
||||||
|
Handler: _MxAccessGateway_QueryActiveAlarms_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Metadata: "mxaccess_gateway.proto",
|
Metadata: "mxaccess_gateway.proto",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
client, err := Dial(context.Background(), Options{
|
||||||
|
Endpoint: "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,3 +1,14 @@
|
|||||||
|
// 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
|
package mxgateway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -219,10 +230,15 @@ func resolveTransportCredentials(opts Options) (credentials.TransportCredentials
|
|||||||
|
|
||||||
// OpenSessionOptions describes fields used to create an OpenSessionRequest.
|
// OpenSessionOptions describes fields used to create an OpenSessionRequest.
|
||||||
type OpenSessionOptions struct {
|
type OpenSessionOptions struct {
|
||||||
RequestedBackend string
|
// RequestedBackend selects the gateway worker backend (empty for default).
|
||||||
ClientSessionName string
|
RequestedBackend string
|
||||||
|
// ClientSessionName is a human-readable name recorded on the session.
|
||||||
|
ClientSessionName string
|
||||||
|
// ClientCorrelationID echoes through gateway logs and replies for tracing.
|
||||||
ClientCorrelationID string
|
ClientCorrelationID string
|
||||||
CommandTimeout time.Duration
|
// CommandTimeout sets the per-command timeout the gateway forwards to the
|
||||||
|
// worker; zero leaves the gateway default in place.
|
||||||
|
CommandTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request returns the raw protobuf OpenSessionRequest for these options.
|
// Request returns the raw protobuf OpenSessionRequest for these options.
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ import (
|
|||||||
|
|
||||||
// GatewayError wraps transport-level gRPC failures.
|
// GatewayError wraps transport-level gRPC failures.
|
||||||
type GatewayError struct {
|
type GatewayError struct {
|
||||||
Op string
|
// Op names the operation that failed (for example "dial" or "invoke").
|
||||||
|
Op string
|
||||||
|
// Err is the underlying gRPC or transport error.
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error returns the formatted gateway error message.
|
||||||
func (e *GatewayError) Error() string {
|
func (e *GatewayError) Error() string {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -22,6 +25,7 @@ func (e *GatewayError) Error() string {
|
|||||||
return fmt.Sprintf("mxgateway: %s failed: %v", e.Op, e.Err)
|
return fmt.Sprintf("mxgateway: %s failed: %v", e.Op, e.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the wrapped transport error.
|
||||||
func (e *GatewayError) Unwrap() error {
|
func (e *GatewayError) Unwrap() error {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -32,11 +36,15 @@ func (e *GatewayError) Unwrap() error {
|
|||||||
// CommandError reports a non-OK gateway protocol status and keeps the raw
|
// CommandError reports a non-OK gateway protocol status and keeps the raw
|
||||||
// command reply when one exists.
|
// command reply when one exists.
|
||||||
type CommandError struct {
|
type CommandError struct {
|
||||||
Op string
|
// Op names the gateway operation that produced the non-OK status.
|
||||||
|
Op string
|
||||||
|
// Status carries the gateway-reported protocol status.
|
||||||
Status *ProtocolStatus
|
Status *ProtocolStatus
|
||||||
Reply *MxCommandReply
|
// Reply is the raw command reply, when one was returned alongside the status.
|
||||||
|
Reply *MxCommandReply
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error returns the formatted command error message.
|
||||||
func (e *CommandError) Error() string {
|
func (e *CommandError) Error() string {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -53,10 +61,13 @@ func (e *CommandError) Error() string {
|
|||||||
|
|
||||||
// MxAccessError reports HRESULT or MXSTATUS_PROXY failures returned by MXAccess.
|
// MxAccessError reports HRESULT or MXSTATUS_PROXY failures returned by MXAccess.
|
||||||
type MxAccessError struct {
|
type MxAccessError struct {
|
||||||
|
// Command is the wrapped CommandError when the protocol status carried one.
|
||||||
Command *CommandError
|
Command *CommandError
|
||||||
Reply *MxCommandReply
|
// Reply is the raw MXAccess command reply that surfaced the failure.
|
||||||
|
Reply *MxCommandReply
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error returns the formatted MXAccess error message.
|
||||||
func (e *MxAccessError) Error() string {
|
func (e *MxAccessError) Error() string {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -73,6 +84,7 @@ func (e *MxAccessError) Error() string {
|
|||||||
return "mxgateway: MXAccess command failed"
|
return "mxgateway: MXAccess command failed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the wrapped CommandError, when one is present.
|
||||||
func (e *MxAccessError) Unwrap() error {
|
func (e *MxAccessError) Unwrap() error {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -20,16 +20,26 @@ type RawGalaxyRepositoryClient = pb.GalaxyRepositoryClient
|
|||||||
|
|
||||||
// Generated protobuf aliases for Galaxy Repository messages.
|
// Generated protobuf aliases for Galaxy Repository messages.
|
||||||
type (
|
type (
|
||||||
TestConnectionRequest = pb.TestConnectionRequest
|
// TestConnectionRequest is the request for Galaxy Repository TestConnection.
|
||||||
TestConnectionReply = pb.TestConnectionReply
|
TestConnectionRequest = pb.TestConnectionRequest
|
||||||
GetLastDeployTimeRequest = pb.GetLastDeployTimeRequest
|
// TestConnectionReply is the reply for Galaxy Repository TestConnection.
|
||||||
GetLastDeployTimeReply = pb.GetLastDeployTimeReply
|
TestConnectionReply = pb.TestConnectionReply
|
||||||
DiscoverHierarchyRequest = pb.DiscoverHierarchyRequest
|
// GetLastDeployTimeRequest is the request for GetLastDeployTime.
|
||||||
DiscoverHierarchyReply = pb.DiscoverHierarchyReply
|
GetLastDeployTimeRequest = pb.GetLastDeployTimeRequest
|
||||||
GalaxyObject = pb.GalaxyObject
|
// GetLastDeployTimeReply is the reply for GetLastDeployTime.
|
||||||
GalaxyAttribute = pb.GalaxyAttribute
|
GetLastDeployTimeReply = pb.GetLastDeployTimeReply
|
||||||
WatchDeployEventsRequest = pb.WatchDeployEventsRequest
|
// DiscoverHierarchyRequest is the request for DiscoverHierarchy.
|
||||||
DeployEvent = pb.DeployEvent
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
// RawDeployEventStream is the generated WatchDeployEvents client stream.
|
// RawDeployEventStream is the generated WatchDeployEvents client stream.
|
||||||
|
|||||||
@@ -11,16 +11,29 @@ import (
|
|||||||
|
|
||||||
// Options configures gateway connections.
|
// Options configures gateway connections.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Endpoint string
|
// Endpoint is the gateway host:port address to dial.
|
||||||
APIKey string
|
Endpoint string
|
||||||
Plaintext bool
|
// APIKey is the bearer token attached to outgoing gRPC metadata.
|
||||||
CACertFile string
|
APIKey string
|
||||||
ServerNameOverride string
|
// Plaintext disables TLS and uses insecure credentials when true.
|
||||||
DialTimeout time.Duration
|
Plaintext bool
|
||||||
CallTimeout time.Duration
|
// CACertFile points to a PEM file used to verify the gateway certificate.
|
||||||
TLSConfig *tls.Config
|
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.
|
||||||
TransportCredentials credentials.TransportCredentials
|
TransportCredentials credentials.TransportCredentials
|
||||||
DialOptions []grpc.DialOption
|
// DialOptions are appended to the gRPC dial options after the defaults.
|
||||||
|
DialOptions []grpc.DialOption
|
||||||
}
|
}
|
||||||
|
|
||||||
// RedactedAPIKey returns a display-safe representation of the configured API
|
// RedactedAPIKey returns a display-safe representation of the configured API
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ const maxBulkItems = 1000
|
|||||||
|
|
||||||
// EventResult carries either the next ordered event or a terminal stream error.
|
// EventResult carries either the next ordered event or a terminal stream error.
|
||||||
type EventResult struct {
|
type EventResult struct {
|
||||||
|
// Event is the next event from the stream when Err is nil.
|
||||||
Event *MxEvent
|
Event *MxEvent
|
||||||
Err error
|
// Err is the terminal stream error; when non-nil no further results follow.
|
||||||
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventSubscription owns a running gateway event stream.
|
// EventSubscription owns a running gateway event stream.
|
||||||
|
|||||||
+148
-58
@@ -12,77 +12,167 @@ type RawEventStream = pb.MxAccessGateway_StreamEventsClient
|
|||||||
// Generated protobuf aliases keep raw contract access available from the public
|
// Generated protobuf aliases keep raw contract access available from the public
|
||||||
// mxgateway package while generated code remains under internal/generated.
|
// mxgateway package while generated code remains under internal/generated.
|
||||||
type (
|
type (
|
||||||
OpenSessionRequest = pb.OpenSessionRequest
|
// OpenSessionRequest is the gateway OpenSession request message.
|
||||||
OpenSessionReply = pb.OpenSessionReply
|
OpenSessionRequest = pb.OpenSessionRequest
|
||||||
CloseSessionRequest = pb.CloseSessionRequest
|
// OpenSessionReply is the gateway OpenSession reply message.
|
||||||
CloseSessionReply = pb.CloseSessionReply
|
OpenSessionReply = pb.OpenSessionReply
|
||||||
StreamEventsRequest = pb.StreamEventsRequest
|
// CloseSessionRequest is the gateway CloseSession request message.
|
||||||
MxCommandRequest = pb.MxCommandRequest
|
CloseSessionRequest = pb.CloseSessionRequest
|
||||||
MxCommandReply = pb.MxCommandReply
|
// CloseSessionReply is the gateway CloseSession reply message.
|
||||||
MxCommand = pb.MxCommand
|
CloseSessionReply = pb.CloseSessionReply
|
||||||
MxEvent = pb.MxEvent
|
// StreamEventsRequest is the gateway StreamEvents request message.
|
||||||
MxValue = pb.MxValue
|
StreamEventsRequest = pb.StreamEventsRequest
|
||||||
Value = pb.MxValue
|
// MxCommandRequest carries one MXAccess command for Invoke.
|
||||||
MxArray = pb.MxArray
|
MxCommandRequest = pb.MxCommandRequest
|
||||||
MxStatusProxy = pb.MxStatusProxy
|
// MxCommandReply is the reply to an MXAccess command Invoke.
|
||||||
ProtocolStatus = pb.ProtocolStatus
|
MxCommandReply = pb.MxCommandReply
|
||||||
RegisterCommand = pb.RegisterCommand
|
// MxCommand is the discriminated union of MXAccess command payloads.
|
||||||
UnregisterCommand = pb.UnregisterCommand
|
MxCommand = pb.MxCommand
|
||||||
AddItemCommand = pb.AddItemCommand
|
// MxEvent is one ordered event delivered on a session event stream.
|
||||||
AddItem2Command = pb.AddItem2Command
|
MxEvent = pb.MxEvent
|
||||||
RemoveItemCommand = pb.RemoveItemCommand
|
// MxValue is the protobuf representation of an MXAccess value.
|
||||||
AdviseCommand = pb.AdviseCommand
|
MxValue = pb.MxValue
|
||||||
UnAdviseCommand = pb.UnAdviseCommand
|
// Value is an alias for MxValue retained for symmetry with other clients.
|
||||||
AddItemBulkCommand = pb.AddItemBulkCommand
|
Value = pb.MxValue
|
||||||
AdviseItemBulkCommand = pb.AdviseItemBulkCommand
|
// MxArray is the protobuf representation of an MXAccess array value.
|
||||||
RemoveItemBulkCommand = pb.RemoveItemBulkCommand
|
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.
|
||||||
UnAdviseItemBulkCommand = pb.UnAdviseItemBulkCommand
|
UnAdviseItemBulkCommand = pb.UnAdviseItemBulkCommand
|
||||||
SubscribeBulkCommand = pb.SubscribeBulkCommand
|
// SubscribeBulkCommand combines AddItem and Advise for a list of tags.
|
||||||
UnsubscribeBulkCommand = pb.UnsubscribeBulkCommand
|
SubscribeBulkCommand = pb.SubscribeBulkCommand
|
||||||
WriteCommand = pb.WriteCommand
|
// UnsubscribeBulkCommand combines UnAdvise and RemoveItem for a list of items.
|
||||||
Write2Command = pb.Write2Command
|
UnsubscribeBulkCommand = pb.UnsubscribeBulkCommand
|
||||||
RegisterReply = pb.RegisterReply
|
// WriteCommand is the payload of an MXAccess Write command.
|
||||||
AddItemReply = pb.AddItemReply
|
WriteCommand = pb.WriteCommand
|
||||||
AddItem2Reply = pb.AddItem2Reply
|
// Write2Command is the payload of an MXAccess Write2 command.
|
||||||
SubscribeResult = pb.SubscribeResult
|
Write2Command = pb.Write2Command
|
||||||
BulkSubscribeReply = pb.BulkSubscribeReply
|
// 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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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 (
|
type (
|
||||||
MxCommandKind = pb.MxCommandKind
|
// MxCommandKind discriminates which MXAccess command an MxCommand carries.
|
||||||
MxDataType = pb.MxDataType
|
MxCommandKind = pb.MxCommandKind
|
||||||
MxEventFamily = pb.MxEventFamily
|
// MxDataType is the MXAccess data type tag on values and arrays.
|
||||||
MxStatusCategory = pb.MxStatusCategory
|
MxDataType = pb.MxDataType
|
||||||
MxStatusSource = pb.MxStatusSource
|
// 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.
|
||||||
ProtocolStatusCode = pb.ProtocolStatusCode
|
ProtocolStatusCode = pb.ProtocolStatusCode
|
||||||
SessionState = pb.SessionState
|
// SessionState enumerates gateway session lifecycle states.
|
||||||
|
SessionState = pb.SessionState
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MXAccess command kind, data type, and protocol status constants surfaced
|
||||||
|
// from the generated contract.
|
||||||
const (
|
const (
|
||||||
CommandKindRegister = pb.MxCommandKind_MX_COMMAND_KIND_REGISTER
|
// CommandKindRegister selects the MXAccess Register command.
|
||||||
CommandKindUnregister = pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER
|
CommandKindRegister = pb.MxCommandKind_MX_COMMAND_KIND_REGISTER
|
||||||
CommandKindAddItem = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM
|
// CommandKindUnregister selects the MXAccess Unregister command.
|
||||||
CommandKindAddItem2 = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2
|
CommandKindUnregister = pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER
|
||||||
CommandKindRemoveItem = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM
|
// CommandKindAddItem selects the MXAccess AddItem command.
|
||||||
CommandKindAdvise = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE
|
CommandKindAddItem = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM
|
||||||
CommandKindUnAdvise = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE
|
// CommandKindAddItem2 selects the MXAccess AddItem2 command.
|
||||||
CommandKindAddItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM_BULK
|
CommandKindAddItem2 = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2
|
||||||
CommandKindAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_ITEM_BULK
|
// CommandKindRemoveItem selects the MXAccess RemoveItem command.
|
||||||
CommandKindRemoveItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM_BULK
|
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.
|
||||||
CommandKindUnAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK
|
CommandKindUnAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK
|
||||||
CommandKindSubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK
|
// CommandKindSubscribeBulk selects the AddItem+Advise combined bulk command.
|
||||||
CommandKindUnsubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_UNSUBSCRIBE_BULK
|
CommandKindSubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK
|
||||||
CommandKindWrite = pb.MxCommandKind_MX_COMMAND_KIND_WRITE
|
// CommandKindUnsubscribeBulk selects the UnAdvise+RemoveItem combined bulk command.
|
||||||
CommandKindWrite2 = pb.MxCommandKind_MX_COMMAND_KIND_WRITE2
|
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
|
||||||
|
|
||||||
|
// DataTypeUnknown denotes an unrecognized MXAccess data type.
|
||||||
DataTypeUnknown = pb.MxDataType_MX_DATA_TYPE_UNKNOWN
|
DataTypeUnknown = pb.MxDataType_MX_DATA_TYPE_UNKNOWN
|
||||||
|
// DataTypeBoolean denotes an MXAccess Boolean value.
|
||||||
DataTypeBoolean = pb.MxDataType_MX_DATA_TYPE_BOOLEAN
|
DataTypeBoolean = pb.MxDataType_MX_DATA_TYPE_BOOLEAN
|
||||||
|
// DataTypeInteger denotes an MXAccess Integer value.
|
||||||
DataTypeInteger = pb.MxDataType_MX_DATA_TYPE_INTEGER
|
DataTypeInteger = pb.MxDataType_MX_DATA_TYPE_INTEGER
|
||||||
DataTypeFloat = pb.MxDataType_MX_DATA_TYPE_FLOAT
|
// DataTypeFloat denotes an MXAccess Float (single precision) value.
|
||||||
DataTypeDouble = pb.MxDataType_MX_DATA_TYPE_DOUBLE
|
DataTypeFloat = pb.MxDataType_MX_DATA_TYPE_FLOAT
|
||||||
DataTypeString = pb.MxDataType_MX_DATA_TYPE_STRING
|
// DataTypeDouble denotes an MXAccess Double (double precision) value.
|
||||||
DataTypeTime = pb.MxDataType_MX_DATA_TYPE_TIME
|
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
|
||||||
|
|
||||||
ProtocolStatusOK = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK
|
// ProtocolStatusOK indicates the gateway processed the request successfully.
|
||||||
|
ProtocolStatusOK = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK
|
||||||
|
// ProtocolStatusMxAccessFailure indicates the worker reported an MXAccess failure.
|
||||||
ProtocolStatusMxAccessFailure = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE
|
ProtocolStatusMxAccessFailure = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const (
|
|||||||
|
|
||||||
// GatewayProtocolVersion matches GatewayContractInfo.GatewayProtocolVersion
|
// GatewayProtocolVersion matches GatewayContractInfo.GatewayProtocolVersion
|
||||||
// in the shared .NET contracts.
|
// in the shared .NET contracts.
|
||||||
GatewayProtocolVersion uint32 = 1
|
GatewayProtocolVersion uint32 = 3
|
||||||
|
|
||||||
// WorkerProtocolVersion matches GatewayContractInfo.WorkerProtocolVersion
|
// WorkerProtocolVersion matches GatewayContractInfo.WorkerProtocolVersion
|
||||||
// and is exposed for fake-worker and parity tests.
|
// and is exposed for fake-worker and parity tests.
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
Provide a Java client library for MXAccess Gateway, plus a test CLI and unit
|
Provide a Java client library for MXAccess Gateway, plus a test CLI and unit
|
||||||
tests. The Java client should work for JVM services and operator tooling.
|
tests. The Java client should work for JVM services and operator tooling.
|
||||||
|
|
||||||
Follow the [Java Style Guide](./style-guides/JavaStyleGuide.md) for handwritten
|
Follow the [Java Style Guide](../../docs/style-guides/JavaStyleGuide.md) for handwritten
|
||||||
code and the [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md) for
|
code and the [Protobuf Style Guide](../../docs/style-guides/ProtobufStyleGuide.md) for
|
||||||
generated contract inputs.
|
generated contract inputs.
|
||||||
|
|
||||||
## Build Layout
|
## Build Layout
|
||||||
@@ -210,3 +210,10 @@ The `mxgateway-client` project generates the gateway and worker protobuf/gRPC
|
|||||||
bindings into `src/main/generated`, compiles the generated contracts, and runs
|
bindings into `src/main/generated`, compiles the generated contracts, and runs
|
||||||
JUnit 5 tests. The `mxgateway-cli` project builds a Picocli-based `mxgw-java`
|
JUnit 5 tests. The `mxgateway-cli` project builds a Picocli-based `mxgw-java`
|
||||||
entry point for later command implementation.
|
entry point for later command implementation.
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Client Libraries Detailed Design](../../docs/ClientLibrariesDesign.md)
|
||||||
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
||||||
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
|
- [Java Style Guide](../../docs/style-guides/JavaStyleGuide.md)
|
||||||
@@ -223,6 +223,6 @@ gradle :mxgateway-cli:run --args="smoke --endpoint $env:MXGATEWAY_ENDPOINT --pla
|
|||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
||||||
- [Java Client Detailed Design](../../docs/clients-java-design.md)
|
- [Java Client Detailed Design](./JavaClientDesign.md)
|
||||||
- [Java Style Guide](../../docs/style-guides/JavaStyleGuide.md)
|
- [Java Style Guide](../../docs/style-guides/JavaStyleGuide.md)
|
||||||
|
|||||||
+24
@@ -38,6 +38,10 @@ import picocli.CommandLine.Model.CommandSpec;
|
|||||||
import picocli.CommandLine.Option;
|
import picocli.CommandLine.Option;
|
||||||
import picocli.CommandLine.Spec;
|
import picocli.CommandLine.Spec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picocli entry point for the {@code mxgw-java} test CLI used by the
|
||||||
|
* cross-language smoke matrix.
|
||||||
|
*/
|
||||||
@Command(
|
@Command(
|
||||||
name = "mxgw-java",
|
name = "mxgw-java",
|
||||||
mixinStandardHelpOptions = true,
|
mixinStandardHelpOptions = true,
|
||||||
@@ -48,6 +52,9 @@ public final class MxGatewayCli implements Callable<Integer> {
|
|||||||
@Spec
|
@Spec
|
||||||
private CommandSpec spec;
|
private CommandSpec spec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a CLI bound to the default gRPC client factory.
|
||||||
|
*/
|
||||||
public MxGatewayCli() {
|
public MxGatewayCli() {
|
||||||
this(new GrpcMxGatewayCliClientFactory());
|
this(new GrpcMxGatewayCliClientFactory());
|
||||||
}
|
}
|
||||||
@@ -56,11 +63,25 @@ public final class MxGatewayCli implements Callable<Integer> {
|
|||||||
this.clientFactory = clientFactory;
|
this.clientFactory = clientFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process entry point.
|
||||||
|
*
|
||||||
|
* @param args command-line arguments
|
||||||
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
int exitCode = commandLine(new GrpcMxGatewayCliClientFactory()).execute(args);
|
int exitCode = commandLine(new GrpcMxGatewayCliClientFactory()).execute(args);
|
||||||
System.exit(exitCode);
|
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) {
|
public static int execute(PrintWriter out, PrintWriter err, String... args) {
|
||||||
return execute(new GrpcMxGatewayCliClientFactory(), out, err, args);
|
return execute(new GrpcMxGatewayCliClientFactory(), out, err, args);
|
||||||
}
|
}
|
||||||
@@ -280,6 +301,9 @@ public final class MxGatewayCli implements Callable<Integer> {
|
|||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picocli subcommand that prints the client and protocol version numbers.
|
||||||
|
*/
|
||||||
@Command(name = "version", description = "Prints the Java client version.")
|
@Command(name = "version", description = "Prints the Java client version.")
|
||||||
public static final class VersionCommand implements Callable<Integer> {
|
public static final class VersionCommand implements Callable<Integer> {
|
||||||
@Spec
|
@Spec
|
||||||
|
|||||||
+2
-2
@@ -32,7 +32,7 @@ final class MxGatewayCliTests {
|
|||||||
assertEquals(0, run.exitCode());
|
assertEquals(0, run.exitCode());
|
||||||
assertEquals("", run.errors());
|
assertEquals("", run.errors());
|
||||||
assertTrue(run.output().contains("mxgateway-java 0.1.0"));
|
assertTrue(run.output().contains("mxgateway-java 0.1.0"));
|
||||||
assertTrue(run.output().contains("gatewayProtocolVersion=1"));
|
assertTrue(run.output().contains("gatewayProtocolVersion=3"));
|
||||||
assertTrue(run.output().contains("workerProtocolVersion=1"));
|
assertTrue(run.output().contains("workerProtocolVersion=1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ final class MxGatewayCliTests {
|
|||||||
|
|
||||||
assertEquals(0, run.exitCode());
|
assertEquals(0, run.exitCode());
|
||||||
assertTrue(run.output().contains("\"clientVersion\":\"0.1.0\""));
|
assertTrue(run.output().contains("\"clientVersion\":\"0.1.0\""));
|
||||||
assertTrue(run.output().contains("\"gatewayProtocolVersion\":1"));
|
assertTrue(run.output().contains("\"gatewayProtocolVersion\":3"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+7
-3
@@ -11,6 +11,7 @@ import java.util.NoSuchElementException;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Iterator-style adaptor over the {@code WatchDeployEvents} server-streaming
|
* Iterator-style adaptor over the {@code WatchDeployEvents} server-streaming
|
||||||
@@ -22,8 +23,8 @@ public final class DeployEventStream implements Iterator<DeployEvent>, AutoClose
|
|||||||
private static final Object END = new Object();
|
private static final Object END = new Object();
|
||||||
|
|
||||||
private final BlockingQueue<Object> queue;
|
private final BlockingQueue<Object> queue;
|
||||||
|
private final AtomicBoolean closed = new AtomicBoolean();
|
||||||
private volatile ClientCallStreamObserver<WatchDeployEventsRequest> requestStream;
|
private volatile ClientCallStreamObserver<WatchDeployEventsRequest> requestStream;
|
||||||
private volatile boolean closed;
|
|
||||||
private Object next;
|
private Object next;
|
||||||
|
|
||||||
DeployEventStream(int capacity) {
|
DeployEventStream(int capacity) {
|
||||||
@@ -35,6 +36,9 @@ public final class DeployEventStream implements Iterator<DeployEvent>, AutoClose
|
|||||||
@Override
|
@Override
|
||||||
public void beforeStart(ClientCallStreamObserver<WatchDeployEventsRequest> requestStream) {
|
public void beforeStart(ClientCallStreamObserver<WatchDeployEventsRequest> requestStream) {
|
||||||
DeployEventStream.this.requestStream = requestStream;
|
DeployEventStream.this.requestStream = requestStream;
|
||||||
|
if (closed.get()) {
|
||||||
|
requestStream.cancel("client cancelled deploy event stream", null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -44,7 +48,7 @@ public final class DeployEventStream implements Iterator<DeployEvent>, AutoClose
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(Throwable error) {
|
public void onError(Throwable error) {
|
||||||
if (Status.fromThrowable(error).getCode() == Status.Code.CANCELLED && closed) {
|
if (Status.fromThrowable(error).getCode() == Status.Code.CANCELLED && closed.get()) {
|
||||||
offer(END);
|
offer(END);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -90,7 +94,7 @@ public final class DeployEventStream implements Iterator<DeployEvent>, AutoClose
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
closed = true;
|
closed.set(true);
|
||||||
ClientCallStreamObserver<WatchDeployEventsRequest> stream = requestStream;
|
ClientCallStreamObserver<WatchDeployEventsRequest> stream = requestStream;
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
stream.cancel("client cancelled deploy event stream", null);
|
stream.cancel("client cancelled deploy event stream", null);
|
||||||
|
|||||||
+5
@@ -45,6 +45,11 @@ 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() {
|
public void cancel() {
|
||||||
cancelled.set(true);
|
cancelled.set(true);
|
||||||
ClientCallStreamObserver<WatchDeployEventsRequest> stream = requestStream.get();
|
ClientCallStreamObserver<WatchDeployEventsRequest> stream = requestStream.get();
|
||||||
|
|||||||
+120
-16
@@ -36,6 +36,8 @@ import javax.net.ssl.SSLException;
|
|||||||
* {@link MxGatewayClient}.
|
* {@link MxGatewayClient}.
|
||||||
*/
|
*/
|
||||||
public final class GalaxyRepositoryClient implements AutoCloseable {
|
public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||||
|
private static final int DISCOVER_HIERARCHY_PAGE_SIZE = 5000;
|
||||||
|
|
||||||
private final ManagedChannel ownedChannel;
|
private final ManagedChannel ownedChannel;
|
||||||
private final MxGatewayClientOptions options;
|
private final MxGatewayClientOptions options;
|
||||||
private final GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub blockingStub;
|
private final GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub blockingStub;
|
||||||
@@ -52,8 +54,12 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a client over a caller-managed {@link Channel}. The caller owns
|
* Constructs a client over a caller-managed {@link Channel}. The caller owns
|
||||||
* channel lifecycle; {@link #close()} is a no-op for this constructor.
|
* 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) {
|
public GalaxyRepositoryClient(Channel channel, MxGatewayClientOptions options) {
|
||||||
this.ownedChannel = null;
|
this.ownedChannel = null;
|
||||||
@@ -64,25 +70,49 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
asyncStub = GalaxyRepositoryGrpc.newStub(intercepted);
|
asyncStub = GalaxyRepositoryGrpc.newStub(intercepted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build a new client and own its channel; close shuts the channel down. */
|
/**
|
||||||
|
* 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 GalaxyRepositoryClient connect(MxGatewayClientOptions options) {
|
public static GalaxyRepositoryClient connect(MxGatewayClientOptions options) {
|
||||||
return new GalaxyRepositoryClient(createChannel(options), 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() {
|
public GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub rawBlockingStub() {
|
||||||
return withDeadline(blockingStub);
|
return withDeadline(blockingStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying future stub with the per-call deadline applied.
|
||||||
|
*
|
||||||
|
* @return the future stub
|
||||||
|
*/
|
||||||
public GalaxyRepositoryGrpc.GalaxyRepositoryFutureStub rawFutureStub() {
|
public GalaxyRepositoryGrpc.GalaxyRepositoryFutureStub rawFutureStub() {
|
||||||
return withDeadline(futureStub);
|
return withDeadline(futureStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying async stub. Stream deadlines are applied per call.
|
||||||
|
*
|
||||||
|
* @return the async stub
|
||||||
|
*/
|
||||||
public GalaxyRepositoryGrpc.GalaxyRepositoryStub rawAsyncStub() {
|
public GalaxyRepositoryGrpc.GalaxyRepositoryStub rawAsyncStub() {
|
||||||
return asyncStub;
|
return asyncStub;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke the {@code TestConnection} RPC and return the {@code ok} flag.
|
* 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
|
||||||
*/
|
*/
|
||||||
public boolean testConnection() {
|
public boolean testConnection() {
|
||||||
try {
|
try {
|
||||||
@@ -96,14 +126,23 @@ 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() {
|
public CompletableFuture<Boolean> testConnectionAsync() {
|
||||||
return toCompletable(rawFutureStub().testConnection(TestConnectionRequest.getDefaultInstance()))
|
return toCompletable(rawFutureStub().testConnection(TestConnectionRequest.getDefaultInstance()))
|
||||||
.thenApply(TestConnectionReply::getOk);
|
.thenApply(TestConnectionReply::getOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke the {@code GetLastDeployTime} RPC. Returns {@link Optional#empty()}
|
* Invokes the {@code GetLastDeployTime} RPC.
|
||||||
* when the server reports {@code present=false}.
|
*
|
||||||
|
* @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
|
||||||
*/
|
*/
|
||||||
public Optional<Instant> getLastDeployTime() {
|
public Optional<Instant> getLastDeployTime() {
|
||||||
try {
|
try {
|
||||||
@@ -118,21 +157,44 @@ 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() {
|
public CompletableFuture<Optional<Instant>> getLastDeployTimeAsync() {
|
||||||
return toCompletable(rawFutureStub().getLastDeployTime(GetLastDeployTimeRequest.getDefaultInstance()))
|
return toCompletable(rawFutureStub().getLastDeployTime(GetLastDeployTimeRequest.getDefaultInstance()))
|
||||||
.thenApply(GalaxyRepositoryClient::mapDeployTime);
|
.thenApply(GalaxyRepositoryClient::mapDeployTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke the {@code DiscoverHierarchy} RPC and return the generated
|
* Invokes the {@code DiscoverHierarchy} RPC and returns the generated
|
||||||
* {@link GalaxyObject} messages directly. Callers can read every field of
|
* {@link GalaxyObject} messages directly. Callers can read every field of
|
||||||
* the proto message without an extra DTO layer.
|
* the proto message without an extra DTO layer.
|
||||||
|
*
|
||||||
|
* @return the Galaxy object hierarchy
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
*/
|
*/
|
||||||
public List<GalaxyObject> discoverHierarchy() {
|
public List<GalaxyObject> discoverHierarchy() {
|
||||||
try {
|
try {
|
||||||
DiscoverHierarchyReply reply =
|
java.util.ArrayList<GalaxyObject> objects = new java.util.ArrayList<>();
|
||||||
rawBlockingStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance());
|
java.util.HashSet<String> seenPageTokens = new java.util.HashSet<>();
|
||||||
return reply.getObjectsList();
|
String pageToken = "";
|
||||||
|
do {
|
||||||
|
DiscoverHierarchyReply reply = rawBlockingStub().discoverHierarchy(DiscoverHierarchyRequest.newBuilder()
|
||||||
|
.setPageSize(DISCOVER_HIERARCHY_PAGE_SIZE)
|
||||||
|
.setPageToken(pageToken)
|
||||||
|
.build());
|
||||||
|
objects.addAll(reply.getObjectsList());
|
||||||
|
pageToken = reply.getNextPageToken();
|
||||||
|
if (!pageToken.isBlank() && !seenPageTokens.add(pageToken)) {
|
||||||
|
throw new MxGatewayException(
|
||||||
|
"galaxy discover hierarchy returned repeated page token: " + pageToken);
|
||||||
|
}
|
||||||
|
} while (!pageToken.isBlank());
|
||||||
|
return objects;
|
||||||
} catch (RuntimeException error) {
|
} catch (RuntimeException error) {
|
||||||
if (error instanceof MxGatewayException) {
|
if (error instanceof MxGatewayException) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -141,18 +203,24 @@ 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() {
|
public CompletableFuture<List<GalaxyObject>> discoverHierarchyAsync() {
|
||||||
return toCompletable(rawFutureStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance()))
|
return discoverHierarchyPageAsync("", new java.util.ArrayList<>(), new java.util.HashSet<>());
|
||||||
.thenApply(DiscoverHierarchyReply::getObjectsList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to {@code WatchDeployEvents} via the async stub and consume
|
* Subscribes to {@code WatchDeployEvents} via the async stub and consumes
|
||||||
* results through a blocking iterator. Closing the returned stream cancels
|
* results through a blocking iterator. Closing the returned stream cancels
|
||||||
* the underlying gRPC call.
|
* the underlying gRPC call.
|
||||||
*
|
*
|
||||||
* @param lastSeenDeployTime optional. When non-null, the bootstrap event is
|
* @param lastSeenDeployTime optional. When non-{@code null}, the bootstrap
|
||||||
* suppressed if the cached deploy time matches.
|
* event is suppressed if the cached deploy time matches.
|
||||||
|
* @return an iterator-style stream of deploy events
|
||||||
*/
|
*/
|
||||||
public DeployEventStream watchDeployEvents(Instant lastSeenDeployTime) {
|
public DeployEventStream watchDeployEvents(Instant lastSeenDeployTime) {
|
||||||
DeployEventStream stream = new DeployEventStream(16);
|
DeployEventStream stream = new DeployEventStream(16);
|
||||||
@@ -163,15 +231,23 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
/**
|
/**
|
||||||
* Iterator-style alias for {@link #watchDeployEvents(Instant)} matching the
|
* Iterator-style alias for {@link #watchDeployEvents(Instant)} matching the
|
||||||
* task-spec signature.
|
* task-spec signature.
|
||||||
|
*
|
||||||
|
* @param lastSeenDeployTime optional cached deploy time for bootstrap suppression
|
||||||
|
* @return an iterator over deploy events
|
||||||
*/
|
*/
|
||||||
public Iterator<DeployEvent> watchDeployEventsIterator(Instant lastSeenDeployTime) {
|
public Iterator<DeployEvent> watchDeployEventsIterator(Instant lastSeenDeployTime) {
|
||||||
return watchDeployEvents(lastSeenDeployTime);
|
return watchDeployEvents(lastSeenDeployTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to {@code WatchDeployEvents} via the async stub, dispatching
|
* Subscribes to {@code WatchDeployEvents} via the async stub, dispatching
|
||||||
* each event to {@code observer}. The returned subscription is cancellable
|
* each event to {@code observer}. The returned subscription is cancellable
|
||||||
* and {@link AutoCloseable}.
|
* 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(
|
public DeployEventSubscription watchDeployEventsAsync(
|
||||||
Instant lastSeenDeployTime, StreamObserver<DeployEvent> observer) {
|
Instant lastSeenDeployTime, StreamObserver<DeployEvent> observer) {
|
||||||
@@ -207,6 +283,13 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
public void closeAndAwaitTermination() throws InterruptedException {
|
||||||
if (ownedChannel != null) {
|
if (ownedChannel != null) {
|
||||||
ownedChannel.shutdown();
|
ownedChannel.shutdown();
|
||||||
@@ -226,7 +309,7 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
|
|
||||||
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
||||||
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
||||||
.maxInboundMessageSize(16 * 1024 * 1024);
|
.maxInboundMessageSize(options.maxGrpcMessageBytes());
|
||||||
if (!options.connectTimeout().isNegative()) {
|
if (!options.connectTimeout().isNegative()) {
|
||||||
builder.withOption(
|
builder.withOption(
|
||||||
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||||
@@ -258,6 +341,27 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
return stub.withDeadlineAfter(options.callTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
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 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) {
|
private static <T> CompletableFuture<T> toCompletable(com.google.common.util.concurrent.ListenableFuture<T> source) {
|
||||||
CompletableFuture<T> target = new CompletableFuture<>();
|
CompletableFuture<T> target = new CompletableFuture<>();
|
||||||
Futures.addCallback(
|
Futures.addCallback(
|
||||||
|
|||||||
+18
@@ -3,11 +3,29 @@ package com.dohertylan.mxgateway.client;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
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 {
|
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) {
|
public MxAccessException(String operation, ProtocolStatus protocolStatus, MxCommandReply reply) {
|
||||||
super(operation, protocolStatus, 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) {
|
public MxAccessException(String operation, MxCommandReply reply) {
|
||||||
super(operation, reply == null ? null : reply.getProtocolStatus(), reply);
|
super(operation, reply == null ? null : reply.getProtocolStatus(), reply);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -12,6 +12,16 @@ import java.util.concurrent.BlockingQueue;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
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}.
|
||||||
|
*/
|
||||||
public final class MxEventStream implements Iterator<MxEvent>, AutoCloseable {
|
public final class MxEventStream implements Iterator<MxEvent>, AutoCloseable {
|
||||||
private static final Object END = new Object();
|
private static final Object END = new Object();
|
||||||
|
|
||||||
|
|||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
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,12 +8,22 @@ import io.grpc.ForwardingClientCall;
|
|||||||
import io.grpc.Metadata;
|
import io.grpc.Metadata;
|
||||||
import io.grpc.MethodDescriptor;
|
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 {
|
public final class MxGatewayAuthInterceptor implements ClientInterceptor {
|
||||||
static final Metadata.Key<String> AUTHORIZATION_HEADER =
|
static final Metadata.Key<String> AUTHORIZATION_HEADER =
|
||||||
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
|
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
|
||||||
|
|
||||||
private final String apiKey;
|
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) {
|
public MxGatewayAuthInterceptor(String apiKey) {
|
||||||
this.apiKey = apiKey == null ? "" : apiKey;
|
this.apiKey = apiKey == null ? "" : apiKey;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -1,6 +1,16 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
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 {
|
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) {
|
public MxGatewayAuthenticationException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -1,6 +1,16 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
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 {
|
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) {
|
public MxGatewayAuthorizationException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|||||||
+177
-1
@@ -15,6 +15,9 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import javax.net.ssl.SSLException;
|
import javax.net.ssl.SSLException;
|
||||||
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
|
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.CloseSessionReply;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
|
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||||
@@ -23,8 +26,18 @@ import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
|
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
||||||
|
import mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
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 {
|
public final class MxGatewayClient implements AutoCloseable {
|
||||||
private final ManagedChannel ownedChannel;
|
private final ManagedChannel ownedChannel;
|
||||||
private final MxGatewayClientOptions options;
|
private final MxGatewayClientOptions options;
|
||||||
@@ -41,6 +54,14 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
|
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) {
|
public MxGatewayClient(Channel channel, MxGatewayClientOptions options) {
|
||||||
this.ownedChannel = null;
|
this.ownedChannel = null;
|
||||||
this.options = Objects.requireNonNull(options, "options");
|
this.options = Objects.requireNonNull(options, "options");
|
||||||
@@ -50,27 +71,64 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
|
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) {
|
public static MxGatewayClient connect(MxGatewayClientOptions options) {
|
||||||
return new MxGatewayClient(createChannel(options), 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() {
|
public MxAccessGatewayGrpc.MxAccessGatewayBlockingStub rawBlockingStub() {
|
||||||
return withDeadline(blockingStub);
|
return withDeadline(blockingStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying future stub with the per-call deadline applied.
|
||||||
|
*
|
||||||
|
* @return the future stub
|
||||||
|
*/
|
||||||
public MxAccessGatewayGrpc.MxAccessGatewayFutureStub rawFutureStub() {
|
public MxAccessGatewayGrpc.MxAccessGatewayFutureStub rawFutureStub() {
|
||||||
return withDeadline(futureStub);
|
return withDeadline(futureStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying async stub. Stream deadlines are applied per call.
|
||||||
|
*
|
||||||
|
* @return the async stub
|
||||||
|
*/
|
||||||
public MxAccessGatewayGrpc.MxAccessGatewayStub rawAsyncStub() {
|
public MxAccessGatewayGrpc.MxAccessGatewayStub rawAsyncStub() {
|
||||||
return asyncStub;
|
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) {
|
public MxGatewaySession openSession(OpenSessionRequest request) {
|
||||||
OpenSessionReply reply = openSessionRaw(request);
|
OpenSessionReply reply = openSessionRaw(request);
|
||||||
return new MxGatewaySession(this, reply);
|
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) {
|
public MxGatewaySession openSession(String clientSessionName) {
|
||||||
return openSession(OpenSessionRequest.newBuilder()
|
return openSession(OpenSessionRequest.newBuilder()
|
||||||
.setClientSessionName(clientSessionName)
|
.setClientSessionName(clientSessionName)
|
||||||
@@ -81,6 +139,13 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
.build());
|
.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) {
|
public OpenSessionReply openSessionRaw(OpenSessionRequest request) {
|
||||||
try {
|
try {
|
||||||
OpenSessionReply reply = rawBlockingStub().openSession(request);
|
OpenSessionReply reply = rawBlockingStub().openSession(request);
|
||||||
@@ -94,6 +159,13 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
public CompletableFuture<OpenSessionReply> openSessionAsync(OpenSessionRequest request) {
|
||||||
CompletableFuture<OpenSessionReply> future = toCompletable(rawFutureStub().openSession(request));
|
CompletableFuture<OpenSessionReply> future = toCompletable(rawFutureStub().openSession(request));
|
||||||
return future.thenApply(reply -> {
|
return future.thenApply(reply -> {
|
||||||
@@ -102,6 +174,15 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
public MxCommandReply invoke(MxCommandRequest request) {
|
||||||
try {
|
try {
|
||||||
MxCommandReply reply = rawBlockingStub().invoke(request);
|
MxCommandReply reply = rawBlockingStub().invoke(request);
|
||||||
@@ -116,6 +197,14 @@ 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) {
|
public CompletableFuture<MxCommandReply> invokeAsync(MxCommandRequest request) {
|
||||||
CompletableFuture<MxCommandReply> future = toCompletable(rawFutureStub().invoke(request));
|
CompletableFuture<MxCommandReply> future = toCompletable(rawFutureStub().invoke(request));
|
||||||
return future.thenApply(reply -> {
|
return future.thenApply(reply -> {
|
||||||
@@ -125,6 +214,13 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
public CloseSessionReply closeSessionRaw(CloseSessionRequest request) {
|
||||||
try {
|
try {
|
||||||
CloseSessionReply reply = rawBlockingStub().closeSession(request);
|
CloseSessionReply reply = rawBlockingStub().closeSession(request);
|
||||||
@@ -138,12 +234,28 @@ 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) {
|
public MxEventStream streamEvents(StreamEventsRequest request) {
|
||||||
MxEventStream stream = new MxEventStream(16);
|
MxEventStream stream = new MxEventStream(16);
|
||||||
withStreamDeadline(rawAsyncStub()).streamEvents(request, stream.observer());
|
withStreamDeadline(rawAsyncStub()).streamEvents(request, stream.observer());
|
||||||
return stream;
|
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(
|
public MxGatewayEventSubscription streamEventsAsync(
|
||||||
StreamEventsRequest request, StreamObserver<MxEvent> observer) {
|
StreamEventsRequest request, StreamObserver<MxEvent> observer) {
|
||||||
MxGatewayEventSubscription subscription = new MxGatewayEventSubscription();
|
MxGatewayEventSubscription subscription = new MxGatewayEventSubscription();
|
||||||
@@ -151,6 +263,63 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acknowledges an active MXAccess alarm condition through the gateway.
|
||||||
|
*
|
||||||
|
* <p>The gateway authenticates the request against the API key's
|
||||||
|
* {@code 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 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 = toCompletable(rawFutureStub().acknowledgeAlarm(request));
|
||||||
|
return future.thenApply(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();
|
||||||
|
withStreamDeadline(rawAsyncStub()).queryActiveAlarms(request, subscription.wrap(observer));
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
if (ownedChannel != null) {
|
if (ownedChannel != null) {
|
||||||
@@ -158,6 +327,13 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
public void closeAndAwaitTermination() throws InterruptedException {
|
||||||
if (ownedChannel != null) {
|
if (ownedChannel != null) {
|
||||||
ownedChannel.shutdown();
|
ownedChannel.shutdown();
|
||||||
@@ -169,7 +345,7 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
|
|
||||||
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
||||||
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
||||||
.maxInboundMessageSize(16 * 1024 * 1024);
|
.maxInboundMessageSize(options.maxGrpcMessageBytes());
|
||||||
if (!options.connectTimeout().isNegative()) {
|
if (!options.connectTimeout().isNegative()) {
|
||||||
builder.withOption(
|
builder.withOption(
|
||||||
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||||
|
|||||||
+135
@@ -4,9 +4,17 @@ import java.nio.file.Path;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Objects;
|
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 {
|
public final class MxGatewayClientOptions {
|
||||||
private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
|
private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
|
||||||
private static final Duration DEFAULT_CALL_TIMEOUT = Duration.ofSeconds(30);
|
private static final Duration DEFAULT_CALL_TIMEOUT = Duration.ofSeconds(30);
|
||||||
|
private static final int DEFAULT_MAX_GRPC_MESSAGE_BYTES = 16 * 1024 * 1024;
|
||||||
|
|
||||||
private final String endpoint;
|
private final String endpoint;
|
||||||
private final String apiKey;
|
private final String apiKey;
|
||||||
@@ -16,6 +24,7 @@ public final class MxGatewayClientOptions {
|
|||||||
private final Duration connectTimeout;
|
private final Duration connectTimeout;
|
||||||
private final Duration callTimeout;
|
private final Duration callTimeout;
|
||||||
private final Duration streamTimeout;
|
private final Duration streamTimeout;
|
||||||
|
private final int maxGrpcMessageBytes;
|
||||||
|
|
||||||
private MxGatewayClientOptions(Builder builder) {
|
private MxGatewayClientOptions(Builder builder) {
|
||||||
endpoint = requireText(builder.endpoint, "endpoint");
|
endpoint = requireText(builder.endpoint, "endpoint");
|
||||||
@@ -26,48 +35,106 @@ public final class MxGatewayClientOptions {
|
|||||||
connectTimeout = builder.connectTimeout == null ? DEFAULT_CONNECT_TIMEOUT : builder.connectTimeout;
|
connectTimeout = builder.connectTimeout == null ? DEFAULT_CONNECT_TIMEOUT : builder.connectTimeout;
|
||||||
callTimeout = builder.callTimeout == null ? DEFAULT_CALL_TIMEOUT : builder.callTimeout;
|
callTimeout = builder.callTimeout == null ? DEFAULT_CALL_TIMEOUT : builder.callTimeout;
|
||||||
streamTimeout = builder.streamTimeout;
|
streamTimeout = builder.streamTimeout;
|
||||||
|
maxGrpcMessageBytes = builder.maxGrpcMessageBytes <= 0
|
||||||
|
? DEFAULT_MAX_GRPC_MESSAGE_BYTES
|
||||||
|
: builder.maxGrpcMessageBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a fresh builder with default timeouts and no endpoint set.
|
||||||
|
*
|
||||||
|
* @return a new {@link Builder}
|
||||||
|
*/
|
||||||
public static Builder builder() {
|
public static Builder builder() {
|
||||||
return new 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() {
|
public String endpoint() {
|
||||||
return endpoint;
|
return endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the configured API key, or an empty string if none was supplied.
|
||||||
|
*
|
||||||
|
* @return the raw API key
|
||||||
|
*/
|
||||||
public String apiKey() {
|
public String apiKey() {
|
||||||
return 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() {
|
public String redactedApiKey() {
|
||||||
return MxGatewaySecrets.redactApiKey(apiKey);
|
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() {
|
public boolean plaintext() {
|
||||||
return 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() {
|
public Path caCertificatePath() {
|
||||||
return 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() {
|
public String serverNameOverride() {
|
||||||
return serverNameOverride;
|
return serverNameOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the channel connect timeout.
|
||||||
|
*
|
||||||
|
* @return the connect timeout duration
|
||||||
|
*/
|
||||||
public Duration connectTimeout() {
|
public Duration connectTimeout() {
|
||||||
return connectTimeout;
|
return connectTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the per-call deadline applied to unary RPCs.
|
||||||
|
*
|
||||||
|
* @return the call timeout duration
|
||||||
|
*/
|
||||||
public Duration callTimeout() {
|
public Duration callTimeout() {
|
||||||
return 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() {
|
public Duration streamTimeout() {
|
||||||
return streamTimeout;
|
return streamTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int maxGrpcMessageBytes() {
|
||||||
|
return maxGrpcMessageBytes;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "MxGatewayClientOptions{"
|
return "MxGatewayClientOptions{"
|
||||||
@@ -90,6 +157,8 @@ public final class MxGatewayClientOptions {
|
|||||||
+ callTimeout
|
+ callTimeout
|
||||||
+ ", streamTimeout="
|
+ ", streamTimeout="
|
||||||
+ streamTimeout
|
+ streamTimeout
|
||||||
|
+ ", maxGrpcMessageBytes="
|
||||||
|
+ maxGrpcMessageBytes
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +169,9 @@ public final class MxGatewayClientOptions {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutable builder for {@link MxGatewayClientOptions}.
|
||||||
|
*/
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
private String endpoint;
|
private String endpoint;
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
@@ -109,50 +181,113 @@ public final class MxGatewayClientOptions {
|
|||||||
private Duration connectTimeout;
|
private Duration connectTimeout;
|
||||||
private Duration callTimeout;
|
private Duration callTimeout;
|
||||||
private Duration streamTimeout;
|
private Duration streamTimeout;
|
||||||
|
private int maxGrpcMessageBytes;
|
||||||
|
|
||||||
private Builder() {
|
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) {
|
public Builder endpoint(String value) {
|
||||||
endpoint = value;
|
endpoint = value;
|
||||||
return this;
|
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) {
|
public Builder apiKey(String value) {
|
||||||
apiKey = value;
|
apiKey = value;
|
||||||
return this;
|
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) {
|
public Builder plaintext(boolean value) {
|
||||||
plaintext = value;
|
plaintext = value;
|
||||||
return this;
|
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) {
|
public Builder caCertificatePath(Path value) {
|
||||||
caCertificatePath = value;
|
caCertificatePath = value;
|
||||||
return this;
|
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) {
|
public Builder serverNameOverride(String value) {
|
||||||
serverNameOverride = value;
|
serverNameOverride = value;
|
||||||
return this;
|
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) {
|
public Builder connectTimeout(Duration value) {
|
||||||
connectTimeout = Objects.requireNonNull(value, "connectTimeout");
|
connectTimeout = Objects.requireNonNull(value, "connectTimeout");
|
||||||
return this;
|
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) {
|
public Builder callTimeout(Duration value) {
|
||||||
callTimeout = Objects.requireNonNull(value, "callTimeout");
|
callTimeout = Objects.requireNonNull(value, "callTimeout");
|
||||||
return this;
|
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) {
|
public Builder streamTimeout(Duration value) {
|
||||||
streamTimeout = Objects.requireNonNull(value, "streamTimeout");
|
streamTimeout = Objects.requireNonNull(value, "streamTimeout");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder maxGrpcMessageBytes(int value) {
|
||||||
|
maxGrpcMessageBytes = value;
|
||||||
|
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() {
|
public MxGatewayClientOptions build() {
|
||||||
return new MxGatewayClientOptions(this);
|
return new MxGatewayClientOptions(this);
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-1
@@ -1,21 +1,42 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
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 {
|
public final class MxGatewayClientVersion {
|
||||||
private static final int GATEWAY_PROTOCOL_VERSION = 1;
|
private static final int GATEWAY_PROTOCOL_VERSION = 3;
|
||||||
private static final int WORKER_PROTOCOL_VERSION = 1;
|
private static final int WORKER_PROTOCOL_VERSION = 1;
|
||||||
private static final String CLIENT_VERSION = "0.1.0";
|
private static final String CLIENT_VERSION = "0.1.0";
|
||||||
|
|
||||||
private MxGatewayClientVersion() {
|
private MxGatewayClientVersion() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the human-readable client release version.
|
||||||
|
*
|
||||||
|
* @return the client version string
|
||||||
|
*/
|
||||||
public static String clientVersion() {
|
public static String clientVersion() {
|
||||||
return CLIENT_VERSION;
|
return CLIENT_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the gRPC gateway protocol version this client targets.
|
||||||
|
*
|
||||||
|
* @return the gateway protocol version
|
||||||
|
*/
|
||||||
public static int gatewayProtocolVersion() {
|
public static int gatewayProtocolVersion() {
|
||||||
return GATEWAY_PROTOCOL_VERSION;
|
return GATEWAY_PROTOCOL_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the worker IPC protocol version this client targets.
|
||||||
|
*
|
||||||
|
* @return the worker protocol version
|
||||||
|
*/
|
||||||
public static int workerProtocolVersion() {
|
public static int workerProtocolVersion() {
|
||||||
return WORKER_PROTOCOL_VERSION;
|
return WORKER_PROTOCOL_VERSION;
|
||||||
}
|
}
|
||||||
|
|||||||
+22
@@ -3,20 +3,42 @@ package com.dohertylan.mxgateway.client;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
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 {
|
public class MxGatewayCommandException extends MxGatewayException {
|
||||||
private final ProtocolStatus protocolStatus;
|
private final ProtocolStatus protocolStatus;
|
||||||
private final MxCommandReply reply;
|
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) {
|
public MxGatewayCommandException(String operation, ProtocolStatus protocolStatus, MxCommandReply reply) {
|
||||||
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
||||||
this.protocolStatus = protocolStatus;
|
this.protocolStatus = protocolStatus;
|
||||||
this.reply = reply;
|
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() {
|
public ProtocolStatus protocolStatus() {
|
||||||
return 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() {
|
public MxCommandReply reply() {
|
||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -8,6 +8,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
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 {
|
public final class MxGatewayEventSubscription implements AutoCloseable {
|
||||||
private final AtomicReference<ClientCallStreamObserver<StreamEventsRequest>> requestStream = new AtomicReference<>();
|
private final AtomicReference<ClientCallStreamObserver<StreamEventsRequest>> requestStream = new AtomicReference<>();
|
||||||
private final AtomicBoolean cancelled = new AtomicBoolean();
|
private final AtomicBoolean cancelled = new AtomicBoolean();
|
||||||
@@ -39,6 +47,11 @@ 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() {
|
public void cancel() {
|
||||||
cancelled.set(true);
|
cancelled.set(true);
|
||||||
ClientCallStreamObserver<StreamEventsRequest> stream = requestStream.get();
|
ClientCallStreamObserver<StreamEventsRequest> stream = requestStream.get();
|
||||||
|
|||||||
+18
@@ -1,10 +1,28 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
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 {
|
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) {
|
public MxGatewayException(String message) {
|
||||||
super(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) {
|
public MxGatewayException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|||||||
+24
@@ -1,9 +1,24 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
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 {
|
public final class MxGatewaySecrets {
|
||||||
private MxGatewaySecrets() {
|
private MxGatewaySecrets() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redacts the body of an API key, leaving only short prefix and suffix
|
||||||
|
* windows so it remains comparable in logs.
|
||||||
|
*
|
||||||
|
* @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 keys eight characters or shorter, or a masked form preserving
|
||||||
|
* the leading and trailing four characters
|
||||||
|
*/
|
||||||
public static String redactApiKey(String apiKey) {
|
public static String redactApiKey(String apiKey) {
|
||||||
if (apiKey == null || apiKey.isEmpty()) {
|
if (apiKey == null || apiKey.isEmpty()) {
|
||||||
return "";
|
return "";
|
||||||
@@ -17,6 +32,15 @@ public final class MxGatewaySecrets {
|
|||||||
+ apiKey.substring(apiKey.length() - 4);
|
+ 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) {
|
public static String redactCredentials(String value) {
|
||||||
if (value == null || value.isBlank()) {
|
if (value == null || value.isBlank()) {
|
||||||
return value == null ? "" : value;
|
return value == null ? "" : value;
|
||||||
|
|||||||
+240
@@ -30,6 +30,14 @@ import mxaccess_gateway.v1.MxaccessGateway.UnregisterCommand;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.Write2Command;
|
import mxaccess_gateway.v1.MxaccessGateway.Write2Command;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.WriteCommand;
|
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 {
|
public final class MxGatewaySession implements AutoCloseable {
|
||||||
private static final SecureRandom RANDOM = new SecureRandom();
|
private static final SecureRandom RANDOM = new SecureRandom();
|
||||||
|
|
||||||
@@ -42,19 +50,45 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
this.openReply = Objects.requireNonNull(openReply, "openReply");
|
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) {
|
public static MxGatewaySession forSessionId(MxGatewayClient client, String sessionId) {
|
||||||
return new MxGatewaySession(
|
return new MxGatewaySession(
|
||||||
client, OpenSessionReply.newBuilder().setSessionId(sessionId).build());
|
client, OpenSessionReply.newBuilder().setSessionId(sessionId).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the gateway-assigned session id.
|
||||||
|
*
|
||||||
|
* @return the session id
|
||||||
|
*/
|
||||||
public String sessionId() {
|
public String sessionId() {
|
||||||
return openReply.getSessionId();
|
return openReply.getSessionId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the original {@link OpenSessionReply} that this session was opened with.
|
||||||
|
*
|
||||||
|
* @return the open-session reply
|
||||||
|
*/
|
||||||
public OpenSessionReply openReply() {
|
public OpenSessionReply openReply() {
|
||||||
return 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() {
|
public synchronized CloseSessionReply closeRaw() {
|
||||||
if (closeReply == null) {
|
if (closeReply == null) {
|
||||||
closeReply = client.closeSessionRaw(CloseSessionRequest.newBuilder()
|
closeReply = client.closeSessionRaw(CloseSessionRequest.newBuilder()
|
||||||
@@ -70,6 +104,13 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
closeRaw();
|
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) {
|
public int register(String clientName) {
|
||||||
MxCommandReply reply = registerRaw(clientName);
|
MxCommandReply reply = registerRaw(clientName);
|
||||||
if (reply.hasRegister()) {
|
if (reply.hasRegister()) {
|
||||||
@@ -78,6 +119,13 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getReturnValue().getInt32Value();
|
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) {
|
public MxCommandReply registerRaw(String clientName) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_REGISTER)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_REGISTER)
|
||||||
@@ -85,6 +133,12 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.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) {
|
public void unregister(int serverHandle) {
|
||||||
invokeCommand(MxCommand.newBuilder()
|
invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_UNREGISTER)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_UNREGISTER)
|
||||||
@@ -92,6 +146,14 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.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) {
|
public int addItem(int serverHandle, String itemDefinition) {
|
||||||
MxCommandReply reply = addItemRaw(serverHandle, itemDefinition);
|
MxCommandReply reply = addItemRaw(serverHandle, itemDefinition);
|
||||||
if (reply.hasAddItem()) {
|
if (reply.hasAddItem()) {
|
||||||
@@ -100,6 +162,14 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getReturnValue().getInt32Value();
|
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) {
|
public MxCommandReply addItemRaw(int serverHandle, String itemDefinition) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM)
|
||||||
@@ -109,6 +179,15 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.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) {
|
public int addItem2(int serverHandle, String itemDefinition, String itemContext) {
|
||||||
MxCommandReply reply = addItem2Raw(serverHandle, itemDefinition, itemContext);
|
MxCommandReply reply = addItem2Raw(serverHandle, itemDefinition, itemContext);
|
||||||
if (reply.hasAddItem2()) {
|
if (reply.hasAddItem2()) {
|
||||||
@@ -117,6 +196,15 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getReturnValue().getInt32Value();
|
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) {
|
public MxCommandReply addItem2Raw(int serverHandle, String itemDefinition, String itemContext) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM2)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM2)
|
||||||
@@ -127,10 +215,25 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.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) {
|
public void removeItem(int serverHandle, int itemHandle) {
|
||||||
removeItemRaw(serverHandle, 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) {
|
public MxCommandReply removeItemRaw(int serverHandle, int itemHandle) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_REMOVE_ITEM)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_REMOVE_ITEM)
|
||||||
@@ -140,10 +243,25 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.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) {
|
public void advise(int serverHandle, int itemHandle) {
|
||||||
adviseRaw(serverHandle, 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) {
|
public MxCommandReply adviseRaw(int serverHandle, int itemHandle) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE)
|
||||||
@@ -153,10 +271,25 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.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) {
|
public void unAdvise(int serverHandle, int itemHandle) {
|
||||||
unAdviseRaw(serverHandle, 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) {
|
public MxCommandReply unAdviseRaw(int serverHandle, int itemHandle) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_UN_ADVISE)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_UN_ADVISE)
|
||||||
@@ -166,6 +299,15 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.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) {
|
public List<SubscribeResult> addItemBulk(int serverHandle, List<String> tagAddresses) {
|
||||||
Objects.requireNonNull(tagAddresses, "tagAddresses");
|
Objects.requireNonNull(tagAddresses, "tagAddresses");
|
||||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||||
@@ -177,6 +319,15 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getAddItemBulk().getResultsList();
|
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) {
|
public List<SubscribeResult> adviseItemBulk(int serverHandle, List<Integer> itemHandles) {
|
||||||
Objects.requireNonNull(itemHandles, "itemHandles");
|
Objects.requireNonNull(itemHandles, "itemHandles");
|
||||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||||
@@ -188,6 +339,15 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getAdviseItemBulk().getResultsList();
|
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) {
|
public List<SubscribeResult> removeItemBulk(int serverHandle, List<Integer> itemHandles) {
|
||||||
Objects.requireNonNull(itemHandles, "itemHandles");
|
Objects.requireNonNull(itemHandles, "itemHandles");
|
||||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||||
@@ -199,6 +359,15 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getRemoveItemBulk().getResultsList();
|
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) {
|
public List<SubscribeResult> unAdviseItemBulk(int serverHandle, List<Integer> itemHandles) {
|
||||||
Objects.requireNonNull(itemHandles, "itemHandles");
|
Objects.requireNonNull(itemHandles, "itemHandles");
|
||||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||||
@@ -210,6 +379,16 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getUnAdviseItemBulk().getResultsList();
|
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) {
|
public List<SubscribeResult> subscribeBulk(int serverHandle, List<String> tagAddresses) {
|
||||||
Objects.requireNonNull(tagAddresses, "tagAddresses");
|
Objects.requireNonNull(tagAddresses, "tagAddresses");
|
||||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||||
@@ -221,6 +400,16 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getSubscribeBulk().getResultsList();
|
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) {
|
public List<SubscribeResult> unsubscribeBulk(int serverHandle, List<Integer> itemHandles) {
|
||||||
Objects.requireNonNull(itemHandles, "itemHandles");
|
Objects.requireNonNull(itemHandles, "itemHandles");
|
||||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||||
@@ -232,10 +421,29 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getUnsubscribeBulk().getResultsList();
|
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) {
|
public void write(int serverHandle, int itemHandle, MxValue value, int userId) {
|
||||||
writeRaw(serverHandle, itemHandle, value, 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) {
|
public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE)
|
||||||
@@ -247,6 +455,16 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.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) {
|
public void write2(int serverHandle, int itemHandle, MxValue value, MxValue timestampValue, int userId) {
|
||||||
invokeCommand(MxCommand.newBuilder()
|
invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE2)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE2)
|
||||||
@@ -259,10 +477,24 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.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() {
|
public MxEventStream streamEvents() {
|
||||||
return streamEventsAfter(0);
|
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) {
|
public MxEventStream streamEventsAfter(long afterWorkerSequence) {
|
||||||
return client.streamEvents(StreamEventsRequest.newBuilder()
|
return client.streamEvents(StreamEventsRequest.newBuilder()
|
||||||
.setSessionId(sessionId())
|
.setSessionId(sessionId())
|
||||||
@@ -270,6 +502,14 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.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) {
|
public MxCommandReply invokeCommand(MxCommand command) {
|
||||||
return client.invoke(MxCommandRequest.newBuilder()
|
return client.invoke(MxCommandRequest.newBuilder()
|
||||||
.setSessionId(sessionId())
|
.setSessionId(sessionId())
|
||||||
|
|||||||
+15
@@ -2,14 +2,29 @@ package com.dohertylan.mxgateway.client;
|
|||||||
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
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 {
|
public final class MxGatewaySessionException extends MxGatewayException {
|
||||||
private final ProtocolStatus protocolStatus;
|
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) {
|
public MxGatewaySessionException(String operation, ProtocolStatus protocolStatus) {
|
||||||
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
||||||
this.protocolStatus = 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() {
|
public ProtocolStatus protocolStatus() {
|
||||||
return protocolStatus;
|
return protocolStatus;
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -2,14 +2,29 @@ package com.dohertylan.mxgateway.client;
|
|||||||
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
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 {
|
public final class MxGatewayWorkerException extends MxGatewayException {
|
||||||
private final ProtocolStatus protocolStatus;
|
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) {
|
public MxGatewayWorkerException(String operation, ProtocolStatus protocolStatus) {
|
||||||
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
||||||
this.protocolStatus = 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() {
|
public ProtocolStatus protocolStatus() {
|
||||||
return protocolStatus;
|
return protocolStatus;
|
||||||
}
|
}
|
||||||
|
|||||||
+61
@@ -4,43 +4,104 @@ import mxaccess_gateway.v1.MxaccessGateway.MxStatusCategory;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy;
|
import mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusSource;
|
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 {
|
public final class MxStatuses {
|
||||||
private 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) {
|
public static boolean succeeded(MxStatusProxy status) {
|
||||||
return status == null || status.getSuccess() != 0;
|
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) {
|
public static MxStatusView view(MxStatusProxy status) {
|
||||||
return new MxStatusView(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) {
|
public record MxStatusView(MxStatusProxy raw) {
|
||||||
|
/**
|
||||||
|
* Returns the raw success flag (non-zero indicates success).
|
||||||
|
*
|
||||||
|
* @return the success flag value
|
||||||
|
*/
|
||||||
public int success() {
|
public int success() {
|
||||||
return raw.getSuccess();
|
return raw.getSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the high-level status category.
|
||||||
|
*
|
||||||
|
* @return the status category enum value
|
||||||
|
*/
|
||||||
public MxStatusCategory category() {
|
public MxStatusCategory category() {
|
||||||
return raw.getCategory();
|
return raw.getCategory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns which subsystem detected the status.
|
||||||
|
*
|
||||||
|
* @return the detection source enum value
|
||||||
|
*/
|
||||||
public MxStatusSource detectedBy() {
|
public MxStatusSource detectedBy() {
|
||||||
return raw.getDetectedBy();
|
return raw.getDetectedBy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the detail code accompanying the status.
|
||||||
|
*
|
||||||
|
* @return the raw detail code
|
||||||
|
*/
|
||||||
public int detail() {
|
public int detail() {
|
||||||
return raw.getDetail();
|
return raw.getDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw, unmapped category code from MXAccess.
|
||||||
|
*
|
||||||
|
* @return the raw category integer
|
||||||
|
*/
|
||||||
public int rawCategory() {
|
public int rawCategory() {
|
||||||
return raw.getRawCategory();
|
return raw.getRawCategory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw, unmapped detection-source code from MXAccess.
|
||||||
|
*
|
||||||
|
* @return the raw detection-source integer
|
||||||
|
*/
|
||||||
public int rawDetectedBy() {
|
public int rawDetectedBy() {
|
||||||
return raw.getRawDetectedBy();
|
return raw.getRawDetectedBy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the diagnostic text supplied by MXAccess, if any.
|
||||||
|
*
|
||||||
|
* @return the diagnostic message, possibly empty
|
||||||
|
*/
|
||||||
public String diagnosticText() {
|
public String diagnosticText() {
|
||||||
return raw.getDiagnosticText();
|
return raw.getDiagnosticText();
|
||||||
}
|
}
|
||||||
|
|||||||
+84
@@ -17,10 +17,24 @@ import mxaccess_gateway.v1.MxaccessGateway.RawArray;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.StringArray;
|
import mxaccess_gateway.v1.MxaccessGateway.StringArray;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.TimestampArray;
|
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 {
|
public final class MxValues {
|
||||||
private MxValues() {
|
private MxValues() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a boolean {@link MxValue}.
|
||||||
|
*
|
||||||
|
* @param value the boolean payload
|
||||||
|
* @return a populated {@code MxValue}
|
||||||
|
*/
|
||||||
public static MxValue boolValue(boolean value) {
|
public static MxValue boolValue(boolean value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_BOOLEAN)
|
.setDataType(MxDataType.MX_DATA_TYPE_BOOLEAN)
|
||||||
@@ -29,6 +43,12 @@ public final class MxValues {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a 32-bit integer {@link MxValue}.
|
||||||
|
*
|
||||||
|
* @param value the int32 payload
|
||||||
|
* @return a populated {@code MxValue}
|
||||||
|
*/
|
||||||
public static MxValue int32Value(int value) {
|
public static MxValue int32Value(int value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
||||||
@@ -37,6 +57,12 @@ public final class MxValues {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a 64-bit integer {@link MxValue}.
|
||||||
|
*
|
||||||
|
* @param value the int64 payload
|
||||||
|
* @return a populated {@code MxValue}
|
||||||
|
*/
|
||||||
public static MxValue int64Value(long value) {
|
public static MxValue int64Value(long value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
||||||
@@ -45,6 +71,12 @@ public final class MxValues {
|
|||||||
.build();
|
.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) {
|
public static MxValue floatValue(float value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_FLOAT)
|
.setDataType(MxDataType.MX_DATA_TYPE_FLOAT)
|
||||||
@@ -53,6 +85,12 @@ public final class MxValues {
|
|||||||
.build();
|
.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) {
|
public static MxValue doubleValue(double value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_DOUBLE)
|
.setDataType(MxDataType.MX_DATA_TYPE_DOUBLE)
|
||||||
@@ -61,6 +99,12 @@ public final class MxValues {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a string {@link MxValue}.
|
||||||
|
*
|
||||||
|
* @param value the string payload
|
||||||
|
* @return a populated {@code MxValue}
|
||||||
|
*/
|
||||||
public static MxValue stringValue(String value) {
|
public static MxValue stringValue(String value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_STRING)
|
.setDataType(MxDataType.MX_DATA_TYPE_STRING)
|
||||||
@@ -69,6 +113,12 @@ public final class MxValues {
|
|||||||
.build();
|
.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) {
|
public static MxValue timestampValue(Instant value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_TIME)
|
.setDataType(MxDataType.MX_DATA_TYPE_TIME)
|
||||||
@@ -80,6 +130,14 @@ public final class MxValues {
|
|||||||
.build();
|
.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) {
|
public static Object nativeValue(MxValue value) {
|
||||||
if (value == null || value.getIsNull()) {
|
if (value == null || value.getIsNull()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -99,6 +157,14 @@ 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) {
|
public static Object nativeArray(MxArray array) {
|
||||||
if (array == null) {
|
if (array == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -117,6 +183,12 @@ 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) {
|
public static MxArray stringArray(List<String> values) {
|
||||||
return MxArray.newBuilder()
|
return MxArray.newBuilder()
|
||||||
.setElementDataType(MxDataType.MX_DATA_TYPE_STRING)
|
.setElementDataType(MxDataType.MX_DATA_TYPE_STRING)
|
||||||
@@ -126,6 +198,12 @@ public final class MxValues {
|
|||||||
.build();
|
.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) {
|
public static MxArray int32Array(List<Integer> values) {
|
||||||
return MxArray.newBuilder()
|
return MxArray.newBuilder()
|
||||||
.setElementDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
.setElementDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
||||||
@@ -135,6 +213,12 @@ public final class MxValues {
|
|||||||
.build();
|
.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) {
|
public static String kindName(MxValue value) {
|
||||||
return value == null ? "KIND_NOT_SET" : value.getKindCase().name();
|
return value == null ? "KIND_NOT_SET" : value.getKindCase().name();
|
||||||
}
|
}
|
||||||
|
|||||||
+121
-22
@@ -3,6 +3,7 @@ package com.dohertylan.mxgateway.client;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
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 static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import com.google.protobuf.Timestamp;
|
import com.google.protobuf.Timestamp;
|
||||||
@@ -25,6 +26,8 @@ import io.grpc.ServerCallHandler;
|
|||||||
import io.grpc.ServerInterceptor;
|
import io.grpc.ServerInterceptor;
|
||||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||||
import io.grpc.inprocess.InProcessServerBuilder;
|
import io.grpc.inprocess.InProcessServerBuilder;
|
||||||
|
import io.grpc.stub.ClientCallStreamObserver;
|
||||||
|
import io.grpc.stub.ClientResponseObserver;
|
||||||
import io.grpc.stub.StreamObserver;
|
import io.grpc.stub.StreamObserver;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -100,31 +103,44 @@ final class GalaxyRepositoryClientTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void discoverHierarchyReturnsObjectsAndAttributes() throws Exception {
|
void discoverHierarchyReturnsObjectsAndAttributes() throws Exception {
|
||||||
AtomicReference<DiscoverHierarchyRequest> seenRequest = new AtomicReference<>();
|
AtomicReference<DiscoverHierarchyRequest> firstRequest = new AtomicReference<>();
|
||||||
|
AtomicReference<DiscoverHierarchyRequest> secondRequest = new AtomicReference<>();
|
||||||
TestService service = new TestService() {
|
TestService service = new TestService() {
|
||||||
@Override
|
@Override
|
||||||
public void discoverHierarchy(
|
public void discoverHierarchy(
|
||||||
DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
|
DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
|
||||||
seenRequest.set(request);
|
if (request.getPageToken().isEmpty()) {
|
||||||
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
|
firstRequest.set(request);
|
||||||
.addObjects(GalaxyObject.newBuilder()
|
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
|
||||||
.setGobjectId(7)
|
.setNextPageToken("page-2")
|
||||||
.setTagName("Pump_001")
|
.setTotalObjectCount(2)
|
||||||
.setContainedName("Pump")
|
.addObjects(GalaxyObject.newBuilder()
|
||||||
.setBrowseName("Pump")
|
.setGobjectId(7)
|
||||||
.setParentGobjectId(1)
|
.setTagName("Pump_001")
|
||||||
.setIsArea(false)
|
.setContainedName("Pump")
|
||||||
.setCategoryId(3)
|
.setBrowseName("Pump")
|
||||||
.setHostedByGobjectId(0)
|
.setParentGobjectId(1)
|
||||||
.addTemplateChain("$Pump")
|
.setIsArea(false)
|
||||||
.addAttributes(GalaxyAttribute.newBuilder()
|
.setCategoryId(3)
|
||||||
.setAttributeName("Speed")
|
.setHostedByGobjectId(0)
|
||||||
.setFullTagReference("Pump_001.Speed")
|
.addTemplateChain("$Pump")
|
||||||
.setMxDataType(5)
|
.addAttributes(GalaxyAttribute.newBuilder()
|
||||||
.setDataTypeName("MxFloat")
|
.setAttributeName("Speed")
|
||||||
.setIsArray(false)
|
.setFullTagReference("Pump_001.Speed")
|
||||||
.setIsHistorized(true)))
|
.setMxDataType(5)
|
||||||
.build());
|
.setDataTypeName("MxFloat")
|
||||||
|
.setIsArray(false)
|
||||||
|
.setIsHistorized(true)))
|
||||||
|
.build());
|
||||||
|
} else {
|
||||||
|
secondRequest.set(request);
|
||||||
|
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
|
||||||
|
.setTotalObjectCount(2)
|
||||||
|
.addObjects(GalaxyObject.newBuilder()
|
||||||
|
.setGobjectId(8)
|
||||||
|
.setTagName("Pump_002"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -132,7 +148,10 @@ final class GalaxyRepositoryClientTests {
|
|||||||
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
||||||
GalaxyRepositoryClient client = g.client("")) {
|
GalaxyRepositoryClient client = g.client("")) {
|
||||||
List<GalaxyObject> objects = client.discoverHierarchy();
|
List<GalaxyObject> objects = client.discoverHierarchy();
|
||||||
assertEquals(1, objects.size());
|
assertEquals(2, objects.size());
|
||||||
|
assertEquals(5000, firstRequest.get().getPageSize());
|
||||||
|
assertEquals("", firstRequest.get().getPageToken());
|
||||||
|
assertEquals("page-2", secondRequest.get().getPageToken());
|
||||||
GalaxyObject only = objects.get(0);
|
GalaxyObject only = objects.get(0);
|
||||||
assertEquals(7, only.getGobjectId());
|
assertEquals(7, only.getGobjectId());
|
||||||
assertEquals("Pump_001", only.getTagName());
|
assertEquals("Pump_001", only.getTagName());
|
||||||
@@ -142,6 +161,41 @@ final class GalaxyRepositoryClientTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deployEventStreamCloseBeforeBeforeStartCancelsStream() {
|
||||||
|
DeployEventStream stream = new DeployEventStream(4);
|
||||||
|
ClientResponseObserver<WatchDeployEventsRequest, DeployEvent> observer = stream.observer();
|
||||||
|
RecordingClientCallStreamObserver requestStream = new RecordingClientCallStreamObserver();
|
||||||
|
|
||||||
|
stream.close();
|
||||||
|
observer.beforeStart(requestStream);
|
||||||
|
|
||||||
|
assertTrue(requestStream.cancelled);
|
||||||
|
assertEquals("client cancelled deploy event stream", requestStream.cancelMessage);
|
||||||
|
assertFalse(stream.hasNext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void discoverHierarchyRejectsRepeatedPageToken() throws Exception {
|
||||||
|
TestService service = new TestService() {
|
||||||
|
@Override
|
||||||
|
public void discoverHierarchy(
|
||||||
|
DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
|
||||||
|
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
|
||||||
|
.setNextPageToken("7:1")
|
||||||
|
.build());
|
||||||
|
responseObserver.onCompleted();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
||||||
|
GalaxyRepositoryClient client = g.client("")) {
|
||||||
|
MxGatewayException error = assertThrows(MxGatewayException.class, client::discoverHierarchy);
|
||||||
|
|
||||||
|
assertTrue(error.getMessage().contains("repeated page token"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void watchDeployEventsReceivesEventsInOrder() throws Exception {
|
void watchDeployEventsReceivesEventsInOrder() throws Exception {
|
||||||
DeployEvent first = DeployEvent.newBuilder()
|
DeployEvent first = DeployEvent.newBuilder()
|
||||||
@@ -281,6 +335,51 @@ final class GalaxyRepositoryClientTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class RecordingClientCallStreamObserver
|
||||||
|
extends ClientCallStreamObserver<WatchDeployEventsRequest> {
|
||||||
|
private boolean cancelled;
|
||||||
|
private String cancelMessage;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOnReadyHandler(Runnable onReadyHandler) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableAutoInboundFlowControl() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void request(int count) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMessageCompression(boolean enable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancel(String message, Throwable cause) {
|
||||||
|
cancelled = true;
|
||||||
|
cancelMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNext(WatchDeployEventsRequest value) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCompleted() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private record InProcessGalaxy(Server server, ManagedChannel channel) implements AutoCloseable {
|
private record InProcessGalaxy(Server server, ManagedChannel channel) implements AutoCloseable {
|
||||||
static InProcessGalaxy start(
|
static InProcessGalaxy start(
|
||||||
GalaxyRepositoryGrpc.GalaxyRepositoryImplBase service, AtomicReference<String> authorization)
|
GalaxyRepositoryGrpc.GalaxyRepositoryImplBase service, AtomicReference<String> authorization)
|
||||||
|
|||||||
+2540
-93
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -2,7 +2,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"fixtureSet": "mxaccess-gateway-client-behavior",
|
"fixtureSet": "mxaccess-gateway-client-behavior",
|
||||||
"contractName": "mxaccess-gateway",
|
"contractName": "mxaccess-gateway",
|
||||||
"gatewayProtocolVersion": 1,
|
"gatewayProtocolVersion": 3,
|
||||||
"workerProtocolVersion": 1,
|
"workerProtocolVersion": 1,
|
||||||
"protoInputManifest": "clients/proto/proto-inputs.json",
|
"protoInputManifest": "clients/proto/proto-inputs.json",
|
||||||
"fixtures": [
|
"fixtures": [
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
"backendName": "mxaccess-worker",
|
"backendName": "mxaccess-worker",
|
||||||
"workerProcessId": 1234,
|
"workerProcessId": 1234,
|
||||||
"workerProtocolVersion": 1,
|
"workerProtocolVersion": 1,
|
||||||
"gatewayProtocolVersion": 1,
|
"gatewayProtocolVersion": 3,
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"unary-open-session",
|
"unary-open-session",
|
||||||
"unary-close-session",
|
"unary-close-session",
|
||||||
"unary-invoke",
|
"unary-invoke",
|
||||||
"server-stream-events"
|
"server-stream-events",
|
||||||
|
"unary-acknowledge-alarm",
|
||||||
|
"server-stream-active-alarms"
|
||||||
],
|
],
|
||||||
"defaultCommandTimeout": "30s",
|
"defaultCommandTimeout": "30s",
|
||||||
"protocolStatus": {
|
"protocolStatus": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"fixtureSet": "mxaccess-gateway-parity-fixture-matrix",
|
"fixtureSet": "mxaccess-gateway-parity-fixture-matrix",
|
||||||
"contractName": "mxaccess-gateway",
|
"contractName": "mxaccess-gateway",
|
||||||
"gatewayProtocolVersion": 1,
|
"gatewayProtocolVersion": 3,
|
||||||
"workerProtocolVersion": 1,
|
"workerProtocolVersion": 1,
|
||||||
"sourceCaptureRoot": "C:/Users/dohertj2/Desktop/mxaccess/captures",
|
"sourceCaptureRoot": "C:/Users/dohertj2/Desktop/mxaccess/captures",
|
||||||
"sourceDocs": [
|
"sourceDocs": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"contractName": "mxaccess-gateway",
|
"contractName": "mxaccess-gateway",
|
||||||
"gatewayProtocolVersion": 1,
|
"gatewayProtocolVersion": 3,
|
||||||
"workerProtocolVersion": 1,
|
"workerProtocolVersion": 1,
|
||||||
"protoRoot": "src/MxGateway.Contracts/Protos",
|
"protoRoot": "src/MxGateway.Contracts/Protos",
|
||||||
"sourceFiles": [
|
"sourceFiles": [
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ Provide an async Python client package for MXAccess Gateway, plus a test CLI and
|
|||||||
unit tests. The Python client should be useful for automation, diagnostics, and
|
unit tests. The Python client should be useful for automation, diagnostics, and
|
||||||
test harnesses.
|
test harnesses.
|
||||||
|
|
||||||
Follow the [Python Style Guide](./style-guides/PythonStyleGuide.md) for
|
Follow the [Python Style Guide](../../docs/style-guides/PythonStyleGuide.md) for
|
||||||
handwritten code and the [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md)
|
handwritten code and the [Protobuf Style Guide](../../docs/style-guides/ProtobufStyleGuide.md)
|
||||||
for generated contract inputs.
|
for generated contract inputs.
|
||||||
|
|
||||||
## Package Layout
|
## Package Layout
|
||||||
@@ -195,3 +195,10 @@ mxaccess-gateway-client
|
|||||||
|
|
||||||
Generated protobuf code should be regenerated through a documented command, not
|
Generated protobuf code should be regenerated through a documented command, not
|
||||||
edited by hand.
|
edited by hand.
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Client Libraries Detailed Design](../../docs/ClientLibrariesDesign.md)
|
||||||
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
||||||
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
|
- [Python Style Guide](../../docs/style-guides/PythonStyleGuide.md)
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
The Python client package contains generated MXAccess Gateway protobuf
|
The Python client package contains generated MXAccess Gateway protobuf
|
||||||
bindings, the async `mxgateway` package, and the `mxgw-py` test CLI. The
|
bindings, the async `mxgateway` package, and the `mxgw-py` test CLI. The
|
||||||
package uses the shared proto inputs documented in
|
package uses the shared proto inputs documented in
|
||||||
`../../docs/client-proto-generation.md` so gateway and client contracts stay in
|
`../../docs/ClientProtoGeneration.md` so gateway and client contracts stay in
|
||||||
sync.
|
sync.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
@@ -31,7 +31,7 @@ changes:
|
|||||||
```
|
```
|
||||||
|
|
||||||
The script uses the Python tool path recorded in
|
The script uses the Python tool path recorded in
|
||||||
`../../docs/toolchain-links.md`.
|
`../../docs/ToolchainLinks.md`.
|
||||||
|
|
||||||
## Build And Test
|
## Build And Test
|
||||||
|
|
||||||
@@ -219,5 +219,5 @@ mxgw-py smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGAT
|
|||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
||||||
- [Python Client Detailed Design](../../docs/clients-python-design.md)
|
- [Python Client Detailed Design](./PythonClientDesign.md)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ $outputRoot = Join-Path $PSScriptRoot 'src\mxgateway\generated'
|
|||||||
$python = 'C:\Users\dohertj2\AppData\Local\Programs\Python\Python312\python.exe'
|
$python = 'C:\Users\dohertj2\AppData\Local\Programs\Python\Python312\python.exe'
|
||||||
|
|
||||||
if (-not (Test-Path $python)) {
|
if (-not (Test-Path $python)) {
|
||||||
throw "Python was not found at $python. See docs/toolchain-links.md."
|
throw "Python was not found at $python. See docs/ToolchainLinks.md."
|
||||||
}
|
}
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $outputRoot -Force | Out-Null
|
New-Item -ItemType Directory -Path $outputRoot -Force | Out-Null
|
||||||
|
|||||||
@@ -14,13 +14,16 @@ class ApiKey:
|
|||||||
value: str
|
value: str
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
|
"""Validate that the API key value is non-empty."""
|
||||||
if not self.value:
|
if not self.value:
|
||||||
raise ValueError("api_key must not be empty")
|
raise ValueError("api_key must not be empty")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
"""Return a repr that redacts the secret value."""
|
||||||
return f"{type(self).__name__}({REDACTED!r})"
|
return f"{type(self).__name__}({REDACTED!r})"
|
||||||
|
|
||||||
def bearer_value(self) -> str:
|
def bearer_value(self) -> str:
|
||||||
|
"""Return the value formatted as an HTTP `Bearer` token."""
|
||||||
return f"Bearer {self.value}"
|
return f"Bearer {self.value}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class GatewayClient:
|
|||||||
stub: Any,
|
stub: Any,
|
||||||
channel: grpc.aio.Channel | None = None,
|
channel: grpc.aio.Channel | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Initialize the client with resolved options and a gRPC stub."""
|
||||||
self.options = options
|
self.options = options
|
||||||
self.raw_stub = stub
|
self.raw_stub = stub
|
||||||
self._channel = channel
|
self._channel = channel
|
||||||
@@ -63,9 +64,11 @@ class GatewayClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def __aenter__(self) -> "GatewayClient":
|
async def __aenter__(self) -> "GatewayClient":
|
||||||
|
"""Return self to support ``async with`` usage."""
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, *_exc_info: object) -> None:
|
async def __aexit__(self, *_exc_info: object) -> None:
|
||||||
|
"""Close the client when leaving an ``async with`` block."""
|
||||||
await self.close()
|
await self.close()
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
@@ -99,6 +102,7 @@ class GatewayClient:
|
|||||||
return Session(client=self, session_id=reply.session_id, open_reply=reply)
|
return Session(client=self, session_id=reply.session_id, open_reply=reply)
|
||||||
|
|
||||||
async def open_session_raw(self, request: pb.OpenSessionRequest) -> pb.OpenSessionReply:
|
async def open_session_raw(self, request: pb.OpenSessionRequest) -> pb.OpenSessionReply:
|
||||||
|
"""Send an `OpenSession` RPC and return the raw reply."""
|
||||||
reply = await self._unary("open session", self.raw_stub.OpenSession, request)
|
reply = await self._unary("open session", self.raw_stub.OpenSession, request)
|
||||||
ensure_protocol_success("open session", reply.protocol_status, reply)
|
ensure_protocol_success("open session", reply.protocol_status, reply)
|
||||||
return reply
|
return reply
|
||||||
@@ -107,11 +111,13 @@ class GatewayClient:
|
|||||||
self,
|
self,
|
||||||
request: pb.CloseSessionRequest,
|
request: pb.CloseSessionRequest,
|
||||||
) -> pb.CloseSessionReply:
|
) -> pb.CloseSessionReply:
|
||||||
|
"""Send a `CloseSession` RPC and return the raw reply."""
|
||||||
reply = await self._unary("close session", self.raw_stub.CloseSession, request)
|
reply = await self._unary("close session", self.raw_stub.CloseSession, request)
|
||||||
ensure_protocol_success("close session", reply.protocol_status, reply)
|
ensure_protocol_success("close session", reply.protocol_status, reply)
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
async def invoke_raw(self, request: pb.MxCommandRequest) -> pb.MxCommandReply:
|
async def invoke_raw(self, request: pb.MxCommandRequest) -> pb.MxCommandReply:
|
||||||
|
"""Send an `Invoke` RPC and return the raw reply."""
|
||||||
reply = await self._unary("invoke", self.raw_stub.Invoke, request)
|
reply = await self._unary("invoke", self.raw_stub.Invoke, request)
|
||||||
ensure_protocol_success("invoke", reply.protocol_status, reply)
|
ensure_protocol_success("invoke", reply.protocol_status, reply)
|
||||||
return reply
|
return reply
|
||||||
@@ -130,6 +136,42 @@ class GatewayClient:
|
|||||||
call = self.raw_stub.StreamEvents(request, **kwargs)
|
call = self.raw_stub.StreamEvents(request, **kwargs)
|
||||||
return _canceling_iterator(call)
|
return _canceling_iterator(call)
|
||||||
|
|
||||||
|
async def acknowledge_alarm(
|
||||||
|
self,
|
||||||
|
request: pb.AcknowledgeAlarmRequest,
|
||||||
|
) -> pb.AcknowledgeAlarmReply:
|
||||||
|
"""Acknowledge 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.
|
||||||
|
"""
|
||||||
|
reply = await self._unary("acknowledge alarm", self.raw_stub.AcknowledgeAlarm, request)
|
||||||
|
ensure_protocol_success("acknowledge alarm", reply.protocol_status, reply)
|
||||||
|
return reply
|
||||||
|
|
||||||
|
def query_active_alarms(
|
||||||
|
self,
|
||||||
|
request: pb.QueryActiveAlarmsRequest,
|
||||||
|
*,
|
||||||
|
metadata: Sequence[tuple[str, str]] | None = None,
|
||||||
|
) -> AsyncIterator[pb.ActiveAlarmSnapshot]:
|
||||||
|
"""Stream a snapshot of all alarms currently Active or ActiveAcked.
|
||||||
|
|
||||||
|
The gateway's ConditionRefresh equivalent. Use after reconnect to seed
|
||||||
|
local Part 9 state, or to reconcile alarms that may have been missed
|
||||||
|
during a transport blip. Optionally scoped by alarm-reference prefix
|
||||||
|
(``request.alarm_filter_prefix``) so a partial refresh can target an
|
||||||
|
equipment sub-tree.
|
||||||
|
"""
|
||||||
|
kwargs: dict[str, Any] = {"metadata": merge_metadata(self.options.api_key, metadata)}
|
||||||
|
if self.options.stream_timeout is not None:
|
||||||
|
kwargs["timeout"] = self.options.stream_timeout
|
||||||
|
call = self.raw_stub.QueryActiveAlarms(request, **kwargs)
|
||||||
|
return _canceling_active_alarms_iterator(call)
|
||||||
|
|
||||||
async def _unary(
|
async def _unary(
|
||||||
self,
|
self,
|
||||||
operation: str,
|
operation: str,
|
||||||
@@ -169,3 +211,15 @@ async def _canceling_iterator(call: Any) -> AsyncIterator[pb.MxEvent]:
|
|||||||
cancel = getattr(call, "cancel", None)
|
cancel = getattr(call, "cancel", None)
|
||||||
if cancel is not None:
|
if cancel is not None:
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
|
||||||
|
async def _canceling_active_alarms_iterator(call: Any) -> AsyncIterator[pb.ActiveAlarmSnapshot]:
|
||||||
|
try:
|
||||||
|
async for snapshot in call:
|
||||||
|
yield snapshot
|
||||||
|
except grpc.RpcError as error:
|
||||||
|
raise map_rpc_error("query active alarms", error) from error
|
||||||
|
finally:
|
||||||
|
cancel = getattr(call, "cancel", None)
|
||||||
|
if cancel is not None:
|
||||||
|
cancel()
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class MxGatewayError(Exception):
|
|||||||
protocol_status: pb.ProtocolStatus | None = None,
|
protocol_status: pb.ProtocolStatus | None = None,
|
||||||
raw_reply: Any | None = None,
|
raw_reply: Any | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Initialize with a message and the optional raw protocol context."""
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
self.protocol_status = protocol_status
|
self.protocol_status = protocol_status
|
||||||
self.raw_reply = raw_reply
|
self.raw_reply = raw_reply
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ import grpc
|
|||||||
from google.protobuf.timestamp_pb2 import Timestamp
|
from google.protobuf.timestamp_pb2 import Timestamp
|
||||||
|
|
||||||
from .auth import merge_metadata
|
from .auth import merge_metadata
|
||||||
from .errors import map_rpc_error
|
from .errors import MxGatewayError, map_rpc_error
|
||||||
from .generated import galaxy_repository_pb2 as galaxy_pb
|
from .generated import galaxy_repository_pb2 as galaxy_pb
|
||||||
from .generated import galaxy_repository_pb2_grpc as galaxy_pb_grpc
|
from .generated import galaxy_repository_pb2_grpc as galaxy_pb_grpc
|
||||||
from .options import ClientOptions, create_channel
|
from .options import ClientOptions, create_channel
|
||||||
|
|
||||||
|
_DISCOVER_HIERARCHY_PAGE_SIZE = 5000
|
||||||
|
|
||||||
|
|
||||||
class GalaxyRepositoryClient:
|
class GalaxyRepositoryClient:
|
||||||
"""Async client for the Galaxy Repository gRPC service."""
|
"""Async client for the Galaxy Repository gRPC service."""
|
||||||
@@ -34,6 +36,7 @@ class GalaxyRepositoryClient:
|
|||||||
stub: Any,
|
stub: Any,
|
||||||
channel: grpc.aio.Channel | None = None,
|
channel: grpc.aio.Channel | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Initialize the client with resolved options and a gRPC stub."""
|
||||||
self.options = options
|
self.options = options
|
||||||
self.raw_stub = stub
|
self.raw_stub = stub
|
||||||
self._channel = channel
|
self._channel = channel
|
||||||
@@ -72,9 +75,11 @@ class GalaxyRepositoryClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def __aenter__(self) -> "GalaxyRepositoryClient":
|
async def __aenter__(self) -> "GalaxyRepositoryClient":
|
||||||
|
"""Return self to support ``async with`` usage."""
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, *_exc_info: object) -> None:
|
async def __aexit__(self, *_exc_info: object) -> None:
|
||||||
|
"""Close the client when leaving an ``async with`` block."""
|
||||||
await self.close()
|
await self.close()
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
@@ -112,12 +117,27 @@ class GalaxyRepositoryClient:
|
|||||||
async def discover_hierarchy(self) -> list[galaxy_pb.GalaxyObject]:
|
async def discover_hierarchy(self) -> list[galaxy_pb.GalaxyObject]:
|
||||||
"""Return the deployed Galaxy object hierarchy as raw proto messages."""
|
"""Return the deployed Galaxy object hierarchy as raw proto messages."""
|
||||||
|
|
||||||
reply = await self._unary(
|
objects: list[galaxy_pb.GalaxyObject] = []
|
||||||
"discover hierarchy",
|
seen_page_tokens: set[str] = set()
|
||||||
self.raw_stub.DiscoverHierarchy,
|
page_token = ""
|
||||||
galaxy_pb.DiscoverHierarchyRequest(),
|
while True:
|
||||||
)
|
reply = await self._unary(
|
||||||
return list(reply.objects)
|
"discover hierarchy",
|
||||||
|
self.raw_stub.DiscoverHierarchy,
|
||||||
|
galaxy_pb.DiscoverHierarchyRequest(
|
||||||
|
page_size=_DISCOVER_HIERARCHY_PAGE_SIZE,
|
||||||
|
page_token=page_token,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
objects.extend(reply.objects)
|
||||||
|
page_token = reply.next_page_token
|
||||||
|
if not page_token:
|
||||||
|
return objects
|
||||||
|
if page_token in seen_page_tokens:
|
||||||
|
raise MxGatewayError(
|
||||||
|
f"galaxy discover hierarchy returned repeated page token {page_token!r}"
|
||||||
|
)
|
||||||
|
seen_page_tokens.add(page_token)
|
||||||
|
|
||||||
def watch_deploy_events(
|
def watch_deploy_events(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
|
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
|
||||||
|
from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17galaxy_repository.proto\x12\x14galaxy_repository.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x17\n\x15TestConnectionRequest\"!\n\x13TestConnectionReply\x12\n\n\x02ok\x18\x01 \x01(\x08\"\x1a\n\x18GetLastDeployTimeRequest\"b\n\x16GetLastDeployTimeReply\x12\x0f\n\x07present\x18\x01 \x01(\x08\x12\x37\n\x13time_of_last_deploy\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x1a\n\x18\x44iscoverHierarchyRequest\"M\n\x16\x44iscoverHierarchyReply\x12\x33\n\x07objects\x18\x01 \x03(\x0b\x32\".galaxy_repository.v1.GalaxyObject\"U\n\x18WatchDeployEventsRequest\x12\x39\n\x15last_seen_deploy_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdd\x01\n\x0b\x44\x65ployEvent\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12/\n\x0bobserved_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13time_of_last_deploy\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12#\n\x1btime_of_last_deploy_present\x18\x04 \x01(\x08\x12\x14\n\x0cobject_count\x18\x05 \x01(\x05\x12\x17\n\x0f\x61ttribute_count\x18\x06 \x01(\x05\"\x93\x02\n\x0cGalaxyObject\x12\x12\n\ngobject_id\x18\x01 \x01(\x05\x12\x10\n\x08tag_name\x18\x02 \x01(\t\x12\x16\n\x0e\x63ontained_name\x18\x03 \x01(\t\x12\x13\n\x0b\x62rowse_name\x18\x04 \x01(\t\x12\x19\n\x11parent_gobject_id\x18\x05 \x01(\x05\x12\x0f\n\x07is_area\x18\x06 \x01(\x08\x12\x13\n\x0b\x63\x61tegory_id\x18\x07 \x01(\x05\x12\x1c\n\x14hosted_by_gobject_id\x18\x08 \x01(\x05\x12\x16\n\x0etemplate_chain\x18\t \x03(\t\x12\x39\n\nattributes\x18\n \x03(\x0b\x32%.galaxy_repository.v1.GalaxyAttribute\"\xa8\x02\n\x0fGalaxyAttribute\x12\x16\n\x0e\x61ttribute_name\x18\x01 \x01(\t\x12\x1a\n\x12\x66ull_tag_reference\x18\x02 \x01(\t\x12\x14\n\x0cmx_data_type\x18\x03 \x01(\x05\x12\x16\n\x0e\x64\x61ta_type_name\x18\x04 \x01(\t\x12\x10\n\x08is_array\x18\x05 \x01(\x08\x12\x17\n\x0f\x61rray_dimension\x18\x06 \x01(\x05\x12\x1f\n\x17\x61rray_dimension_present\x18\x07 \x01(\x08\x12\x1d\n\x15mx_attribute_category\x18\x08 \x01(\x05\x12\x1f\n\x17security_classification\x18\t \x01(\x05\x12\x15\n\ris_historized\x18\n \x01(\x08\x12\x10\n\x08is_alarm\x18\x0b \x01(\x08\x32\xcc\x03\n\x10GalaxyRepository\x12h\n\x0eTestConnection\x12+.galaxy_repository.v1.TestConnectionRequest\x1a).galaxy_repository.v1.TestConnectionReply\x12q\n\x11GetLastDeployTime\x12..galaxy_repository.v1.GetLastDeployTimeRequest\x1a,.galaxy_repository.v1.GetLastDeployTimeReply\x12q\n\x11\x44iscoverHierarchy\x12..galaxy_repository.v1.DiscoverHierarchyRequest\x1a,.galaxy_repository.v1.DiscoverHierarchyReply\x12h\n\x11WatchDeployEvents\x12..galaxy_repository.v1.WatchDeployEventsRequest\x1a!.galaxy_repository.v1.DeployEvent0\x01\x42#\xaa\x02 MxGateway.Contracts.Proto.Galaxyb\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17galaxy_repository.proto\x12\x14galaxy_repository.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\x17\n\x15TestConnectionRequest\"!\n\x13TestConnectionReply\x12\n\n\x02ok\x18\x01 \x01(\x08\"\x1a\n\x18GetLastDeployTimeRequest\"b\n\x16GetLastDeployTimeReply\x12\x0f\n\x07present\x18\x01 \x01(\x08\x12\x37\n\x13time_of_last_deploy\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x87\x03\n\x18\x44iscoverHierarchyRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x19\n\x0froot_gobject_id\x18\x03 \x01(\x05H\x00\x12\x17\n\rroot_tag_name\x18\x04 \x01(\tH\x00\x12\x1d\n\x13root_contained_path\x18\x05 \x01(\tH\x00\x12.\n\tmax_depth\x18\x06 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x14\n\x0c\x63\x61tegory_ids\x18\x07 \x03(\x05\x12\x1f\n\x17template_chain_contains\x18\x08 \x03(\t\x12\x15\n\rtag_name_glob\x18\t \x01(\t\x12\x1f\n\x12include_attributes\x18\n \x01(\x08H\x01\x88\x01\x01\x12\x1a\n\x12\x61larm_bearing_only\x18\x0b \x01(\x08\x12\x17\n\x0fhistorized_only\x18\x0c \x01(\x08\x42\x06\n\x04rootB\x15\n\x13_include_attributes\"\x82\x01\n\x16\x44iscoverHierarchyReply\x12\x33\n\x07objects\x18\x01 \x03(\x0b\x32\".galaxy_repository.v1.GalaxyObject\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\x12\x1a\n\x12total_object_count\x18\x03 \x01(\x05\"U\n\x18WatchDeployEventsRequest\x12\x39\n\x15last_seen_deploy_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdd\x01\n\x0b\x44\x65ployEvent\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12/\n\x0bobserved_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13time_of_last_deploy\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12#\n\x1btime_of_last_deploy_present\x18\x04 \x01(\x08\x12\x14\n\x0cobject_count\x18\x05 \x01(\x05\x12\x17\n\x0f\x61ttribute_count\x18\x06 \x01(\x05\"\x93\x02\n\x0cGalaxyObject\x12\x12\n\ngobject_id\x18\x01 \x01(\x05\x12\x10\n\x08tag_name\x18\x02 \x01(\t\x12\x16\n\x0e\x63ontained_name\x18\x03 \x01(\t\x12\x13\n\x0b\x62rowse_name\x18\x04 \x01(\t\x12\x19\n\x11parent_gobject_id\x18\x05 \x01(\x05\x12\x0f\n\x07is_area\x18\x06 \x01(\x08\x12\x13\n\x0b\x63\x61tegory_id\x18\x07 \x01(\x05\x12\x1c\n\x14hosted_by_gobject_id\x18\x08 \x01(\x05\x12\x16\n\x0etemplate_chain\x18\t \x03(\t\x12\x39\n\nattributes\x18\n \x03(\x0b\x32%.galaxy_repository.v1.GalaxyAttribute\"\xa8\x02\n\x0fGalaxyAttribute\x12\x16\n\x0e\x61ttribute_name\x18\x01 \x01(\t\x12\x1a\n\x12\x66ull_tag_reference\x18\x02 \x01(\t\x12\x14\n\x0cmx_data_type\x18\x03 \x01(\x05\x12\x16\n\x0e\x64\x61ta_type_name\x18\x04 \x01(\t\x12\x10\n\x08is_array\x18\x05 \x01(\x08\x12\x17\n\x0f\x61rray_dimension\x18\x06 \x01(\x05\x12\x1f\n\x17\x61rray_dimension_present\x18\x07 \x01(\x08\x12\x1d\n\x15mx_attribute_category\x18\x08 \x01(\x05\x12\x1f\n\x17security_classification\x18\t \x01(\x05\x12\x15\n\ris_historized\x18\n \x01(\x08\x12\x10\n\x08is_alarm\x18\x0b \x01(\x08\x32\xcc\x03\n\x10GalaxyRepository\x12h\n\x0eTestConnection\x12+.galaxy_repository.v1.TestConnectionRequest\x1a).galaxy_repository.v1.TestConnectionReply\x12q\n\x11GetLastDeployTime\x12..galaxy_repository.v1.GetLastDeployTimeRequest\x1a,.galaxy_repository.v1.GetLastDeployTimeReply\x12q\n\x11\x44iscoverHierarchy\x12..galaxy_repository.v1.DiscoverHierarchyRequest\x1a,.galaxy_repository.v1.DiscoverHierarchyReply\x12h\n\x11WatchDeployEvents\x12..galaxy_repository.v1.WatchDeployEventsRequest\x1a!.galaxy_repository.v1.DeployEvent0\x01\x42#\xaa\x02 MxGateway.Contracts.Proto.Galaxyb\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
@@ -33,26 +34,26 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'galaxy_repository_pb2', _gl
|
|||||||
if not _descriptor._USE_C_DESCRIPTORS:
|
if not _descriptor._USE_C_DESCRIPTORS:
|
||||||
_globals['DESCRIPTOR']._loaded_options = None
|
_globals['DESCRIPTOR']._loaded_options = None
|
||||||
_globals['DESCRIPTOR']._serialized_options = b'\252\002 MxGateway.Contracts.Proto.Galaxy'
|
_globals['DESCRIPTOR']._serialized_options = b'\252\002 MxGateway.Contracts.Proto.Galaxy'
|
||||||
_globals['_TESTCONNECTIONREQUEST']._serialized_start=82
|
_globals['_TESTCONNECTIONREQUEST']._serialized_start=114
|
||||||
_globals['_TESTCONNECTIONREQUEST']._serialized_end=105
|
_globals['_TESTCONNECTIONREQUEST']._serialized_end=137
|
||||||
_globals['_TESTCONNECTIONREPLY']._serialized_start=107
|
_globals['_TESTCONNECTIONREPLY']._serialized_start=139
|
||||||
_globals['_TESTCONNECTIONREPLY']._serialized_end=140
|
_globals['_TESTCONNECTIONREPLY']._serialized_end=172
|
||||||
_globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_start=142
|
_globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_start=174
|
||||||
_globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_end=168
|
_globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_end=200
|
||||||
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_start=170
|
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_start=202
|
||||||
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_end=268
|
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_end=300
|
||||||
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_start=270
|
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_start=303
|
||||||
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_end=296
|
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_end=694
|
||||||
_globals['_DISCOVERHIERARCHYREPLY']._serialized_start=298
|
_globals['_DISCOVERHIERARCHYREPLY']._serialized_start=697
|
||||||
_globals['_DISCOVERHIERARCHYREPLY']._serialized_end=375
|
_globals['_DISCOVERHIERARCHYREPLY']._serialized_end=827
|
||||||
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_start=377
|
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_start=829
|
||||||
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_end=462
|
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_end=914
|
||||||
_globals['_DEPLOYEVENT']._serialized_start=465
|
_globals['_DEPLOYEVENT']._serialized_start=917
|
||||||
_globals['_DEPLOYEVENT']._serialized_end=686
|
_globals['_DEPLOYEVENT']._serialized_end=1138
|
||||||
_globals['_GALAXYOBJECT']._serialized_start=689
|
_globals['_GALAXYOBJECT']._serialized_start=1141
|
||||||
_globals['_GALAXYOBJECT']._serialized_end=964
|
_globals['_GALAXYOBJECT']._serialized_end=1416
|
||||||
_globals['_GALAXYATTRIBUTE']._serialized_start=967
|
_globals['_GALAXYATTRIBUTE']._serialized_start=1419
|
||||||
_globals['_GALAXYATTRIBUTE']._serialized_end=1263
|
_globals['_GALAXYATTRIBUTE']._serialized_end=1715
|
||||||
_globals['_GALAXYREPOSITORY']._serialized_start=1266
|
_globals['_GALAXYREPOSITORY']._serialized_start=1718
|
||||||
_globals['_GALAXYREPOSITORY']._serialized_end=1726
|
_globals['_GALAXYREPOSITORY']._serialized_end=2178
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -55,6 +55,16 @@ class MxAccessGatewayStub(object):
|
|||||||
request_serializer=mxaccess__gateway__pb2.StreamEventsRequest.SerializeToString,
|
request_serializer=mxaccess__gateway__pb2.StreamEventsRequest.SerializeToString,
|
||||||
response_deserializer=mxaccess__gateway__pb2.MxEvent.FromString,
|
response_deserializer=mxaccess__gateway__pb2.MxEvent.FromString,
|
||||||
_registered_method=True)
|
_registered_method=True)
|
||||||
|
self.AcknowledgeAlarm = channel.unary_unary(
|
||||||
|
'/mxaccess_gateway.v1.MxAccessGateway/AcknowledgeAlarm',
|
||||||
|
request_serializer=mxaccess__gateway__pb2.AcknowledgeAlarmRequest.SerializeToString,
|
||||||
|
response_deserializer=mxaccess__gateway__pb2.AcknowledgeAlarmReply.FromString,
|
||||||
|
_registered_method=True)
|
||||||
|
self.QueryActiveAlarms = channel.unary_stream(
|
||||||
|
'/mxaccess_gateway.v1.MxAccessGateway/QueryActiveAlarms',
|
||||||
|
request_serializer=mxaccess__gateway__pb2.QueryActiveAlarmsRequest.SerializeToString,
|
||||||
|
response_deserializer=mxaccess__gateway__pb2.ActiveAlarmSnapshot.FromString,
|
||||||
|
_registered_method=True)
|
||||||
|
|
||||||
|
|
||||||
class MxAccessGatewayServicer(object):
|
class MxAccessGatewayServicer(object):
|
||||||
@@ -85,6 +95,18 @@ class MxAccessGatewayServicer(object):
|
|||||||
context.set_details('Method not implemented!')
|
context.set_details('Method not implemented!')
|
||||||
raise NotImplementedError('Method not implemented!')
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def AcknowledgeAlarm(self, request, context):
|
||||||
|
"""Missing associated documentation comment in .proto file."""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def QueryActiveAlarms(self, request, context):
|
||||||
|
"""Missing associated documentation comment in .proto file."""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
|
||||||
def add_MxAccessGatewayServicer_to_server(servicer, server):
|
def add_MxAccessGatewayServicer_to_server(servicer, server):
|
||||||
rpc_method_handlers = {
|
rpc_method_handlers = {
|
||||||
@@ -108,6 +130,16 @@ def add_MxAccessGatewayServicer_to_server(servicer, server):
|
|||||||
request_deserializer=mxaccess__gateway__pb2.StreamEventsRequest.FromString,
|
request_deserializer=mxaccess__gateway__pb2.StreamEventsRequest.FromString,
|
||||||
response_serializer=mxaccess__gateway__pb2.MxEvent.SerializeToString,
|
response_serializer=mxaccess__gateway__pb2.MxEvent.SerializeToString,
|
||||||
),
|
),
|
||||||
|
'AcknowledgeAlarm': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.AcknowledgeAlarm,
|
||||||
|
request_deserializer=mxaccess__gateway__pb2.AcknowledgeAlarmRequest.FromString,
|
||||||
|
response_serializer=mxaccess__gateway__pb2.AcknowledgeAlarmReply.SerializeToString,
|
||||||
|
),
|
||||||
|
'QueryActiveAlarms': grpc.unary_stream_rpc_method_handler(
|
||||||
|
servicer.QueryActiveAlarms,
|
||||||
|
request_deserializer=mxaccess__gateway__pb2.QueryActiveAlarmsRequest.FromString,
|
||||||
|
response_serializer=mxaccess__gateway__pb2.ActiveAlarmSnapshot.SerializeToString,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
generic_handler = grpc.method_handlers_generic_handler(
|
generic_handler = grpc.method_handlers_generic_handler(
|
||||||
'mxaccess_gateway.v1.MxAccessGateway', rpc_method_handlers)
|
'mxaccess_gateway.v1.MxAccessGateway', rpc_method_handlers)
|
||||||
@@ -227,3 +259,57 @@ class MxAccessGateway(object):
|
|||||||
timeout,
|
timeout,
|
||||||
metadata,
|
metadata,
|
||||||
_registered_method=True)
|
_registered_method=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def AcknowledgeAlarm(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
'/mxaccess_gateway.v1.MxAccessGateway/AcknowledgeAlarm',
|
||||||
|
mxaccess__gateway__pb2.AcknowledgeAlarmRequest.SerializeToString,
|
||||||
|
mxaccess__gateway__pb2.AcknowledgeAlarmReply.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def QueryActiveAlarms(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_stream(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
'/mxaccess_gateway.v1.MxAccessGateway/QueryActiveAlarms',
|
||||||
|
mxaccess__gateway__pb2.QueryActiveAlarmsRequest.SerializeToString,
|
||||||
|
mxaccess__gateway__pb2.ActiveAlarmSnapshot.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user