Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf20142634 | |||
| b995c174eb | |||
| ac2787f619 | |||
| d543679044 |
@@ -45,7 +45,6 @@ build/
|
|||||||
out/
|
out/
|
||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
install/
|
|
||||||
|
|
||||||
# .NET
|
# .NET
|
||||||
**/bin/
|
**/bin/
|
||||||
@@ -147,8 +146,3 @@ generated-scratch/
|
|||||||
|
|
||||||
# Keep empty directories with .gitkeep files when needed
|
# Keep empty directories with .gitkeep files when needed
|
||||||
!.gitkeep
|
!.gitkeep
|
||||||
|
|
||||||
# Documentation review artifacts (CommentChecker output)
|
|
||||||
*-docs-issues.md
|
|
||||||
*-docs-fixed.md
|
|
||||||
*-docs-final.md
|
|
||||||
|
|||||||
@@ -0,0 +1,481 @@
|
|||||||
|
# MXAccess Gateway Agent Guide
|
||||||
|
|
||||||
|
Repository: https://gitea.dohertylan.com/dohertj2/mxaccessgw
|
||||||
|
|
||||||
|
This project builds a gateway that gives modern clients full MXAccess parity
|
||||||
|
without requiring those clients to load MXAccess COM, run as x86, or own an STA
|
||||||
|
message pump. Treat the installed MXAccess COM component as the compatibility
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
Toolchain paths, versions, and external analysis locations are recorded in
|
||||||
|
`docs/toolchain-links.md`. Use that file before searching for compilers,
|
||||||
|
runtimes, protobuf tools, MXAccess notes, or Galaxy Repository SQL notes.
|
||||||
|
|
||||||
|
Implementation planning is recorded in `docs/implementation-plan-index.md`.
|
||||||
|
Follow the order there unless the user explicitly reprioritizes: gateway first,
|
||||||
|
MXAccess worker instance second, clients third.
|
||||||
|
|
||||||
|
## Core Contract
|
||||||
|
|
||||||
|
Preserve MXAccess behavior first:
|
||||||
|
|
||||||
|
- public MXAccess command semantics,
|
||||||
|
- native MXAccess event families,
|
||||||
|
- STA/message-pump delivery behavior,
|
||||||
|
- installed-provider quirks,
|
||||||
|
- HRESULT/status/value marshaling,
|
||||||
|
- per-client isolation.
|
||||||
|
|
||||||
|
Do not simplify, normalize, or "fix" MXAccess behavior unless an explicit
|
||||||
|
non-parity mode is being implemented and tested. `MxAsbClient` and managed NMX
|
||||||
|
are future acceleration paths only; they do not define the parity contract.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The intended split is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
client
|
||||||
|
-> gRPC over TCP
|
||||||
|
-> .NET 10 x64 gateway
|
||||||
|
-> session manager
|
||||||
|
-> per-session .NET Framework 4.8 x86 worker process
|
||||||
|
-> dedicated STA thread
|
||||||
|
-> MXAccess COM instance
|
||||||
|
-> Windows/COM message pump
|
||||||
|
-> command queue
|
||||||
|
-> event sink
|
||||||
|
```
|
||||||
|
|
||||||
|
The gateway must never instantiate or call MXAccess directly. All MXAccess COM
|
||||||
|
interaction belongs in the worker process on its dedicated STA thread.
|
||||||
|
|
||||||
|
The worker must not host public gRPC. Gateway-to-worker communication should use
|
||||||
|
a small local IPC protocol, with named pipes and protobuf-framed messages as the
|
||||||
|
default design.
|
||||||
|
|
||||||
|
## Runtime Targets
|
||||||
|
|
||||||
|
- Gateway: .NET 10, C#, x64 preferred, ASP.NET Core gRPC.
|
||||||
|
- Worker: .NET Framework 4.8, C#, x86 by default.
|
||||||
|
- Worker IPC: one bidirectional named pipe per worker.
|
||||||
|
- Worker process model: one external client session maps to one worker by
|
||||||
|
default.
|
||||||
|
|
||||||
|
## Style Guides
|
||||||
|
|
||||||
|
Follow the project documentation guide and the language guide for every changed
|
||||||
|
area:
|
||||||
|
|
||||||
|
| Area | Style guide |
|
||||||
|
|------|-------------|
|
||||||
|
| Documentation | `StyleGuide.md` |
|
||||||
|
| Gateway, worker, .NET client, and C# tests | `docs/style-guides/CSharpStyleGuide.md` |
|
||||||
|
| Public gRPC and worker IPC contracts | `docs/style-guides/ProtobufStyleGuide.md` |
|
||||||
|
| Go client | `docs/style-guides/GoStyleGuide.md` |
|
||||||
|
| Rust client | `docs/style-guides/RustStyleGuide.md` |
|
||||||
|
| Python client | `docs/style-guides/PythonStyleGuide.md` |
|
||||||
|
| Java client | `docs/style-guides/JavaStyleGuide.md` |
|
||||||
|
|
||||||
|
When a change crosses languages, apply every affected style guide. Generated
|
||||||
|
code follows its generator output; do not hand-edit it to match handwritten
|
||||||
|
style.
|
||||||
|
|
||||||
|
## Expected Layout
|
||||||
|
|
||||||
|
Prefer this structure unless there is a strong reason to adjust it:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/MxGateway.Contracts/
|
||||||
|
Protos/
|
||||||
|
mxaccess_gateway.proto
|
||||||
|
mxaccess_worker.proto
|
||||||
|
Generated/
|
||||||
|
|
||||||
|
src/MxGateway.Server/
|
||||||
|
Program.cs
|
||||||
|
Sessions/
|
||||||
|
Workers/
|
||||||
|
Grpc/
|
||||||
|
Dashboard/
|
||||||
|
Metrics/
|
||||||
|
|
||||||
|
src/MxGateway.Worker/
|
||||||
|
Program.cs
|
||||||
|
Ipc/
|
||||||
|
Sta/
|
||||||
|
MxAccess/
|
||||||
|
Conversion/
|
||||||
|
|
||||||
|
src/MxGateway.Tests/
|
||||||
|
contract tests
|
||||||
|
gateway session tests
|
||||||
|
fake worker tests
|
||||||
|
|
||||||
|
src/MxGateway.Worker.Tests/
|
||||||
|
value/status conversion tests
|
||||||
|
STA queue tests
|
||||||
|
|
||||||
|
src/MxGateway.IntegrationTests/
|
||||||
|
optional live MXAccess tests
|
||||||
|
|
||||||
|
clients/dotnet/
|
||||||
|
.NET 10 C# client library, test CLI, and tests
|
||||||
|
|
||||||
|
clients/go/
|
||||||
|
Go client module, test CLI, and tests
|
||||||
|
|
||||||
|
clients/rust/
|
||||||
|
Rust client crate, test CLI, and tests
|
||||||
|
|
||||||
|
clients/python/
|
||||||
|
Python client package, test CLI, and tests
|
||||||
|
|
||||||
|
clients/java/
|
||||||
|
Java client library, test CLI, and tests
|
||||||
|
```
|
||||||
|
|
||||||
|
The contracts project may multi-target, or the `.proto` files may be shared as
|
||||||
|
source inputs to both gateway and worker builds.
|
||||||
|
|
||||||
|
## Public API Shape
|
||||||
|
|
||||||
|
The external API should be session-oriented. Initial rollout should prefer
|
||||||
|
unary `OpenSession`, `CloseSession`, and `Invoke`, plus server-streaming
|
||||||
|
`StreamEvents`. Add a bidirectional `Session` stream after the command and event
|
||||||
|
model is stable.
|
||||||
|
|
||||||
|
Do not compress MXAccess into generic verbs too early. Use a command enum with
|
||||||
|
method-specific payloads so parity can be tested method by method.
|
||||||
|
|
||||||
|
Core MXAccess commands to represent:
|
||||||
|
|
||||||
|
- `Register`
|
||||||
|
- `Unregister`
|
||||||
|
- `AddItem`
|
||||||
|
- `AddItem2`
|
||||||
|
- `RemoveItem`
|
||||||
|
- `Advise`
|
||||||
|
- `UnAdvise`
|
||||||
|
- `AdviseSupervisory`
|
||||||
|
- `AddBufferedItem`
|
||||||
|
- `SetBufferedUpdateInterval`
|
||||||
|
- `Suspend`
|
||||||
|
- `Activate`
|
||||||
|
- `Write`
|
||||||
|
- `Write2`
|
||||||
|
- `WriteSecured`
|
||||||
|
- `WriteSecured2`
|
||||||
|
- `AuthenticateUser`
|
||||||
|
- `ArchestrAUserToId`
|
||||||
|
|
||||||
|
Diagnostics may include `Ping`, `GetSessionState`, `GetWorkerInfo`,
|
||||||
|
`DrainEvents`, and `ShutdownWorker`.
|
||||||
|
|
||||||
|
## Event Requirements
|
||||||
|
|
||||||
|
Represent every public MXAccess event family:
|
||||||
|
|
||||||
|
- `OnDataChange`
|
||||||
|
- `OnWriteComplete`
|
||||||
|
- `OperationComplete`
|
||||||
|
- `OnBufferedDataChange`
|
||||||
|
|
||||||
|
Preserve per-worker event order. The gateway must not reorder events emitted by
|
||||||
|
the same MXAccess instance.
|
||||||
|
|
||||||
|
Event DTOs should carry event family, session id, server handle, item handle,
|
||||||
|
value, quality, timestamp, `MXSTATUS_PROXY[]` equivalent, raw HRESULT/status
|
||||||
|
fields when available, event sequence, worker timestamp, and gateway receive
|
||||||
|
timestamp.
|
||||||
|
|
||||||
|
## Value And Status Rules
|
||||||
|
|
||||||
|
Use a protobuf value union that can represent COM `VARIANT` values and arrays.
|
||||||
|
When a value cannot be losslessly converted, preserve both the best typed
|
||||||
|
projection and enough raw diagnostic metadata to reproduce the case.
|
||||||
|
|
||||||
|
Represent `MXSTATUS_PROXY` explicitly. Do not collapse status arrays into a
|
||||||
|
single success flag.
|
||||||
|
|
||||||
|
Command replies should include protocol status, COM HRESULT if available,
|
||||||
|
MXAccess return values, method-specific out parameters, and status arrays where
|
||||||
|
the MXAccess method emits them.
|
||||||
|
|
||||||
|
## Galaxy Repository SQL Discovery
|
||||||
|
|
||||||
|
Galaxy tags, hierarchy, and attribute details can be queried from the AVEVA /
|
||||||
|
Wonderware System Platform Galaxy Repository SQL Server database. Use this as a
|
||||||
|
discovery and metadata path only; runtime MXAccess parity still belongs to the
|
||||||
|
MXAccess-backed worker unless an explicit non-parity backend is being designed.
|
||||||
|
|
||||||
|
Full notes, schema details, screenshots, and query examples are in:
|
||||||
|
|
||||||
|
```text
|
||||||
|
C:\Users\dohertj2\Desktop\lmxopcua\gr
|
||||||
|
```
|
||||||
|
|
||||||
|
Important files in that notes directory:
|
||||||
|
|
||||||
|
- `connectioninfo.md` - SQL Server connection details and `sqlcmd` usage.
|
||||||
|
- `layout.md` - hierarchy vs `tag_name` relationship.
|
||||||
|
- `build_layout_plan.md` - extraction plan for hierarchy and attributes.
|
||||||
|
- `schema.md` and `ddl/` - Galaxy Repository schema reference.
|
||||||
|
- `queries/hierarchy.sql` - deployed object hierarchy.
|
||||||
|
- `queries/attributes.sql` - user-defined dynamic attributes.
|
||||||
|
- `queries/attributes_extended.sql` - system plus user-defined attributes.
|
||||||
|
- `queries/change_detection.sql` - deployment-change polling via
|
||||||
|
`galaxy.time_of_last_deploy`.
|
||||||
|
|
||||||
|
Current documented connection is SQL Server `localhost`, database `ZB`, Windows
|
||||||
|
Auth. Example:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
sqlcmd -S localhost -d ZB -E -Q "SELECT time_of_last_deploy FROM galaxy;"
|
||||||
|
```
|
||||||
|
|
||||||
|
Key tables from the notes are `gobject`, `template_definition`,
|
||||||
|
`dynamic_attribute`, `attribute_definition`, `primitive_instance`, and
|
||||||
|
`galaxy`. The hierarchy uses contained names for human-readable browsing, while
|
||||||
|
runtime tag references use globally unique `tag_name` values such as
|
||||||
|
`<tag_name>.<AttributeName>`.
|
||||||
|
|
||||||
|
## MXAccess Analysis Source
|
||||||
|
|
||||||
|
Use the local MXAccess analysis project when answering questions about installed
|
||||||
|
MXAccess classes, interfaces, fields, events, HRESULT/status behavior, value
|
||||||
|
projection, captures, and parity gaps:
|
||||||
|
|
||||||
|
```text
|
||||||
|
C:\Users\dohertj2\Desktop\mxaccess
|
||||||
|
```
|
||||||
|
|
||||||
|
Primary files:
|
||||||
|
|
||||||
|
- `README.md` - overview of available analysis and capture artifacts.
|
||||||
|
- `docs/MXAccess-Public-API.md` - COM class, ProgID, CLSID, method list,
|
||||||
|
event signatures, `MxDataType`, `MxStatus`, and `MXSTATUS_PROXY`.
|
||||||
|
- `docs/MXAccess-Reverse-Engineering.md` - installed runtime path and x86 COM
|
||||||
|
constraints.
|
||||||
|
- `docs/Current-Sprint-State.md` and `docs/DotNet10-Native-Library-Plan.md` -
|
||||||
|
current parity gaps and managed native-client research status.
|
||||||
|
- `src/MxTraceHarness/` - x86 MXAccess harness examples using the real COM
|
||||||
|
interop assembly.
|
||||||
|
- `captures/` and `analysis/` - observed native behavior and generated
|
||||||
|
reverse-engineering artifacts.
|
||||||
|
|
||||||
|
Concrete MXAccess COM target from the analysis:
|
||||||
|
|
||||||
|
- class: `ArchestrA.MxAccess.LMXProxyServerClass`
|
||||||
|
- CLSID: `{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}`
|
||||||
|
- ProgID: `LMXProxy.LMXProxyServer.1`
|
||||||
|
- version-independent ProgID: `LMXProxy.LMXProxyServer`
|
||||||
|
- registered server: `C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll`
|
||||||
|
- interop assembly:
|
||||||
|
`C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`
|
||||||
|
- threading model: `Apartment`
|
||||||
|
|
||||||
|
## Worker Rules
|
||||||
|
|
||||||
|
Each worker owns:
|
||||||
|
|
||||||
|
- one process,
|
||||||
|
- one MXAccess session,
|
||||||
|
- one dedicated STA thread,
|
||||||
|
- one MXAccess COM object,
|
||||||
|
- one inbound command queue,
|
||||||
|
- one outbound event queue.
|
||||||
|
|
||||||
|
All MXAccess operations must run on the STA. A plain blocking queue is not
|
||||||
|
enough for the STA; the STA loop must pump Windows/COM messages and service
|
||||||
|
queued commands.
|
||||||
|
|
||||||
|
Do not block the STA on pipe writes, gRPC calls, or slow consumers. Event
|
||||||
|
handlers should convert event args, enqueue outbound events, and return to
|
||||||
|
pumping messages.
|
||||||
|
|
||||||
|
On graceful shutdown, reject new commands, optionally clean up active MXAccess
|
||||||
|
handles, detach events, release the COM object, uninitialize COM, and exit. If
|
||||||
|
graceful shutdown exceeds the configured timeout, the gateway may kill the
|
||||||
|
worker.
|
||||||
|
|
||||||
|
## IPC Rules
|
||||||
|
|
||||||
|
Default pipe name shape:
|
||||||
|
|
||||||
|
```text
|
||||||
|
mxaccess-gateway-{gatewayProcessId}-{sessionId}
|
||||||
|
```
|
||||||
|
|
||||||
|
Frame messages as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
uint32 little-endian payload_length
|
||||||
|
payload_length bytes protobuf WorkerEnvelope
|
||||||
|
```
|
||||||
|
|
||||||
|
Every envelope should include protocol version, session id, monotonic sender
|
||||||
|
sequence, correlation id, and a typed body. Protocol version mismatch should
|
||||||
|
fail session creation.
|
||||||
|
|
||||||
|
Pipe security should be local-machine only, with ACLs restricted to the gateway
|
||||||
|
identity and launched worker identity. Prefer a per-session nonce handshake.
|
||||||
|
|
||||||
|
## Gateway Rules
|
||||||
|
|
||||||
|
The gateway is responsible for:
|
||||||
|
|
||||||
|
- public TCP/gRPC API,
|
||||||
|
- Blazor Server dashboard using Bootstrap CSS/JS only,
|
||||||
|
- authn/authz when needed,
|
||||||
|
- session creation and teardown,
|
||||||
|
- worker launch and lifecycle management,
|
||||||
|
- command routing,
|
||||||
|
- event streaming,
|
||||||
|
- leases, heartbeats, timeouts, and quotas,
|
||||||
|
- worker kill/restart policy,
|
||||||
|
- metrics and structured logs.
|
||||||
|
|
||||||
|
The gRPC layer should stay thin: validate request, find session, call the
|
||||||
|
session worker client, map worker replies to public replies, and stream events.
|
||||||
|
Keep MXAccess-specific translation logic testable outside the gRPC handlers.
|
||||||
|
|
||||||
|
Dashboard code should also stay thin and read-only for v1. Use a snapshot
|
||||||
|
service over session/worker/metrics state; do not let Razor components mutate
|
||||||
|
gateway sessions or workers directly. Do not use MudBlazor or other Blazor UI
|
||||||
|
component libraries.
|
||||||
|
|
||||||
|
Gateway restart should not try to reattach old workers in the first version.
|
||||||
|
Terminate orphaned workers on startup if that behavior is implemented.
|
||||||
|
|
||||||
|
## Command, Timeout, And Cancellation Semantics
|
||||||
|
|
||||||
|
Command lifecycle:
|
||||||
|
|
||||||
|
```text
|
||||||
|
client gRPC command
|
||||||
|
gateway validates session and payload
|
||||||
|
gateway assigns correlation id
|
||||||
|
gateway writes WorkerCommand to pipe
|
||||||
|
worker queues command to STA
|
||||||
|
STA executes MXAccess method
|
||||||
|
worker captures return/out/status/HRESULT
|
||||||
|
worker sends WorkerCommandReply
|
||||||
|
gateway completes gRPC response
|
||||||
|
```
|
||||||
|
|
||||||
|
Canceling a gRPC call should stop waiting in the gateway, but it cannot safely
|
||||||
|
abort an in-flight COM call on the STA. Hard cancellation means killing the
|
||||||
|
worker process.
|
||||||
|
|
||||||
|
If a command wedges the STA beyond a configured grace period, the gateway should
|
||||||
|
kill the worker and fail the session.
|
||||||
|
|
||||||
|
## Backpressure Policy
|
||||||
|
|
||||||
|
Worker outbound events must use a bounded queue. For parity testing, prefer
|
||||||
|
fail-fast behavior over silent drops. Production coalescing or drop policies
|
||||||
|
must be explicit and observable.
|
||||||
|
|
||||||
|
The gateway should preserve per-session event order, apply backpressure from
|
||||||
|
slow gRPC streams, and disconnect or coalesce only according to an explicit
|
||||||
|
policy.
|
||||||
|
|
||||||
|
## Security And Logging
|
||||||
|
|
||||||
|
Use TLS for remote gRPC when crossing machine boundaries. Authentication may be
|
||||||
|
Windows auth, mTLS, or a deployment-specific token.
|
||||||
|
|
||||||
|
Commands that write, authenticate users, or alter runtime state need explicit
|
||||||
|
authorization design.
|
||||||
|
|
||||||
|
Never log passwords or raw credential values for `AuthenticateUser`,
|
||||||
|
`WriteSecured`, or related secured operations. Do not log full values by
|
||||||
|
default; make value logging opt-in and redacted.
|
||||||
|
|
||||||
|
## Testing Expectations
|
||||||
|
|
||||||
|
Use focused tests for:
|
||||||
|
|
||||||
|
- contract/protobuf compatibility,
|
||||||
|
- gateway session state and worker lifecycle,
|
||||||
|
- gateway behavior with a fake worker,
|
||||||
|
- worker value/status conversion,
|
||||||
|
- STA queue and message-pump behavior.
|
||||||
|
|
||||||
|
Live MXAccess integration tests are optional but should be isolated because they
|
||||||
|
depend on installed COM components and provider behavior.
|
||||||
|
|
||||||
|
Parity tests should compare direct MXAccess behavior against the gateway:
|
||||||
|
|
||||||
|
- return values,
|
||||||
|
- HRESULTs and exceptions,
|
||||||
|
- event sequence,
|
||||||
|
- value projection,
|
||||||
|
- quality/status arrays,
|
||||||
|
- invalid handle behavior,
|
||||||
|
- cross-server handle behavior,
|
||||||
|
- cleanup behavior.
|
||||||
|
|
||||||
|
Known important parity areas:
|
||||||
|
|
||||||
|
- `WriteSecured` may fail before a value-bearing NMX body is emitted.
|
||||||
|
- `WriteSecured2` can succeed in observed native paths.
|
||||||
|
- `OperationComplete` is distinct from write completion.
|
||||||
|
- `OnBufferedDataChange` has a distinct public event shape.
|
||||||
|
- Invalid handles and cross-server handles have specific exception/status
|
||||||
|
behavior.
|
||||||
|
- STA message pumping is required for event delivery.
|
||||||
|
|
||||||
|
## Source Update Workflow
|
||||||
|
|
||||||
|
When source code changes, build the affected component before handing work
|
||||||
|
back. If the change crosses component boundaries, build each affected component
|
||||||
|
instead of relying on a single top-level build.
|
||||||
|
|
||||||
|
Use the native build and test command for each changed area:
|
||||||
|
|
||||||
|
| Changed area | Required verification |
|
||||||
|
|--------------|-----------------------|
|
||||||
|
| Contracts or `.proto` files | regenerate generated code, then build gateway, worker, and every generated client touched by the contract |
|
||||||
|
| Gateway server, sessions, workers, gRPC, dashboard, or metrics | build the .NET 10 gateway project and run affected gateway or fake-worker tests |
|
||||||
|
| Worker IPC, STA, MXAccess, or conversion code | build the .NET Framework 4.8 x86 worker project and run affected worker tests |
|
||||||
|
| Shared test infrastructure | run every test suite that consumes the changed helpers |
|
||||||
|
| .NET client | build the .NET client library, CLI, and tests |
|
||||||
|
| Go client | run Go formatting, build, and tests for the Go module |
|
||||||
|
| Rust client | run Rust formatting, build or check, and tests for the Rust crate |
|
||||||
|
| Python client | run Python formatting or linting if configured, package/build checks, and tests |
|
||||||
|
| Java client | build the Java client library, CLI, and tests |
|
||||||
|
| Integration tests | run them only when the required MXAccess COM component, provider state, and external services are available; otherwise document why they were skipped |
|
||||||
|
|
||||||
|
Update affected documentation in the same change as the source update. This
|
||||||
|
includes `gateway.md`, component design docs under `docs/`, client docs, API
|
||||||
|
contract notes, test instructions, and operational guidance. Documentation must
|
||||||
|
follow `StyleGuide.md`: write technical present-tense prose, explain the reason
|
||||||
|
for non-obvious choices, use exact code names, specify languages on code
|
||||||
|
blocks, use relative links for internal docs, and avoid stale temporary notes.
|
||||||
|
Source code and contract changes must also follow the relevant language guide
|
||||||
|
from the Style Guides section.
|
||||||
|
|
||||||
|
Do not leave documentation describing old behavior after changing public APIs,
|
||||||
|
contracts, configuration, build steps, security behavior, event shapes, value
|
||||||
|
conversion, status mapping, lifecycle rules, or client semantics.
|
||||||
|
|
||||||
|
## Implementation Priority
|
||||||
|
|
||||||
|
Build the smallest end-to-end slice first:
|
||||||
|
|
||||||
|
1. .NET 10 gateway starts.
|
||||||
|
2. Client calls `OpenSession`.
|
||||||
|
3. Gateway launches .NET Framework 4.8 x86 worker.
|
||||||
|
4. Worker creates STA and MXAccess COM object.
|
||||||
|
5. Client calls `Register`.
|
||||||
|
6. Client calls `AddItem`.
|
||||||
|
7. Client calls `Advise`.
|
||||||
|
8. Worker forwards one `OnDataChange` event to the gateway.
|
||||||
|
9. Gateway streams the event to the client.
|
||||||
|
10. Client calls `CloseSession`.
|
||||||
|
11. Gateway shuts down the worker.
|
||||||
|
|
||||||
|
That slice proves the high-risk requirements: process isolation, STA ownership,
|
||||||
|
message pumping, command routing, and event streaming.
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
`mxaccessgw` is the MXAccess Gateway: a gRPC service that gives modern (.NET, Go, Rust, Python, Java) clients full MXAccess parity without forcing them to load 32-bit MXAccess COM, run x86, or own an STA message pump.
|
|
||||||
|
|
||||||
The architecture is a two-process design — read `gateway.md` before making structural changes:
|
|
||||||
|
|
||||||
- **Gateway** (`src/MxGateway.Server`, .NET 10, x64): ASP.NET Core gRPC server. Owns the public API, sessions, auth, the Blazor dashboard, and the Galaxy Repository SQL browse RPCs. **Never instantiates MXAccess COM directly.**
|
|
||||||
- **Worker** (`src/MxGateway.Worker`, .NET Framework 4.8, **x86**): one process per session. Owns one MXAccess COM instance on a dedicated STA, pumps Windows messages, and converts COM events to protobuf.
|
|
||||||
- **IPC**: gateway↔worker uses one bidirectional named pipe per worker (`mxaccess-gateway-{gatewayPid}-{sessionId}`) with length-prefixed `WorkerEnvelope` protobuf frames. Gateway hosts the pipe server and launches the worker. **gRPC is not used inside the worker** — .NET Framework 4.8 doesn't have a first-class gRPC stack.
|
|
||||||
- **Contracts** (`src/MxGateway.Contracts`): multi-targets `net10.0;net48` and owns the `.proto` files (`mxaccess_gateway.proto`, `mxaccess_worker.proto`, `galaxy_repository.proto`). All other projects consume the generated types from here. Do not hand-edit anything under `Generated/`.
|
|
||||||
|
|
||||||
The worker must do all MXAccess COM calls on its dedicated STA thread, and the STA loop must pump Windows messages (`MsgWaitForMultipleObjectsEx` + `PeekMessage`/`DispatchMessage`) so MXAccess events deliver. A plain blocking queue on an STA is not enough.
|
|
||||||
|
|
||||||
## Build, Test, Run
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Full solution build (gateway, worker, contracts, tests)
|
|
||||||
dotnet build src/MxGateway.sln
|
|
||||||
|
|
||||||
# Worker must be built x86 — the gateway looks for MxGateway.Worker.exe under bin\x86
|
|
||||||
dotnet build src/MxGateway.Worker/MxGateway.Worker.csproj -p:Platform=x86
|
|
||||||
|
|
||||||
# Gateway tests (no MXAccess required — uses FakeWorkerHarness)
|
|
||||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj
|
|
||||||
dotnet test src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj -p:Platform=x86
|
|
||||||
|
|
||||||
# Run gateway locally (defaults bound under MxGateway:* in src/MxGateway.Server/appsettings.json)
|
|
||||||
dotnet run --project src/MxGateway.Server/MxGateway.Server.csproj
|
|
||||||
|
|
||||||
# API-key admin CLI (same exe, "apikey" subcommand)
|
|
||||||
dotnet run --project src/MxGateway.Server/MxGateway.Server.csproj -- apikey create --display-name "dev" --scopes session,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=zb,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 is LDAP-backed (separate from the gRPC API-key model). `/login` binds against `MxGateway:Ldap` and maps the user's LDAP groups to `Admin` or `Viewer` via `MxGateway:Dashboard:GroupToRole`, then issues an HTTP-only secure `__Host-MxGatewayDashboard` cookie. SignalR hubs at `/hubs/{snapshot,alarms,events}` accept either the cookie or a 30-minute bearer minted at `/hubs/token`. `Dashboard:AllowAnonymousLocalhost` bypasses auth on loopback when enabled.
|
|
||||||
|
|
||||||
## Process / Platform Notes
|
|
||||||
|
|
||||||
- Working tree is on Windows (`C:\Users\dohertj2\Desktop\mxaccessgw`). PowerShell is the native shell for tooling commands; bash is fine for git/grep/find.
|
|
||||||
- The worker reference to `ArchestrA.MXAccess.dll` uses an absolute `HintPath` to `C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`. The worker only builds where MXAccess is installed (this dev box).
|
|
||||||
- The repo is not a git repository at the top level — there's no `.git` directory in the working tree.
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
# Code Review Process
|
|
||||||
|
|
||||||
This document describes how to perform a comprehensive, per-module code review of
|
|
||||||
the `mxaccessgw` codebase and how to track findings to resolution.
|
|
||||||
|
|
||||||
A **module** is one buildable project under `src/` (e.g. `src/ZB.MOM.WW.MxGateway.Worker`)
|
|
||||||
or one language client under `clients/` (e.g. `clients/rust`). Each module has
|
|
||||||
its own folder under `code-reviews/` containing a single `findings.md`.
|
|
||||||
|
|
||||||
## 1. Before you start
|
|
||||||
|
|
||||||
1. Pick the module to review. Its folder is `code-reviews/<Module>/`:
|
|
||||||
- For a `src/` project, `<Module>` is the project name with the `ZB.MOM.WW.MxGateway.`
|
|
||||||
prefix stripped — `src/ZB.MOM.WW.MxGateway.Server` is reviewed in `code-reviews/Server/`.
|
|
||||||
- For a language client, `<Module>` is `Client.<Lang>` — `clients/rust` is
|
|
||||||
reviewed in `code-reviews/Client.Rust/`.
|
|
||||||
2. Identify the design context for the module:
|
|
||||||
- `gateway.md` — top-level architecture, command/event surface, IPC envelope,
|
|
||||||
STA thread model, fault handling.
|
|
||||||
- The relevant component design docs under `docs/` (e.g.
|
|
||||||
`docs/MxAccessWorkerInstanceDesign.md`, `docs/GatewayProcessDesign.md`,
|
|
||||||
`docs/Sessions.md`, `docs/Authentication.md`, `docs/GalaxyRepository.md`).
|
|
||||||
- `docs/DesignDecisions.md` for the v1 design choices.
|
|
||||||
- The **Repository-Specific Conventions** and **Process / Platform Notes** in
|
|
||||||
`CLAUDE.md`.
|
|
||||||
3. Record the exact commit being reviewed: `git rev-parse --short HEAD`. Every
|
|
||||||
review is a snapshot — a finding only means something relative to a known
|
|
||||||
commit.
|
|
||||||
4. Open `code-reviews/<Module>/findings.md` and fill in the header table
|
|
||||||
(reviewer, date, commit SHA, status).
|
|
||||||
|
|
||||||
## 2. Review checklist
|
|
||||||
|
|
||||||
Work through **every** category below for the module. A comprehensive review
|
|
||||||
means the checklist is completed even where it produces no findings — record
|
|
||||||
"No issues found" for a category rather than leaving it ambiguous.
|
|
||||||
|
|
||||||
1. **Correctness & logic bugs** — off-by-one, null handling, incorrect
|
|
||||||
conditionals, misuse of APIs, broken edge cases.
|
|
||||||
2. **mxaccessgw conventions** — the rules in `CLAUDE.md` and the style guides
|
|
||||||
under `docs/style-guides/`: the gateway never instantiates MXAccess COM
|
|
||||||
directly; all MXAccess COM calls run on the worker's dedicated STA thread and
|
|
||||||
the STA loop pumps Windows messages; IPC uses one bidirectional named pipe per
|
|
||||||
worker carrying length-prefixed `WorkerEnvelope` protobuf frames; MXAccess
|
|
||||||
parity is the contract (don't "fix" surprising MXAccess behaviour, never
|
|
||||||
synthesize events); one worker and one event subscriber per session; the
|
|
||||||
gateway terminates orphan workers on startup and does not reattach; C# style
|
|
||||||
(file-scoped namespaces, `sealed` by default, `Async` suffix, MXAccess-aligned
|
|
||||||
names); no Blazor UI component libraries; no logging of secrets or full tag
|
|
||||||
values; generated code is never hand-edited.
|
|
||||||
3. **Concurrency & thread safety** — shared mutable state, STA affinity, race
|
|
||||||
conditions, correct use of `async`/`await`, locking, disposal races.
|
|
||||||
4. **Error handling & resilience** — exception paths, worker crash / reconnect
|
|
||||||
handling, fail-fast event backpressure, transient vs permanent error
|
|
||||||
classification, graceful degradation, correct gRPC status codes.
|
|
||||||
5. **Security** — authentication/authorization checks, API-key scope enforcement,
|
|
||||||
input validation, SQL injection in the Galaxy Repository RPCs, secret
|
|
||||||
handling, the dashboard anonymous-localhost bypass, logging of sensitive data.
|
|
||||||
6. **Performance & resource management** — `IDisposable` disposal, pipe / stream
|
|
||||||
/ COM lifetimes, buffering and back-pressure, unnecessary allocations on hot
|
|
||||||
paths, N+1 queries.
|
|
||||||
7. **Design-document adherence** — does the code match `gateway.md`, the relevant
|
|
||||||
`docs/` component designs, `docs/DesignDecisions.md`, and `CLAUDE.md`? Flag
|
|
||||||
both code that drifts from the design and design docs that are now stale.
|
|
||||||
8. **Code organization & conventions** — namespace hierarchy, project layout, the
|
|
||||||
Options pattern, separation of concerns, additive-only contract evolution.
|
|
||||||
9. **Testing coverage** — are the module's behaviours covered by tests
|
|
||||||
(`src/ZB.MOM.WW.MxGateway.Tests`, `src/ZB.MOM.WW.MxGateway.Worker.Tests`,
|
|
||||||
`src/ZB.MOM.WW.MxGateway.IntegrationTests`)? Note untested critical paths and missing
|
|
||||||
edge-case tests.
|
|
||||||
10. **Documentation & comments** — XML doc accuracy, misleading or stale comments,
|
|
||||||
undocumented non-obvious behaviour.
|
|
||||||
|
|
||||||
## 3. Recording findings
|
|
||||||
|
|
||||||
Add one entry per finding to the `## Findings` section of the module's
|
|
||||||
`findings.md`, using the entry format in
|
|
||||||
[`_template/findings.md`](code-reviews/_template/findings.md).
|
|
||||||
|
|
||||||
- **Finding ID** — `<Module>-NNN`, numbered sequentially within the module and
|
|
||||||
never reused (e.g. `Worker-001`). IDs are permanent even after resolution.
|
|
||||||
- **Severity:**
|
|
||||||
- **Critical** — data loss, security breach, crash/deadlock, or outage.
|
|
||||||
- **High** — incorrect behaviour with significant impact; no safe workaround.
|
|
||||||
- **Medium** — incorrect or risky behaviour with limited impact or a workaround.
|
|
||||||
- **Low** — minor issues, style, maintainability, documentation.
|
|
||||||
- **Category** — one of the 10 checklist categories above.
|
|
||||||
- **Location** — `file:line` (clickable), or a list of locations.
|
|
||||||
- **Description** — what is wrong and why it matters.
|
|
||||||
- **Recommendation** — concrete suggested fix.
|
|
||||||
|
|
||||||
After recording findings, update the module header table (status, open-finding
|
|
||||||
count) and regenerate the base README (step 5).
|
|
||||||
|
|
||||||
## 4. Marking an item resolved
|
|
||||||
|
|
||||||
Findings are **never deleted** — they are an audit trail. To close one, change
|
|
||||||
its **Status** and complete the **Resolution** field:
|
|
||||||
|
|
||||||
- `Open` — newly recorded, not yet addressed.
|
|
||||||
- `In Progress` — a fix is actively being worked on.
|
|
||||||
- `Resolved` — fixed. The Resolution field must state the fixing commit SHA, the
|
|
||||||
date, and a one-line description of the fix.
|
|
||||||
- `Won't Fix` — intentionally not fixed. The Resolution field must justify why.
|
|
||||||
- `Deferred` — valid but postponed. The Resolution field must say what it is
|
|
||||||
waiting on (e.g. a tracked issue or a later milestone).
|
|
||||||
|
|
||||||
`Resolved`, `Won't Fix`, and `Deferred` findings are all considered **closed**.
|
|
||||||
`Open` and `In Progress` are **pending** and appear in the base README's Pending
|
|
||||||
Findings table.
|
|
||||||
|
|
||||||
## 5. Updating the base README
|
|
||||||
|
|
||||||
`code-reviews/README.md` holds the single cross-module view (the Module Status
|
|
||||||
table and the Pending / Closed Findings tables). It is **generated** from the
|
|
||||||
per-module `findings.md` files — do not edit it by hand.
|
|
||||||
|
|
||||||
After any review or status change, regenerate it:
|
|
||||||
|
|
||||||
```
|
|
||||||
python code-reviews/regen-readme.py
|
|
||||||
```
|
|
||||||
|
|
||||||
`regen-readme.py --check` exits non-zero if `README.md` is stale, if a module
|
|
||||||
header's `Open findings` count disagrees with its finding statuses, or if a
|
|
||||||
finding carries an unrecognised Status value. The PowerShell wrapper
|
|
||||||
`scripts/check-code-reviews-readme.ps1` runs that check and is the intended hook
|
|
||||||
for CI or a pre-commit step.
|
|
||||||
|
|
||||||
> The repo's installed `python` is the real interpreter; the bare `python3`
|
|
||||||
> alias resolves to the Windows Store stub and fails. Use `python`.
|
|
||||||
|
|
||||||
The per-module `findings.md` files are the source of truth; `README.md` is the
|
|
||||||
aggregated index and must always agree with them — which the script guarantees.
|
|
||||||
|
|
||||||
## 6. Re-reviewing a module
|
|
||||||
|
|
||||||
Re-reviews append to the same `findings.md`. Update the header to the new commit
|
|
||||||
and date, continue the finding numbering from the last used ID, and leave prior
|
|
||||||
findings (including closed ones) in place as history.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<Project>
|
|
||||||
<PropertyGroup>
|
|
||||||
<!-- Shared package metadata for clients/dotnet/. Individual projects opt in via <IsPackable>true</IsPackable>. -->
|
|
||||||
<Authors>Joseph Doherty</Authors>
|
|
||||||
<Company>ZB MOM WW</Company>
|
|
||||||
<Copyright>Copyright (c) ZB MOM WW. All rights reserved.</Copyright>
|
|
||||||
<Product>MxAccessGateway Client</Product>
|
|
||||||
<RepositoryUrl>https://gitea.dohertylan.com/dohertj2/mxaccessgw</RepositoryUrl>
|
|
||||||
<RepositoryType>git</RepositoryType>
|
|
||||||
<PackageProjectUrl>https://gitea.dohertylan.com/dohertj2/mxaccessgw</PackageProjectUrl>
|
|
||||||
<PackageTags>mxaccess;mxgateway;grpc;client;archestra</PackageTags>
|
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
|
||||||
<!-- Versioning: bump per release. Symbols ship as snupkg. -->
|
|
||||||
<Version>0.1.0</Version>
|
|
||||||
<IncludeSymbols>true</IncludeSymbols>
|
|
||||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
<!-- Default: do NOT pack. Each project opts in. -->
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
+1
-29
@@ -1,15 +1,12 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.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;
|
||||||
@@ -42,17 +39,11 @@ internal sealed class CliArguments
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Returns whether the named flag was present in the arguments.</summary>
|
|
||||||
/// <param name="name">The flag name (without '--' prefix).</param>
|
|
||||||
/// <returns>True if the flag was present; otherwise false.</returns>
|
|
||||||
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>
|
|
||||||
/// <returns>The argument value, or null if the argument was not provided.</returns>
|
|
||||||
public string? GetOptional(string name)
|
public string? GetOptional(string name)
|
||||||
{
|
{
|
||||||
return _values.TryGetValue(name, out string? value)
|
return _values.TryGetValue(name, out string? value)
|
||||||
@@ -60,9 +51,6 @@ 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>
|
|
||||||
/// <returns>The argument value.</returns>
|
|
||||||
public string GetRequired(string name)
|
public string GetRequired(string name)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
@@ -74,10 +62,6 @@ 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>
|
|
||||||
/// <returns>The parsed int32 value, or the default if absent.</returns>
|
|
||||||
public int GetInt32(string name, int? defaultValue = null)
|
public int GetInt32(string name, int? defaultValue = null)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
@@ -94,10 +78,6 @@ 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>
|
|
||||||
/// <returns>The parsed uint32 value, or the default if absent.</returns>
|
|
||||||
public uint GetUInt32(string name, uint defaultValue)
|
public uint GetUInt32(string name, uint defaultValue)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
@@ -106,10 +86,6 @@ 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>
|
|
||||||
/// <returns>The parsed uint64 value, or the default if absent.</returns>
|
|
||||||
public ulong GetUInt64(string name, ulong defaultValue)
|
public ulong GetUInt64(string name, ulong defaultValue)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
@@ -118,10 +94,6 @@ 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>
|
|
||||||
/// <returns>The parsed TimeSpan value, or the default if absent.</returns>
|
|
||||||
public TimeSpan GetDuration(string name, TimeSpan defaultValue)
|
public TimeSpan GetDuration(string name, TimeSpan defaultValue)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
using MxGateway.Contracts.Proto.Galaxy;
|
||||||
|
|
||||||
|
namespace MxGateway.Client.Cli;
|
||||||
|
|
||||||
|
public interface IMxGatewayCliClient : IAsyncDisposable
|
||||||
|
{
|
||||||
|
Task<OpenSessionReply> OpenSessionAsync(
|
||||||
|
OpenSessionRequest request,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<CloseSessionReply> CloseSessionAsync(
|
||||||
|
CloseSessionRequest request,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<MxCommandReply> InvokeAsync(
|
||||||
|
MxCommandRequest request,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
|
StreamEventsRequest request,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
||||||
|
TestConnectionRequest request,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
||||||
|
GetLastDeployTimeRequest request,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
||||||
|
DiscoverHierarchyRequest request,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
||||||
|
WatchDeployEventsRequest request,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.MxGateway.Client\ZB.MOM.WW.MxGateway.Client.csproj" />
|
<ProjectReference Include="..\MxGateway.Client\MxGateway.Client.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
+4
-34
@@ -1,18 +1,14 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Client;
|
using MxGateway.Client;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
using MxGateway.Contracts.Proto;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
using MxGateway.Contracts.Proto.Galaxy;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client.Cli;
|
namespace MxGateway.Client.Cli;
|
||||||
|
|
||||||
internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
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;
|
||||||
@@ -20,7 +16,6 @@ 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)
|
||||||
@@ -28,7 +23,6 @@ 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)
|
||||||
@@ -36,7 +30,6 @@ 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)
|
||||||
@@ -44,7 +37,6 @@ 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)
|
||||||
@@ -52,23 +44,6 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _client.StreamEventsAsync(request, cancellationToken);
|
return _client.StreamEventsAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<AcknowledgeAlarmReply> AcknowledgeAlarmAsync(
|
|
||||||
AcknowledgeAlarmRequest request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return _client.AcknowledgeAlarmAsync(request, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IAsyncEnumerable<AlarmFeedMessage> StreamAlarmsAsync(
|
|
||||||
StreamAlarmsRequest request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return _client.StreamAlarmsAsync(request, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -76,7 +51,6 @@ 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)
|
||||||
@@ -84,7 +58,6 @@ 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)
|
||||||
@@ -92,7 +65,6 @@ 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)
|
||||||
@@ -100,8 +72,6 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken);
|
return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Disposes the galaxy client (if created) and the underlying gateway client.</summary>
|
|
||||||
/// <returns>A value task that completes when both clients are disposed.</returns>
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_galaxyClient.IsValueCreated)
|
if (_galaxyClient.IsValueCreated)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace MxGateway.Client.Cli;
|
||||||
|
|
||||||
|
internal static class MxGatewayCliSecretRedactor
|
||||||
|
{
|
||||||
|
public static string Redact(string value, string? apiKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(apiKey))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Replace(apiKey, "[redacted]", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-774
@@ -1,13 +1,12 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Google.Protobuf;
|
using Google.Protobuf;
|
||||||
using ZB.MOM.WW.MxGateway.Client;
|
using MxGateway.Client;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
using MxGateway.Contracts.Proto;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
using MxGateway.Contracts.Proto.Galaxy;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.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;
|
||||||
@@ -16,36 +15,21 @@ public static class MxGatewayClientCli
|
|||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
private const string BatchEndOfRecord = "__MXGW_BATCH_EOR__";
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
/// <returns>The process exit code (0 for success, 1 for error).</returns>
|
|
||||||
public static int Run(
|
public static int Run(
|
||||||
string[] args,
|
string[] args,
|
||||||
TextWriter standardOutput,
|
TextWriter standardOutput,
|
||||||
TextWriter standardError)
|
TextWriter standardError)
|
||||||
{
|
{
|
||||||
return RunAsync(args, standardOutput, standardError, clientFactory: null, standardInput: null)
|
return RunAsync(args, standardOutput, standardError)
|
||||||
.GetAwaiter()
|
.GetAwaiter()
|
||||||
.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>
|
|
||||||
/// <param name="standardInput">Optional TextReader for batch-mode stdin; defaults to <see cref="Console.In"/>.</param>
|
|
||||||
/// <returns>A task that resolves to the process exit code (0 for success, 1 for error).</returns>
|
|
||||||
public static Task<int> RunAsync(
|
public static Task<int> RunAsync(
|
||||||
string[] args,
|
string[] args,
|
||||||
TextWriter standardOutput,
|
TextWriter standardOutput,
|
||||||
TextWriter standardError,
|
TextWriter standardError,
|
||||||
Func<MxGatewayClientOptions, IMxGatewayCliClient>? clientFactory = null,
|
Func<MxGatewayClientOptions, IMxGatewayCliClient>? clientFactory = null)
|
||||||
TextReader? standardInput = null)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(args);
|
ArgumentNullException.ThrowIfNull(args);
|
||||||
ArgumentNullException.ThrowIfNull(standardOutput);
|
ArgumentNullException.ThrowIfNull(standardOutput);
|
||||||
@@ -55,17 +39,14 @@ public static class MxGatewayClientCli
|
|||||||
args,
|
args,
|
||||||
standardOutput,
|
standardOutput,
|
||||||
standardError,
|
standardError,
|
||||||
clientFactory ?? CreateDefaultClient,
|
clientFactory ?? CreateDefaultClient);
|
||||||
standardInput ?? Console.In);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<int> RunCoreAsync(
|
private static async Task<int> RunCoreAsync(
|
||||||
string[] args,
|
string[] args,
|
||||||
TextWriter standardOutput,
|
TextWriter standardOutput,
|
||||||
TextWriter standardError,
|
TextWriter standardError,
|
||||||
Func<MxGatewayClientOptions, IMxGatewayCliClient> clientFactory,
|
Func<MxGatewayClientOptions, IMxGatewayCliClient> clientFactory)
|
||||||
TextReader standardInput,
|
|
||||||
bool forceJsonErrors = false)
|
|
||||||
{
|
{
|
||||||
if (args.Length is 0 || IsHelp(args[0]))
|
if (args.Length is 0 || IsHelp(args[0]))
|
||||||
{
|
{
|
||||||
@@ -74,12 +55,6 @@ public static class MxGatewayClientCli
|
|||||||
}
|
}
|
||||||
|
|
||||||
string command = args[0].ToLowerInvariant();
|
string command = args[0].ToLowerInvariant();
|
||||||
|
|
||||||
if (command is "batch")
|
|
||||||
{
|
|
||||||
return await RunBatchAsync(standardOutput, clientFactory, standardInput).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
CliArguments arguments = new(args.Skip(1));
|
CliArguments arguments = new(args.Skip(1));
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -116,24 +91,8 @@ public static class MxGatewayClientCli
|
|||||||
.ConfigureAwait(false),
|
.ConfigureAwait(false),
|
||||||
"unsubscribe-bulk" => await UnsubscribeBulkAsync(arguments, client, standardOutput, cancellation.Token)
|
"unsubscribe-bulk" => await UnsubscribeBulkAsync(arguments, client, standardOutput, cancellation.Token)
|
||||||
.ConfigureAwait(false),
|
.ConfigureAwait(false),
|
||||||
"read-bulk" => await ReadBulkAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"write-bulk" => await WriteBulkAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"write2-bulk" => await Write2BulkAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"write-secured-bulk" => await WriteSecuredBulkAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"write-secured2-bulk" => await WriteSecured2BulkAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"bench-read-bulk" => await BenchReadBulkAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"stream-events" => await StreamEventsAsync(arguments, client, standardOutput, cancellation.Token)
|
"stream-events" => await StreamEventsAsync(arguments, client, standardOutput, cancellation.Token)
|
||||||
.ConfigureAwait(false),
|
.ConfigureAwait(false),
|
||||||
"stream-alarms" => await StreamAlarmsAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"acknowledge-alarm" => await AcknowledgeAlarmAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"write" => await WriteAsync(arguments, client, standardOutput, cancellation.Token)
|
"write" => await WriteAsync(arguments, client, standardOutput, cancellation.Token)
|
||||||
.ConfigureAwait(false),
|
.ConfigureAwait(false),
|
||||||
"write2" => await Write2Async(arguments, client, standardOutput, cancellation.Token)
|
"write2" => await Write2Async(arguments, client, standardOutput, cancellation.Token)
|
||||||
@@ -156,7 +115,7 @@ public static class MxGatewayClientCli
|
|||||||
string? apiKey = arguments.GetOptional("api-key");
|
string? apiKey = arguments.GetOptional("api-key");
|
||||||
string message = MxGatewayCliSecretRedactor.Redact(exception.Message, apiKey);
|
string message = MxGatewayCliSecretRedactor.Redact(exception.Message, apiKey);
|
||||||
|
|
||||||
if (forceJsonErrors || arguments.HasFlag("json"))
|
if (arguments.HasFlag("json"))
|
||||||
{
|
{
|
||||||
standardError.WriteLine(JsonSerializer.Serialize(
|
standardError.WriteLine(JsonSerializer.Serialize(
|
||||||
new { error = message, type = exception.GetType().Name },
|
new { error = message, type = exception.GetType().Name },
|
||||||
@@ -171,86 +130,6 @@ public static class MxGatewayClientCli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Runs the CLI in batch mode: reads one command line at a time from
|
|
||||||
/// <paramref name="standardInput"/>, dispatches it through the normal
|
|
||||||
/// routing, writes all output to <paramref name="standardOutput"/>, and
|
|
||||||
/// then appends <see cref="BatchEndOfRecord"/> as a sentinel so the
|
|
||||||
/// caller can delimit command results. Continues on failure; errors are
|
|
||||||
/// written as JSON to <paramref name="standardOutput"/> (not stderr) so
|
|
||||||
/// that the harness sees them inside the same delimited block. Exits 0
|
|
||||||
/// on EOF or empty line.
|
|
||||||
/// </summary>
|
|
||||||
private static async Task<int> RunBatchAsync(
|
|
||||||
TextWriter standardOutput,
|
|
||||||
Func<MxGatewayClientOptions, IMxGatewayCliClient> clientFactory,
|
|
||||||
TextReader standardInput)
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
string? line = await standardInput.ReadLineAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
// EOF or empty line signals clean exit.
|
|
||||||
if (line is null || line.Length is 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split on runs of ASCII whitespace — no quoting support by design.
|
|
||||||
string[] lineArgs = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
// Per-command output is buffered so we can redirect errors to stdout.
|
|
||||||
using StringWriter commandOutput = new();
|
|
||||||
|
|
||||||
// Errors in batch mode go to stdout (same delimited block), formatted as JSON.
|
|
||||||
// We use a capturing error writer and re-emit through commandOutput after the
|
|
||||||
// command returns, so the EOR sentinel always follows the complete result.
|
|
||||||
using StringWriter commandError = new();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await RunCoreAsync(
|
|
||||||
lineArgs,
|
|
||||||
commandOutput,
|
|
||||||
commandError,
|
|
||||||
clientFactory,
|
|
||||||
standardInput,
|
|
||||||
forceJsonErrors: true)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
// Unexpected exception that escaped RunCoreAsync (shouldn't happen, but be safe).
|
|
||||||
// OperationCanceledException from long-running streaming commands
|
|
||||||
// (e.g. galaxy-watch hit by --timeout) is caught here too — the
|
|
||||||
// batch process must continue with the next command rather than
|
|
||||||
// unwinding.
|
|
||||||
commandError.WriteLine(JsonSerializer.Serialize(
|
|
||||||
new { error = exception.Message, type = exception.GetType().Name },
|
|
||||||
JsonOptions));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write any buffered normal output first.
|
|
||||||
string commandOutputText = commandOutput.ToString();
|
|
||||||
if (commandOutputText.Length > 0)
|
|
||||||
{
|
|
||||||
standardOutput.Write(commandOutputText);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then any error output — in batch mode it belongs on stdout so the harness
|
|
||||||
// sees it inside the delimited record.
|
|
||||||
string commandErrorText = commandError.ToString();
|
|
||||||
if (commandErrorText.Length > 0)
|
|
||||||
{
|
|
||||||
standardOutput.Write(commandErrorText);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the end-of-record sentinel and flush so the harness can unblock.
|
|
||||||
standardOutput.WriteLine(BatchEndOfRecord);
|
|
||||||
await standardOutput.FlushAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IMxGatewayCliClient CreateDefaultClient(MxGatewayClientOptions options)
|
private static IMxGatewayCliClient CreateDefaultClient(MxGatewayClientOptions options)
|
||||||
{
|
{
|
||||||
return new MxGatewayCliClientAdapter(MxGatewayClient.Create(options));
|
return new MxGatewayCliClientAdapter(MxGatewayClient.Create(options));
|
||||||
@@ -480,499 +359,6 @@ public static class MxGatewayClientCli
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task<int> ReadBulkAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
ReadBulkCommand command = new()
|
|
||||||
{
|
|
||||||
ServerHandle = arguments.GetInt32("server-handle"),
|
|
||||||
TimeoutMs = ParseTimeoutMs(arguments, defaultValue: 0),
|
|
||||||
};
|
|
||||||
command.TagAddresses.Add(ParseStringList(arguments.GetRequired("items")));
|
|
||||||
|
|
||||||
return InvokeAndWriteAsync(
|
|
||||||
arguments,
|
|
||||||
client,
|
|
||||||
output,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.ReadBulk,
|
|
||||||
ReadBulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> WriteBulkAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
WriteBulkCommand command = new()
|
|
||||||
{
|
|
||||||
ServerHandle = arguments.GetInt32("server-handle"),
|
|
||||||
};
|
|
||||||
|
|
||||||
IReadOnlyList<int> handles = ParseInt32List(arguments.GetRequired("item-handles"));
|
|
||||||
IReadOnlyList<MxValue> values = ParseValuesList(arguments);
|
|
||||||
int userId = arguments.GetInt32("user-id", 0);
|
|
||||||
EnsureSameLength(handles.Count, values.Count);
|
|
||||||
|
|
||||||
for (int i = 0; i < handles.Count; i++)
|
|
||||||
{
|
|
||||||
command.Entries.Add(new WriteBulkEntry
|
|
||||||
{
|
|
||||||
ItemHandle = handles[i],
|
|
||||||
Value = values[i],
|
|
||||||
UserId = userId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return InvokeAndWriteAsync(
|
|
||||||
arguments,
|
|
||||||
client,
|
|
||||||
output,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.WriteBulk,
|
|
||||||
WriteBulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> Write2BulkAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Write2BulkCommand command = new()
|
|
||||||
{
|
|
||||||
ServerHandle = arguments.GetInt32("server-handle"),
|
|
||||||
};
|
|
||||||
|
|
||||||
IReadOnlyList<int> handles = ParseInt32List(arguments.GetRequired("item-handles"));
|
|
||||||
IReadOnlyList<MxValue> values = ParseValuesList(arguments);
|
|
||||||
MxValue timestampValue = ParseTimestampValue(arguments);
|
|
||||||
int userId = arguments.GetInt32("user-id", 0);
|
|
||||||
EnsureSameLength(handles.Count, values.Count);
|
|
||||||
|
|
||||||
for (int i = 0; i < handles.Count; i++)
|
|
||||||
{
|
|
||||||
command.Entries.Add(new Write2BulkEntry
|
|
||||||
{
|
|
||||||
ItemHandle = handles[i],
|
|
||||||
Value = values[i],
|
|
||||||
TimestampValue = timestampValue,
|
|
||||||
UserId = userId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return InvokeAndWriteAsync(
|
|
||||||
arguments,
|
|
||||||
client,
|
|
||||||
output,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Write2Bulk,
|
|
||||||
Write2Bulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> WriteSecuredBulkAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
WriteSecuredBulkCommand command = new()
|
|
||||||
{
|
|
||||||
ServerHandle = arguments.GetInt32("server-handle"),
|
|
||||||
};
|
|
||||||
|
|
||||||
IReadOnlyList<int> handles = ParseInt32List(arguments.GetRequired("item-handles"));
|
|
||||||
IReadOnlyList<MxValue> values = ParseValuesList(arguments);
|
|
||||||
int currentUserId = arguments.GetInt32("current-user-id");
|
|
||||||
int verifierUserId = arguments.GetInt32("verifier-user-id", 0);
|
|
||||||
EnsureSameLength(handles.Count, values.Count);
|
|
||||||
|
|
||||||
for (int i = 0; i < handles.Count; i++)
|
|
||||||
{
|
|
||||||
command.Entries.Add(new WriteSecuredBulkEntry
|
|
||||||
{
|
|
||||||
ItemHandle = handles[i],
|
|
||||||
Value = values[i],
|
|
||||||
CurrentUserId = currentUserId,
|
|
||||||
VerifierUserId = verifierUserId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return InvokeAndWriteAsync(
|
|
||||||
arguments,
|
|
||||||
client,
|
|
||||||
output,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.WriteSecuredBulk,
|
|
||||||
WriteSecuredBulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> WriteSecured2BulkAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
WriteSecured2BulkCommand command = new()
|
|
||||||
{
|
|
||||||
ServerHandle = arguments.GetInt32("server-handle"),
|
|
||||||
};
|
|
||||||
|
|
||||||
IReadOnlyList<int> handles = ParseInt32List(arguments.GetRequired("item-handles"));
|
|
||||||
IReadOnlyList<MxValue> values = ParseValuesList(arguments);
|
|
||||||
MxValue timestampValue = ParseTimestampValue(arguments);
|
|
||||||
int currentUserId = arguments.GetInt32("current-user-id");
|
|
||||||
int verifierUserId = arguments.GetInt32("verifier-user-id", 0);
|
|
||||||
EnsureSameLength(handles.Count, values.Count);
|
|
||||||
|
|
||||||
for (int i = 0; i < handles.Count; i++)
|
|
||||||
{
|
|
||||||
command.Entries.Add(new WriteSecured2BulkEntry
|
|
||||||
{
|
|
||||||
ItemHandle = handles[i],
|
|
||||||
Value = values[i],
|
|
||||||
TimestampValue = timestampValue,
|
|
||||||
CurrentUserId = currentUserId,
|
|
||||||
VerifierUserId = verifierUserId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return InvokeAndWriteAsync(
|
|
||||||
arguments,
|
|
||||||
client,
|
|
||||||
output,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.WriteSecured2Bulk,
|
|
||||||
WriteSecured2Bulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses the bulk-write CLI's <c>--values</c> list. All entries share
|
|
||||||
/// the single <c>--type</c> argument; the comma-separated values are
|
|
||||||
/// each parsed via <see cref="ParseValue(string, string)"/> on a per-entry basis.
|
|
||||||
/// This keeps the CLI simple for e2e use (one type, N values) — callers
|
|
||||||
/// that need heterogeneous types per entry should drive the library
|
|
||||||
/// directly.
|
|
||||||
/// </summary>
|
|
||||||
private static IReadOnlyList<MxValue> ParseValuesList(CliArguments arguments)
|
|
||||||
{
|
|
||||||
string type = arguments.GetRequired("type");
|
|
||||||
string[] values = ParseStringList(arguments.GetRequired("values")).ToArray();
|
|
||||||
MxValue[] result = new MxValue[values.Length];
|
|
||||||
for (int i = 0; i < values.Length; i++)
|
|
||||||
{
|
|
||||||
result[i] = ParseValue(type, values[i]);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsureSameLength(int handles, int values)
|
|
||||||
{
|
|
||||||
if (handles != values)
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
$"Bulk write requires the same number of --item-handles ({handles}) and --values ({values}).");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses the optional <c>--timeout-ms</c> argument as a non-negative
|
|
||||||
/// unsigned millisecond count. Mirrors the SDK-side <c>(uint)Math.Min</c>
|
|
||||||
/// guard on <c>MxGatewaySession.ReadBulkAsync</c>: a negative value
|
|
||||||
/// (e.g. <c>-1</c>, an easy copy-paste mistake for "unbounded") is
|
|
||||||
/// rejected loudly rather than silently wrapped to <c>~49.7 days</c>,
|
|
||||||
/// which would park one worker thread per pending tag for hours.
|
|
||||||
/// Resolves Client.Dotnet-021.
|
|
||||||
/// </summary>
|
|
||||||
private static uint ParseTimeoutMs(CliArguments arguments, int defaultValue)
|
|
||||||
{
|
|
||||||
int raw = arguments.GetInt32("timeout-ms", defaultValue);
|
|
||||||
if (raw < 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
"--timeout-ms must be a non-negative integer (use 0 for the gateway default).");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (uint)raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extracts the <c>ServerHandle</c> from a Register reply, throwing a
|
|
||||||
/// descriptive <see cref="MxGatewayException"/> when the typed
|
|
||||||
/// <c>Register</c> payload is absent on an otherwise-successful reply.
|
|
||||||
/// The typed sub-message is the contract for the Register command, so
|
|
||||||
/// its absence must not silently fall through to
|
|
||||||
/// <c>ReturnValue.Int32Value</c> (which would be <c>0</c> for an empty
|
|
||||||
/// reply, driving the rest of the bench against an invalid handle).
|
|
||||||
/// Resolves Client.Dotnet-019.
|
|
||||||
/// </summary>
|
|
||||||
private static int RequireRegisterServerHandle(MxCommandReply reply, string sessionId)
|
|
||||||
{
|
|
||||||
if (reply.Register is null)
|
|
||||||
{
|
|
||||||
throw new MxGatewayException(
|
|
||||||
$"Gateway reply for Register on session '{sessionId}' (correlation '{reply.CorrelationId}') "
|
|
||||||
+ "succeeded but is missing the typed 'register' payload required to read ServerHandle.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return reply.Register.ServerHandle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cross-language stress benchmark for ReadBulk. Opens its own session,
|
|
||||||
/// subscribes to N tags so the worker's MxAccessValueCache populates from
|
|
||||||
/// real OnDataChange events, then hammers ReadBulk in a tight in-process
|
|
||||||
/// loop with per-call Stopwatch timing. Emits a single JSON object on
|
|
||||||
/// stdout that the scripts/bench-read-bulk.ps1 driver collates across
|
|
||||||
/// all five language clients.
|
|
||||||
/// </summary>
|
|
||||||
private static async Task<int> BenchReadBulkAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
int durationSeconds = arguments.GetInt32("duration-seconds", 30);
|
|
||||||
int warmupSeconds = arguments.GetInt32("warmup-seconds", 3);
|
|
||||||
int bulkSize = arguments.GetInt32("bulk-size", 6);
|
|
||||||
int tagStart = arguments.GetInt32("tag-start", 1);
|
|
||||||
string tagPrefix = arguments.GetOptional("tag-prefix") ?? "TestMachine_";
|
|
||||||
string tagAttribute = arguments.GetOptional("tag-attribute") ?? "TestChangingInt";
|
|
||||||
uint timeoutMs = ParseTimeoutMs(arguments, defaultValue: 1500);
|
|
||||||
string clientName = arguments.GetOptional("client-name") ?? "mxgw-dotnet-bench";
|
|
||||||
|
|
||||||
string[] tags = new string[bulkSize];
|
|
||||||
for (int i = 0; i < bulkSize; i++)
|
|
||||||
{
|
|
||||||
// TestMachine_NNN.<attribute>, three-digit machine numbers matching
|
|
||||||
// the existing e2e tag-discovery convention.
|
|
||||||
tags[i] = $"{tagPrefix}{(tagStart + i):D3}.{tagAttribute}";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open + register + subscribe-bulk so the cache populates before the
|
|
||||||
// measurement window opens.
|
|
||||||
OpenSessionReply openReply = await client.OpenSessionAsync(
|
|
||||||
new OpenSessionRequest { ClientSessionName = clientName, ClientCorrelationId = CreateCorrelationId() },
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
string sessionId = openReply.SessionId;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
MxCommandReply registerReply = await InvokeAndEnsureAsync(
|
|
||||||
client,
|
|
||||||
CreateCommandRequest(sessionId, new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Register,
|
|
||||||
Register = new RegisterCommand { ClientName = clientName },
|
|
||||||
}),
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
int serverHandle = RequireRegisterServerHandle(registerReply, sessionId);
|
|
||||||
|
|
||||||
SubscribeBulkCommand subscribe = new() { ServerHandle = serverHandle };
|
|
||||||
subscribe.TagAddresses.Add(tags);
|
|
||||||
MxCommandReply subscribeReply = await InvokeAndEnsureAsync(
|
|
||||||
client,
|
|
||||||
CreateCommandRequest(sessionId, new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.SubscribeBulk,
|
|
||||||
SubscribeBulk = subscribe,
|
|
||||||
}),
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
int[] itemHandles = subscribeReply.SubscribeBulk?.Results
|
|
||||||
.Where(r => r.WasSuccessful)
|
|
||||||
.Select(r => r.ItemHandle)
|
|
||||||
.ToArray() ?? [];
|
|
||||||
|
|
||||||
// Warm-up: drive the same call shape so the JIT / connection
|
|
||||||
// pipelines settle before the measurement window opens.
|
|
||||||
DateTime warmupDeadline = DateTime.UtcNow + TimeSpan.FromSeconds(warmupSeconds);
|
|
||||||
ReadBulkCommand readBulkCommand = new()
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
TimeoutMs = timeoutMs,
|
|
||||||
};
|
|
||||||
readBulkCommand.TagAddresses.Add(tags);
|
|
||||||
MxCommand readBulkMxCommand = new() { Kind = MxCommandKind.ReadBulk, ReadBulk = readBulkCommand };
|
|
||||||
|
|
||||||
while (DateTime.UtcNow < warmupDeadline)
|
|
||||||
{
|
|
||||||
_ = await client.InvokeAsync(
|
|
||||||
CreateCommandRequest(sessionId, readBulkMxCommand),
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Steady state — capture per-call wall latency with a high-res
|
|
||||||
// Stopwatch so the resolution is sub-millisecond on modern Windows.
|
|
||||||
List<double> latencyMillis = new(capacity: 65536);
|
|
||||||
long totalReadResults = 0;
|
|
||||||
long cachedReadResults = 0;
|
|
||||||
int successfulCalls = 0;
|
|
||||||
int failedCalls = 0;
|
|
||||||
DateTime steadyDeadline = DateTime.UtcNow + TimeSpan.FromSeconds(durationSeconds);
|
|
||||||
DateTime steadyStart = DateTime.UtcNow;
|
|
||||||
|
|
||||||
while (DateTime.UtcNow < steadyDeadline)
|
|
||||||
{
|
|
||||||
System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
|
|
||||||
MxCommandReply reply;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
reply = await client.InvokeAsync(
|
|
||||||
CreateCommandRequest(sessionId, readBulkMxCommand),
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
sw.Stop();
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
||||||
{
|
|
||||||
// Client.Dotnet-020: never swallow OperationCanceledException
|
|
||||||
// here. A bare `catch` would let Ctrl+C / parent CTS /
|
|
||||||
// wall-clock timeouts keep spinning until --duration-seconds
|
|
||||||
// elapsed, burning CPU and skewing the p99/max latency numbers
|
|
||||||
// with hundreds of immediate-OCE iterations.
|
|
||||||
sw.Stop();
|
|
||||||
failedCalls++;
|
|
||||||
latencyMillis.Add(sw.Elapsed.TotalMilliseconds);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
latencyMillis.Add(sw.Elapsed.TotalMilliseconds);
|
|
||||||
if (reply.ProtocolStatus?.Code != ProtocolStatusCode.Ok)
|
|
||||||
{
|
|
||||||
failedCalls++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
successfulCalls++;
|
|
||||||
if (reply.ReadBulk is not null)
|
|
||||||
{
|
|
||||||
foreach (BulkReadResult r in reply.ReadBulk.Results)
|
|
||||||
{
|
|
||||||
totalReadResults++;
|
|
||||||
if (r.WasCached)
|
|
||||||
{
|
|
||||||
cachedReadResults++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double steadyElapsedSeconds = (DateTime.UtcNow - steadyStart).TotalSeconds;
|
|
||||||
|
|
||||||
if (itemHandles.Length > 0)
|
|
||||||
{
|
|
||||||
UnsubscribeBulkCommand unsubscribe = new() { ServerHandle = serverHandle };
|
|
||||||
unsubscribe.ItemHandles.Add(itemHandles);
|
|
||||||
_ = await client.InvokeAsync(
|
|
||||||
CreateCommandRequest(sessionId, new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.UnsubscribeBulk,
|
|
||||||
UnsubscribeBulk = unsubscribe,
|
|
||||||
}),
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
int totalCalls = successfulCalls + failedCalls;
|
|
||||||
double callsPerSecond = steadyElapsedSeconds > 0
|
|
||||||
? totalCalls / steadyElapsedSeconds
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
object stats = new
|
|
||||||
{
|
|
||||||
language = "dotnet",
|
|
||||||
command = "bench-read-bulk",
|
|
||||||
endpoint = arguments.GetOptional("endpoint") ?? "(default)",
|
|
||||||
clientName,
|
|
||||||
bulkSize,
|
|
||||||
durationSeconds,
|
|
||||||
warmupSeconds,
|
|
||||||
durationMs = (long)(steadyElapsedSeconds * 1000),
|
|
||||||
tags,
|
|
||||||
totalCalls,
|
|
||||||
successfulCalls,
|
|
||||||
failedCalls,
|
|
||||||
totalReadResults,
|
|
||||||
cachedReadResults,
|
|
||||||
callsPerSecond = Math.Round(callsPerSecond, 2),
|
|
||||||
latencyMs = new
|
|
||||||
{
|
|
||||||
p50 = Percentile(latencyMillis, 0.50),
|
|
||||||
p95 = Percentile(latencyMillis, 0.95),
|
|
||||||
p99 = Percentile(latencyMillis, 0.99),
|
|
||||||
max = latencyMillis.Count > 0 ? Math.Round(latencyMillis.Max(), 3) : 0,
|
|
||||||
mean = latencyMillis.Count > 0 ? Math.Round(latencyMillis.Average(), 3) : 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
output.WriteLine(JsonSerializer.Serialize(stats, JsonOptions));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await client.CloseSessionAsync(
|
|
||||||
new CloseSessionRequest { SessionId = sessionId, ClientCorrelationId = CreateCorrelationId() },
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Closing the session is best-effort — never let it mask a real bench error.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Computes the requested percentile from an unsorted latency sample using
|
|
||||||
/// nearest-rank with linear interpolation. Rounds to 3 decimal places to
|
|
||||||
/// match the JSON schema the PS driver collates.
|
|
||||||
/// </summary>
|
|
||||||
private static double Percentile(IReadOnlyList<double> sample, double quantile)
|
|
||||||
{
|
|
||||||
if (sample.Count == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
double[] sorted = sample.ToArray();
|
|
||||||
Array.Sort(sorted);
|
|
||||||
if (sorted.Length == 1)
|
|
||||||
{
|
|
||||||
return Math.Round(sorted[0], 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
double rank = quantile * (sorted.Length - 1);
|
|
||||||
int lower = (int)Math.Floor(rank);
|
|
||||||
int upper = (int)Math.Ceiling(rank);
|
|
||||||
double fraction = rank - lower;
|
|
||||||
double value = sorted[lower] + (sorted[upper] - sorted[lower]) * fraction;
|
|
||||||
return Math.Round(value, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> WriteAsync(
|
private static Task<int> WriteAsync(
|
||||||
CliArguments arguments,
|
CliArguments arguments,
|
||||||
IMxGatewayCliClient client,
|
IMxGatewayCliClient client,
|
||||||
@@ -1051,8 +437,6 @@ public static class MxGatewayClientCli
|
|||||||
AfterWorkerSequence = arguments.GetUInt64("after-worker-sequence", 0),
|
AfterWorkerSequence = arguments.GetUInt64("after-worker-sequence", 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await foreach (MxEvent gatewayEvent in client.StreamEventsAsync(request, cancellationToken)
|
await foreach (MxEvent gatewayEvent in client.StreamEventsAsync(request, cancellationToken)
|
||||||
.WithCancellation(cancellationToken)
|
.WithCancellation(cancellationToken)
|
||||||
.ConfigureAwait(false))
|
.ConfigureAwait(false))
|
||||||
@@ -1076,12 +460,6 @@ public static class MxGatewayClientCli
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
// Client.Dotnet-017: graceful end-of-window completion mode for a
|
|
||||||
// finite-window event collector. Emit aggregate JSON below and exit 0.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json && !jsonLines)
|
if (json && !jsonLines)
|
||||||
{
|
{
|
||||||
@@ -1093,124 +471,6 @@ public static class MxGatewayClientCli
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<int> StreamAlarmsAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
uint maxEvents = arguments.GetUInt32("max-events", 0);
|
|
||||||
bool json = arguments.HasFlag("json");
|
|
||||||
bool jsonLines = arguments.HasFlag("jsonl");
|
|
||||||
if (json && !jsonLines && maxEvents is 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("--json stream-alarms requires --max-events to bound aggregate output.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxEvents > MaxAggregateEvents)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"--max-events cannot exceed {MaxAggregateEvents}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var messages = json && !jsonLines
|
|
||||||
? new List<AlarmFeedMessage>(checked((int)maxEvents))
|
|
||||||
: [];
|
|
||||||
uint messageCount = 0;
|
|
||||||
var request = new StreamAlarmsRequest
|
|
||||||
{
|
|
||||||
ClientCorrelationId = CreateCorrelationId(),
|
|
||||||
AlarmFilterPrefix = arguments.GetOptional("filter-prefix") ?? string.Empty,
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await foreach (AlarmFeedMessage feedMessage in client.StreamAlarmsAsync(request, cancellationToken)
|
|
||||||
.WithCancellation(cancellationToken)
|
|
||||||
.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
if (jsonLines)
|
|
||||||
{
|
|
||||||
output.WriteLine(ProtobufJsonFormatter.Format(feedMessage));
|
|
||||||
}
|
|
||||||
else if (json)
|
|
||||||
{
|
|
||||||
messages.Add(feedMessage);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
output.WriteLine(FormatAlarmFeedMessage(feedMessage));
|
|
||||||
}
|
|
||||||
|
|
||||||
messageCount++;
|
|
||||||
if (maxEvents > 0 && messageCount >= maxEvents)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
// Mirrors stream-events (Client.Dotnet-017): the supplied token covers
|
|
||||||
// the user's --timeout wall-clock budget and external Ctrl+C / parent
|
|
||||||
// CTS cancellation. All are graceful completion modes for a
|
|
||||||
// finite-window alarm-feed collector: emit what arrived and exit 0.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json && !jsonLines)
|
|
||||||
{
|
|
||||||
output.WriteLine(JsonSerializer.Serialize(
|
|
||||||
new { alarms = messages.Select(AlarmFeedMessageToJsonElement).ToArray() },
|
|
||||||
JsonOptions));
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> AcknowledgeAlarmAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var request = new AcknowledgeAlarmRequest
|
|
||||||
{
|
|
||||||
ClientCorrelationId = CreateCorrelationId(),
|
|
||||||
AlarmFullReference = arguments.GetRequired("reference"),
|
|
||||||
Comment = arguments.GetOptional("comment") ?? string.Empty,
|
|
||||||
OperatorUser = arguments.GetOptional("operator") ?? string.Empty,
|
|
||||||
};
|
|
||||||
|
|
||||||
return WriteReplyAsync(
|
|
||||||
client.AcknowledgeAlarmAsync(request, cancellationToken),
|
|
||||||
arguments,
|
|
||||||
output);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Renders one <see cref="AlarmFeedMessage"/> for the human-readable
|
|
||||||
/// (non-JSON) stream-alarms output, distinguishing the <c>payload</c> oneof
|
|
||||||
/// arms: a snapshot active alarm, the snapshot-complete sentinel, or a live
|
|
||||||
/// transition.
|
|
||||||
/// </summary>
|
|
||||||
private static string FormatAlarmFeedMessage(AlarmFeedMessage feedMessage)
|
|
||||||
{
|
|
||||||
return feedMessage.PayloadCase switch
|
|
||||||
{
|
|
||||||
AlarmFeedMessage.PayloadOneofCase.ActiveAlarm =>
|
|
||||||
$"active-alarm {ProtobufJsonFormatter.Format(feedMessage.ActiveAlarm)}",
|
|
||||||
AlarmFeedMessage.PayloadOneofCase.SnapshotComplete =>
|
|
||||||
$"snapshot-complete {feedMessage.SnapshotComplete}",
|
|
||||||
AlarmFeedMessage.PayloadOneofCase.Transition =>
|
|
||||||
$"transition {ProtobufJsonFormatter.Format(feedMessage.Transition)}",
|
|
||||||
_ => $"unknown-payload {feedMessage.PayloadCase}",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static JsonElement AlarmFeedMessageToJsonElement(AlarmFeedMessage feedMessage)
|
|
||||||
{
|
|
||||||
return JsonDocument.Parse(ProtobufJsonFormatter.Format(feedMessage)).RootElement.Clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<int> SmokeAsync(
|
private static async Task<int> SmokeAsync(
|
||||||
CliArguments arguments,
|
CliArguments arguments,
|
||||||
IMxGatewayCliClient client,
|
IMxGatewayCliClient client,
|
||||||
@@ -1485,15 +745,11 @@ public static class MxGatewayClientCli
|
|||||||
|
|
||||||
private static MxValue ParseValue(CliArguments arguments)
|
private static MxValue ParseValue(CliArguments arguments)
|
||||||
{
|
{
|
||||||
return ParseValue(arguments.GetRequired("type"), arguments.GetRequired("value"));
|
string type = arguments.GetRequired("type").ToLowerInvariant();
|
||||||
}
|
string value = arguments.GetRequired("value");
|
||||||
|
|
||||||
private static MxValue ParseValue(string type, string value)
|
|
||||||
{
|
|
||||||
string normalisedType = type.ToLowerInvariant();
|
|
||||||
string[] values = value.Split(',', StringSplitOptions.TrimEntries);
|
string[] values = value.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
return normalisedType switch
|
return type switch
|
||||||
{
|
{
|
||||||
"bool" or "boolean" => bool.Parse(value).ToMxValue(),
|
"bool" or "boolean" => bool.Parse(value).ToMxValue(),
|
||||||
"bool-array" or "boolean-array" => values.Select(bool.Parse).ToArray().ToMxValue(),
|
"bool-array" or "boolean-array" => values.Select(bool.Parse).ToArray().ToMxValue(),
|
||||||
@@ -1512,7 +768,7 @@ public static class MxGatewayClientCli
|
|||||||
.Select(item => DateTimeOffset.Parse(item, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal))
|
.Select(item => DateTimeOffset.Parse(item, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal))
|
||||||
.ToArray()
|
.ToArray()
|
||||||
.ToMxValue(),
|
.ToMxValue(),
|
||||||
_ => throw new ArgumentException($"Unsupported MX value type '{normalisedType}'."),
|
_ => throw new ArgumentException($"Unsupported MX value type '{type}'."),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1723,15 +979,7 @@ public static class MxGatewayClientCli
|
|||||||
or "advise"
|
or "advise"
|
||||||
or "subscribe-bulk"
|
or "subscribe-bulk"
|
||||||
or "unsubscribe-bulk"
|
or "unsubscribe-bulk"
|
||||||
or "read-bulk"
|
|
||||||
or "write-bulk"
|
|
||||||
or "write2-bulk"
|
|
||||||
or "write-secured-bulk"
|
|
||||||
or "write-secured2-bulk"
|
|
||||||
or "bench-read-bulk"
|
|
||||||
or "stream-events"
|
or "stream-events"
|
||||||
or "stream-alarms"
|
|
||||||
or "acknowledge-alarm"
|
|
||||||
or "write"
|
or "write"
|
||||||
or "write2"
|
or "write2"
|
||||||
or "smoke"
|
or "smoke"
|
||||||
@@ -1774,7 +1022,6 @@ public static class MxGatewayClientCli
|
|||||||
|
|
||||||
private static void WriteUsage(TextWriter writer)
|
private static void WriteUsage(TextWriter writer)
|
||||||
{
|
{
|
||||||
writer.WriteLine("mxgw-dotnet batch (reads commands from stdin; writes output + __MXGW_BATCH_EOR__ after each)");
|
|
||||||
writer.WriteLine("mxgw-dotnet version [--json]");
|
writer.WriteLine("mxgw-dotnet version [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet ping --session-id <id> [--json]");
|
writer.WriteLine("mxgw-dotnet ping --session-id <id> [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet open-session [--client-name <name>] [--json]");
|
writer.WriteLine("mxgw-dotnet open-session [--client-name <name>] [--json]");
|
||||||
@@ -1784,15 +1031,7 @@ public static class MxGatewayClientCli
|
|||||||
writer.WriteLine("mxgw-dotnet advise --session-id <id> --server-handle <n> --item-handle <n> [--json]");
|
writer.WriteLine("mxgw-dotnet advise --session-id <id> --server-handle <n> --item-handle <n> [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet subscribe-bulk --session-id <id> --server-handle <n> --items <ref,ref> [--json]");
|
writer.WriteLine("mxgw-dotnet subscribe-bulk --session-id <id> --server-handle <n> --items <ref,ref> [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet unsubscribe-bulk --session-id <id> --server-handle <n> --item-handles <n,n> [--json]");
|
writer.WriteLine("mxgw-dotnet unsubscribe-bulk --session-id <id> --server-handle <n> --item-handles <n,n> [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet read-bulk --session-id <id> --server-handle <n> --items <ref,ref> [--timeout-ms <n>] [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet write-bulk --session-id <id> --server-handle <n> --item-handles <n,n> --type <type> --values <v,v> [--user-id <n>] [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet write2-bulk --session-id <id> --server-handle <n> --item-handles <n,n> --type <type> --values <v,v> [--timestamp <iso>] [--user-id <n>] [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet write-secured-bulk --session-id <id> --server-handle <n> --item-handles <n,n> --type <type> --values <v,v> --current-user-id <n> [--verifier-user-id <n>] [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet write-secured2-bulk --session-id <id> --server-handle <n> --item-handles <n,n> --type <type> --values <v,v> --current-user-id <n> [--verifier-user-id <n>] [--timestamp <iso>] [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet bench-read-bulk [--duration-seconds <n>] [--warmup-seconds <n>] [--bulk-size <n>] [--tag-start <n>] [--tag-prefix <s>] [--tag-attribute <s>] [--timeout-ms <n>] [--client-name <name>]");
|
|
||||||
writer.WriteLine("mxgw-dotnet stream-events --session-id <id> [--max-events <n>] [--json]");
|
writer.WriteLine("mxgw-dotnet stream-events --session-id <id> [--max-events <n>] [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet stream-alarms [--filter-prefix <ref>] [--max-events <n>] [--json] [--jsonl]");
|
|
||||||
writer.WriteLine("mxgw-dotnet acknowledge-alarm --reference <ref> [--comment <text>] [--operator <user>] [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet write --session-id <id> --server-handle <n> --item-handle <n> --type <type> --value <value> [--json]");
|
writer.WriteLine("mxgw-dotnet write --session-id <id> --server-handle <n> --item-handle <n> --type <type> --value <value> [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet write2 --session-id <id> --server-handle <n> --item-handle <n> --type <type> --value <value> [--timestamp <iso>] [--json]");
|
writer.WriteLine("mxgw-dotnet write2 --session-id <id> --server-handle <n> --item-handle <n> --type <type> --value <value> [--timestamp <iso>] [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet smoke --item <ref> [--value <value> --type <type>] [--json]");
|
writer.WriteLine("mxgw-dotnet smoke --item <ref> [--value <value> --type <type>] [--json]");
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Client.Cli;
|
using MxGateway.Client.Cli;
|
||||||
|
|
||||||
return await MxGatewayClientCli.RunAsync(args, Console.Out, Console.Error);
|
return await MxGatewayClientCli.RunAsync(args, Console.Out, Console.Error);
|
||||||
+2
-77
@@ -1,68 +1,34 @@
|
|||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
using MxGateway.Contracts.Proto.Galaxy;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.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
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public MxGatewayClientOptions Options { get; } = options;
|
public MxGatewayClientOptions Options { get; } = options;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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();
|
||||||
|
|
||||||
/// <summary>Gets the queue of discover hierarchy replies; dequeued in FIFO order.</summary>
|
|
||||||
public Queue<DiscoverHierarchyReply> DiscoverHierarchyReplies { get; } = 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();
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<TestConnectionReply> TestConnectionAsync(
|
public Task<TestConnectionReply> TestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -76,7 +42,6 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
return Task.FromResult(TestConnectionReply);
|
return Task.FromResult(TestConnectionReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
public Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -90,7 +55,6 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
return Task.FromResult(GetLastDeployTimeReply);
|
return Task.FromResult(GetLastDeployTimeReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
public Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -107,48 +71,10 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
: DiscoverHierarchyReply);
|
: DiscoverHierarchyReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Records BrowseChildren RPC calls made by the client.</summary>
|
|
||||||
public List<(BrowseChildrenRequest Request, CallOptions CallOptions)> BrowseChildrenCalls { get; } = [];
|
|
||||||
|
|
||||||
/// <summary>Default reply returned from BrowseChildren when the queue is empty.</summary>
|
|
||||||
public BrowseChildrenReply BrowseChildrenReply { get; set; } = new();
|
|
||||||
|
|
||||||
/// <summary>Queue of replies returned from BrowseChildren; dequeued in FIFO order.</summary>
|
|
||||||
public Queue<BrowseChildrenReply> BrowseChildrenReplies { get; } = new();
|
|
||||||
|
|
||||||
/// <summary>Queue of exceptions to throw from BrowseChildren; dequeued in FIFO order.</summary>
|
|
||||||
public Queue<Exception> BrowseChildrenExceptions { get; } = new();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<BrowseChildrenReply> BrowseChildrenAsync(
|
|
||||||
BrowseChildrenRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
BrowseChildrenCalls.Add((request, callOptions));
|
|
||||||
if (BrowseChildrenExceptions.TryDequeue(out Exception? exception))
|
|
||||||
{
|
|
||||||
return Task.FromException<BrowseChildrenReply>(exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(
|
|
||||||
BrowseChildrenReplies.TryDequeue(out BrowseChildrenReply? reply)
|
|
||||||
? reply
|
|
||||||
: BrowseChildrenReply);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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>
|
||||||
@@ -157,7 +83,6 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<CancellationToken, Task>? WatchDeployEventsBeforeYield { get; set; }
|
public Func<CancellationToken, Task>? WatchDeployEventsBeforeYield { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Client.Tests;
|
||||||
|
|
||||||
|
internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMxGatewayClientTransport
|
||||||
|
{
|
||||||
|
private readonly Queue<MxCommandReply> _invokeReplies = new();
|
||||||
|
private readonly List<MxEvent> _events = [];
|
||||||
|
|
||||||
|
public MxGatewayClientOptions Options { get; } = options;
|
||||||
|
|
||||||
|
public MxAccessGateway.MxAccessGatewayClient? RawClient => null;
|
||||||
|
|
||||||
|
public List<(OpenSessionRequest Request, CallOptions CallOptions)> OpenSessionCalls { get; } = [];
|
||||||
|
|
||||||
|
public List<(CloseSessionRequest Request, CallOptions CallOptions)> CloseSessionCalls { get; } = [];
|
||||||
|
|
||||||
|
public List<(MxCommandRequest Request, CallOptions CallOptions)> InvokeCalls { get; } = [];
|
||||||
|
|
||||||
|
public List<(StreamEventsRequest Request, CallOptions CallOptions)> StreamEventsCalls { get; } = [];
|
||||||
|
|
||||||
|
public OpenSessionReply OpenSessionReply { get; set; } = new()
|
||||||
|
{
|
||||||
|
SessionId = "session-fixture",
|
||||||
|
BackendName = "mxaccess-worker",
|
||||||
|
GatewayProtocolVersion = 1,
|
||||||
|
WorkerProtocolVersion = 1,
|
||||||
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||||
|
};
|
||||||
|
|
||||||
|
public CloseSessionReply CloseSessionReply { get; set; } = new()
|
||||||
|
{
|
||||||
|
SessionId = "session-fixture",
|
||||||
|
FinalState = SessionState.Closed,
|
||||||
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||||
|
};
|
||||||
|
|
||||||
|
public Queue<Exception> OpenSessionExceptions { get; } = new();
|
||||||
|
|
||||||
|
public Queue<Exception> CloseSessionExceptions { get; } = new();
|
||||||
|
|
||||||
|
public Queue<Exception> InvokeExceptions { get; } = new();
|
||||||
|
|
||||||
|
public Task<OpenSessionReply> OpenSessionAsync(
|
||||||
|
OpenSessionRequest request,
|
||||||
|
CallOptions callOptions)
|
||||||
|
{
|
||||||
|
OpenSessionCalls.Add((request, callOptions));
|
||||||
|
if (OpenSessionExceptions.TryDequeue(out Exception? exception))
|
||||||
|
{
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(OpenSessionReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CloseSessionReply> CloseSessionAsync(
|
||||||
|
CloseSessionRequest request,
|
||||||
|
CallOptions callOptions)
|
||||||
|
{
|
||||||
|
CloseSessionCalls.Add((request, callOptions));
|
||||||
|
if (CloseSessionExceptions.TryDequeue(out Exception? exception))
|
||||||
|
{
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(CloseSessionReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MxCommandReply> InvokeAsync(
|
||||||
|
MxCommandRequest request,
|
||||||
|
CallOptions callOptions)
|
||||||
|
{
|
||||||
|
InvokeCalls.Add((request, callOptions));
|
||||||
|
if (InvokeExceptions.TryDequeue(out Exception? exception))
|
||||||
|
{
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(_invokeReplies.Dequeue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
|
StreamEventsRequest request,
|
||||||
|
CallOptions callOptions)
|
||||||
|
{
|
||||||
|
StreamEventsCalls.Add((request, callOptions));
|
||||||
|
|
||||||
|
foreach (MxEvent gatewayEvent in _events)
|
||||||
|
{
|
||||||
|
callOptions.CancellationToken.ThrowIfCancellationRequested();
|
||||||
|
await Task.Yield();
|
||||||
|
yield return gatewayEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddInvokeReply(MxCommandReply reply)
|
||||||
|
{
|
||||||
|
_invokeReplies.Enqueue(reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddEvent(MxEvent gatewayEvent)
|
||||||
|
{
|
||||||
|
_events.Add(gatewayEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-62
@@ -1,15 +1,11 @@
|
|||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
using MxGateway.Contracts.Proto.Galaxy;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client.Tests;
|
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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestConnectionAsync_AttachesApiKeyMetadataAndReturnsOkFlag()
|
public async Task TestConnectionAsync_AttachesApiKeyMetadataAndReturnsOkFlag()
|
||||||
{
|
{
|
||||||
@@ -25,10 +21,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestConnectionAsync_ReturnsFalseWhenServerReportsNotOk()
|
public async Task TestConnectionAsync_ReturnsFalseWhenServerReportsNotOk()
|
||||||
{
|
{
|
||||||
@@ -41,10 +33,6 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.False(ok);
|
Assert.False(ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that GetLastDeployTimeAsync returns null when the server reports not present.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetLastDeployTimeAsync_ReturnsNullWhenNotPresent()
|
public async Task GetLastDeployTimeAsync_ReturnsNullWhenNotPresent()
|
||||||
{
|
{
|
||||||
@@ -58,10 +46,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetLastDeployTimeAsync_ReturnsTimestampWhenPresent()
|
public async Task GetLastDeployTimeAsync_ReturnsTimestampWhenPresent()
|
||||||
{
|
{
|
||||||
@@ -80,10 +64,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
|
public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
|
||||||
{
|
{
|
||||||
@@ -143,10 +123,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DiscoverHierarchyAsync_PropagatesCancellationToTransport()
|
public async Task DiscoverHierarchyAsync_PropagatesCancellationToTransport()
|
||||||
{
|
{
|
||||||
@@ -164,10 +140,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DiscoverHierarchyAsync_WithRepeatedPageToken_ThrowsProtocolError()
|
public async Task DiscoverHierarchyAsync_WithRepeatedPageToken_ThrowsProtocolError()
|
||||||
{
|
{
|
||||||
@@ -188,10 +160,6 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Contains("repeated page token", exception.Message, StringComparison.Ordinal);
|
Assert.Contains("repeated page token", exception.Message, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that DiscoverHierarchyAsync maps typed filter options correctly to the request.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DiscoverHierarchyAsync_WithOptions_MapsTypedFilters()
|
public async Task DiscoverHierarchyAsync_WithOptions_MapsTypedFilters()
|
||||||
{
|
{
|
||||||
@@ -223,10 +191,6 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.True(request.HistorizedOnly);
|
Assert.True(request.HistorizedOnly);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that TestConnectionAsync retries on transient gRPC failures.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
|
public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
|
||||||
{
|
{
|
||||||
@@ -241,10 +205,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DiscoverHierarchyAsync_RetriesOnTransientGrpcFailure()
|
public async Task DiscoverHierarchyAsync_RetriesOnTransientGrpcFailure()
|
||||||
{
|
{
|
||||||
@@ -258,10 +218,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WatchDeployEventsAsync_DeliversBootstrapEvent()
|
public async Task WatchDeployEventsAsync_DeliversBootstrapEvent()
|
||||||
{
|
{
|
||||||
@@ -295,10 +251,6 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Null(call.Request.LastSeenDeployTime);
|
Assert.Null(call.Request.LastSeenDeployTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that WatchDeployEventsAsync delivers multiple events in order.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WatchDeployEventsAsync_DeliversMultipleEventsInOrder()
|
public async Task WatchDeployEventsAsync_DeliversMultipleEventsInOrder()
|
||||||
{
|
{
|
||||||
@@ -334,10 +286,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WatchDeployEventsAsync_CancellationStopsIterationCleanly()
|
public async Task WatchDeployEventsAsync_CancellationStopsIterationCleanly()
|
||||||
{
|
{
|
||||||
@@ -379,10 +327,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WatchDeployEventsAsync_ThrowsAfterDisposal()
|
public async Task WatchDeployEventsAsync_ThrowsAfterDisposal()
|
||||||
{
|
{
|
||||||
@@ -395,10 +339,6 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
client.WatchDeployEventsAsync());
|
client.WatchDeployEventsAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that TestConnectionAsync throws ObjectDisposedException after the client is disposed.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestConnectionAsync_ThrowsAfterDisposal()
|
public async Task TestConnectionAsync_ThrowsAfterDisposal()
|
||||||
{
|
{
|
||||||
+3
-6
@@ -1,12 +1,11 @@
|
|||||||
using Google.Protobuf;
|
using Google.Protobuf;
|
||||||
using ZB.MOM.WW.MxGateway.Client;
|
using MxGateway.Client;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client.Tests;
|
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()
|
||||||
{
|
{
|
||||||
@@ -16,7 +15,6 @@ 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()
|
||||||
{
|
{
|
||||||
@@ -32,7 +30,6 @@ 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()
|
||||||
{
|
{
|
||||||
+2
-2
@@ -19,8 +19,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.MxGateway.Client\ZB.MOM.WW.MxGateway.Client.csproj" />
|
<ProjectReference Include="..\MxGateway.Client\MxGateway.Client.csproj" />
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.MxGateway.Client.Cli\ZB.MOM.WW.MxGateway.Client.Cli.csproj" />
|
<ProjectReference Include="..\MxGateway.Client.Cli\MxGateway.Client.Cli.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
using MxGateway.Client.Cli;
|
||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
using MxGateway.Contracts.Proto.Galaxy;
|
||||||
|
|
||||||
|
namespace MxGateway.Client.Tests;
|
||||||
|
|
||||||
|
public sealed class MxGatewayClientCliTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Run_Version_PrintsCompiledProtocolVersions()
|
||||||
|
{
|
||||||
|
using var output = new StringWriter();
|
||||||
|
using var error = new StringWriter();
|
||||||
|
|
||||||
|
var exitCode = MxGatewayClientCli.Run(["version"], output, error);
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
Assert.Contains("gateway-protocol=2", output.ToString());
|
||||||
|
Assert.Contains("worker-protocol=1", output.ToString());
|
||||||
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_VersionJson_PrintsJsonProtocolVersions()
|
||||||
|
{
|
||||||
|
using var output = new StringWriter();
|
||||||
|
using var error = new StringWriter();
|
||||||
|
|
||||||
|
int exitCode = await MxGatewayClientCli.RunAsync(["version", "--json"], output, error);
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
Assert.Contains("\"gatewayProtocolVersion\":2", output.ToString());
|
||||||
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_Write_BuildsWriteCommandAndPrintsJsonReply()
|
||||||
|
{
|
||||||
|
using var output = new StringWriter();
|
||||||
|
using var error = new StringWriter();
|
||||||
|
FakeCliClient fakeClient = new();
|
||||||
|
fakeClient.InvokeReplies.Enqueue(new MxCommandReply
|
||||||
|
{
|
||||||
|
SessionId = "session-fixture",
|
||||||
|
Kind = MxCommandKind.Write,
|
||||||
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||||
|
});
|
||||||
|
|
||||||
|
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||||
|
[
|
||||||
|
"write",
|
||||||
|
"--endpoint",
|
||||||
|
"http://localhost:5000",
|
||||||
|
"--api-key",
|
||||||
|
"test-api-key",
|
||||||
|
"--session-id",
|
||||||
|
"session-fixture",
|
||||||
|
"--server-handle",
|
||||||
|
"12",
|
||||||
|
"--item-handle",
|
||||||
|
"34",
|
||||||
|
"--type",
|
||||||
|
"int32",
|
||||||
|
"--value",
|
||||||
|
"123",
|
||||||
|
"--json",
|
||||||
|
],
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
_ => fakeClient);
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
MxCommandRequest request = Assert.Single(fakeClient.InvokeRequests);
|
||||||
|
Assert.Equal(MxCommandKind.Write, request.Command.Kind);
|
||||||
|
Assert.Equal(123, request.Command.Write.Value.Int32Value);
|
||||||
|
Assert.Contains("MX_COMMAND_KIND_WRITE", output.ToString());
|
||||||
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_ErrorOutput_RedactsApiKey()
|
||||||
|
{
|
||||||
|
using var output = new StringWriter();
|
||||||
|
using var error = new StringWriter();
|
||||||
|
|
||||||
|
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||||
|
[
|
||||||
|
"open-session",
|
||||||
|
"--endpoint",
|
||||||
|
"http://localhost:5000",
|
||||||
|
"--api-key",
|
||||||
|
"secret-api-key",
|
||||||
|
],
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
_ => throw new InvalidOperationException("boom secret-api-key"));
|
||||||
|
|
||||||
|
Assert.Equal(1, exitCode);
|
||||||
|
Assert.DoesNotContain("secret-api-key", error.ToString());
|
||||||
|
Assert.Contains("[redacted]", error.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_StreamEvents_WithMaxEventsStopsNonJsonOutput()
|
||||||
|
{
|
||||||
|
using var output = new StringWriter();
|
||||||
|
using var error = new StringWriter();
|
||||||
|
FakeCliClient fakeClient = new();
|
||||||
|
fakeClient.Events.Add(new MxEvent
|
||||||
|
{
|
||||||
|
SessionId = "session-fixture",
|
||||||
|
Family = MxEventFamily.OnDataChange,
|
||||||
|
WorkerSequence = 1,
|
||||||
|
});
|
||||||
|
fakeClient.Events.Add(new MxEvent
|
||||||
|
{
|
||||||
|
SessionId = "session-fixture",
|
||||||
|
Family = MxEventFamily.OnWriteComplete,
|
||||||
|
WorkerSequence = 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||||
|
[
|
||||||
|
"stream-events",
|
||||||
|
"--endpoint",
|
||||||
|
"http://localhost:5000",
|
||||||
|
"--api-key",
|
||||||
|
"test-api-key",
|
||||||
|
"--session-id",
|
||||||
|
"session-fixture",
|
||||||
|
"--max-events",
|
||||||
|
"1",
|
||||||
|
],
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
_ => fakeClient);
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
Assert.Contains("workerSequence", output.ToString());
|
||||||
|
Assert.DoesNotContain("ON_WRITE_COMPLETE", output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_Smoke_WhenCommandFails_ClosesOpenedSession()
|
||||||
|
{
|
||||||
|
using var output = new StringWriter();
|
||||||
|
using var error = new StringWriter();
|
||||||
|
FakeCliClient fakeClient = new()
|
||||||
|
{
|
||||||
|
InvokeFailure = new InvalidOperationException("register failed"),
|
||||||
|
};
|
||||||
|
|
||||||
|
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||||
|
[
|
||||||
|
"smoke",
|
||||||
|
"--endpoint",
|
||||||
|
"http://localhost:5000",
|
||||||
|
"--api-key",
|
||||||
|
"test-api-key",
|
||||||
|
"--item",
|
||||||
|
"Area001.Pump001.Speed",
|
||||||
|
"--json",
|
||||||
|
],
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
_ => fakeClient);
|
||||||
|
|
||||||
|
Assert.Equal(1, exitCode);
|
||||||
|
CloseSessionRequest closeRequest = Assert.Single(fakeClient.CloseSessionRequests);
|
||||||
|
Assert.Equal("session-fixture", closeRequest.SessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_GalaxyTestConnection_PrintsJsonReply()
|
||||||
|
{
|
||||||
|
using var output = new StringWriter();
|
||||||
|
using var error = new StringWriter();
|
||||||
|
FakeCliClient fakeClient = new()
|
||||||
|
{
|
||||||
|
GalaxyTestConnectionReply = new TestConnectionReply { Ok = true },
|
||||||
|
};
|
||||||
|
|
||||||
|
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||||
|
[
|
||||||
|
"galaxy-test-connection",
|
||||||
|
"--endpoint",
|
||||||
|
"http://localhost:5000",
|
||||||
|
"--api-key",
|
||||||
|
"test-api-key",
|
||||||
|
"--json",
|
||||||
|
],
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
_ => fakeClient);
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
Assert.Single(fakeClient.GalaxyTestConnectionRequests);
|
||||||
|
Assert.Contains("\"ok\": true", output.ToString());
|
||||||
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary()
|
||||||
|
{
|
||||||
|
using var output = new StringWriter();
|
||||||
|
using var error = new StringWriter();
|
||||||
|
FakeCliClient fakeClient = new();
|
||||||
|
fakeClient.GalaxyDiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||||
|
{
|
||||||
|
NextPageToken = "7:1",
|
||||||
|
TotalObjectCount = 2,
|
||||||
|
Objects =
|
||||||
|
{
|
||||||
|
new GalaxyObject
|
||||||
|
{
|
||||||
|
GobjectId = 7,
|
||||||
|
TagName = "DelmiaReceiver_001",
|
||||||
|
ContainedName = "DelmiaReceiver",
|
||||||
|
ParentGobjectId = 1,
|
||||||
|
Attributes =
|
||||||
|
{
|
||||||
|
new GalaxyAttribute
|
||||||
|
{
|
||||||
|
AttributeName = "DownloadPath",
|
||||||
|
FullTagReference = "DelmiaReceiver_001.DownloadPath",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fakeClient.GalaxyDiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||||
|
{
|
||||||
|
TotalObjectCount = 2,
|
||||||
|
Objects =
|
||||||
|
{
|
||||||
|
new GalaxyObject
|
||||||
|
{
|
||||||
|
GobjectId = 8,
|
||||||
|
TagName = "DelmiaReceiver_002",
|
||||||
|
ContainedName = "DelmiaReceiver",
|
||||||
|
ParentGobjectId = 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||||
|
[
|
||||||
|
"galaxy-discover",
|
||||||
|
"--endpoint",
|
||||||
|
"http://localhost:5000",
|
||||||
|
"--api-key",
|
||||||
|
"test-api-key",
|
||||||
|
],
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
_ => fakeClient);
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
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();
|
||||||
|
Assert.Contains("objects=2", text);
|
||||||
|
Assert.Contains("DelmiaReceiver_001", text);
|
||||||
|
Assert.Contains("DelmiaReceiver_002", text);
|
||||||
|
Assert.Contains("attributes=1", text);
|
||||||
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents()
|
||||||
|
{
|
||||||
|
using var output = new StringWriter();
|
||||||
|
using var error = new StringWriter();
|
||||||
|
FakeCliClient fakeClient = new();
|
||||||
|
DateTime deploy = new(2026, 4, 28, 14, 30, 0, DateTimeKind.Utc);
|
||||||
|
fakeClient.GalaxyDeployEvents.Add(new DeployEvent
|
||||||
|
{
|
||||||
|
Sequence = 1,
|
||||||
|
ObservedAt = Timestamp.FromDateTime(deploy),
|
||||||
|
TimeOfLastDeploy = Timestamp.FromDateTime(deploy),
|
||||||
|
TimeOfLastDeployPresent = true,
|
||||||
|
ObjectCount = 5,
|
||||||
|
AttributeCount = 17,
|
||||||
|
});
|
||||||
|
fakeClient.GalaxyDeployEvents.Add(new DeployEvent
|
||||||
|
{
|
||||||
|
Sequence = 2,
|
||||||
|
ObservedAt = Timestamp.FromDateTime(deploy.AddSeconds(30)),
|
||||||
|
TimeOfLastDeploy = Timestamp.FromDateTime(deploy.AddSeconds(30)),
|
||||||
|
TimeOfLastDeployPresent = true,
|
||||||
|
ObjectCount = 6,
|
||||||
|
AttributeCount = 18,
|
||||||
|
});
|
||||||
|
|
||||||
|
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||||
|
[
|
||||||
|
"galaxy-watch",
|
||||||
|
"--endpoint",
|
||||||
|
"http://localhost:5000",
|
||||||
|
"--api-key",
|
||||||
|
"test-api-key",
|
||||||
|
"--last-seen-deploy-time",
|
||||||
|
"2026-04-28T14:00:00Z",
|
||||||
|
"--max-events",
|
||||||
|
"2",
|
||||||
|
],
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
_ => fakeClient);
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
WatchDeployEventsRequest request = Assert.Single(fakeClient.GalaxyWatchDeployEventsRequests);
|
||||||
|
Assert.NotNull(request.LastSeenDeployTime);
|
||||||
|
string text = output.ToString();
|
||||||
|
Assert.Contains("sequence=1", text);
|
||||||
|
Assert.Contains("sequence=2", text);
|
||||||
|
Assert.Contains("objects=5", text);
|
||||||
|
Assert.Contains("attributes=18", text);
|
||||||
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_GalaxyWatch_JsonEmitsOneObjectPerEvent()
|
||||||
|
{
|
||||||
|
using var output = new StringWriter();
|
||||||
|
using var error = new StringWriter();
|
||||||
|
FakeCliClient fakeClient = new();
|
||||||
|
fakeClient.GalaxyDeployEvents.Add(new DeployEvent
|
||||||
|
{
|
||||||
|
Sequence = 42,
|
||||||
|
ObjectCount = 99,
|
||||||
|
AttributeCount = 1024,
|
||||||
|
});
|
||||||
|
|
||||||
|
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||||
|
[
|
||||||
|
"galaxy-watch",
|
||||||
|
"--endpoint",
|
||||||
|
"http://localhost:5000",
|
||||||
|
"--api-key",
|
||||||
|
"test-api-key",
|
||||||
|
"--max-events",
|
||||||
|
"1",
|
||||||
|
"--json",
|
||||||
|
],
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
_ => fakeClient);
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
string text = output.ToString();
|
||||||
|
Assert.Contains("\"sequence\": \"42\"", text);
|
||||||
|
Assert.Contains("\"objectCount\": 99", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeCliClient : IMxGatewayCliClient
|
||||||
|
{
|
||||||
|
public Queue<MxCommandReply> InvokeReplies { get; } = new();
|
||||||
|
|
||||||
|
public List<MxCommandRequest> InvokeRequests { get; } = [];
|
||||||
|
|
||||||
|
public List<CloseSessionRequest> CloseSessionRequests { get; } = [];
|
||||||
|
|
||||||
|
public List<MxEvent> Events { get; } = [];
|
||||||
|
|
||||||
|
public Exception? InvokeFailure { get; init; }
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<OpenSessionReply> OpenSessionAsync(
|
||||||
|
OpenSessionRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new OpenSessionReply
|
||||||
|
{
|
||||||
|
SessionId = "session-fixture",
|
||||||
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||||
|
GatewayProtocolVersion = 1,
|
||||||
|
WorkerProtocolVersion = 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CloseSessionReply> CloseSessionAsync(
|
||||||
|
CloseSessionRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
CloseSessionRequests.Add(request);
|
||||||
|
return Task.FromResult(new CloseSessionReply
|
||||||
|
{
|
||||||
|
SessionId = request.SessionId,
|
||||||
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||||
|
FinalState = SessionState.Closed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MxCommandReply> InvokeAsync(
|
||||||
|
MxCommandRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
InvokeRequests.Add(request);
|
||||||
|
if (InvokeFailure is not null)
|
||||||
|
{
|
||||||
|
throw InvokeFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(InvokeReplies.Dequeue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
|
StreamEventsRequest request,
|
||||||
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
foreach (MxEvent gatewayEvent in Events)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
await Task.Yield();
|
||||||
|
yield return gatewayEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestConnectionReply GalaxyTestConnectionReply { get; set; } = new() { Ok = true };
|
||||||
|
|
||||||
|
public GetLastDeployTimeReply GalaxyGetLastDeployTimeReply { get; set; } = new() { Present = false };
|
||||||
|
|
||||||
|
public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new();
|
||||||
|
|
||||||
|
public Queue<DiscoverHierarchyReply> GalaxyDiscoverHierarchyReplies { get; } = new();
|
||||||
|
|
||||||
|
public List<TestConnectionRequest> GalaxyTestConnectionRequests { get; } = [];
|
||||||
|
|
||||||
|
public List<GetLastDeployTimeRequest> GalaxyGetLastDeployTimeRequests { get; } = [];
|
||||||
|
|
||||||
|
public List<DiscoverHierarchyRequest> GalaxyDiscoverHierarchyRequests { get; } = [];
|
||||||
|
|
||||||
|
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
||||||
|
TestConnectionRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
GalaxyTestConnectionRequests.Add(request);
|
||||||
|
return Task.FromResult(GalaxyTestConnectionReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
||||||
|
GetLastDeployTimeRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
GalaxyGetLastDeployTimeRequests.Add(request);
|
||||||
|
return Task.FromResult(GalaxyGetLastDeployTimeReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
||||||
|
DiscoverHierarchyRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
GalaxyDiscoverHierarchyRequests.Add(request);
|
||||||
|
return Task.FromResult(
|
||||||
|
GalaxyDiscoverHierarchyReplies.TryDequeue(out DiscoverHierarchyReply? reply)
|
||||||
|
? reply
|
||||||
|
: GalaxyDiscoverHierarchyReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<WatchDeployEventsRequest> GalaxyWatchDeployEventsRequests { get; } = [];
|
||||||
|
|
||||||
|
public List<DeployEvent> GalaxyDeployEvents { get; } = [];
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
||||||
|
WatchDeployEventsRequest request,
|
||||||
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
GalaxyWatchDeployEventsRequests.Add(request);
|
||||||
|
foreach (DeployEvent deployEvent in GalaxyDeployEvents)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
await Task.Yield();
|
||||||
|
yield return deployEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-4
@@ -1,10 +1,9 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts;
|
using MxGateway.Contracts;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client.Tests;
|
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()
|
||||||
{
|
{
|
||||||
@@ -13,7 +12,6 @@ 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()
|
||||||
{
|
{
|
||||||
+1
-4
@@ -1,8 +1,7 @@
|
|||||||
namespace ZB.MOM.WW.MxGateway.Client.Tests;
|
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()
|
||||||
{
|
{
|
||||||
@@ -15,7 +14,6 @@ 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()
|
||||||
{
|
{
|
||||||
@@ -28,7 +26,6 @@ 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()
|
||||||
{
|
{
|
||||||
+2
-29
@@ -1,13 +1,10 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
using MxGateway.Contracts.Proto;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenSessionRawAsync_AttachesApiKeyMetadataAndCancellation()
|
public async Task OpenSessionRawAsync_AttachesApiKeyMetadataAndCancellation()
|
||||||
{
|
{
|
||||||
@@ -22,8 +19,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenSessionAsync_ReturnsSessionWithRawOpenReply()
|
public async Task OpenSessionAsync_ReturnsSessionWithRawOpenReply()
|
||||||
{
|
{
|
||||||
@@ -38,8 +33,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RegisterAsync_BuildsRegisterCommandAndReturnsServerHandle()
|
public async Task RegisterAsync_BuildsRegisterCommandAndReturnsServerHandle()
|
||||||
{
|
{
|
||||||
@@ -64,8 +57,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddItem2Async_BuildsAddItem2CommandWithContext()
|
public async Task AddItem2Async_BuildsAddItem2CommandWithContext()
|
||||||
{
|
{
|
||||||
@@ -90,8 +81,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WriteRawAsync_BuildsWriteCommandWithRawValue()
|
public async Task WriteRawAsync_BuildsWriteCommandWithRawValue()
|
||||||
{
|
{
|
||||||
@@ -122,8 +111,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Write2RawAsync_BuildsWrite2CommandWithValueAndTimestamp()
|
public async Task Write2RawAsync_BuildsWrite2CommandWithValueAndTimestamp()
|
||||||
{
|
{
|
||||||
@@ -151,8 +138,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SubscribeBulkAsync_BuildsOneBulkCommandAndReturnsPerItemResults()
|
public async Task SubscribeBulkAsync_BuildsOneBulkCommandAndReturnsPerItemResults()
|
||||||
{
|
{
|
||||||
@@ -191,8 +176,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task StreamEventsAsync_YieldsEventsInGatewayOrder()
|
public async Task StreamEventsAsync_YieldsEventsInGatewayOrder()
|
||||||
{
|
{
|
||||||
@@ -223,8 +206,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CloseAsync_IsExplicitAndIdempotent()
|
public async Task CloseAsync_IsExplicitAndIdempotent()
|
||||||
{
|
{
|
||||||
@@ -240,8 +221,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task InvokeAsync_RetriesSafeDiagnosticCommandOnTransientGrpcFailure()
|
public async Task InvokeAsync_RetriesSafeDiagnosticCommandOnTransientGrpcFailure()
|
||||||
{
|
{
|
||||||
@@ -265,8 +244,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenSessionAsync_DoesNotRetryTransientGrpcFailure()
|
public async Task OpenSessionAsync_DoesNotRetryTransientGrpcFailure()
|
||||||
{
|
{
|
||||||
@@ -279,8 +256,6 @@ 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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task InvokeAsync_DoesNotRetryWriteCommand()
|
public async Task InvokeAsync_DoesNotRetryWriteCommand()
|
||||||
{
|
{
|
||||||
@@ -295,8 +270,6 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Single(transport.InvokeCalls);
|
Assert.Single(transport.InvokeCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Verifies that invoke helpers pass cancellation token to the transport.</summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task InvokeHelpers_PassCancellationTokenToTransport()
|
public async Task InvokeHelpers_PassCancellationTokenToTransport()
|
||||||
{
|
{
|
||||||
+1
-3
@@ -1,9 +1,7 @@
|
|||||||
namespace ZB.MOM.WW.MxGateway.Client.Tests;
|
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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GeneratedGrpcClient_CanBeConstructedFromClientFactory()
|
public async Task GeneratedGrpcClient_CanBeConstructedFromClientFactory()
|
||||||
{
|
{
|
||||||
+3
-4
@@ -1,13 +1,12 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Google.Protobuf;
|
using Google.Protobuf;
|
||||||
using ZB.MOM.WW.MxGateway.Client;
|
using MxGateway.Client;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client.Tests;
|
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()
|
||||||
{
|
{
|
||||||
+3
-6
@@ -1,13 +1,12 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Google.Protobuf;
|
using Google.Protobuf;
|
||||||
using ZB.MOM.WW.MxGateway.Client;
|
using MxGateway.Client;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client.Tests;
|
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()
|
||||||
{
|
{
|
||||||
@@ -19,7 +18,6 @@ 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()
|
||||||
{
|
{
|
||||||
@@ -31,7 +29,6 @@ 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,76 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Client", "MxGateway.Client\MxGateway.Client.csproj", "{7CF9ED88-1F32-4040-BEB1-D0902E304C70}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Contracts", "..\..\src\MxGateway.Contracts\MxGateway.Contracts.csproj", "{9AB807A8-0469-40F7-A000-D240F36B6E5D}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Client.Cli", "MxGateway.Client.Cli\MxGateway.Client.Cli.csproj", "{EB061E77-2475-4322-9257-3F2456DD141C}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Client.Tests", "MxGateway.Client.Tests\MxGateway.Client.Tests.csproj", "{B77B5A8E-0C53-4419-9BCD-227C9753A074}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{EB061E77-2475-4322-9257-3F2456DD141C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{EB061E77-2475-4322-9257-3F2456DD141C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{EB061E77-2475-4322-9257-3F2456DD141C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{EB061E77-2475-4322-9257-3F2456DD141C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{EB061E77-2475-4322-9257-3F2456DD141C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{EB061E77-2475-4322-9257-3F2456DD141C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{EB061E77-2475-4322-9257-3F2456DD141C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{EB061E77-2475-4322-9257-3F2456DD141C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{EB061E77-2475-4322-9257-3F2456DD141C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{EB061E77-2475-4322-9257-3F2456DD141C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{EB061E77-2475-4322-9257-3F2456DD141C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{EB061E77-2475-4322-9257-3F2456DD141C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
+3
-178
@@ -2,14 +2,14 @@ using Google.Protobuf.WellKnownTypes;
|
|||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Grpc.Net.Client;
|
using Grpc.Net.Client;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
using MxGateway.Contracts.Proto.Galaxy;
|
||||||
using Polly;
|
using Polly;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Security;
|
using System.Net.Security;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides the .NET client entry point for the public Galaxy Repository gRPC API.
|
/// Provides the .NET client entry point for the public Galaxy Repository gRPC API.
|
||||||
@@ -19,18 +19,12 @@ namespace ZB.MOM.WW.MxGateway.Client;
|
|||||||
public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||||
{
|
{
|
||||||
private const int DiscoverHierarchyPageSize = 5000;
|
private const int DiscoverHierarchyPageSize = 5000;
|
||||||
private const int BrowseChildrenPageSize = 500;
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -58,23 +52,12 @@ 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);
|
||||||
@@ -102,8 +85,6 @@ 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(
|
||||||
@@ -114,12 +95,6 @@ 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)
|
||||||
@@ -136,8 +111,6 @@ 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(
|
||||||
@@ -153,12 +126,6 @@ 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)
|
||||||
@@ -176,17 +143,11 @@ 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)
|
||||||
{
|
{
|
||||||
return await DiscoverHierarchyAsync(new DiscoverHierarchyOptions(), cancellationToken).ConfigureAwait(false);
|
return await DiscoverHierarchyAsync(new DiscoverHierarchyOptions(), cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Discovers the Galaxy object hierarchy.</summary>
|
|
||||||
/// <param name="options">Client configuration options.</param>
|
|
||||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
|
||||||
/// <returns>The collection of Galaxy objects in the hierarchy.</returns>
|
|
||||||
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(
|
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyOptions options,
|
DiscoverHierarchyOptions options,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -261,12 +222,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
return request;
|
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)
|
||||||
@@ -279,92 +234,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Returns root-level browse nodes (objects with no parent).</summary>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The list of root <see cref="LazyBrowseNode"/> instances.</returns>
|
|
||||||
public Task<IReadOnlyList<LazyBrowseNode>> BrowseAsync(CancellationToken cancellationToken = default)
|
|
||||||
=> BrowseAsync(null, cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>Returns root-level browse nodes filtered by the given options.</summary>
|
|
||||||
/// <param name="options">Browse filter options. Null applies no filter.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The list of root <see cref="LazyBrowseNode"/> instances.</returns>
|
|
||||||
public async Task<IReadOnlyList<LazyBrowseNode>> BrowseAsync(
|
|
||||||
BrowseChildrenOptions? options,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
BrowseChildrenOptions effective = options ?? new BrowseChildrenOptions();
|
|
||||||
List<LazyBrowseNode> roots = [];
|
|
||||||
string pageToken = string.Empty;
|
|
||||||
HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
|
|
||||||
do
|
|
||||||
{
|
|
||||||
BrowseChildrenRequest request = BuildBrowseChildrenRequest(effective);
|
|
||||||
request.PageToken = pageToken;
|
|
||||||
BrowseChildrenReply reply = await BrowseChildrenRawAsync(request, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
for (int i = 0; i < reply.Children.Count; i++)
|
|
||||||
{
|
|
||||||
bool hint = i < reply.ChildHasChildren.Count && reply.ChildHasChildren[i];
|
|
||||||
roots.Add(new LazyBrowseNode(this, reply.Children[i], hint, effective));
|
|
||||||
}
|
|
||||||
|
|
||||||
pageToken = reply.NextPageToken;
|
|
||||||
if (!string.IsNullOrWhiteSpace(pageToken) && !seenPageTokens.Add(pageToken))
|
|
||||||
{
|
|
||||||
throw new MxGatewayException(
|
|
||||||
$"Galaxy BrowseChildren returned a repeated page token '{pageToken}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (!string.IsNullOrWhiteSpace(pageToken));
|
|
||||||
|
|
||||||
return roots;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Issues a raw BrowseChildren RPC without result wrapping.</summary>
|
|
||||||
/// <param name="request">The browse-children request.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The raw server reply.</returns>
|
|
||||||
public Task<BrowseChildrenReply> BrowseChildrenRawAsync(
|
|
||||||
BrowseChildrenRequest request,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
return ExecuteSafeUnaryAsync(
|
|
||||||
token => _transport.BrowseChildrenAsync(request, CreateCallOptions(token)),
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Builds a <see cref="BrowseChildrenRequest"/> from the provided options.</summary>
|
|
||||||
/// <param name="options">Browse children options to convert.</param>
|
|
||||||
/// <returns>The constructed request message.</returns>
|
|
||||||
internal static BrowseChildrenRequest BuildBrowseChildrenRequest(BrowseChildrenOptions options)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
|
||||||
|
|
||||||
BrowseChildrenRequest request = new()
|
|
||||||
{
|
|
||||||
PageSize = BrowseChildrenPageSize,
|
|
||||||
AlarmBearingOnly = options.AlarmBearingOnly,
|
|
||||||
HistorizedOnly = options.HistorizedOnly,
|
|
||||||
};
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Subscribes to Galaxy deploy events. The server emits a bootstrap event with the
|
/// Subscribes to Galaxy deploy events. The server emits a bootstrap event with the
|
||||||
/// current state on subscribe so callers can prime their cache, then emits one event
|
/// current state on subscribe so callers can prime their cache, then emits one event
|
||||||
@@ -377,9 +246,6 @@ 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)
|
||||||
@@ -395,12 +261,6 @@ 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)
|
||||||
@@ -424,10 +284,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Closes the gRPC channel and releases resources.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous dispose operation.</returns>
|
|
||||||
public ValueTask DisposeAsync()
|
public ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
@@ -440,32 +296,16 @@ 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)
|
||||||
@@ -494,13 +334,7 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpMessageHandler CreateHttpHandler(MxGatewayClientOptions options) =>
|
private static HttpMessageHandler CreateHttpHandler(MxGatewayClientOptions options)
|
||||||
CreateHttpHandlerForTests(options);
|
|
||||||
|
|
||||||
/// <summary>Creates an <see cref="HttpMessageHandler"/> configured from the provided options for test use.</summary>
|
|
||||||
/// <param name="options">Client options used to configure TLS and timeouts.</param>
|
|
||||||
/// <returns>The configured HTTP message handler.</returns>
|
|
||||||
internal static SocketsHttpHandler CreateHttpHandlerForTests(MxGatewayClientOptions options)
|
|
||||||
{
|
{
|
||||||
SocketsHttpHandler handler = new()
|
SocketsHttpHandler handler = new()
|
||||||
{
|
{
|
||||||
@@ -520,11 +354,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
X509Certificate2 trustedRoot = X509CertificateLoader.LoadCertificateFromFile(options.CaCertificatePath);
|
X509Certificate2 trustedRoot = X509CertificateLoader.LoadCertificateFromFile(options.CaCertificatePath);
|
||||||
handler.SslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, errors) =>
|
handler.SslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, errors) =>
|
||||||
{
|
{
|
||||||
if ((errors & System.Net.Security.SslPolicyErrors.RemoteCertificateNameMismatch) != 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (certificate is null)
|
if (certificate is null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -540,10 +369,6 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
return customChain.Build(certificateToValidate);
|
return customChain.Build(certificateToValidate);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (!options.RequireCertificateValidation)
|
|
||||||
{
|
|
||||||
handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler;
|
return handler;
|
||||||
+2
-36
@@ -1,27 +1,18 @@
|
|||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
using MxGateway.Contracts.Proto.Galaxy;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.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
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
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)
|
||||||
@@ -38,7 +29,6 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
public async Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -55,7 +45,6 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
public async Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -72,28 +61,6 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<BrowseChildrenReply> BrowseChildrenAsync(
|
|
||||||
BrowseChildrenRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await RawClient.BrowseChildrenAsync(request, callOptions)
|
|
||||||
.ResponseAsync
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (RpcException exception)
|
|
||||||
{
|
|
||||||
throw MapRpcException(exception, callOptions.CancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Streams deploy events from the Galaxy Repository, using an explicit cancellation token that overrides the call options token when provided.</summary>
|
|
||||||
/// <param name="request">The watch deploy events request.</param>
|
|
||||||
/// <param name="callOptions">Call options for the underlying gRPC call.</param>
|
|
||||||
/// <param name="cancellationToken">Optional cancellation token; takes precedence over the token in <paramref name="callOptions"/> when cancellable.</param>
|
|
||||||
/// <returns>An async enumerable of deploy events.</returns>
|
|
||||||
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CallOptions callOptions,
|
CallOptions callOptions,
|
||||||
@@ -127,7 +94,6 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
IAsyncEnumerable<DeployEvent> IGalaxyRepositoryClientTransport.WatchDeployEventsAsync(
|
IAsyncEnumerable<DeployEvent> IGalaxyRepositoryClientTransport.WatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
internal sealed class GrpcMxGatewayClientTransport(
|
||||||
|
MxGatewayClientOptions options,
|
||||||
|
MxAccessGateway.MxAccessGatewayClient rawClient) : IMxGatewayClientTransport
|
||||||
|
{
|
||||||
|
public MxGatewayClientOptions Options { get; } = options;
|
||||||
|
|
||||||
|
public MxAccessGateway.MxAccessGatewayClient RawClient { get; } = rawClient;
|
||||||
|
|
||||||
|
MxAccessGateway.MxAccessGatewayClient? IMxGatewayClientTransport.RawClient => RawClient;
|
||||||
|
|
||||||
|
public async Task<OpenSessionReply> OpenSessionAsync(
|
||||||
|
OpenSessionRequest request,
|
||||||
|
CallOptions callOptions)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await RawClient.OpenSessionAsync(request, callOptions)
|
||||||
|
.ResponseAsync
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (RpcException exception)
|
||||||
|
{
|
||||||
|
throw MapRpcException(exception, callOptions.CancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CloseSessionReply> CloseSessionAsync(
|
||||||
|
CloseSessionRequest request,
|
||||||
|
CallOptions callOptions)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await RawClient.CloseSessionAsync(request, callOptions)
|
||||||
|
.ResponseAsync
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (RpcException exception)
|
||||||
|
{
|
||||||
|
throw MapRpcException(exception, callOptions.CancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MxCommandReply> InvokeAsync(
|
||||||
|
MxCommandRequest request,
|
||||||
|
CallOptions callOptions)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await RawClient.InvokeAsync(request, callOptions)
|
||||||
|
.ResponseAsync
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (RpcException exception)
|
||||||
|
{
|
||||||
|
throw MapRpcException(exception, callOptions.CancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
|
StreamEventsRequest request,
|
||||||
|
CallOptions callOptions,
|
||||||
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
CancellationToken effectiveCancellationToken = cancellationToken.CanBeCanceled
|
||||||
|
? cancellationToken
|
||||||
|
: callOptions.CancellationToken;
|
||||||
|
|
||||||
|
using AsyncServerStreamingCall<MxEvent> call = RawClient.StreamEvents(request, callOptions);
|
||||||
|
|
||||||
|
IAsyncStreamReader<MxEvent> responseStream = call.ResponseStream;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
MxEvent? gatewayEvent;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!await responseStream.MoveNext(effectiveCancellationToken).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
gatewayEvent = responseStream.Current;
|
||||||
|
}
|
||||||
|
catch (RpcException exception)
|
||||||
|
{
|
||||||
|
throw MapRpcException(exception, effectiveCancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return gatewayEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IAsyncEnumerable<MxEvent> IMxGatewayClientTransport.StreamEventsAsync(
|
||||||
|
StreamEventsRequest request,
|
||||||
|
CallOptions callOptions)
|
||||||
|
{
|
||||||
|
return StreamEventsAsync(request, callOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Exception MapRpcException(
|
||||||
|
RpcException exception,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested || exception.StatusCode == StatusCode.Cancelled)
|
||||||
|
{
|
||||||
|
return new OperationCanceledException(
|
||||||
|
exception.Status.Detail,
|
||||||
|
exception,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return exception.StatusCode switch
|
||||||
|
{
|
||||||
|
StatusCode.Unauthenticated => new MxGatewayAuthenticationException(
|
||||||
|
exception.Status.Detail,
|
||||||
|
innerException: exception),
|
||||||
|
StatusCode.PermissionDenied => new MxGatewayAuthorizationException(
|
||||||
|
exception.Status.Detail,
|
||||||
|
innerException: exception),
|
||||||
|
_ => new MxGatewayException(exception.Status.Detail, exception),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
using MxGateway.Contracts.Proto.Galaxy;
|
||||||
|
|
||||||
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
internal interface IGalaxyRepositoryClientTransport
|
||||||
|
{
|
||||||
|
MxGatewayClientOptions Options { get; }
|
||||||
|
|
||||||
|
GalaxyRepository.GalaxyRepositoryClient? RawClient { get; }
|
||||||
|
|
||||||
|
Task<TestConnectionReply> TestConnectionAsync(
|
||||||
|
TestConnectionRequest request,
|
||||||
|
CallOptions callOptions);
|
||||||
|
|
||||||
|
Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
||||||
|
GetLastDeployTimeRequest request,
|
||||||
|
CallOptions callOptions);
|
||||||
|
|
||||||
|
Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
||||||
|
DiscoverHierarchyRequest request,
|
||||||
|
CallOptions callOptions);
|
||||||
|
|
||||||
|
IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||||
|
WatchDeployEventsRequest request,
|
||||||
|
CallOptions callOptions);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
internal interface IMxGatewayClientTransport
|
||||||
|
{
|
||||||
|
MxGatewayClientOptions Options { get; }
|
||||||
|
|
||||||
|
MxAccessGateway.MxAccessGatewayClient? RawClient { get; }
|
||||||
|
|
||||||
|
Task<OpenSessionReply> OpenSessionAsync(
|
||||||
|
OpenSessionRequest request,
|
||||||
|
CallOptions callOptions);
|
||||||
|
|
||||||
|
Task<CloseSessionReply> CloseSessionAsync(
|
||||||
|
CloseSessionRequest request,
|
||||||
|
CallOptions callOptions);
|
||||||
|
|
||||||
|
Task<MxCommandReply> InvokeAsync(
|
||||||
|
MxCommandRequest request,
|
||||||
|
CallOptions callOptions);
|
||||||
|
|
||||||
|
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
|
StreamEventsRequest request,
|
||||||
|
CallOptions callOptions);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
public sealed class MxAccessException : MxGatewayCommandException
|
||||||
|
{
|
||||||
|
public MxAccessException(
|
||||||
|
string message,
|
||||||
|
MxCommandReply reply,
|
||||||
|
Exception? innerException = null)
|
||||||
|
: base(
|
||||||
|
message,
|
||||||
|
reply.SessionId,
|
||||||
|
reply.CorrelationId,
|
||||||
|
reply.ProtocolStatus,
|
||||||
|
reply.HasHresult ? reply.Hresult : null,
|
||||||
|
reply.Statuses.ToArray(),
|
||||||
|
innerException)
|
||||||
|
{
|
||||||
|
Reply = reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MxCommandReply Reply { get; }
|
||||||
|
}
|
||||||
+2
-9
@@ -1,13 +1,9 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.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>
|
|
||||||
/// <returns>The same <paramref name="reply"/> for fluent chaining when validation passes.</returns>
|
|
||||||
public static MxCommandReply EnsureProtocolSuccess(this MxCommandReply reply)
|
public static MxCommandReply EnsureProtocolSuccess(this MxCommandReply reply)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(reply);
|
ArgumentNullException.ThrowIfNull(reply);
|
||||||
@@ -23,9 +19,6 @@ 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>
|
|
||||||
/// <returns>The same <paramref name="reply"/> for fluent chaining when validation passes.</returns>
|
|
||||||
public static MxCommandReply EnsureMxAccessSuccess(this MxCommandReply reply)
|
public static MxCommandReply EnsureMxAccessSuccess(this MxCommandReply reply)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(reply);
|
ArgumentNullException.ThrowIfNull(reply);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\MxGateway.Contracts\MxGateway.Contracts.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Grpc.Net.Client" Version="2.76.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
|
||||||
|
<PackageReference Include="Polly.Core" Version="8.6.6" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
public sealed class MxGatewayAuthenticationException : MxGatewayException
|
||||||
|
{
|
||||||
|
public MxGatewayAuthenticationException(
|
||||||
|
string message,
|
||||||
|
string? sessionId = null,
|
||||||
|
string? correlationId = null,
|
||||||
|
ProtocolStatus? protocolStatus = null,
|
||||||
|
int? hResult = null,
|
||||||
|
IReadOnlyList<MxStatusProxy>? statuses = null,
|
||||||
|
Exception? innerException = null)
|
||||||
|
: base(
|
||||||
|
message,
|
||||||
|
sessionId,
|
||||||
|
correlationId,
|
||||||
|
protocolStatus,
|
||||||
|
hResult,
|
||||||
|
statuses ?? [],
|
||||||
|
innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
public sealed class MxGatewayAuthorizationException : MxGatewayException
|
||||||
|
{
|
||||||
|
public MxGatewayAuthorizationException(
|
||||||
|
string message,
|
||||||
|
string? sessionId = null,
|
||||||
|
string? correlationId = null,
|
||||||
|
ProtocolStatus? protocolStatus = null,
|
||||||
|
int? hResult = null,
|
||||||
|
IReadOnlyList<MxStatusProxy>? statuses = null,
|
||||||
|
Exception? innerException = null)
|
||||||
|
: base(
|
||||||
|
message,
|
||||||
|
sessionId,
|
||||||
|
correlationId,
|
||||||
|
protocolStatus,
|
||||||
|
hResult,
|
||||||
|
statuses ?? [],
|
||||||
|
innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-148
@@ -1,13 +1,13 @@
|
|||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Grpc.Net.Client;
|
using Grpc.Net.Client;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
using MxGateway.Contracts.Proto;
|
||||||
using Polly;
|
using Polly;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Security;
|
using System.Net.Security;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides the .NET client entry point for the public MXAccess Gateway gRPC API.
|
/// Provides the .NET client entry point for the public MXAccess Gateway gRPC API.
|
||||||
@@ -19,11 +19,6 @@ 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)
|
||||||
@@ -51,23 +46,12 @@ 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);
|
||||||
@@ -91,12 +75,6 @@ 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)
|
||||||
@@ -109,12 +87,6 @@ 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)
|
||||||
@@ -125,12 +97,6 @@ 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)
|
||||||
@@ -143,12 +109,6 @@ 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)
|
||||||
@@ -166,12 +126,6 @@ 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)
|
||||||
@@ -182,74 +136,6 @@ 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>
|
|
||||||
/// Attaches to the gateway's central alarm feed. The stream opens with one
|
|
||||||
/// <see cref="AlarmFeedMessage"/> per currently-active alarm (the
|
|
||||||
/// ConditionRefresh snapshot), then a single <c>snapshot_complete</c>, then a
|
|
||||||
/// <c>transition</c> for every subsequent raise / acknowledge / clear. Served
|
|
||||||
/// by the gateway's always-on alarm monitor — no worker session is opened, so
|
|
||||||
/// any number of clients may attach. Optionally scoped by alarm-reference
|
|
||||||
/// prefix (<see cref="StreamAlarmsRequest.AlarmFilterPrefix"/>).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The stream request, optionally scoped by alarm-reference prefix.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token for the stream.</param>
|
|
||||||
/// <returns>An async enumerable of alarm feed messages.</returns>
|
|
||||||
public IAsyncEnumerable<AlarmFeedMessage> StreamAlarmsAsync(
|
|
||||||
StreamAlarmsRequest request,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
return _transport.StreamAlarmsAsync(request, CreateStreamCallOptions(cancellationToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disposes the client and releases all resources.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous dispose operation.</returns>
|
|
||||||
public ValueTask DisposeAsync()
|
public ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
@@ -262,32 +148,16 @@ 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)
|
||||||
@@ -316,13 +186,7 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpMessageHandler CreateHttpHandler(MxGatewayClientOptions options) =>
|
private static HttpMessageHandler CreateHttpHandler(MxGatewayClientOptions options)
|
||||||
CreateHttpHandlerForTests(options);
|
|
||||||
|
|
||||||
/// <summary>Creates an <see cref="HttpMessageHandler"/> configured from the provided options for test use.</summary>
|
|
||||||
/// <param name="options">Client options used to configure TLS and timeouts.</param>
|
|
||||||
/// <returns>The configured HTTP message handler.</returns>
|
|
||||||
internal static SocketsHttpHandler CreateHttpHandlerForTests(MxGatewayClientOptions options)
|
|
||||||
{
|
{
|
||||||
SocketsHttpHandler handler = new()
|
SocketsHttpHandler handler = new()
|
||||||
{
|
{
|
||||||
@@ -342,11 +206,6 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
X509Certificate2 trustedRoot = X509CertificateLoader.LoadCertificateFromFile(options.CaCertificatePath);
|
X509Certificate2 trustedRoot = X509CertificateLoader.LoadCertificateFromFile(options.CaCertificatePath);
|
||||||
handler.SslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, errors) =>
|
handler.SslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, errors) =>
|
||||||
{
|
{
|
||||||
if ((errors & System.Net.Security.SslPolicyErrors.RemoteCertificateNameMismatch) != 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (certificate is null)
|
if (certificate is null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -362,10 +221,6 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
return customChain.Build(certificateToValidate);
|
return customChain.Build(certificateToValidate);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (!options.RequireCertificateValidation)
|
|
||||||
{
|
|
||||||
handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler;
|
return handler;
|
||||||
+2
-4
@@ -1,17 +1,15 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts;
|
using MxGateway.Contracts;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exposes the protocol versions compiled into this client package.
|
/// Exposes the protocol versions compiled into this client package.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class MxGatewayClientContractInfo
|
public static class MxGatewayClientContractInfo
|
||||||
{
|
{
|
||||||
/// <inheritdoc cref="GatewayContractInfo.GatewayProtocolVersion"/>
|
|
||||||
public const uint GatewayProtocolVersion =
|
public const uint GatewayProtocolVersion =
|
||||||
GatewayContractInfo.GatewayProtocolVersion;
|
GatewayContractInfo.GatewayProtocolVersion;
|
||||||
|
|
||||||
/// <inheritdoc cref="GatewayContractInfo.WorkerProtocolVersion"/>
|
|
||||||
public const uint WorkerProtocolVersion =
|
public const uint WorkerProtocolVersion =
|
||||||
GatewayContractInfo.WorkerProtocolVersion;
|
GatewayContractInfo.WorkerProtocolVersion;
|
||||||
}
|
}
|
||||||
+1
-48
@@ -1,81 +1,34 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configures the gRPC channel used by the .NET MXAccess Gateway client.
|
/// Configures the gRPC channel used by the .NET MXAccess Gateway 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>
|
|
||||||
/// When true, TLS connections without a pinned <see cref="CaCertificatePath"/>
|
|
||||||
/// use the OS trust store. When false (default), the gateway certificate is
|
|
||||||
/// accepted without verification — appropriate for this internal tool's
|
|
||||||
/// auto-generated self-signed certificate. Pinning a CA always verifies.
|
|
||||||
/// </summary>
|
|
||||||
public bool RequireCertificateValidation { 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; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the maximum size in bytes for gRPC messages.
|
|
||||||
/// </summary>
|
|
||||||
public int MaxGrpcMessageBytes { get; init; } = 16 * 1024 * 1024;
|
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);
|
||||||
+1
-7
@@ -1,21 +1,15 @@
|
|||||||
namespace ZB.MOM.WW.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)
|
||||||
+2
-10
@@ -1,18 +1,13 @@
|
|||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
using MxGateway.Contracts.Proto;
|
||||||
using Polly;
|
using Polly;
|
||||||
using Polly.Retry;
|
using Polly.Retry;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.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>
|
|
||||||
/// <returns>A configured <see cref="ResiliencePipeline"/> with exponential-backoff retry.</returns>
|
|
||||||
public static ResiliencePipeline Create(
|
public static ResiliencePipeline Create(
|
||||||
MxGatewayClientRetryOptions options,
|
MxGatewayClientRetryOptions options,
|
||||||
ILogger? logger)
|
ILogger? logger)
|
||||||
@@ -41,9 +36,6 @@ 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>
|
|
||||||
/// <returns><see langword="true"/> if the command kind is safe to retry; otherwise <see langword="false"/>.</returns>
|
|
||||||
public static bool IsRetryableCommand(MxCommandKind kind)
|
public static bool IsRetryableCommand(MxCommandKind kind)
|
||||||
{
|
{
|
||||||
return kind is MxCommandKind.Ping
|
return kind is MxCommandKind.Ping
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
public class MxGatewayCommandException : MxGatewayException
|
||||||
|
{
|
||||||
|
public MxGatewayCommandException(
|
||||||
|
string message,
|
||||||
|
string? sessionId = null,
|
||||||
|
string? correlationId = null,
|
||||||
|
ProtocolStatus? protocolStatus = null,
|
||||||
|
int? hResult = null,
|
||||||
|
IReadOnlyList<MxStatusProxy>? statuses = null,
|
||||||
|
Exception? innerException = null)
|
||||||
|
: base(
|
||||||
|
message,
|
||||||
|
sessionId,
|
||||||
|
correlationId,
|
||||||
|
protocolStatus,
|
||||||
|
hResult,
|
||||||
|
statuses ?? [],
|
||||||
|
innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
public class MxGatewayException : Exception
|
||||||
|
{
|
||||||
|
public MxGatewayException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
Statuses = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public MxGatewayException(string message, Exception? innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
Statuses = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public MxGatewayException(
|
||||||
|
string message,
|
||||||
|
string? sessionId,
|
||||||
|
string? correlationId,
|
||||||
|
ProtocolStatus? protocolStatus,
|
||||||
|
int? hResult,
|
||||||
|
IReadOnlyList<MxStatusProxy> statuses,
|
||||||
|
Exception? innerException = null)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
SessionId = sessionId;
|
||||||
|
CorrelationId = correlationId;
|
||||||
|
ProtocolStatus = protocolStatus;
|
||||||
|
HResultCode = hResult;
|
||||||
|
Statuses = statuses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? SessionId { get; }
|
||||||
|
|
||||||
|
public string? CorrelationId { get; }
|
||||||
|
|
||||||
|
public ProtocolStatus? ProtocolStatus { get; }
|
||||||
|
|
||||||
|
public int? HResultCode { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<MxStatusProxy> Statuses { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,489 @@
|
|||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents one gateway-backed MXAccess session.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MxGatewaySession : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly MxGatewayClient _client;
|
||||||
|
private readonly SemaphoreSlim _closeLock = new(1, 1);
|
||||||
|
private CloseSessionReply? _closeReply;
|
||||||
|
|
||||||
|
internal MxGatewaySession(
|
||||||
|
MxGatewayClient client,
|
||||||
|
OpenSessionReply openSessionReply)
|
||||||
|
{
|
||||||
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
OpenSessionReply = openSessionReply ?? throw new ArgumentNullException(nameof(openSessionReply));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SessionId => OpenSessionReply.SessionId;
|
||||||
|
|
||||||
|
public OpenSessionReply OpenSessionReply { get; }
|
||||||
|
|
||||||
|
public async Task<CloseSessionReply> CloseAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (_closeReply is not null)
|
||||||
|
{
|
||||||
|
return _closeReply;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _closeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_closeReply is not null)
|
||||||
|
{
|
||||||
|
return _closeReply;
|
||||||
|
}
|
||||||
|
|
||||||
|
_closeReply = await _client.CloseSessionRawAsync(
|
||||||
|
new CloseSessionRequest { SessionId = SessionId },
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
return _closeReply;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_closeLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> RegisterAsync(
|
||||||
|
string clientName,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
MxCommandReply reply = await RegisterRawAsync(clientName, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
return reply.Register?.ServerHandle ?? reply.ReturnValue.Int32Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MxCommandReply> RegisterRawAsync(
|
||||||
|
string clientName,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(clientName);
|
||||||
|
|
||||||
|
return InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.Register,
|
||||||
|
Register = new RegisterCommand { ClientName = clientName },
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> AddItemAsync(
|
||||||
|
int serverHandle,
|
||||||
|
string itemDefinition,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
MxCommandReply reply = await AddItemRawAsync(
|
||||||
|
serverHandle,
|
||||||
|
itemDefinition,
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
return reply.AddItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MxCommandReply> AddItemRawAsync(
|
||||||
|
int serverHandle,
|
||||||
|
string itemDefinition,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
|
||||||
|
|
||||||
|
return InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.AddItem,
|
||||||
|
AddItem = new AddItemCommand
|
||||||
|
{
|
||||||
|
ServerHandle = serverHandle,
|
||||||
|
ItemDefinition = itemDefinition,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> AddItem2Async(
|
||||||
|
int serverHandle,
|
||||||
|
string itemDefinition,
|
||||||
|
string itemContext,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
MxCommandReply reply = await AddItem2RawAsync(
|
||||||
|
serverHandle,
|
||||||
|
itemDefinition,
|
||||||
|
itemContext,
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
return reply.AddItem2?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MxCommandReply> AddItem2RawAsync(
|
||||||
|
int serverHandle,
|
||||||
|
string itemDefinition,
|
||||||
|
string itemContext,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
|
||||||
|
|
||||||
|
return InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.AddItem2,
|
||||||
|
AddItem2 = new AddItem2Command
|
||||||
|
{
|
||||||
|
ServerHandle = serverHandle,
|
||||||
|
ItemDefinition = itemDefinition,
|
||||||
|
ItemContext = itemContext ?? string.Empty,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AdviseAsync(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
MxCommandReply reply = await AdviseRawAsync(serverHandle, itemHandle, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MxCommandReply> AdviseRawAsync(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.Advise,
|
||||||
|
Advise = new AdviseCommand
|
||||||
|
{
|
||||||
|
ServerHandle = serverHandle,
|
||||||
|
ItemHandle = itemHandle,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UnAdviseAsync(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
MxCommandReply reply = await UnAdviseRawAsync(serverHandle, itemHandle, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MxCommandReply> UnAdviseRawAsync(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.UnAdvise,
|
||||||
|
UnAdvise = new UnAdviseCommand
|
||||||
|
{
|
||||||
|
ServerHandle = serverHandle,
|
||||||
|
ItemHandle = itemHandle,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveItemAsync(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
MxCommandReply reply = await RemoveItemRawAsync(serverHandle, itemHandle, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MxCommandReply> RemoveItemRawAsync(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.RemoveItem,
|
||||||
|
RemoveItem = new RemoveItemCommand
|
||||||
|
{
|
||||||
|
ServerHandle = serverHandle,
|
||||||
|
ItemHandle = itemHandle,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(
|
||||||
|
int serverHandle,
|
||||||
|
IReadOnlyList<string> tagAddresses,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(tagAddresses);
|
||||||
|
|
||||||
|
AddItemBulkCommand command = new() { ServerHandle = serverHandle };
|
||||||
|
command.TagAddresses.Add(tagAddresses);
|
||||||
|
|
||||||
|
MxCommandReply reply = await InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.AddItemBulk,
|
||||||
|
AddItemBulk = command,
|
||||||
|
},
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
return reply.AddItemBulk?.Results.ToArray() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(
|
||||||
|
int serverHandle,
|
||||||
|
IReadOnlyList<int> itemHandles,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(itemHandles);
|
||||||
|
|
||||||
|
AdviseItemBulkCommand command = new() { ServerHandle = serverHandle };
|
||||||
|
command.ItemHandles.Add(itemHandles);
|
||||||
|
|
||||||
|
MxCommandReply reply = await InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.AdviseItemBulk,
|
||||||
|
AdviseItemBulk = command,
|
||||||
|
},
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
return reply.AdviseItemBulk?.Results.ToArray() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(
|
||||||
|
int serverHandle,
|
||||||
|
IReadOnlyList<int> itemHandles,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(itemHandles);
|
||||||
|
|
||||||
|
RemoveItemBulkCommand command = new() { ServerHandle = serverHandle };
|
||||||
|
command.ItemHandles.Add(itemHandles);
|
||||||
|
|
||||||
|
MxCommandReply reply = await InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.RemoveItemBulk,
|
||||||
|
RemoveItemBulk = command,
|
||||||
|
},
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
return reply.RemoveItemBulk?.Results.ToArray() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(
|
||||||
|
int serverHandle,
|
||||||
|
IReadOnlyList<int> itemHandles,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(itemHandles);
|
||||||
|
|
||||||
|
UnAdviseItemBulkCommand command = new() { ServerHandle = serverHandle };
|
||||||
|
command.ItemHandles.Add(itemHandles);
|
||||||
|
|
||||||
|
MxCommandReply reply = await InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.UnAdviseItemBulk,
|
||||||
|
UnAdviseItemBulk = command,
|
||||||
|
},
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
return reply.UnAdviseItemBulk?.Results.ToArray() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||||
|
int serverHandle,
|
||||||
|
IReadOnlyList<string> tagAddresses,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(tagAddresses);
|
||||||
|
|
||||||
|
SubscribeBulkCommand command = new() { ServerHandle = serverHandle };
|
||||||
|
command.TagAddresses.Add(tagAddresses);
|
||||||
|
|
||||||
|
MxCommandReply reply = await InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.SubscribeBulk,
|
||||||
|
SubscribeBulk = command,
|
||||||
|
},
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
return reply.SubscribeBulk?.Results.ToArray() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(
|
||||||
|
int serverHandle,
|
||||||
|
IReadOnlyList<int> itemHandles,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(itemHandles);
|
||||||
|
|
||||||
|
UnsubscribeBulkCommand command = new() { ServerHandle = serverHandle };
|
||||||
|
command.ItemHandles.Add(itemHandles);
|
||||||
|
|
||||||
|
MxCommandReply reply = await InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.UnsubscribeBulk,
|
||||||
|
UnsubscribeBulk = command,
|
||||||
|
},
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
return reply.UnsubscribeBulk?.Results.ToArray() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WriteAsync(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
MxValue value,
|
||||||
|
int userId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
MxCommandReply reply = await WriteRawAsync(serverHandle, itemHandle, value, userId, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MxCommandReply> WriteRawAsync(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
MxValue value,
|
||||||
|
int userId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
|
||||||
|
return InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.Write,
|
||||||
|
Write = new WriteCommand
|
||||||
|
{
|
||||||
|
ServerHandle = serverHandle,
|
||||||
|
ItemHandle = itemHandle,
|
||||||
|
Value = value,
|
||||||
|
UserId = userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Write2Async(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
MxValue value,
|
||||||
|
MxValue timestampValue,
|
||||||
|
int userId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
MxCommandReply reply = await Write2RawAsync(
|
||||||
|
serverHandle,
|
||||||
|
itemHandle,
|
||||||
|
value,
|
||||||
|
timestampValue,
|
||||||
|
userId,
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MxCommandReply> Write2RawAsync(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
MxValue value,
|
||||||
|
MxValue timestampValue,
|
||||||
|
int userId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
ArgumentNullException.ThrowIfNull(timestampValue);
|
||||||
|
|
||||||
|
return InvokeCommandAsync(
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.Write2,
|
||||||
|
Write2 = new Write2Command
|
||||||
|
{
|
||||||
|
ServerHandle = serverHandle,
|
||||||
|
ItemHandle = itemHandle,
|
||||||
|
Value = value,
|
||||||
|
TimestampValue = timestampValue,
|
||||||
|
UserId = userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MxCommandReply> InvokeAsync(
|
||||||
|
MxCommandRequest request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
return _client.InvokeAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
|
ulong afterWorkerSequence = 0,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return _client.StreamEventsAsync(
|
||||||
|
new StreamEventsRequest
|
||||||
|
{
|
||||||
|
SessionId = SessionId,
|
||||||
|
AfterWorkerSequence = afterWorkerSequence,
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await CloseAsync().ConfigureAwait(false);
|
||||||
|
_closeLock.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<MxCommandReply> InvokeCommandAsync(
|
||||||
|
MxCommand command,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return _client.InvokeAsync(
|
||||||
|
new MxCommandRequest
|
||||||
|
{
|
||||||
|
SessionId = SessionId,
|
||||||
|
ClientCorrelationId = Guid.NewGuid().ToString("N"),
|
||||||
|
Command = command,
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
public sealed class MxGatewaySessionException : MxGatewayException
|
||||||
|
{
|
||||||
|
public MxGatewaySessionException(
|
||||||
|
string message,
|
||||||
|
string? sessionId = null,
|
||||||
|
string? correlationId = null,
|
||||||
|
ProtocolStatus? protocolStatus = null,
|
||||||
|
int? hResult = null,
|
||||||
|
IReadOnlyList<MxStatusProxy>? statuses = null,
|
||||||
|
Exception? innerException = null)
|
||||||
|
: base(
|
||||||
|
message,
|
||||||
|
sessionId,
|
||||||
|
correlationId,
|
||||||
|
protocolStatus,
|
||||||
|
hResult,
|
||||||
|
statuses ?? [],
|
||||||
|
innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
public sealed class MxGatewayWorkerException : MxGatewayException
|
||||||
|
{
|
||||||
|
public MxGatewayWorkerException(
|
||||||
|
string message,
|
||||||
|
string? sessionId = null,
|
||||||
|
string? correlationId = null,
|
||||||
|
ProtocolStatus? protocolStatus = null,
|
||||||
|
int? hResult = null,
|
||||||
|
IReadOnlyList<MxStatusProxy>? statuses = null,
|
||||||
|
Exception? innerException = null)
|
||||||
|
: base(
|
||||||
|
message,
|
||||||
|
sessionId,
|
||||||
|
correlationId,
|
||||||
|
protocolStatus,
|
||||||
|
hResult,
|
||||||
|
statuses ?? [],
|
||||||
|
innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
public static class MxStatusProxyExtensions
|
||||||
|
{
|
||||||
|
public static bool IsSuccess(this MxStatusProxy status)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(status);
|
||||||
|
|
||||||
|
return status.Success != 0
|
||||||
|
&& status.Category is MxStatusCategory.Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToDiagnosticSummary(this MxStatusProxy status)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(status);
|
||||||
|
|
||||||
|
string diagnosticText = string.IsNullOrWhiteSpace(status.DiagnosticText)
|
||||||
|
? "no diagnostic text"
|
||||||
|
: status.DiagnosticText;
|
||||||
|
|
||||||
|
return $"{status.Category} by {status.DetectedBy}; detail={status.Detail}; {diagnosticText}";
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-100
@@ -1,8 +1,8 @@
|
|||||||
using Google.Protobuf;
|
using Google.Protobuf;
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates and projects gateway MXAccess values without hiding the raw
|
/// Creates and projects gateway MXAccess values without hiding the raw
|
||||||
@@ -10,11 +10,6 @@ namespace ZB.MOM.WW.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>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Boolean</c>.</returns>
|
|
||||||
public static MxValue ToMxValue(this bool value)
|
public static MxValue ToMxValue(this bool value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -25,11 +20,6 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a 32-bit integer value to an MxValue with MxDataType.Integer.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">32-bit integer value to wrap.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Integer</c>.</returns>
|
|
||||||
public static MxValue ToMxValue(this int value)
|
public static MxValue ToMxValue(this int value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -40,11 +30,6 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a 64-bit integer value to an MxValue with MxDataType.Integer.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">64-bit integer value to wrap.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Integer</c>.</returns>
|
|
||||||
public static MxValue ToMxValue(this long value)
|
public static MxValue ToMxValue(this long value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -55,11 +40,6 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a single-precision floating-point value to an MxValue with MxDataType.Float.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">Single-precision floating-point value to wrap.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Float</c>.</returns>
|
|
||||||
public static MxValue ToMxValue(this float value)
|
public static MxValue ToMxValue(this float value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -70,11 +50,6 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a double-precision floating-point value to an MxValue with MxDataType.Double.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">Double-precision floating-point value to wrap.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Double</c>.</returns>
|
|
||||||
public static MxValue ToMxValue(this double value)
|
public static MxValue ToMxValue(this double value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -85,11 +60,6 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a string value to an MxValue with MxDataType.String.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">String value to wrap.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.String</c>.</returns>
|
|
||||||
public static MxValue ToMxValue(this string value)
|
public static MxValue ToMxValue(this string value)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
@@ -102,11 +72,6 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a DateTimeOffset value to an MxValue with MxDataType.Time.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">DateTimeOffset value to wrap.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Time</c>.</returns>
|
|
||||||
public static MxValue ToMxValue(this DateTimeOffset value)
|
public static MxValue ToMxValue(this DateTimeOffset value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -117,11 +82,6 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a DateTime value to an MxValue with MxDataType.Time.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">DateTime value to wrap.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Time</c>.</returns>
|
|
||||||
public static MxValue ToMxValue(this DateTime value)
|
public static MxValue ToMxValue(this DateTime value)
|
||||||
{
|
{
|
||||||
return new DateTimeOffset(
|
return new DateTimeOffset(
|
||||||
@@ -131,11 +91,6 @@ 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>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Boolean</c> and an array payload.</returns>
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<bool> values)
|
public static MxValue ToMxValue(this IReadOnlyList<bool> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -150,11 +105,6 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a 32-bit integer array to an MxValue with MxDataType.Integer.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="values">Array of 32-bit integer values to wrap.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Integer</c> and an array payload.</returns>
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<int> values)
|
public static MxValue ToMxValue(this IReadOnlyList<int> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -169,11 +119,6 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a 64-bit integer array to an MxValue with MxDataType.Integer.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="values">Array of 64-bit integer values to wrap.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Integer</c> and an array payload.</returns>
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<long> values)
|
public static MxValue ToMxValue(this IReadOnlyList<long> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -188,11 +133,6 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a single-precision floating-point array to an MxValue with MxDataType.Float.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="values">Array of single-precision floating-point values to wrap.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Float</c> and an array payload.</returns>
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<float> values)
|
public static MxValue ToMxValue(this IReadOnlyList<float> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -207,11 +147,6 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a double-precision floating-point array to an MxValue with MxDataType.Double.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="values">Array of double-precision floating-point values to wrap.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Double</c> and an array payload.</returns>
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<double> values)
|
public static MxValue ToMxValue(this IReadOnlyList<double> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -226,11 +161,6 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a string array to an MxValue with MxDataType.String.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="values">Array of string values to wrap.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.String</c> and an array payload.</returns>
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<string> values)
|
public static MxValue ToMxValue(this IReadOnlyList<string> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -245,11 +175,6 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a DateTimeOffset array to an MxValue with MxDataType.Time.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="values">Array of DateTimeOffset values to wrap.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Time</c> and an array payload.</returns>
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<DateTimeOffset> values)
|
public static MxValue ToMxValue(this IReadOnlyList<DateTimeOffset> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -264,11 +189,6 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the projection kind (field name) of the given MxValue's current oneof value.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The MxValue whose oneof projection kind is returned.</param>
|
|
||||||
/// <returns>The JSON field name of the active oneof case, or <c>"nullValue"</c>/<c>"unspecified"</c> for null/unset values.</returns>
|
|
||||||
public static string GetProjectionKind(this MxValue value)
|
public static string GetProjectionKind(this MxValue value)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
@@ -288,11 +208,6 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts an MxValue to a CLR object; returns the boxed value or null for null MxValues.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The MxValue to convert.</param>
|
|
||||||
/// <returns>The boxed CLR value, or null if the MxValue represents a null.</returns>
|
|
||||||
public static object? ToClrValue(this MxValue value)
|
public static object? ToClrValue(this MxValue value)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
@@ -312,11 +227,6 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts an MxArray to a CLR array; returns null if the array does not have a known element type.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="array">The MxArray to convert.</param>
|
|
||||||
/// <returns>A CLR array of the appropriate element type, or null for unknown element types.</returns>
|
|
||||||
public static object? ToClrArrayValue(this MxArray array)
|
public static object? ToClrArrayValue(this MxArray array)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(array);
|
ArgumentNullException.ThrowIfNull(array);
|
||||||
@@ -339,14 +249,6 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates an MxValue with MxDataType.Unknown from raw byte data, variant type, and diagnostic info.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">Raw byte data representing the value.</param>
|
|
||||||
/// <param name="variantType">Variant type string (e.g., "VT_BSTR").</param>
|
|
||||||
/// <param name="rawDiagnostic">Diagnostic string describing the raw value.</param>
|
|
||||||
/// <param name="rawDataType">Optional MXAccess data type override.</param>
|
|
||||||
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Unknown</c> and the raw byte payload.</returns>
|
|
||||||
public static MxValue ToRawMxValue(
|
public static MxValue ToRawMxValue(
|
||||||
byte[] value,
|
byte[] value,
|
||||||
string variantType,
|
string variantType,
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("MxGateway.Client.Tests")]
|
||||||
+29
-122
@@ -7,11 +7,11 @@ CLI, and unit tests.
|
|||||||
|
|
||||||
| Project | Purpose |
|
| Project | Purpose |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| `ZB.MOM.WW.MxGateway.Client` | .NET 10 library entry point, raw gRPC calls, and session helpers. |
|
| `MxGateway.Client` | .NET 10 library entry point, raw gRPC calls, and session helpers. |
|
||||||
| `ZB.MOM.WW.MxGateway.Client.Cli` | Test CLI for smoke and diagnostic commands. |
|
| `MxGateway.Client.Cli` | Test CLI for smoke and diagnostic commands. |
|
||||||
| `ZB.MOM.WW.MxGateway.Client.Tests` | Unit tests for client options, generated contract wiring, auth metadata, session helpers, cancellation, and event streaming. |
|
| `MxGateway.Client.Tests` | Unit tests for client options, generated contract wiring, auth metadata, session helpers, cancellation, and event streaming. |
|
||||||
|
|
||||||
The projects reference `src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj` so
|
The projects reference `src/MxGateway.Contracts/MxGateway.Contracts.csproj` so
|
||||||
the client compiles against the same generated protobuf and gRPC types as the
|
the client compiles against the same generated protobuf and gRPC types as the
|
||||||
gateway. `clients/dotnet/generated` remains reserved for generator output if a
|
gateway. `clients/dotnet/generated` remains reserved for generator output if a
|
||||||
future client build switches to client-local `Grpc.Tools` generation.
|
future client build switches to client-local `Grpc.Tools` generation.
|
||||||
@@ -19,8 +19,8 @@ future client build switches to client-local `Grpc.Tools` generation.
|
|||||||
## Build And Test
|
## Build And Test
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx
|
dotnet build clients/dotnet/MxGateway.Client.sln
|
||||||
dotnet test clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx --no-build
|
dotnet test clients/dotnet/MxGateway.Client.sln --no-build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Packaging
|
## Packaging
|
||||||
@@ -29,8 +29,8 @@ Create local library and CLI artifacts from the repository root:
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$dotnetPackageOutput = Join-Path (Get-Location) 'artifacts/clients/dotnet'
|
$dotnetPackageOutput = Join-Path (Get-Location) 'artifacts/clients/dotnet'
|
||||||
dotnet pack clients/dotnet/ZB.MOM.WW.MxGateway.Client/ZB.MOM.WW.MxGateway.Client.csproj -c Release -p:PackageOutputPath="$dotnetPackageOutput"
|
dotnet pack clients/dotnet/MxGateway.Client/MxGateway.Client.csproj -c Release -p:PackageOutputPath="$dotnetPackageOutput"
|
||||||
dotnet publish clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/ZB.MOM.WW.MxGateway.Client.Cli.csproj -c Release -o artifacts/clients/dotnet/mxgw-dotnet
|
dotnet publish clients/dotnet/MxGateway.Client.Cli/MxGateway.Client.Cli.csproj -c Release -o artifacts/clients/dotnet/mxgw-dotnet
|
||||||
```
|
```
|
||||||
|
|
||||||
The library package references the shared contracts project at build time. The
|
The library package references the shared contracts project at build time. The
|
||||||
@@ -39,11 +39,11 @@ published CLI runs from `artifacts/clients/dotnet/mxgw-dotnet`.
|
|||||||
## Regenerating Protobuf Bindings
|
## Regenerating Protobuf Bindings
|
||||||
|
|
||||||
The .NET client uses the generated C# types from
|
The .NET client uses the generated C# types from
|
||||||
`src/ZB.MOM.WW.MxGateway.Contracts/Generated`. Regenerate those files through the
|
`src/MxGateway.Contracts/Generated`. Regenerate those files through the
|
||||||
contracts project:
|
contracts project:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj
|
dotnet build src/MxGateway.Contracts/MxGateway.Contracts.csproj
|
||||||
```
|
```
|
||||||
|
|
||||||
## Client Usage
|
## Client Usage
|
||||||
@@ -84,15 +84,6 @@ messages. `MxGatewaySession.OpenSessionReply` keeps the raw session-open reply
|
|||||||
available, and command helpers have `*RawAsync` variants when callers need the
|
available, and command helpers have `*RawAsync` variants when callers need the
|
||||||
complete `MxCommandReply`.
|
complete `MxCommandReply`.
|
||||||
|
|
||||||
For alarms, the client exposes `QueryActiveAlarmsAsync` (one-shot snapshot of
|
|
||||||
the active alarms the gateway's central monitor currently holds),
|
|
||||||
`StreamAlarmsAsync` (server-streaming feed of alarm-state-change messages
|
|
||||||
keyed by the same monitor), and `AcknowledgeAlarmAsync` (ack by alarm
|
|
||||||
reference, optional comment, ack target). All three accept a cancellation
|
|
||||||
token and pass through the `MxGateway:Alarms` configuration on the
|
|
||||||
server — when alarms are disabled, the gateway returns an empty list / empty
|
|
||||||
stream rather than failing.
|
|
||||||
|
|
||||||
`MxGatewaySession.CloseAsync` is explicit and idempotent. Repeated calls return
|
`MxGatewaySession.CloseAsync` is explicit and idempotent. Repeated calls return
|
||||||
the first `CloseSessionReply` instead of sending another close request.
|
the first `CloseSessionReply` instead of sending another close request.
|
||||||
|
|
||||||
@@ -126,17 +117,15 @@ reply.
|
|||||||
The test CLI supports deterministic JSON output for automation:
|
The test CLI supports deterministic JSON output for automation:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- version --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- version --json
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- open-session --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- open-session --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- register --session-id <id> --client-name mxgw-dotnet-cli --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- register --session-id <id> --client-name mxgw-dotnet-cli --json
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- add-item --session-id <id> --server-handle 1 --item Area001.Pump001.Speed --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- add-item --session-id <id> --server-handle 1 --item Area001.Pump001.Speed --json
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- advise --session-id <id> --server-handle 1 --item-handle 1 --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- advise --session-id <id> --server-handle 1 --item-handle 1 --json
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --json
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- write2 --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --timestamp 2026-01-01T00:00:00Z --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- write2 --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --timestamp 2026-01-01T00:00:00Z --json
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- stream-events --session-id <id> --max-events 1 --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- stream-events --session-id <id> --max-events 1 --json
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- stream-alarms --filter-prefix Area001 --max-events 1 --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- smoke --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --item Area001.Pump001.Speed --json
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- acknowledge-alarm --reference "\\Galaxy\Area001.Pump001.PumpFault" --comment "ack from cli" --operator operator1 --json
|
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --item Area001.Pump001.Speed --json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`smoke` opens a session, registers a client, adds one item, advises it,
|
`smoke` opens a session, registers a client, adds one item, advises it,
|
||||||
@@ -191,59 +180,11 @@ IReadOnlyList<GalaxyObject> pumps = await repository.DiscoverHierarchyAsync(
|
|||||||
The CLI exposes the same operations:
|
The CLI exposes the same operations:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-test-connection --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- galaxy-test-connection --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-last-deploy --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- galaxy-last-deploy --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-discover --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- galaxy-discover --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY
|
||||||
```
|
```
|
||||||
|
|
||||||
### Browsing lazily
|
|
||||||
|
|
||||||
For UI trees or OPC UA bridges, use `BrowseChildrenAsync` to walk one level at a
|
|
||||||
time instead of paging the full hierarchy. Pass an empty request for root objects;
|
|
||||||
subsequent calls supply `ParentGobjectId`, `ParentTagName`, or
|
|
||||||
`ParentContainedPath`. Each child's `ChildHasChildren[i]` tells you whether to
|
|
||||||
draw an expand triangle. Filter fields match `DiscoverHierarchy`. See
|
|
||||||
[Galaxy Repository](../../docs/GalaxyRepository.md#browsechildren) for full
|
|
||||||
request and filter semantics.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
BrowseChildrenReply roots = await repository.BrowseChildrenAsync(
|
|
||||||
new BrowseChildrenRequest());
|
|
||||||
|
|
||||||
for (int i = 0; i < roots.Children.Count; i++)
|
|
||||||
{
|
|
||||||
GalaxyObject child = roots.Children[i];
|
|
||||||
bool hasChildren = roots.ChildHasChildren[i];
|
|
||||||
Console.WriteLine($"{child.TagName} expand={hasChildren}");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### High-level walker
|
|
||||||
|
|
||||||
For UI trees, the client provides a `LazyBrowseNode` walker that handles
|
|
||||||
sibling pagination and the `child_has_children` hint for you:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
await using GalaxyRepositoryClient repository = GalaxyRepositoryClient.Create(
|
|
||||||
new MxGatewayClientOptions { Endpoint = new Uri("http://localhost:5000"), ApiKey = apiKey });
|
|
||||||
IReadOnlyList<LazyBrowseNode> roots = await repository.BrowseAsync();
|
|
||||||
foreach (LazyBrowseNode root in roots)
|
|
||||||
{
|
|
||||||
if (root.HasChildrenHint)
|
|
||||||
{
|
|
||||||
await root.ExpandAsync();
|
|
||||||
}
|
|
||||||
foreach (LazyBrowseNode child in root.Children)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"{child.Object.TagName} ({(child.HasChildrenHint ? "has children" : "leaf")})");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`ExpandAsync` is idempotent — calling it twice fires only one RPC,
|
|
||||||
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
|
|
||||||
`BrowseAsync` again from the root.
|
|
||||||
|
|
||||||
### Watching deploy events
|
### Watching deploy events
|
||||||
|
|
||||||
`WatchDeployEventsAsync` opens the `WatchDeployEvents` server-streaming RPC. The
|
`WatchDeployEventsAsync` opens the `WatchDeployEvents` server-streaming RPC. The
|
||||||
@@ -276,28 +217,17 @@ await foreach (DeployEvent evt in repository.WatchDeployEventsAsync(
|
|||||||
The CLI counterpart streams events until Ctrl+C (or `--max-events`):
|
The CLI counterpart streams events until Ctrl+C (or `--max-events`):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- galaxy-watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --last-seen-deploy-time 2026-04-28T14:30:00Z --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- galaxy-watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --last-seen-deploy-time 2026-04-28T14:30:00Z --json
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --max-events 5 --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- galaxy-watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --max-events 5 --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Use TLS options for a secured gateway:
|
Use TLS options for a secured gateway:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint https://ZB.MOM.WW.MxGateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name ZB.MOM.WW.MxGateway.example.local --api-key-env MXGATEWAY_API_KEY --item Area001.Pump001.Speed --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- smoke --endpoint https://mxgateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item Area001.Pump001.Speed --json
|
||||||
```
|
```
|
||||||
|
|
||||||
### TLS trust
|
|
||||||
|
|
||||||
The gateway can auto-generate its own self-signed certificate (it has no PKI), so
|
|
||||||
the client is **lenient by default**: a TLS connection (`UseTls` / `--tls`) with
|
|
||||||
no pinned CA accepts whatever certificate the gateway presents. To verify
|
|
||||||
instead, pin a CA with `CaCertificatePath` / `--ca-file` (this path also enforces
|
|
||||||
the certificate hostname/SAN match), or set `RequireCertificateValidation` to
|
|
||||||
force OS/system-trust verification without pinning. Use `ServerNameOverride` /
|
|
||||||
`--server-name` when the dialed host differs from the certificate SAN. See
|
|
||||||
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).
|
|
||||||
|
|
||||||
## Integration Checks
|
## Integration Checks
|
||||||
|
|
||||||
Run live checks only when a gateway and MXAccess-backed worker are available:
|
Run live checks only when a gateway and MXAccess-backed worker are available:
|
||||||
@@ -307,34 +237,11 @@ $env:MXGATEWAY_INTEGRATION = '1'
|
|||||||
$env:MXGATEWAY_ENDPOINT = 'http://localhost:5000'
|
$env:MXGATEWAY_ENDPOINT = 'http://localhost:5000'
|
||||||
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
|
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
|
||||||
$env:MXGATEWAY_TEST_ITEM = 'Area001.Pump001.Speed'
|
$env:MXGATEWAY_TEST_ITEM = 'Area001.Pump001.Speed'
|
||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint $env:MXGATEWAY_ENDPOINT --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- smoke --endpoint $env:MXGATEWAY_ENDPOINT --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installing as a NuGet Package
|
|
||||||
|
|
||||||
The client publishes to the internal Gitea NuGet feed at
|
|
||||||
`https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json`.
|
|
||||||
|
|
||||||
Add the feed once:
|
|
||||||
|
|
||||||
````bash
|
|
||||||
dotnet nuget add source https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json \
|
|
||||||
--name dohertj2-gitea \
|
|
||||||
--username <gitea-username> \
|
|
||||||
--password <gitea-token-or-password> \
|
|
||||||
--store-password-in-clear-text
|
|
||||||
````
|
|
||||||
|
|
||||||
Then add the package to your project:
|
|
||||||
|
|
||||||
````bash
|
|
||||||
dotnet add package ZB.MOM.WW.MxGateway.Client --version 0.1.0
|
|
||||||
````
|
|
||||||
|
|
||||||
The `ZB.MOM.WW.MxGateway.Contracts` package is pulled in transitively.
|
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
||||||
- [.NET Client Detailed Design](./DotnetClientDesign.md)
|
- [.NET Client Detailed Design](../../docs/clients-dotnet-csharp-design.md)
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client.Cli;
|
|
||||||
|
|
||||||
public interface IMxGatewayCliClient : IAsyncDisposable
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Opens a new gateway session.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">Session open request.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
||||||
/// <returns>The session open reply.</returns>
|
|
||||||
Task<OpenSessionReply> OpenSessionAsync(
|
|
||||||
OpenSessionRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Closes an open gateway session.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">Session close request.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
||||||
/// <returns>The session close reply.</returns>
|
|
||||||
Task<CloseSessionReply> CloseSessionAsync(
|
|
||||||
CloseSessionRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invokes an MXAccess command on the session.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The command request.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
||||||
/// <returns>The command reply.</returns>
|
|
||||||
Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Streams events from the gateway session.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The stream events request.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
||||||
/// <returns>An async enumerable of events.</returns>
|
|
||||||
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Acknowledges an active MXAccess alarm condition through the gateway.
|
|
||||||
/// </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>
|
|
||||||
Task<AcknowledgeAlarmReply> AcknowledgeAlarmAsync(
|
|
||||||
AcknowledgeAlarmRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attaches to the gateway's central alarm feed — the current active-alarm
|
|
||||||
/// snapshot followed by live transitions.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The stream request, optionally scoped by alarm-reference prefix.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
||||||
/// <returns>An async enumerable of alarm feed messages.</returns>
|
|
||||||
IAsyncEnumerable<AlarmFeedMessage> StreamAlarmsAsync(
|
|
||||||
StreamAlarmsRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests connection to the Galaxy Repository.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The connection test request.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
||||||
/// <returns>The connection test reply.</returns>
|
|
||||||
Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
|
||||||
TestConnectionRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the last deployment time from the Galaxy Repository.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The last deploy time request.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
||||||
/// <returns>The last deploy time reply.</returns>
|
|
||||||
Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
|
||||||
GetLastDeployTimeRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Discovers the Galaxy Repository hierarchy.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The discover hierarchy request.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
||||||
/// <returns>The discover hierarchy reply.</returns>
|
|
||||||
Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
|
||||||
DiscoverHierarchyRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Watches for deployment events from the Galaxy Repository.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The watch deploy events request.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
|
||||||
/// <returns>An async enumerable of deployment events.</returns>
|
|
||||||
IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
|
||||||
WatchDeployEventsRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
namespace ZB.MOM.WW.MxGateway.Client.Cli;
|
|
||||||
|
|
||||||
/// <summary>Utility to redact API keys from error messages for safe output.</summary>
|
|
||||||
internal static class MxGatewayCliSecretRedactor
|
|
||||||
{
|
|
||||||
/// <summary>Replaces occurrences of the API key in the value with a redacted placeholder.</summary>
|
|
||||||
/// <param name="value">The message text to redact.</param>
|
|
||||||
/// <param name="apiKey">The API key to remove; no redaction if null or empty.</param>
|
|
||||||
/// <returns>The message text with any API key occurrence replaced by <c>[redacted]</c>.</returns>
|
|
||||||
public static string Redact(string value, string? apiKey)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(apiKey))
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Replace(apiKey, "[redacted]", StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using Grpc.Core;
|
|
||||||
using Grpc.Net.Client;
|
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client.Tests;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Live smoke tests for the BrowseChildren RPC. Skipped by default; set
|
|
||||||
/// MXGATEWAY_API_KEY and MXGATEWAY_ENDPOINT to run against a real gateway.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class BrowseChildrenSmokeTests
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that BrowseChildren returns a non-zero cache sequence and
|
|
||||||
/// a consistent children/child-has-children count from a live gateway.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact(Skip = "Set MXGATEWAY_API_KEY and MXGATEWAY_ENDPOINT to enable.")]
|
|
||||||
public async Task BrowseChildren_LiveGateway_ReturnsRootsWithCacheSequence()
|
|
||||||
{
|
|
||||||
string? apiKey = Environment.GetEnvironmentVariable("MXGATEWAY_API_KEY");
|
|
||||||
string endpoint = Environment.GetEnvironmentVariable("MXGATEWAY_ENDPOINT") ?? "http://localhost:5120";
|
|
||||||
|
|
||||||
Assert.False(string.IsNullOrEmpty(apiKey), "MXGATEWAY_API_KEY must be set.");
|
|
||||||
|
|
||||||
using GrpcChannel channel = GrpcChannel.ForAddress(endpoint);
|
|
||||||
GalaxyRepository.GalaxyRepositoryClient client = new(channel);
|
|
||||||
|
|
||||||
Metadata headers = new() { { "authorization", $"Bearer {apiKey}" } };
|
|
||||||
BrowseChildrenReply reply = await client.BrowseChildrenAsync(new BrowseChildrenRequest(), headers);
|
|
||||||
|
|
||||||
Assert.True(reply.CacheSequence > 0UL);
|
|
||||||
Assert.Equal(reply.Children.Count, reply.ChildHasChildren.Count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
using Grpc.Core;
|
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client.Tests;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fake implementation of IMxGatewayClientTransport for testing.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMxGatewayClientTransport
|
|
||||||
{
|
|
||||||
private readonly Queue<MxCommandReply> _invokeReplies = new();
|
|
||||||
private readonly List<MxEvent> _events = [];
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public MxGatewayClientOptions Options { get; } = options;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public MxAccessGateway.MxAccessGatewayClient? RawClient => null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the list of captured OpenSessionAsync calls.
|
|
||||||
/// </summary>
|
|
||||||
public List<(OpenSessionRequest Request, CallOptions CallOptions)> OpenSessionCalls { get; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the list of captured CloseSessionAsync calls.
|
|
||||||
/// </summary>
|
|
||||||
public List<(CloseSessionRequest Request, CallOptions CallOptions)> CloseSessionCalls { get; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the list of captured InvokeAsync calls.
|
|
||||||
/// </summary>
|
|
||||||
public List<(MxCommandRequest Request, CallOptions CallOptions)> InvokeCalls { get; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the list of captured StreamEventsAsync calls.
|
|
||||||
/// </summary>
|
|
||||||
public List<(StreamEventsRequest Request, CallOptions CallOptions)> StreamEventsCalls { get; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the list of captured AcknowledgeAlarmAsync calls.
|
|
||||||
/// </summary>
|
|
||||||
public List<(AcknowledgeAlarmRequest Request, CallOptions CallOptions)> AcknowledgeAlarmCalls { get; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the list of captured QueryActiveAlarmsAsync calls.
|
|
||||||
/// </summary>
|
|
||||||
public List<(QueryActiveAlarmsRequest Request, CallOptions CallOptions)> QueryActiveAlarmsCalls { get; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the list of captured StreamAlarmsAsync calls.
|
|
||||||
/// </summary>
|
|
||||||
public List<(StreamAlarmsRequest Request, CallOptions CallOptions)> StreamAlarmsCalls { 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 = [];
|
|
||||||
private readonly List<AlarmFeedMessage> _alarmFeedMessages = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the reply to return from OpenSessionAsync.
|
|
||||||
/// </summary>
|
|
||||||
public OpenSessionReply OpenSessionReply { get; set; } = new()
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
BackendName = "mxaccess-worker",
|
|
||||||
GatewayProtocolVersion = 1,
|
|
||||||
WorkerProtocolVersion = 1,
|
|
||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the reply to return from CloseSessionAsync.
|
|
||||||
/// </summary>
|
|
||||||
public CloseSessionReply CloseSessionReply { get; set; } = new()
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
FinalState = SessionState.Closed,
|
|
||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the queue of exceptions to throw from OpenSessionAsync.
|
|
||||||
/// </summary>
|
|
||||||
public Queue<Exception> OpenSessionExceptions { get; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the queue of exceptions to throw from CloseSessionAsync.
|
|
||||||
/// </summary>
|
|
||||||
public Queue<Exception> CloseSessionExceptions { get; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the queue of exceptions to throw from InvokeAsync.
|
|
||||||
/// </summary>
|
|
||||||
public Queue<Exception> InvokeExceptions { get; } = new();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<OpenSessionReply> OpenSessionAsync(
|
|
||||||
OpenSessionRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
OpenSessionCalls.Add((request, callOptions));
|
|
||||||
if (OpenSessionExceptions.TryDequeue(out Exception? exception))
|
|
||||||
{
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(OpenSessionReply);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<CloseSessionReply> CloseSessionAsync(
|
|
||||||
CloseSessionRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
CloseSessionCalls.Add((request, callOptions));
|
|
||||||
if (CloseSessionExceptions.TryDequeue(out Exception? exception))
|
|
||||||
{
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(CloseSessionReply);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
InvokeCalls.Add((request, callOptions));
|
|
||||||
if (InvokeExceptions.TryDequeue(out Exception? exception))
|
|
||||||
{
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(_invokeReplies.Dequeue());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
StreamEventsCalls.Add((request, callOptions));
|
|
||||||
|
|
||||||
foreach (MxEvent gatewayEvent in _events)
|
|
||||||
{
|
|
||||||
callOptions.CancellationToken.ThrowIfCancellationRequested();
|
|
||||||
await Task.Yield();
|
|
||||||
yield return gatewayEvent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enqueues a reply to be returned from the next InvokeAsync call.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reply">The reply to enqueue.</param>
|
|
||||||
public void AddInvokeReply(MxCommandReply reply)
|
|
||||||
{
|
|
||||||
_invokeReplies.Enqueue(reply);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enqueues an event to be yielded from StreamEventsAsync.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="gatewayEvent">The event to enqueue.</param>
|
|
||||||
public void AddEvent(MxEvent gatewayEvent)
|
|
||||||
{
|
|
||||||
_events.Add(gatewayEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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
|
|
||||||
{
|
|
||||||
CorrelationId = request.ClientCorrelationId,
|
|
||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
||||||
Status = new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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>
|
|
||||||
/// <param name="reply">The acknowledge reply to enqueue.</param>
|
|
||||||
public void AddAcknowledgeReply(AcknowledgeAlarmReply reply)
|
|
||||||
{
|
|
||||||
_acknowledgeReplies.Enqueue(reply);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Enqueues a snapshot to be yielded from QueryActiveAlarmsAsync.</summary>
|
|
||||||
/// <param name="snapshot">The snapshot to enqueue.</param>
|
|
||||||
public void AddActiveAlarmSnapshot(ActiveAlarmSnapshot snapshot)
|
|
||||||
{
|
|
||||||
_activeAlarmSnapshots.Add(snapshot);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async IAsyncEnumerable<AlarmFeedMessage> StreamAlarmsAsync(
|
|
||||||
StreamAlarmsRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
StreamAlarmsCalls.Add((request, callOptions));
|
|
||||||
|
|
||||||
foreach (AlarmFeedMessage message in _alarmFeedMessages)
|
|
||||||
{
|
|
||||||
callOptions.CancellationToken.ThrowIfCancellationRequested();
|
|
||||||
await Task.Yield();
|
|
||||||
yield return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Enqueues an alarm feed message to be yielded from StreamAlarmsAsync.</summary>
|
|
||||||
/// <param name="message">The alarm feed message to enqueue.</param>
|
|
||||||
public void AddAlarmFeedMessage(AlarmFeedMessage message)
|
|
||||||
{
|
|
||||||
_alarmFeedMessages.Add(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
using Grpc.Core;
|
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client.Tests;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests for the <see cref="LazyBrowseNode"/> walker over the BrowseChildren RPC.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class LazyBrowseNodeTests
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that calling BrowseAsync with no parent returns the root nodes
|
|
||||||
/// from the first BrowseChildren reply and surfaces the per-child has-children hint.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
|
||||||
public async Task Browse_NoParent_ReturnsRoots()
|
|
||||||
{
|
|
||||||
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
|
||||||
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
|
||||||
children: [BuildObject(1, "Plant", isArea: true), BuildObject(2, "Other")],
|
|
||||||
childHasChildren: [true, false],
|
|
||||||
cacheSequence: 1));
|
|
||||||
await using GalaxyRepositoryClient client = CreateClient(transport);
|
|
||||||
|
|
||||||
IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();
|
|
||||||
|
|
||||||
Assert.Equal(2, roots.Count);
|
|
||||||
Assert.Equal("Plant", roots[0].Object.TagName);
|
|
||||||
Assert.True(roots[0].HasChildrenHint);
|
|
||||||
Assert.False(roots[0].IsExpanded);
|
|
||||||
Assert.Equal("Other", roots[1].Object.TagName);
|
|
||||||
Assert.False(roots[1].HasChildrenHint);
|
|
||||||
Assert.False(roots[1].IsExpanded);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that ExpandAsync populates Children and marks the node expanded after one RPC.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
|
||||||
public async Task Expand_PopulatesChildrenAndMarksExpanded()
|
|
||||||
{
|
|
||||||
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
|
||||||
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
|
||||||
children: [BuildObject(1, "Plant", isArea: true)],
|
|
||||||
childHasChildren: [true],
|
|
||||||
cacheSequence: 1));
|
|
||||||
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
|
||||||
children: [BuildObject(10, "Line1")],
|
|
||||||
childHasChildren: [false],
|
|
||||||
cacheSequence: 1));
|
|
||||||
await using GalaxyRepositoryClient client = CreateClient(transport);
|
|
||||||
|
|
||||||
IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();
|
|
||||||
await roots[0].ExpandAsync();
|
|
||||||
|
|
||||||
Assert.True(roots[0].IsExpanded);
|
|
||||||
Assert.Single(roots[0].Children);
|
|
||||||
Assert.Equal("Line1", roots[0].Children[0].Object.TagName);
|
|
||||||
Assert.Equal(2, transport.BrowseChildrenCalls.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that a second ExpandAsync call is a no-op and issues no additional RPC.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
|
||||||
public async Task Expand_CalledTwice_NoSecondRpc()
|
|
||||||
{
|
|
||||||
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
|
||||||
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
|
||||||
children: [BuildObject(1, "Plant", isArea: true)],
|
|
||||||
childHasChildren: [true],
|
|
||||||
cacheSequence: 1));
|
|
||||||
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
|
||||||
children: [BuildObject(10, "Line1")],
|
|
||||||
childHasChildren: [false],
|
|
||||||
cacheSequence: 1));
|
|
||||||
await using GalaxyRepositoryClient client = CreateClient(transport);
|
|
||||||
|
|
||||||
IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();
|
|
||||||
await roots[0].ExpandAsync();
|
|
||||||
await roots[0].ExpandAsync();
|
|
||||||
|
|
||||||
Assert.Equal(2, transport.BrowseChildrenCalls.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that an RPC failure (NotFound) during expand is wrapped in MxGatewayException.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
|
||||||
public async Task Expand_UnknownParent_ThrowsMxGatewayException()
|
|
||||||
{
|
|
||||||
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
|
||||||
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
|
||||||
children: [BuildObject(1, "Plant", isArea: true)],
|
|
||||||
childHasChildren: [true],
|
|
||||||
cacheSequence: 1));
|
|
||||||
await using GalaxyRepositoryClient client = CreateClient(transport);
|
|
||||||
|
|
||||||
IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();
|
|
||||||
|
|
||||||
// Queue the failure for the upcoming ExpandAsync call so it consumes
|
|
||||||
// the exception on its first RPC rather than the BrowseAsync above.
|
|
||||||
transport.BrowseChildrenExceptions.Enqueue(
|
|
||||||
new MxGatewayException(
|
|
||||||
"Parent not found",
|
|
||||||
new RpcException(new Status(StatusCode.NotFound, "Parent not found"))));
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<MxGatewayException>(async () => await roots[0].ExpandAsync());
|
|
||||||
Assert.False(roots[0].IsExpanded);
|
|
||||||
Assert.Empty(roots[0].Children);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that ExpandAsync drains multi-page sibling replies and forwards the page token.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
|
||||||
public async Task Expand_MultiPageSiblings_GathersAllPages()
|
|
||||||
{
|
|
||||||
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
|
||||||
// Roots
|
|
||||||
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
|
||||||
children: [BuildObject(7, "Plant", isArea: true)],
|
|
||||||
childHasChildren: [true],
|
|
||||||
cacheSequence: 1));
|
|
||||||
// First child page (2 children) with a next token
|
|
||||||
BrowseChildrenReply childPage1 = BuildReply(
|
|
||||||
children: [BuildObject(70, "ChildA"), BuildObject(71, "ChildB")],
|
|
||||||
childHasChildren: [false, false],
|
|
||||||
cacheSequence: 1);
|
|
||||||
childPage1.NextPageToken = "7:abc:2";
|
|
||||||
transport.BrowseChildrenReplies.Enqueue(childPage1);
|
|
||||||
// Second child page (1 child) with no next token
|
|
||||||
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
|
||||||
children: [BuildObject(72, "ChildC")],
|
|
||||||
childHasChildren: [false],
|
|
||||||
cacheSequence: 1));
|
|
||||||
await using GalaxyRepositoryClient client = CreateClient(transport);
|
|
||||||
|
|
||||||
IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();
|
|
||||||
await roots[0].ExpandAsync();
|
|
||||||
|
|
||||||
Assert.Equal(3, roots[0].Children.Count);
|
|
||||||
Assert.Equal(3, transport.BrowseChildrenCalls.Count);
|
|
||||||
Assert.Equal("7:abc:2", transport.BrowseChildrenCalls[2].Request.PageToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that ten concurrent ExpandAsync calls issue exactly one RPC, not ten.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
|
||||||
public async Task Expand_CalledConcurrently_OnlyFiresOneRpc()
|
|
||||||
{
|
|
||||||
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
|
||||||
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
|
||||||
children: [BuildObject(1, "Plant", isArea: true)],
|
|
||||||
childHasChildren: [true],
|
|
||||||
cacheSequence: 7));
|
|
||||||
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
|
||||||
children: [BuildObject(2, "Mixer_001")],
|
|
||||||
childHasChildren: [false],
|
|
||||||
cacheSequence: 7));
|
|
||||||
|
|
||||||
await using GalaxyRepositoryClient client = CreateClient(transport);
|
|
||||||
IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();
|
|
||||||
|
|
||||||
// Fire ten concurrent expands of the same node.
|
|
||||||
Task[] tasks = Enumerable.Range(0, 10)
|
|
||||||
.Select(_ => roots[0].ExpandAsync())
|
|
||||||
.ToArray();
|
|
||||||
await Task.WhenAll(tasks);
|
|
||||||
|
|
||||||
Assert.True(roots[0].IsExpanded);
|
|
||||||
Assert.Single(roots[0].Children);
|
|
||||||
// 1 roots fetch + exactly 1 expand fetch = 2 total
|
|
||||||
Assert.Equal(2, transport.BrowseChildrenCalls.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that BrowseChildrenOptions filter fields are forwarded to the BrowseChildren request.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
|
||||||
public async Task Browse_WithFilter_ForwardsToRequest()
|
|
||||||
{
|
|
||||||
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
|
||||||
await using GalaxyRepositoryClient client = CreateClient(transport);
|
|
||||||
|
|
||||||
await client.BrowseAsync(new BrowseChildrenOptions
|
|
||||||
{
|
|
||||||
TagNameGlob = "Mixer*",
|
|
||||||
AlarmBearingOnly = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
BrowseChildrenRequest request = Assert.Single(transport.BrowseChildrenCalls).Request;
|
|
||||||
Assert.Equal("Mixer*", request.TagNameGlob);
|
|
||||||
Assert.True(request.AlarmBearingOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GalaxyObject BuildObject(int id, string tag, bool isArea = false)
|
|
||||||
=> new() { GobjectId = id, TagName = tag, BrowseName = tag, IsArea = isArea };
|
|
||||||
|
|
||||||
private static BrowseChildrenReply BuildReply(
|
|
||||||
IReadOnlyList<GalaxyObject> children,
|
|
||||||
IReadOnlyList<bool> childHasChildren,
|
|
||||||
ulong cacheSequence)
|
|
||||||
{
|
|
||||||
BrowseChildrenReply reply = new() { TotalChildCount = children.Count, CacheSequence = cacheSequence };
|
|
||||||
reply.Children.AddRange(children);
|
|
||||||
reply.ChildHasChildren.AddRange(childHasChildren);
|
|
||||||
return reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GalaxyRepositoryClient CreateClient(FakeGalaxyRepositoryTransport transport)
|
|
||||||
=> new(transport.Options, transport);
|
|
||||||
|
|
||||||
private static FakeGalaxyRepositoryTransport CreateTransport()
|
|
||||||
=> new(new MxGatewayClientOptions
|
|
||||||
{
|
|
||||||
Endpoint = new Uri("http://localhost:5000"),
|
|
||||||
ApiKey = "test-api-key",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
using Google.Protobuf.WellKnownTypes;
|
|
||||||
using Grpc.Core;
|
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.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
|
|
||||||
{
|
|
||||||
/// <summary>AcknowledgeAlarmAsync records request and returns reply.</summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[Fact]
|
|
||||||
public async Task AcknowledgeAlarmAsync_RecordsRequestShapeAndReturnsReply()
|
|
||||||
{
|
|
||||||
FakeGatewayTransport transport = CreateTransport();
|
|
||||||
transport.AddAcknowledgeReply(new AcknowledgeAlarmReply
|
|
||||||
{
|
|
||||||
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
|
|
||||||
{
|
|
||||||
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"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>AcknowledgeAlarmAsync honors cancellation.</summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[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
|
|
||||||
{
|
|
||||||
AlarmFullReference = "Tank01.Level.HiHi",
|
|
||||||
Comment = string.Empty,
|
|
||||||
OperatorUser = "alice",
|
|
||||||
},
|
|
||||||
cancellation.Token));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>AcknowledgeAlarmAsync maps unauthenticated RPC exception to typed exception.</summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[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
|
|
||||||
{
|
|
||||||
AlarmFullReference = "Tank01.Level.HiHi",
|
|
||||||
Comment = string.Empty,
|
|
||||||
OperatorUser = "alice",
|
|
||||||
}));
|
|
||||||
Assert.Equal(StatusCode.Unauthenticated, ex.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>QueryActiveAlarmsAsync streams enqueued snapshots.</summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>QueryActiveAlarmsAsync passes filter prefix.</summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>QueryActiveAlarmsAsync honors cancellation during enumeration.</summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
[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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,85 +0,0 @@
|
|||||||
using System.Net.Http;
|
|
||||||
using System.Net.Security;
|
|
||||||
using ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client.Tests;
|
|
||||||
|
|
||||||
public sealed class MxGatewayClientTlsHandlerTests
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that when TLS is used with no pinned CA and RequireCertificateValidation is false (default),
|
|
||||||
/// the handler installs an accept-all callback so the gateway's self-signed cert is trusted.
|
|
||||||
/// The callback must return true regardless of chain errors.
|
|
||||||
/// </summary>
|
|
||||||
[Fact]
|
|
||||||
public void Handler_SkipsVerification_WhenTlsAndNoCaPinned()
|
|
||||||
{
|
|
||||||
MxGatewayClientOptions options = new()
|
|
||||||
{
|
|
||||||
Endpoint = new Uri("https://localhost:5120"),
|
|
||||||
ApiKey = "k",
|
|
||||||
UseTls = true,
|
|
||||||
};
|
|
||||||
using SocketsHttpHandler handler = MxGatewayClient.CreateHttpHandlerForTests(options);
|
|
||||||
Assert.NotNull(handler.SslOptions.RemoteCertificateValidationCallback);
|
|
||||||
Assert.True(handler.SslOptions.RemoteCertificateValidationCallback!(null!, null!, null, SslPolicyErrors.RemoteCertificateChainErrors));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that when RequireCertificateValidation is true, the callback is left null
|
|
||||||
/// so the OS trust store performs validation.
|
|
||||||
/// </summary>
|
|
||||||
[Fact]
|
|
||||||
public void Handler_KeepsDefaultVerification_WhenRequireCertificateValidation()
|
|
||||||
{
|
|
||||||
MxGatewayClientOptions options = new()
|
|
||||||
{
|
|
||||||
Endpoint = new Uri("https://localhost:5120"),
|
|
||||||
ApiKey = "k",
|
|
||||||
UseTls = true,
|
|
||||||
RequireCertificateValidation = true,
|
|
||||||
};
|
|
||||||
using SocketsHttpHandler handler = MxGatewayClient.CreateHttpHandlerForTests(options);
|
|
||||||
Assert.Null(handler.SslOptions.RemoteCertificateValidationCallback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class GalaxyRepositoryClientTlsHandlerTests
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that when TLS is used with no pinned CA and RequireCertificateValidation is false (default),
|
|
||||||
/// the Galaxy client handler installs an accept-all callback so the gateway's self-signed cert is trusted.
|
|
||||||
/// The callback must return true regardless of chain errors.
|
|
||||||
/// </summary>
|
|
||||||
[Fact]
|
|
||||||
public void Handler_SkipsVerification_WhenTlsAndNoCaPinned()
|
|
||||||
{
|
|
||||||
MxGatewayClientOptions options = new()
|
|
||||||
{
|
|
||||||
Endpoint = new Uri("https://localhost:5120"),
|
|
||||||
ApiKey = "k",
|
|
||||||
UseTls = true,
|
|
||||||
};
|
|
||||||
using SocketsHttpHandler handler = GalaxyRepositoryClient.CreateHttpHandlerForTests(options);
|
|
||||||
Assert.NotNull(handler.SslOptions.RemoteCertificateValidationCallback);
|
|
||||||
Assert.True(handler.SslOptions.RemoteCertificateValidationCallback!(null!, null!, null, SslPolicyErrors.RemoteCertificateChainErrors));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that when RequireCertificateValidation is true, the Galaxy client callback is left null
|
|
||||||
/// so the OS trust store performs validation.
|
|
||||||
/// </summary>
|
|
||||||
[Fact]
|
|
||||||
public void Handler_KeepsDefaultVerification_WhenRequireCertificateValidation()
|
|
||||||
{
|
|
||||||
MxGatewayClientOptions options = new()
|
|
||||||
{
|
|
||||||
Endpoint = new Uri("https://localhost:5120"),
|
|
||||||
ApiKey = "k",
|
|
||||||
UseTls = true,
|
|
||||||
RequireCertificateValidation = true,
|
|
||||||
};
|
|
||||||
using SocketsHttpHandler handler = GalaxyRepositoryClient.CreateHttpHandlerForTests(options);
|
|
||||||
Assert.Null(handler.SslOptions.RemoteCertificateValidationCallback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<Solution>
|
|
||||||
<Configurations>
|
|
||||||
<Platform Name="Any CPU" />
|
|
||||||
<Platform Name="x64" />
|
|
||||||
<Platform Name="x86" />
|
|
||||||
</Configurations>
|
|
||||||
<Project Path="../../src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj" />
|
|
||||||
<Project Path="ZB.MOM.WW.MxGateway.Client.Cli/ZB.MOM.WW.MxGateway.Client.Cli.csproj" />
|
|
||||||
<Project Path="ZB.MOM.WW.MxGateway.Client.Tests/ZB.MOM.WW.MxGateway.Client.Tests.csproj" />
|
|
||||||
<Project Path="ZB.MOM.WW.MxGateway.Client/ZB.MOM.WW.MxGateway.Client.csproj" />
|
|
||||||
</Solution>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Filters and shape options for <see cref="GalaxyRepositoryClient.BrowseAsync(BrowseChildrenOptions, System.Threading.CancellationToken)"/>.
|
|
||||||
/// Mirror of <see cref="DiscoverHierarchyOptions"/> for the lazy-browse path.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class BrowseChildrenOptions
|
|
||||||
{
|
|
||||||
/// <summary>Restrict to children whose Galaxy category is in this set.</summary>
|
|
||||||
public IReadOnlyList<int> CategoryIds { get; init; } = [];
|
|
||||||
|
|
||||||
/// <summary>Restrict to children whose template chain contains any of these tokens.</summary>
|
|
||||||
public IReadOnlyList<string> TemplateChainContains { get; init; } = [];
|
|
||||||
|
|
||||||
/// <summary>Optional glob-style filter on <c>tag_name</c>.</summary>
|
|
||||||
public string? TagNameGlob { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Whether to populate each <c>GalaxyObject.Attributes</c>. Null leaves the server default.</summary>
|
|
||||||
public bool? IncludeAttributes { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Restrict to children that bear at least one alarm attribute.</summary>
|
|
||||||
public bool AlarmBearingOnly { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Restrict to children that have at least one historized attribute.</summary>
|
|
||||||
public bool HistorizedOnly { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Filters and shape options for <see cref="GalaxyRepositoryClient.DiscoverHierarchyAsync(DiscoverHierarchyOptions, System.Threading.CancellationToken)"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Hand-written ergonomic wrapper around the generated
|
|
||||||
/// <c>DiscoverHierarchyRequest</c>: lets callers express a Galaxy-browse
|
|
||||||
/// slice with .NET-friendly nullable scalars and collection initializers,
|
|
||||||
/// without touching the protobuf message's <c>oneof root</c> directly.
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class DiscoverHierarchyOptions
|
|
||||||
{
|
|
||||||
/// <summary>Restrict to the subtree rooted at this Galaxy <c>gobject_id</c>.</summary>
|
|
||||||
public int? RootGobjectId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Restrict to the subtree rooted at the object with this tag name.</summary>
|
|
||||||
public string? RootTagName { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Restrict to the subtree rooted at this <c>contained_name</c> path.</summary>
|
|
||||||
public string? RootContainedPath { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Maximum traversal depth, measured from the chosen root.</summary>
|
|
||||||
public int? MaxDepth { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Restrict to objects whose Galaxy category is in this set.</summary>
|
|
||||||
public IReadOnlyList<int> CategoryIds { get; init; } = [];
|
|
||||||
|
|
||||||
/// <summary>Restrict to objects whose template chain contains any of these tokens.</summary>
|
|
||||||
public IReadOnlyList<string> TemplateChainContains { get; init; } = [];
|
|
||||||
|
|
||||||
/// <summary>Optional glob-style filter on <c>tag_name</c>.</summary>
|
|
||||||
public string? TagNameGlob { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Whether to populate each <c>GalaxyObject.Attributes</c>. Null leaves the server default.</summary>
|
|
||||||
public bool? IncludeAttributes { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Restrict to objects that bear at least one alarm attribute.</summary>
|
|
||||||
public bool AlarmBearingOnly { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Restrict to objects that have at least one historized attribute.</summary>
|
|
||||||
public bool HistorizedOnly { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
using Grpc.Core;
|
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// gRPC implementation of IMxGatewayClientTransport.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class GrpcMxGatewayClientTransport(
|
|
||||||
MxGatewayClientOptions options,
|
|
||||||
MxAccessGateway.MxAccessGatewayClient rawClient) : IMxGatewayClientTransport
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public MxGatewayClientOptions Options { get; } = options;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the underlying gRPC client.
|
|
||||||
/// </summary>
|
|
||||||
public MxAccessGateway.MxAccessGatewayClient RawClient { get; } = rawClient;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
MxAccessGateway.MxAccessGatewayClient? IMxGatewayClientTransport.RawClient => RawClient;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<OpenSessionReply> OpenSessionAsync(
|
|
||||||
OpenSessionRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await RawClient.OpenSessionAsync(request, callOptions)
|
|
||||||
.ResponseAsync
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (RpcException exception)
|
|
||||||
{
|
|
||||||
throw MapRpcException(exception, callOptions.CancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<CloseSessionReply> CloseSessionAsync(
|
|
||||||
CloseSessionRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await RawClient.CloseSessionAsync(request, callOptions)
|
|
||||||
.ResponseAsync
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (RpcException exception)
|
|
||||||
{
|
|
||||||
throw MapRpcException(exception, callOptions.CancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await RawClient.InvokeAsync(request, callOptions)
|
|
||||||
.ResponseAsync
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (RpcException exception)
|
|
||||||
{
|
|
||||||
throw MapRpcException(exception, callOptions.CancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Streams MXAccess events from the gateway, forwarding an explicit cancellation token to the stream reader.</summary>
|
|
||||||
/// <param name="request">The stream events request.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options.</param>
|
|
||||||
/// <param name="cancellationToken">Token to cancel the streaming enumeration.</param>
|
|
||||||
/// <returns>An async enumerable of MXAccess events.</returns>
|
|
||||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
CallOptions callOptions,
|
|
||||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
CancellationToken effectiveCancellationToken = cancellationToken.CanBeCanceled
|
|
||||||
? cancellationToken
|
|
||||||
: callOptions.CancellationToken;
|
|
||||||
|
|
||||||
using AsyncServerStreamingCall<MxEvent> call = RawClient.StreamEvents(request, callOptions);
|
|
||||||
|
|
||||||
IAsyncStreamReader<MxEvent> responseStream = call.ResponseStream;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
MxEvent? gatewayEvent;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!await responseStream.MoveNext(effectiveCancellationToken).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
gatewayEvent = responseStream.Current;
|
|
||||||
}
|
|
||||||
catch (RpcException exception)
|
|
||||||
{
|
|
||||||
throw MapRpcException(exception, effectiveCancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return gatewayEvent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
IAsyncEnumerable<MxEvent> IMxGatewayClientTransport.StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
CallOptions 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Queries active alarms from the gateway, forwarding an explicit cancellation token to the stream reader.</summary>
|
|
||||||
/// <param name="request">The query active alarms request.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options.</param>
|
|
||||||
/// <param name="cancellationToken">Token to cancel the streaming enumeration.</param>
|
|
||||||
/// <returns>An async enumerable of active alarm snapshots.</returns>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Streams alarm feed messages from the gateway, forwarding an explicit cancellation token to the stream reader.</summary>
|
|
||||||
/// <param name="request">The stream alarms request.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options.</param>
|
|
||||||
/// <param name="cancellationToken">Token to cancel the streaming enumeration.</param>
|
|
||||||
/// <returns>An async enumerable of alarm feed messages.</returns>
|
|
||||||
public async IAsyncEnumerable<AlarmFeedMessage> StreamAlarmsAsync(
|
|
||||||
StreamAlarmsRequest request,
|
|
||||||
CallOptions callOptions,
|
|
||||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
CancellationToken effectiveCancellationToken = cancellationToken.CanBeCanceled
|
|
||||||
? cancellationToken
|
|
||||||
: callOptions.CancellationToken;
|
|
||||||
|
|
||||||
using AsyncServerStreamingCall<AlarmFeedMessage> call = RawClient.StreamAlarms(request, callOptions);
|
|
||||||
|
|
||||||
IAsyncStreamReader<AlarmFeedMessage> responseStream = call.ResponseStream;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
AlarmFeedMessage? message;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!await responseStream.MoveNext(effectiveCancellationToken).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message = responseStream.Current;
|
|
||||||
}
|
|
||||||
catch (RpcException exception)
|
|
||||||
{
|
|
||||||
throw MapRpcException(exception, effectiveCancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
IAsyncEnumerable<AlarmFeedMessage> IMxGatewayClientTransport.StreamAlarmsAsync(
|
|
||||||
StreamAlarmsRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
return StreamAlarmsAsync(request, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Exception MapRpcException(
|
|
||||||
RpcException exception,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (cancellationToken.IsCancellationRequested || exception.StatusCode == StatusCode.Cancelled)
|
|
||||||
{
|
|
||||||
return new OperationCanceledException(
|
|
||||||
exception.Status.Detail,
|
|
||||||
exception,
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
return exception.StatusCode switch
|
|
||||||
{
|
|
||||||
StatusCode.Unauthenticated => new MxGatewayAuthenticationException(
|
|
||||||
exception.Status.Detail,
|
|
||||||
innerException: exception),
|
|
||||||
StatusCode.PermissionDenied => new MxGatewayAuthorizationException(
|
|
||||||
exception.Status.Detail,
|
|
||||||
innerException: exception),
|
|
||||||
_ => new MxGatewayException(exception.Status.Detail, exception),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
using Grpc.Core;
|
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>Transport layer for Galaxy Repository gRPC operations.</summary>
|
|
||||||
internal interface IGalaxyRepositoryClientTransport
|
|
||||||
{
|
|
||||||
/// <summary>Gets the client options used to configure this transport.</summary>
|
|
||||||
MxGatewayClientOptions Options { get; }
|
|
||||||
|
|
||||||
/// <summary>Gets the underlying gRPC client, or <c>null</c> if not yet initialized.</summary>
|
|
||||||
GalaxyRepository.GalaxyRepositoryClient? RawClient { get; }
|
|
||||||
|
|
||||||
/// <summary>Tests the connection to the Galaxy Repository server.</summary>
|
|
||||||
/// <param name="request">The test connection request.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
|
|
||||||
/// <returns>A task that resolves to the test connection reply.</returns>
|
|
||||||
Task<TestConnectionReply> TestConnectionAsync(
|
|
||||||
TestConnectionRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
|
|
||||||
/// <summary>Gets the last deploy time from the Galaxy Repository server.</summary>
|
|
||||||
/// <param name="request">The get last deploy time request.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
|
|
||||||
/// <returns>A task that resolves to the last deploy time reply.</returns>
|
|
||||||
Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
|
||||||
GetLastDeployTimeRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
|
|
||||||
/// <summary>Discovers the object hierarchy in the Galaxy Repository.</summary>
|
|
||||||
/// <param name="request">The discover hierarchy request.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
|
|
||||||
/// <returns>A task that resolves to the hierarchy discovery reply.</returns>
|
|
||||||
Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
|
||||||
DiscoverHierarchyRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
|
|
||||||
/// <summary>Returns direct children of a parent in the Galaxy hierarchy.</summary>
|
|
||||||
/// <param name="request">The browse children request.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
|
|
||||||
/// <returns>A task that resolves to the browse children reply.</returns>
|
|
||||||
Task<BrowseChildrenReply> BrowseChildrenAsync(
|
|
||||||
BrowseChildrenRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
|
|
||||||
/// <summary>Watches for deployment events from the Galaxy Repository server.</summary>
|
|
||||||
/// <param name="request">The watch deploy events request.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
|
|
||||||
/// <returns>An async enumerable of deploy events.</returns>
|
|
||||||
IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
|
||||||
WatchDeployEventsRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using Grpc.Core;
|
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
internal interface IMxGatewayClientTransport
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the client configuration options.
|
|
||||||
/// </summary>
|
|
||||||
MxGatewayClientOptions Options { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the underlying gRPC client, if available.
|
|
||||||
/// </summary>
|
|
||||||
MxAccessGateway.MxAccessGatewayClient? RawClient { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Opens a new gateway session.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">Session open request.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options.</param>
|
|
||||||
/// <returns>The session open reply.</returns>
|
|
||||||
Task<OpenSessionReply> OpenSessionAsync(
|
|
||||||
OpenSessionRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Closes an open gateway session.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">Session close request.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options.</param>
|
|
||||||
/// <returns>The session close reply.</returns>
|
|
||||||
Task<CloseSessionReply> CloseSessionAsync(
|
|
||||||
CloseSessionRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invokes an MXAccess command on the session.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The command request.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options.</param>
|
|
||||||
/// <returns>The command reply.</returns>
|
|
||||||
Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Streams events from the session.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The stream events request.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options.</param>
|
|
||||||
/// <returns>An async enumerable of events.</returns>
|
|
||||||
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Acknowledges an active MXAccess alarm condition.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The acknowledge request.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options.</param>
|
|
||||||
/// <returns>The acknowledge reply with native MxStatus.</returns>
|
|
||||||
Task<AcknowledgeAlarmReply> AcknowledgeAlarmAsync(
|
|
||||||
AcknowledgeAlarmRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Streams a snapshot of all alarms currently in Active or ActiveAcked state — the
|
|
||||||
/// ConditionRefresh equivalent for the gateway.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The query request, optionally scoped by alarm-reference prefix.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options.</param>
|
|
||||||
/// <returns>An async enumerable of active-alarm snapshots.</returns>
|
|
||||||
IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync(
|
|
||||||
QueryActiveAlarmsRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attaches to the gateway's central alarm feed — the current active-alarm
|
|
||||||
/// snapshot followed by live transitions.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The stream request, optionally scoped by alarm-reference prefix.</param>
|
|
||||||
/// <param name="callOptions">gRPC call options.</param>
|
|
||||||
/// <returns>An async enumerable of alarm feed messages.</returns>
|
|
||||||
IAsyncEnumerable<AlarmFeedMessage> StreamAlarmsAsync(
|
|
||||||
StreamAlarmsRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// One node in a lazy-loaded Galaxy browse tree. Holds the underlying
|
|
||||||
/// <see cref="GalaxyObject"/> and exposes <see cref="ExpandAsync"/> to fetch
|
|
||||||
/// its direct children on demand. Expansion is one-shot: a second call is a
|
|
||||||
/// no-op. Pagination of large sibling sets is handled internally.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class LazyBrowseNode
|
|
||||||
{
|
|
||||||
private readonly GalaxyRepositoryClient _client;
|
|
||||||
private readonly BrowseChildrenOptions _options;
|
|
||||||
private readonly List<LazyBrowseNode> _children = [];
|
|
||||||
private readonly SemaphoreSlim _expandLock = new(1, 1);
|
|
||||||
private bool _isExpanded;
|
|
||||||
|
|
||||||
/// <summary>Initializes a new instance of <see cref="LazyBrowseNode"/>.</summary>
|
|
||||||
/// <param name="client">The repository client used to fetch children.</param>
|
|
||||||
/// <param name="object">The underlying Galaxy object for this node.</param>
|
|
||||||
/// <param name="hasChildrenHint">True when the server reports the node has at least one matching descendant.</param>
|
|
||||||
/// <param name="options">Options controlling child browse behavior.</param>
|
|
||||||
internal LazyBrowseNode(
|
|
||||||
GalaxyRepositoryClient client,
|
|
||||||
GalaxyObject @object,
|
|
||||||
bool hasChildrenHint,
|
|
||||||
BrowseChildrenOptions options)
|
|
||||||
{
|
|
||||||
_client = client;
|
|
||||||
Object = @object;
|
|
||||||
HasChildrenHint = hasChildrenHint;
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>The underlying Galaxy object for this node.</summary>
|
|
||||||
public GalaxyObject Object { get; }
|
|
||||||
|
|
||||||
/// <summary>True when the server reports this node has at least one matching descendant.</summary>
|
|
||||||
public bool HasChildrenHint { get; }
|
|
||||||
|
|
||||||
/// <summary>Direct children loaded by <see cref="ExpandAsync"/>; empty until then.</summary>
|
|
||||||
public IReadOnlyList<LazyBrowseNode> Children => _children;
|
|
||||||
|
|
||||||
/// <summary>True after the first <see cref="ExpandAsync"/> call completes.</summary>
|
|
||||||
public bool IsExpanded => _isExpanded;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fetches direct children from the gateway and populates <see cref="Children"/>.
|
|
||||||
/// Idempotent: subsequent calls are no-ops.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Thread-safe: concurrent callers see exactly one fetch; subsequent callers
|
|
||||||
/// (after the first completes) return immediately.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
public async Task ExpandAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (_isExpanded)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _expandLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_isExpanded)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string pageToken = string.Empty;
|
|
||||||
HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
|
|
||||||
do
|
|
||||||
{
|
|
||||||
BrowseChildrenRequest request = GalaxyRepositoryClient.BuildBrowseChildrenRequest(_options);
|
|
||||||
request.ParentGobjectId = Object.GobjectId;
|
|
||||||
request.PageToken = pageToken;
|
|
||||||
|
|
||||||
BrowseChildrenReply reply = await _client
|
|
||||||
.BrowseChildrenRawAsync(request, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
for (int i = 0; i < reply.Children.Count; i++)
|
|
||||||
{
|
|
||||||
bool hint = i < reply.ChildHasChildren.Count && reply.ChildHasChildren[i];
|
|
||||||
_children.Add(new LazyBrowseNode(_client, reply.Children[i], hint, _options));
|
|
||||||
}
|
|
||||||
|
|
||||||
pageToken = reply.NextPageToken;
|
|
||||||
if (!string.IsNullOrWhiteSpace(pageToken) && !seenPageTokens.Add(pageToken))
|
|
||||||
{
|
|
||||||
throw new MxGatewayException(
|
|
||||||
$"Galaxy BrowseChildren returned a repeated page token '{pageToken}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (!string.IsNullOrWhiteSpace(pageToken));
|
|
||||||
|
|
||||||
_isExpanded = true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_expandLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>Exception thrown when an MXAccess command fails with a non-zero HResult or failing status.</summary>
|
|
||||||
public sealed class MxAccessException : MxGatewayCommandException
|
|
||||||
{
|
|
||||||
/// <summary>Initializes a new instance with the given message, reply, and optional inner exception.</summary>
|
|
||||||
/// <param name="message">The error message describing the MXAccess failure.</param>
|
|
||||||
/// <param name="reply">The MxCommandReply containing the failure details (statuses, HResult, etc.).</param>
|
|
||||||
/// <param name="innerException">The underlying exception, if any.</param>
|
|
||||||
public MxAccessException(
|
|
||||||
string message,
|
|
||||||
MxCommandReply reply,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(
|
|
||||||
message,
|
|
||||||
reply.SessionId,
|
|
||||||
reply.CorrelationId,
|
|
||||||
reply.ProtocolStatus,
|
|
||||||
reply.HasHresult ? reply.Hresult : null,
|
|
||||||
reply.Statuses.ToArray(),
|
|
||||||
innerException)
|
|
||||||
{
|
|
||||||
Reply = reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Gets the underlying MxCommandReply containing full failure details.</summary>
|
|
||||||
public MxCommandReply Reply { get; }
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>Exception thrown when an API key is invalid, expired, or malformed.</summary>
|
|
||||||
public sealed class MxGatewayAuthenticationException : MxGatewayException
|
|
||||||
{
|
|
||||||
/// <summary>Initializes a new instance with the given details.</summary>
|
|
||||||
/// <param name="message">The error message describing the authentication failure.</param>
|
|
||||||
/// <param name="sessionId">The session ID, if available.</param>
|
|
||||||
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
|
||||||
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
|
||||||
/// <param name="hResult">The HResult code, if available.</param>
|
|
||||||
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
|
||||||
/// <param name="innerException">The underlying exception, if any.</param>
|
|
||||||
public MxGatewayAuthenticationException(
|
|
||||||
string message,
|
|
||||||
string? sessionId = null,
|
|
||||||
string? correlationId = null,
|
|
||||||
ProtocolStatus? protocolStatus = null,
|
|
||||||
int? hResult = null,
|
|
||||||
IReadOnlyList<MxStatusProxy>? statuses = null,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(
|
|
||||||
message,
|
|
||||||
sessionId,
|
|
||||||
correlationId,
|
|
||||||
protocolStatus,
|
|
||||||
hResult,
|
|
||||||
statuses ?? [],
|
|
||||||
innerException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>Exception thrown when the API key lacks required scopes for an operation.</summary>
|
|
||||||
public sealed class MxGatewayAuthorizationException : MxGatewayException
|
|
||||||
{
|
|
||||||
/// <summary>Initializes a new instance with the given details.</summary>
|
|
||||||
/// <param name="message">The error message describing the authorization failure.</param>
|
|
||||||
/// <param name="sessionId">The session ID, if available.</param>
|
|
||||||
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
|
||||||
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
|
||||||
/// <param name="hResult">The HResult code, if available.</param>
|
|
||||||
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
|
||||||
/// <param name="innerException">The underlying exception, if any.</param>
|
|
||||||
public MxGatewayAuthorizationException(
|
|
||||||
string message,
|
|
||||||
string? sessionId = null,
|
|
||||||
string? correlationId = null,
|
|
||||||
ProtocolStatus? protocolStatus = null,
|
|
||||||
int? hResult = null,
|
|
||||||
IReadOnlyList<MxStatusProxy>? statuses = null,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(
|
|
||||||
message,
|
|
||||||
sessionId,
|
|
||||||
correlationId,
|
|
||||||
protocolStatus,
|
|
||||||
hResult,
|
|
||||||
statuses ?? [],
|
|
||||||
innerException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>Exception thrown when a gateway command fails due to an unclassified protocol error.</summary>
|
|
||||||
public class MxGatewayCommandException : MxGatewayException
|
|
||||||
{
|
|
||||||
/// <summary>Initializes a new instance with the given details.</summary>
|
|
||||||
/// <param name="message">The error message describing the command failure.</param>
|
|
||||||
/// <param name="sessionId">The session ID, if available.</param>
|
|
||||||
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
|
||||||
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
|
||||||
/// <param name="hResult">The HResult code, if available.</param>
|
|
||||||
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
|
||||||
/// <param name="innerException">The underlying exception, if any.</param>
|
|
||||||
public MxGatewayCommandException(
|
|
||||||
string message,
|
|
||||||
string? sessionId = null,
|
|
||||||
string? correlationId = null,
|
|
||||||
ProtocolStatus? protocolStatus = null,
|
|
||||||
int? hResult = null,
|
|
||||||
IReadOnlyList<MxStatusProxy>? statuses = null,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(
|
|
||||||
message,
|
|
||||||
sessionId,
|
|
||||||
correlationId,
|
|
||||||
protocolStatus,
|
|
||||||
hResult,
|
|
||||||
statuses ?? [],
|
|
||||||
innerException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Exception thrown when a gateway RPC call fails or returns an error status.
|
|
||||||
/// </summary>
|
|
||||||
public class MxGatewayException : Exception
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the MxGatewayException class with the specified message.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">Diagnostic message describing the failure.</param>
|
|
||||||
public MxGatewayException(string message)
|
|
||||||
: base(message)
|
|
||||||
{
|
|
||||||
Statuses = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the MxGatewayException class with the specified message and inner exception.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">Diagnostic message describing the failure.</param>
|
|
||||||
/// <param name="innerException">Underlying exception that caused this failure.</param>
|
|
||||||
public MxGatewayException(string message, Exception? innerException)
|
|
||||||
: base(message, innerException)
|
|
||||||
{
|
|
||||||
Statuses = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the MxGatewayException class 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(
|
|
||||||
string message,
|
|
||||||
string? sessionId,
|
|
||||||
string? correlationId,
|
|
||||||
ProtocolStatus? protocolStatus,
|
|
||||||
int? hResult,
|
|
||||||
IReadOnlyList<MxStatusProxy> statuses,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(message, innerException)
|
|
||||||
{
|
|
||||||
SessionId = sessionId;
|
|
||||||
CorrelationId = correlationId;
|
|
||||||
ProtocolStatus = protocolStatus;
|
|
||||||
HResultCode = hResult;
|
|
||||||
Statuses = statuses;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the session ID associated with the exception, if available.
|
|
||||||
/// </summary>
|
|
||||||
public string? SessionId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the correlation ID associated with the exception, if available.
|
|
||||||
/// </summary>
|
|
||||||
public string? CorrelationId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the protocol-level status returned by the gateway, if available.
|
|
||||||
/// </summary>
|
|
||||||
public ProtocolStatus? ProtocolStatus { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the HRESULT code returned by the worker or MXAccess, if available.
|
|
||||||
/// </summary>
|
|
||||||
public int? HResultCode { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the list of MXAccess status codes returned by the operation.
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyList<MxStatusProxy> Statuses { get; }
|
|
||||||
}
|
|
||||||
@@ -1,850 +0,0 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents one gateway-backed MXAccess session.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class MxGatewaySession : IAsyncDisposable
|
|
||||||
{
|
|
||||||
private readonly MxGatewayClient _client;
|
|
||||||
private readonly SemaphoreSlim _closeLock = new(1, 1);
|
|
||||||
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(
|
|
||||||
MxGatewayClient client,
|
|
||||||
OpenSessionReply openSessionReply)
|
|
||||||
{
|
|
||||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
|
||||||
OpenSessionReply = openSessionReply ?? throw new ArgumentNullException(nameof(openSessionReply));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The session ID assigned by the gateway.
|
|
||||||
/// </summary>
|
|
||||||
public string SessionId => OpenSessionReply.SessionId;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The server's session creation response containing metadata.
|
|
||||||
/// </summary>
|
|
||||||
public OpenSessionReply OpenSessionReply { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Closes the session on the gateway. Idempotent.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The server's close-session reply.</returns>
|
|
||||||
public async Task<CloseSessionReply> CloseAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (_closeReply is not null)
|
|
||||||
{
|
|
||||||
return _closeReply;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _closeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_closeReply is not null)
|
|
||||||
{
|
|
||||||
return _closeReply;
|
|
||||||
}
|
|
||||||
|
|
||||||
_closeReply = await _client.CloseSessionRawAsync(
|
|
||||||
new CloseSessionRequest { SessionId = SessionId },
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return _closeReply;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_closeLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers a client with the MXAccess session, returning a ServerHandle.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="clientName">Name to register.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The server handle assigned to the registered client.</returns>
|
|
||||||
public async Task<int> RegisterAsync(
|
|
||||||
string clientName,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await RegisterRawAsync(clientName, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.Register?.ServerHandle ?? reply.ReturnValue.Int32Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers a client with the MXAccess session without error checking.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="clientName">Name to register.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The raw server reply.</returns>
|
|
||||||
public Task<MxCommandReply> RegisterRawAsync(
|
|
||||||
string clientName,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(clientName);
|
|
||||||
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Register,
|
|
||||||
Register = new RegisterCommand { ClientName = clientName },
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds an item to the MXAccess session, returning an ItemHandle.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="itemDefinition">The item tag address.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The item handle assigned to the new item.</returns>
|
|
||||||
public async Task<int> AddItemAsync(
|
|
||||||
int serverHandle,
|
|
||||||
string itemDefinition,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await AddItemRawAsync(
|
|
||||||
serverHandle,
|
|
||||||
itemDefinition,
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.AddItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds an item to the MXAccess session without error checking.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="itemDefinition">The item tag address.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The raw server reply.</returns>
|
|
||||||
public Task<MxCommandReply> AddItemRawAsync(
|
|
||||||
int serverHandle,
|
|
||||||
string itemDefinition,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
|
|
||||||
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.AddItem,
|
|
||||||
AddItem = new AddItemCommand
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemDefinition = itemDefinition,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds an item with context to the MXAccess session, returning an ItemHandle.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="itemDefinition">The item tag address.</param>
|
|
||||||
/// <param name="itemContext">Additional context for the item.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The item handle assigned to the new item.</returns>
|
|
||||||
public async Task<int> AddItem2Async(
|
|
||||||
int serverHandle,
|
|
||||||
string itemDefinition,
|
|
||||||
string itemContext,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await AddItem2RawAsync(
|
|
||||||
serverHandle,
|
|
||||||
itemDefinition,
|
|
||||||
itemContext,
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.AddItem2?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds an item with context to the MXAccess session without error checking.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="itemDefinition">The item tag address.</param>
|
|
||||||
/// <param name="itemContext">Additional context for the item.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The raw server reply.</returns>
|
|
||||||
public Task<MxCommandReply> AddItem2RawAsync(
|
|
||||||
int serverHandle,
|
|
||||||
string itemDefinition,
|
|
||||||
string itemContext,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
|
|
||||||
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.AddItem2,
|
|
||||||
AddItem2 = new AddItem2Command
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemDefinition = itemDefinition,
|
|
||||||
ItemContext = itemContext ?? string.Empty,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
public async Task AdviseAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await AdviseRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subscribes to events for an item without error checking.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The raw server reply.</returns>
|
|
||||||
public Task<MxCommandReply> AdviseRawAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Advise,
|
|
||||||
Advise = new AdviseCommand
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemHandle = itemHandle,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
public async Task UnAdviseAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await UnAdviseRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unsubscribes from events for an item without error checking.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The raw server reply.</returns>
|
|
||||||
public Task<MxCommandReply> UnAdviseRawAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.UnAdvise,
|
|
||||||
UnAdvise = new UnAdviseCommand
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemHandle = itemHandle,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
public async Task RemoveItemAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await RemoveItemRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes an item from the MXAccess session without error checking.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The raw server reply.</returns>
|
|
||||||
public Task<MxCommandReply> RemoveItemRawAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.RemoveItem,
|
|
||||||
RemoveItem = new RemoveItemCommand
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemHandle = itemHandle,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds multiple items to the MXAccess session in a single command.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="tagAddresses">The item tag addresses to add.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>Per-item subscription results.</returns>
|
|
||||||
public async Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(
|
|
||||||
int serverHandle,
|
|
||||||
IReadOnlyList<string> tagAddresses,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(tagAddresses);
|
|
||||||
|
|
||||||
AddItemBulkCommand command = new() { ServerHandle = serverHandle };
|
|
||||||
command.TagAddresses.Add(tagAddresses);
|
|
||||||
|
|
||||||
MxCommandReply reply = await InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.AddItemBulk,
|
|
||||||
AddItemBulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.AddItemBulk?.Results.ToArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Advises multiple items in a single command.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="itemHandles">The ItemHandles to advise.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>Per-item subscription results.</returns>
|
|
||||||
public async Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(
|
|
||||||
int serverHandle,
|
|
||||||
IReadOnlyList<int> itemHandles,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(itemHandles);
|
|
||||||
|
|
||||||
AdviseItemBulkCommand command = new() { ServerHandle = serverHandle };
|
|
||||||
command.ItemHandles.Add(itemHandles);
|
|
||||||
|
|
||||||
MxCommandReply reply = await InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.AdviseItemBulk,
|
|
||||||
AdviseItemBulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.AdviseItemBulk?.Results.ToArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes multiple items in a single command.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="itemHandles">The ItemHandles to remove.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>Per-item subscription results.</returns>
|
|
||||||
public async Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(
|
|
||||||
int serverHandle,
|
|
||||||
IReadOnlyList<int> itemHandles,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(itemHandles);
|
|
||||||
|
|
||||||
RemoveItemBulkCommand command = new() { ServerHandle = serverHandle };
|
|
||||||
command.ItemHandles.Add(itemHandles);
|
|
||||||
|
|
||||||
MxCommandReply reply = await InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.RemoveItemBulk,
|
|
||||||
RemoveItemBulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.RemoveItemBulk?.Results.ToArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unadvises multiple items in a single command.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="itemHandles">The ItemHandles to unadvise.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>Per-item subscription results.</returns>
|
|
||||||
public async Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(
|
|
||||||
int serverHandle,
|
|
||||||
IReadOnlyList<int> itemHandles,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(itemHandles);
|
|
||||||
|
|
||||||
UnAdviseItemBulkCommand command = new() { ServerHandle = serverHandle };
|
|
||||||
command.ItemHandles.Add(itemHandles);
|
|
||||||
|
|
||||||
MxCommandReply reply = await InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.UnAdviseItemBulk,
|
|
||||||
UnAdviseItemBulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.UnAdviseItemBulk?.Results.ToArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds and advises multiple items in a single command.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="tagAddresses">The item tag addresses to add and advise.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>Per-item subscription results.</returns>
|
|
||||||
public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
|
||||||
int serverHandle,
|
|
||||||
IReadOnlyList<string> tagAddresses,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(tagAddresses);
|
|
||||||
|
|
||||||
SubscribeBulkCommand command = new() { ServerHandle = serverHandle };
|
|
||||||
command.TagAddresses.Add(tagAddresses);
|
|
||||||
|
|
||||||
MxCommandReply reply = await InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.SubscribeBulk,
|
|
||||||
SubscribeBulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.SubscribeBulk?.Results.ToArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unadvises and removes multiple items in a single command.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="itemHandles">The ItemHandles to unsubscribe.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>Per-item subscription results.</returns>
|
|
||||||
public async Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(
|
|
||||||
int serverHandle,
|
|
||||||
IReadOnlyList<int> itemHandles,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(itemHandles);
|
|
||||||
|
|
||||||
UnsubscribeBulkCommand command = new() { ServerHandle = serverHandle };
|
|
||||||
command.ItemHandles.Add(itemHandles);
|
|
||||||
|
|
||||||
MxCommandReply reply = await InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.UnsubscribeBulk,
|
|
||||||
UnsubscribeBulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.UnsubscribeBulk?.Results.ToArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Bulk Write — sequential MXAccess Write per entry on the worker's STA.
|
|
||||||
/// Per-item failures appear as <see cref="BulkWriteResult"/> entries with
|
|
||||||
/// <c>WasSuccessful = false</c>; the call never throws on per-item errors.
|
|
||||||
/// Protocol-level failures still throw via EnsureProtocolSuccess.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="entries">Per-item write entries; each carries the item handle, value, and user id.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
|
|
||||||
public async Task<IReadOnlyList<BulkWriteResult>> WriteBulkAsync(
|
|
||||||
int serverHandle,
|
|
||||||
IReadOnlyList<WriteBulkEntry> entries,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(entries);
|
|
||||||
|
|
||||||
WriteBulkCommand command = new() { ServerHandle = serverHandle };
|
|
||||||
command.Entries.Add(entries);
|
|
||||||
|
|
||||||
MxCommandReply reply = await InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.WriteBulk,
|
|
||||||
WriteBulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.WriteBulk?.Results.ToArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Bulk Write2 — sequential MXAccess Write2 (timestamped) per entry.
|
|
||||||
/// Per-item failures appear as <see cref="BulkWriteResult"/> entries with
|
|
||||||
/// <c>WasSuccessful = false</c>; the call never throws on per-item errors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="entries">Per-item write entries; each carries the item handle, value, timestamp, and user id.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
|
|
||||||
public async Task<IReadOnlyList<BulkWriteResult>> Write2BulkAsync(
|
|
||||||
int serverHandle,
|
|
||||||
IReadOnlyList<Write2BulkEntry> entries,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(entries);
|
|
||||||
|
|
||||||
Write2BulkCommand command = new() { ServerHandle = serverHandle };
|
|
||||||
command.Entries.Add(entries);
|
|
||||||
|
|
||||||
MxCommandReply reply = await InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Write2Bulk,
|
|
||||||
Write2Bulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.Write2Bulk?.Results.ToArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Bulk WriteSecured — sequential MXAccess WriteSecured per entry.
|
|
||||||
/// Credential-sensitive values must never reach logs; the client mirrors
|
|
||||||
/// the single-item WriteSecured redaction contract.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="entries">Per-item write entries; each carries the item handle, value, current user id, and verifier user id.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
|
|
||||||
public async Task<IReadOnlyList<BulkWriteResult>> WriteSecuredBulkAsync(
|
|
||||||
int serverHandle,
|
|
||||||
IReadOnlyList<WriteSecuredBulkEntry> entries,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(entries);
|
|
||||||
|
|
||||||
WriteSecuredBulkCommand command = new() { ServerHandle = serverHandle };
|
|
||||||
command.Entries.Add(entries);
|
|
||||||
|
|
||||||
MxCommandReply reply = await InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.WriteSecuredBulk,
|
|
||||||
WriteSecuredBulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.WriteSecuredBulk?.Results.ToArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Bulk WriteSecured2 — sequential MXAccess WriteSecured2 (timestamped) per entry.
|
|
||||||
/// Same redaction rules as <see cref="WriteSecuredBulkAsync"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="entries">Per-item write entries; each carries the item handle, value, timestamp, current user id, and verifier user id.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
|
|
||||||
public async Task<IReadOnlyList<BulkWriteResult>> WriteSecured2BulkAsync(
|
|
||||||
int serverHandle,
|
|
||||||
IReadOnlyList<WriteSecured2BulkEntry> entries,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(entries);
|
|
||||||
|
|
||||||
WriteSecured2BulkCommand command = new() { ServerHandle = serverHandle };
|
|
||||||
command.Entries.Add(entries);
|
|
||||||
|
|
||||||
MxCommandReply reply = await InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.WriteSecured2Bulk,
|
|
||||||
WriteSecured2Bulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.WriteSecured2Bulk?.Results.ToArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Bulk Read — snapshot the current value for each requested tag.
|
|
||||||
/// Returns the cached OnDataChange value when the tag is already advised
|
|
||||||
/// (<c>WasCached = true</c>), otherwise the worker takes the full AddItem +
|
|
||||||
/// Advise + wait + UnAdvise + RemoveItem snapshot lifecycle. Per-tag
|
|
||||||
/// failures (timeout, invalid tag) appear as <see cref="BulkReadResult"/>
|
|
||||||
/// entries with <c>WasSuccessful = false</c>; the call never throws on
|
|
||||||
/// per-tag errors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="tagAddresses">Tag addresses to read (one per result).</param>
|
|
||||||
/// <param name="timeout">Per-call timeout for the snapshot lifecycle path; <see cref="TimeSpan.Zero"/> uses the gateway default.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>One <see cref="BulkReadResult"/> per requested tag, in request order.</returns>
|
|
||||||
public async Task<IReadOnlyList<BulkReadResult>> ReadBulkAsync(
|
|
||||||
int serverHandle,
|
|
||||||
IReadOnlyList<string> tagAddresses,
|
|
||||||
TimeSpan timeout,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(tagAddresses);
|
|
||||||
|
|
||||||
ReadBulkCommand command = new()
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
TimeoutMs = timeout <= TimeSpan.Zero ? 0u : (uint)Math.Min(timeout.TotalMilliseconds, uint.MaxValue),
|
|
||||||
};
|
|
||||||
command.TagAddresses.Add(tagAddresses);
|
|
||||||
|
|
||||||
MxCommandReply reply = await InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.ReadBulk,
|
|
||||||
ReadBulk = command,
|
|
||||||
},
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply.ReadBulk?.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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
public async Task WriteAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
MxValue value,
|
|
||||||
int userId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await WriteRawAsync(serverHandle, itemHandle, value, userId, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes a value to an item on the MXAccess server without error checking.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
||||||
/// <param name="value">The value to write.</param>
|
|
||||||
/// <param name="userId">User ID context for the write.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The raw server reply.</returns>
|
|
||||||
public Task<MxCommandReply> WriteRawAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
MxValue value,
|
|
||||||
int userId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
|
||||||
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Write,
|
|
||||||
Write = new WriteCommand
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemHandle = itemHandle,
|
|
||||||
Value = value,
|
|
||||||
UserId = userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
public async Task Write2Async(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
MxValue value,
|
|
||||||
MxValue timestampValue,
|
|
||||||
int userId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await Write2RawAsync(
|
|
||||||
serverHandle,
|
|
||||||
itemHandle,
|
|
||||||
value,
|
|
||||||
timestampValue,
|
|
||||||
userId,
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes a value and timestamp to an item on the MXAccess server without error checking.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
||||||
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
||||||
/// <param name="value">The value to write.</param>
|
|
||||||
/// <param name="timestampValue">The timestamp to write with the value.</param>
|
|
||||||
/// <param name="userId">User ID context for the write.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The raw server reply.</returns>
|
|
||||||
public Task<MxCommandReply> Write2RawAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
MxValue value,
|
|
||||||
MxValue timestampValue,
|
|
||||||
int userId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
|
||||||
ArgumentNullException.ThrowIfNull(timestampValue);
|
|
||||||
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Write2,
|
|
||||||
Write2 = new Write2Command
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemHandle = itemHandle,
|
|
||||||
Value = value,
|
|
||||||
TimestampValue = timestampValue,
|
|
||||||
UserId = userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invokes an MXAccess command on this session.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The command request.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The raw server reply.</returns>
|
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
|
||||||
return _client.InvokeAsync(request, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Streams events from the worker for this session, optionally starting after a given sequence number.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="afterWorkerSequence">The sequence number to stream from. Defaults to 0.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>An async enumerable of events.</returns>
|
|
||||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
||||||
ulong afterWorkerSequence = 0,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return _client.StreamEventsAsync(
|
|
||||||
new StreamEventsRequest
|
|
||||||
{
|
|
||||||
SessionId = SessionId,
|
|
||||||
AfterWorkerSequence = afterWorkerSequence,
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Closes the session and releases resources.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
await CloseAsync().ConfigureAwait(false);
|
|
||||||
_closeLock.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<MxCommandReply> InvokeCommandAsync(
|
|
||||||
MxCommand command,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return _client.InvokeAsync(
|
|
||||||
new MxCommandRequest
|
|
||||||
{
|
|
||||||
SessionId = SessionId,
|
|
||||||
ClientCorrelationId = Guid.NewGuid().ToString("N"),
|
|
||||||
Command = command,
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>Exception thrown when a session is not found, not ready, or invalid.</summary>
|
|
||||||
public sealed class MxGatewaySessionException : MxGatewayException
|
|
||||||
{
|
|
||||||
/// <summary>Initializes a new instance with the given details.</summary>
|
|
||||||
/// <param name="message">The error message describing the session failure.</param>
|
|
||||||
/// <param name="sessionId">The session ID, if available.</param>
|
|
||||||
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
|
||||||
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
|
||||||
/// <param name="hResult">The HResult code, if available.</param>
|
|
||||||
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
|
||||||
/// <param name="innerException">The underlying exception, if any.</param>
|
|
||||||
public MxGatewaySessionException(
|
|
||||||
string message,
|
|
||||||
string? sessionId = null,
|
|
||||||
string? correlationId = null,
|
|
||||||
ProtocolStatus? protocolStatus = null,
|
|
||||||
int? hResult = null,
|
|
||||||
IReadOnlyList<MxStatusProxy>? statuses = null,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(
|
|
||||||
message,
|
|
||||||
sessionId,
|
|
||||||
correlationId,
|
|
||||||
protocolStatus,
|
|
||||||
hResult,
|
|
||||||
statuses ?? [],
|
|
||||||
innerException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>Exception thrown when the worker process is unavailable or fails to process a command.</summary>
|
|
||||||
public sealed class MxGatewayWorkerException : MxGatewayException
|
|
||||||
{
|
|
||||||
/// <summary>Initializes a new instance with the given details.</summary>
|
|
||||||
/// <param name="message">The error message describing the worker failure.</param>
|
|
||||||
/// <param name="sessionId">The session ID, if available.</param>
|
|
||||||
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
|
||||||
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
|
||||||
/// <param name="hResult">The HResult code, if available.</param>
|
|
||||||
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
|
||||||
/// <param name="innerException">The underlying exception, if any.</param>
|
|
||||||
public MxGatewayWorkerException(
|
|
||||||
string message,
|
|
||||||
string? sessionId = null,
|
|
||||||
string? correlationId = null,
|
|
||||||
ProtocolStatus? protocolStatus = null,
|
|
||||||
int? hResult = null,
|
|
||||||
IReadOnlyList<MxStatusProxy>? statuses = null,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(
|
|
||||||
message,
|
|
||||||
sessionId,
|
|
||||||
correlationId,
|
|
||||||
protocolStatus,
|
|
||||||
hResult,
|
|
||||||
statuses ?? [],
|
|
||||||
innerException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>Extension methods for MxStatusProxy values.</summary>
|
|
||||||
public static class MxStatusProxyExtensions
|
|
||||||
{
|
|
||||||
/// <summary>Returns whether the status indicates success (success flag set and category is Ok).</summary>
|
|
||||||
/// <param name="status">The status to check.</param>
|
|
||||||
/// <returns><c>true</c> if the status is successful; <c>false</c> otherwise.</returns>
|
|
||||||
public static bool IsSuccess(this MxStatusProxy status)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(status);
|
|
||||||
|
|
||||||
return status.Success != 0
|
|
||||||
&& 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>
|
|
||||||
/// <returns>A human-readable string combining category, source, detail, and diagnostic text.</returns>
|
|
||||||
public static string ToDiagnosticSummary(this MxStatusProxy status)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(status);
|
|
||||||
|
|
||||||
string diagnosticText = string.IsNullOrWhiteSpace(status.DiagnosticText)
|
|
||||||
? "no diagnostic text"
|
|
||||||
: status.DiagnosticText;
|
|
||||||
|
|
||||||
return $"{status.Category} by {status.DetectedBy}; detail={status.Detail}; {diagnosticText}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("ZB.MOM.WW.MxGateway.Client.Tests")]
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\..\src\ZB.MOM.WW.MxGateway.Contracts\ZB.MOM.WW.MxGateway.Contracts.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Grpc.Net.Client" Version="2.76.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Polly.Core" Version="8.6.6" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<IsPackable>true</IsPackable>
|
|
||||||
<PackageId>ZB.MOM.WW.MxGateway.Client</PackageId>
|
|
||||||
<Description>.NET 10 gRPC client for the MxAccessGateway service. Provides typed wrappers, retry, and a lazy-browse walker over the Galaxy Repository hierarchy.</Description>
|
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
|
||||||
<_Parameter1>ZB.MOM.WW.MxGateway.Client.Tests</_Parameter1>
|
|
||||||
</AssemblyAttribute>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
+4
-113
@@ -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/ClientProtoGeneration.md` so gateway and client contracts stay in
|
`../../docs/client-proto-generation.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/ToolchainLinks.md`.
|
The script uses the tool paths recorded in `../../docs/toolchain-links.md`.
|
||||||
|
|
||||||
## Build And Test
|
## Build And Test
|
||||||
|
|
||||||
@@ -75,14 +75,6 @@ client, err := mxgateway.Dial(ctx, mxgateway.Options{
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
The gateway can auto-generate its own self-signed certificate (it has no PKI), so
|
|
||||||
the client is **lenient by default**: a TLS connection (`Plaintext: false`) with
|
|
||||||
no `CACertFile`/`TLSConfig` accepts whatever certificate the gateway presents
|
|
||||||
(`InsecureSkipVerify`, with `ServerNameOverride` as the SNI when set). To verify
|
|
||||||
instead, set `CACertFile` to pin a CA, or set `RequireCertificateValidation:
|
|
||||||
true` to verify against the OS/system trust roots without pinning. See
|
|
||||||
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).
|
|
||||||
|
|
||||||
`Client.OpenSession` returns a `Session` with helpers for `Register`,
|
`Client.OpenSession` returns a `Session` with helpers for `Register`,
|
||||||
`AddItem`, `AddItem2`, `Advise`, `Write`, `Events`, and `Close`. Prefer
|
`AddItem`, `AddItem2`, `Advise`, `Write`, `Events`, and `Close`. Prefer
|
||||||
`SubscribeEvents` or `SubscribeEventsAfter` for long-running streams because the
|
`SubscribeEvents` or `SubscribeEventsAfter` for long-running streams because the
|
||||||
@@ -92,13 +84,6 @@ goroutine cleanup. Raw protobuf messages remain available through the
|
|||||||
`errors.As` for `GatewayError`, `CommandError`, and `MxAccessError`; command
|
`errors.As` for `GatewayError`, `CommandError`, and `MxAccessError`; command
|
||||||
errors preserve the raw reply.
|
errors preserve the raw reply.
|
||||||
|
|
||||||
For alarms, the package exposes `Client.QueryActiveAlarms` for one-shot
|
|
||||||
snapshots, `Client.StreamAlarms` for the server-streaming feed, and
|
|
||||||
`Client.AcknowledgeAlarm` to ack an alarm by full reference. The streaming
|
|
||||||
call returns a `StreamAlarmsClient`; cancel its context to terminate the
|
|
||||||
stream. All three pass straight through to the gateway's central alarm
|
|
||||||
monitor.
|
|
||||||
|
|
||||||
## Galaxy Repository browse
|
## Galaxy Repository browse
|
||||||
|
|
||||||
The `GalaxyRepository` service (proto package `galaxy_repository.v1`) is a
|
The `GalaxyRepository` service (proto package `galaxy_repository.v1`) is a
|
||||||
@@ -129,68 +114,6 @@ reports `present=false` (no deploy recorded). `DiscoverHierarchy` returns
|
|||||||
the generated `*GalaxyObject` slice with each object's dynamic attributes
|
the generated `*GalaxyObject` slice with each object's dynamic attributes
|
||||||
populated for direct contract access.
|
populated for direct contract access.
|
||||||
|
|
||||||
### Browsing lazily
|
|
||||||
|
|
||||||
For UI trees or OPC UA bridges, use `BrowseChildren` to walk one level at a
|
|
||||||
time instead of loading the full hierarchy. Pass an empty request for root
|
|
||||||
objects; subsequent calls set `ParentGobjectId`, `ParentTagName`, or
|
|
||||||
`ParentContainedPath`. Filter fields match `DiscoverHierarchy`. Each response
|
|
||||||
pairs `Children` with `ChildHasChildren` so you know which nodes to expand. See
|
|
||||||
[Galaxy Repository](../../docs/GalaxyRepository.md#browsechildren) for full
|
|
||||||
request and filter semantics.
|
|
||||||
|
|
||||||
```go
|
|
||||||
import pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated/galaxy_repository/v1"
|
|
||||||
|
|
||||||
reply, err := galaxy.BrowseChildren(ctx, &pb.BrowseChildrenRequest{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for i, child := range reply.GetChildren() {
|
|
||||||
fmt.Printf("%s expand=%v\n", child.GetTagName(), reply.GetChildHasChildren()[i])
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### High-level walker
|
|
||||||
|
|
||||||
For UI trees, the client provides a `LazyBrowseNode` walker that handles
|
|
||||||
sibling pagination and the `child_has_children` hint for you:
|
|
||||||
|
|
||||||
```go
|
|
||||||
galaxy, err := mxgateway.DialGalaxy(ctx, mxgateway.Options{
|
|
||||||
Endpoint: "localhost:5000",
|
|
||||||
APIKey: os.Getenv("MXGATEWAY_API_KEY"),
|
|
||||||
Plaintext: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer galaxy.Close()
|
|
||||||
|
|
||||||
roots, err := galaxy.Browse(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, root := range roots {
|
|
||||||
if root.HasChildrenHint() {
|
|
||||||
if err := root.Expand(ctx); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, child := range root.Children() {
|
|
||||||
kind := "leaf"
|
|
||||||
if child.HasChildrenHint() {
|
|
||||||
kind = "has children"
|
|
||||||
}
|
|
||||||
fmt.Printf("%s (%s)\n", child.Object().GetTagName(), kind)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`Expand` is idempotent — calling it twice fires only one RPC,
|
|
||||||
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
|
|
||||||
`Browse` again from the root.
|
|
||||||
|
|
||||||
### Watching deploy events
|
### Watching deploy events
|
||||||
|
|
||||||
`WatchDeployEvents` opens a server-streaming subscription. The server emits a
|
`WatchDeployEvents` opens a server-streaming subscription. The server emits a
|
||||||
@@ -283,40 +206,8 @@ $env:MXGATEWAY_TEST_ITEM = 'Area001.Tag.Value'
|
|||||||
go run ./cmd/mxgw-go smoke -endpoint $env:MXGATEWAY_ENDPOINT -plaintext -api-key-env MXGATEWAY_API_KEY -item $env:MXGATEWAY_TEST_ITEM -json
|
go run ./cmd/mxgw-go smoke -endpoint $env:MXGATEWAY_ENDPOINT -plaintext -api-key-env MXGATEWAY_API_KEY -item $env:MXGATEWAY_TEST_ITEM -json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installing the Go client
|
|
||||||
|
|
||||||
The module is resolved directly from the git repo — no package registry:
|
|
||||||
|
|
||||||
````bash
|
|
||||||
go get gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go@v0.1.0
|
|
||||||
````
|
|
||||||
|
|
||||||
Then import:
|
|
||||||
|
|
||||||
````go
|
|
||||||
import "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/mxgateway"
|
|
||||||
````
|
|
||||||
|
|
||||||
If your build environment cannot reach `gitea.dohertylan.com` directly,
|
|
||||||
configure `GOPROXY` to point at an internal proxy that fronts the Gitea
|
|
||||||
repo, or use `GONOSUMCHECK` + `GOPRIVATE` to bypass the checksum database
|
|
||||||
for the internal module path.
|
|
||||||
|
|
||||||
## Releasing a new version
|
|
||||||
|
|
||||||
Go modules in monorepo subdirectories use prefixed tags. To tag a release
|
|
||||||
from this repo:
|
|
||||||
|
|
||||||
````bash
|
|
||||||
pwsh scripts/tag-go-module.ps1 -Version v0.1.1 -Push
|
|
||||||
````
|
|
||||||
|
|
||||||
The script validates semver, refuses to tag with uncommitted tracked
|
|
||||||
changes, creates an annotated tag `clients/go/v0.1.1`, and (with `-Push`)
|
|
||||||
pushes it to origin.
|
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
||||||
- [Go Client Detailed Design](./GoClientDesign.md)
|
- [Go Client Detailed Design](../../docs/clients-golang-design.md)
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
// 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 (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -15,7 +9,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -91,26 +84,10 @@ func runWithIO(ctx context.Context, args []string, stdout, stderr io.Writer) err
|
|||||||
return runSubscribeBulk(ctx, args[1:], stdout, stderr)
|
return runSubscribeBulk(ctx, args[1:], stdout, stderr)
|
||||||
case "unsubscribe-bulk":
|
case "unsubscribe-bulk":
|
||||||
return runUnsubscribeBulk(ctx, args[1:], stdout, stderr)
|
return runUnsubscribeBulk(ctx, args[1:], stdout, stderr)
|
||||||
case "read-bulk":
|
|
||||||
return runReadBulk(ctx, args[1:], stdout, stderr)
|
|
||||||
case "write-bulk":
|
|
||||||
return runWriteBulk(ctx, args[1:], stdout, stderr)
|
|
||||||
case "write2-bulk":
|
|
||||||
return runWrite2Bulk(ctx, args[1:], stdout, stderr)
|
|
||||||
case "write-secured-bulk":
|
|
||||||
return runWriteSecuredBulk(ctx, args[1:], stdout, stderr)
|
|
||||||
case "write-secured2-bulk":
|
|
||||||
return runWriteSecured2Bulk(ctx, args[1:], stdout, stderr)
|
|
||||||
case "bench-read-bulk":
|
|
||||||
return runBenchReadBulk(ctx, args[1:], stdout, stderr)
|
|
||||||
case "write":
|
case "write":
|
||||||
return runWrite(ctx, args[1:], stdout, stderr)
|
return runWrite(ctx, args[1:], stdout, stderr)
|
||||||
case "stream-events":
|
case "stream-events":
|
||||||
return runStreamEvents(ctx, args[1:], stdout, stderr)
|
return runStreamEvents(ctx, args[1:], stdout, stderr)
|
||||||
case "stream-alarms":
|
|
||||||
return runStreamAlarms(ctx, args[1:], stdout, stderr)
|
|
||||||
case "acknowledge-alarm":
|
|
||||||
return runAcknowledgeAlarm(ctx, args[1:], stdout, stderr)
|
|
||||||
case "smoke":
|
case "smoke":
|
||||||
return runSmoke(ctx, args[1:], stdout, stderr)
|
return runSmoke(ctx, args[1:], stdout, stderr)
|
||||||
case "galaxy-test-connection":
|
case "galaxy-test-connection":
|
||||||
@@ -121,8 +98,6 @@ func runWithIO(ctx context.Context, args []string, stdout, stderr io.Writer) err
|
|||||||
return runGalaxyDiscover(ctx, args[1:], stdout, stderr)
|
return runGalaxyDiscover(ctx, args[1:], stdout, stderr)
|
||||||
case "galaxy-watch":
|
case "galaxy-watch":
|
||||||
return runGalaxyWatch(ctx, args[1:], stdout, stderr)
|
return runGalaxyWatch(ctx, args[1:], stdout, stderr)
|
||||||
case "batch":
|
|
||||||
return runBatch(ctx, os.Stdin, stdout, stderr)
|
|
||||||
default:
|
default:
|
||||||
writeUsage(stderr)
|
writeUsage(stderr)
|
||||||
return fmt.Errorf("unknown command %q", args[0])
|
return fmt.Errorf("unknown command %q", args[0])
|
||||||
@@ -357,378 +332,11 @@ func runUnsubscribeBulk(ctx context.Context, args []string, stdout, stderr io.Wr
|
|||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
handles, err := parseInt32List(*itemHandles)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
session := mxgateway.NewSessionForID(client, *sessionID)
|
session := mxgateway.NewSessionForID(client, *sessionID)
|
||||||
results, err := session.UnsubscribeBulk(ctx, int32(*serverHandle), handles)
|
results, err := session.UnsubscribeBulk(ctx, int32(*serverHandle), parseInt32List(*itemHandles))
|
||||||
return writeBulkOutput(stdout, *jsonOutput, "unsubscribe-bulk", options, results, err)
|
return writeBulkOutput(stdout, *jsonOutput, "unsubscribe-bulk", options, results, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runReadBulk(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
flags := flag.NewFlagSet("read-bulk", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
common := bindCommonFlags(flags)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
sessionID := flags.String("session-id", "", "gateway session id")
|
|
||||||
serverHandle := flags.Int("server-handle", 0, "MXAccess server handle")
|
|
||||||
items := flags.String("items", "", "comma-separated tag addresses")
|
|
||||||
timeoutMs := flags.Int("timeout-ms", 0, "per-tag snapshot timeout in milliseconds (0 = worker default)")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *sessionID == "" || *items == "" {
|
|
||||||
return errors.New("session-id and items are required")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, options, err := dialForCommand(ctx, common)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
session := mxgateway.NewSessionForID(client, *sessionID)
|
|
||||||
results, err := session.ReadBulk(ctx, int32(*serverHandle), parseStringList(*items), time.Duration(*timeoutMs)*time.Millisecond)
|
|
||||||
return writeReadBulkOutput(stdout, *jsonOutput, "read-bulk", options, results, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runWriteBulk(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
return runWriteBulkVariant(ctx, args, stdout, stderr, "write-bulk", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runWrite2Bulk(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
return runWriteBulkVariant(ctx, args, stdout, stderr, "write2-bulk", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runWriteSecuredBulk(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
return runWriteBulkVariant(ctx, args, stdout, stderr, "write-secured-bulk", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runWriteSecured2Bulk(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
return runWriteBulkVariant(ctx, args, stdout, stderr, "write-secured2-bulk", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// runWriteBulkVariant shares the flag-parsing + entry-build skeleton across
|
|
||||||
// the four bulk-write families. The variant is derived from command alone;
|
|
||||||
// withTimestamp adds a --timestamp-value flag. To keep wrong-variant flags
|
|
||||||
// from silently no-op'ing, secured-only flags (-current-user-id /
|
|
||||||
// -verifier-user-id) are only registered for the secured variants, and
|
|
||||||
// -user-id only for the non-secured Write/Write2 variants — a wrong-variant
|
|
||||||
// flag then surfaces as a clean "flag provided but not defined" error.
|
|
||||||
func runWriteBulkVariant(ctx context.Context, args []string, stdout, stderr io.Writer, command string, withTimestamp bool) error {
|
|
||||||
secured := command == "write-secured-bulk" || command == "write-secured2-bulk"
|
|
||||||
|
|
||||||
flags := flag.NewFlagSet(command, flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
common := bindCommonFlags(flags)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
sessionID := flags.String("session-id", "", "gateway session id")
|
|
||||||
serverHandle := flags.Int("server-handle", 0, "MXAccess server handle")
|
|
||||||
itemHandles := flags.String("item-handles", "", "comma-separated item handles")
|
|
||||||
valueType := flags.String("type", "string", "value type: bool, int32, int64, float, double, string")
|
|
||||||
values := flags.String("values", "", "comma-separated values (one per item handle)")
|
|
||||||
var (
|
|
||||||
userID *int
|
|
||||||
currentUserID *int
|
|
||||||
verifierUserID *int
|
|
||||||
)
|
|
||||||
if secured {
|
|
||||||
currentUserID = flags.Int("current-user-id", 0, "MXAccess current user id (Secured variants)")
|
|
||||||
verifierUserID = flags.Int("verifier-user-id", 0, "MXAccess verifier user id (Secured variants)")
|
|
||||||
} else {
|
|
||||||
userID = flags.Int("user-id", 0, "MXAccess user id (Write/Write2 variants)")
|
|
||||||
}
|
|
||||||
timestampValue := flags.String("timestamp-value", "", "RFC 3339 timestamp shared across all entries (Write2/WriteSecured2 variants)")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *sessionID == "" || *itemHandles == "" || *values == "" {
|
|
||||||
return errors.New("session-id, item-handles, and values are required")
|
|
||||||
}
|
|
||||||
|
|
||||||
handles, err := parseInt32List(*itemHandles)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
valueTexts := parseStringList(*values)
|
|
||||||
if len(handles) != len(valueTexts) {
|
|
||||||
return fmt.Errorf("item-handles count (%d) does not match values count (%d)", len(handles), len(valueTexts))
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedValues := make([]*mxgateway.MxValue, len(handles))
|
|
||||||
for i, text := range valueTexts {
|
|
||||||
v, err := parseValue(*valueType, text)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("entry %d: %w", i, err)
|
|
||||||
}
|
|
||||||
parsedValues[i] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
var tsValue *mxgateway.MxValue
|
|
||||||
if withTimestamp {
|
|
||||||
if *timestampValue == "" {
|
|
||||||
return errors.New("timestamp-value is required for write2/write-secured2 bulk variants")
|
|
||||||
}
|
|
||||||
parsed, err := parseRfc3339Timestamp(*timestampValue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tsValue = parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
client, options, err := dialForCommand(ctx, common)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
session := mxgateway.NewSessionForID(client, *sessionID)
|
|
||||||
|
|
||||||
var results []*mxgateway.BulkWriteResult
|
|
||||||
switch command {
|
|
||||||
case "write-bulk":
|
|
||||||
entries := make([]*mxgateway.WriteBulkEntry, len(handles))
|
|
||||||
for i := range handles {
|
|
||||||
entries[i] = &mxgateway.WriteBulkEntry{ItemHandle: handles[i], Value: parsedValues[i], UserId: int32(*userID)}
|
|
||||||
}
|
|
||||||
results, err = session.WriteBulk(ctx, int32(*serverHandle), entries)
|
|
||||||
case "write2-bulk":
|
|
||||||
entries := make([]*mxgateway.Write2BulkEntry, len(handles))
|
|
||||||
for i := range handles {
|
|
||||||
entries[i] = &mxgateway.Write2BulkEntry{ItemHandle: handles[i], Value: parsedValues[i], TimestampValue: tsValue, UserId: int32(*userID)}
|
|
||||||
}
|
|
||||||
results, err = session.Write2Bulk(ctx, int32(*serverHandle), entries)
|
|
||||||
case "write-secured-bulk":
|
|
||||||
entries := make([]*mxgateway.WriteSecuredBulkEntry, len(handles))
|
|
||||||
for i := range handles {
|
|
||||||
entries[i] = &mxgateway.WriteSecuredBulkEntry{
|
|
||||||
ItemHandle: handles[i],
|
|
||||||
Value: parsedValues[i],
|
|
||||||
CurrentUserId: int32(*currentUserID),
|
|
||||||
VerifierUserId: int32(*verifierUserID),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results, err = session.WriteSecuredBulk(ctx, int32(*serverHandle), entries)
|
|
||||||
case "write-secured2-bulk":
|
|
||||||
entries := make([]*mxgateway.WriteSecured2BulkEntry, len(handles))
|
|
||||||
for i := range handles {
|
|
||||||
entries[i] = &mxgateway.WriteSecured2BulkEntry{
|
|
||||||
ItemHandle: handles[i],
|
|
||||||
Value: parsedValues[i],
|
|
||||||
TimestampValue: tsValue,
|
|
||||||
CurrentUserId: int32(*currentUserID),
|
|
||||||
VerifierUserId: int32(*verifierUserID),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results, err = session.WriteSecured2Bulk(ctx, int32(*serverHandle), entries)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported bulk write command %q", command)
|
|
||||||
}
|
|
||||||
return writeWriteBulkOutput(stdout, *jsonOutput, command, options, results, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseRfc3339Timestamp parses an RFC 3339 timestamp and returns the
|
|
||||||
// MxValue protobuf representation used for the timestamped write families.
|
|
||||||
func parseRfc3339Timestamp(text string) (*mxgateway.MxValue, error) {
|
|
||||||
t, err := time.Parse(time.RFC3339Nano, text)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid RFC 3339 timestamp %q: %w", text, err)
|
|
||||||
}
|
|
||||||
return mxgateway.TimestampValue(t), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// runBenchReadBulk drives the cross-language ReadBulk stress benchmark from Go:
|
|
||||||
// opens its own session, subscribes to bulk-size tags so the worker value cache
|
|
||||||
// populates from real OnDataChange events, runs ReadBulk in a tight loop for
|
|
||||||
// duration-seconds with per-call timing, and emits the shared JSON schema the
|
|
||||||
// scripts/bench-read-bulk.ps1 driver collates across all five clients.
|
|
||||||
func runBenchReadBulk(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
flags := flag.NewFlagSet("bench-read-bulk", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
common := bindCommonFlags(flags)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
clientName := flags.String("client-name", "mxgw-go-bench", "session client name")
|
|
||||||
durationSeconds := flags.Int("duration-seconds", 30, "steady-state measurement window in seconds")
|
|
||||||
warmupSeconds := flags.Int("warmup-seconds", 3, "warm-up window before measurement, in seconds")
|
|
||||||
bulkSize := flags.Int("bulk-size", 6, "tags per ReadBulk call")
|
|
||||||
tagStart := flags.Int("tag-start", 1, "first machine number")
|
|
||||||
tagPrefix := flags.String("tag-prefix", "TestMachine_", "tag prefix (machine number appended as %03d)")
|
|
||||||
tagAttribute := flags.String("tag-attribute", "TestChangingInt", "attribute appended to each tag prefix")
|
|
||||||
timeoutMs := flags.Int("timeout-ms", 1500, "per-tag snapshot timeout in milliseconds")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *bulkSize < 1 {
|
|
||||||
return errors.New("bulk-size must be positive")
|
|
||||||
}
|
|
||||||
if *durationSeconds < 1 {
|
|
||||||
return errors.New("duration-seconds must be positive")
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := make([]string, *bulkSize)
|
|
||||||
for i := 0; i < *bulkSize; i++ {
|
|
||||||
tags[i] = fmt.Sprintf("%s%03d.%s", *tagPrefix, *tagStart+i, *tagAttribute)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, options, err := dialForCommand(ctx, common)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
session, err := client.OpenSession(ctx, mxgateway.OpenSessionOptions{ClientSessionName: *clientName})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_, _ = session.Close(context.Background())
|
|
||||||
}()
|
|
||||||
|
|
||||||
serverHandle, err := session.Register(ctx, *clientName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribeResults, err := session.SubscribeBulk(ctx, serverHandle, tags)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
itemHandles := make([]int32, 0, len(subscribeResults))
|
|
||||||
for _, result := range subscribeResults {
|
|
||||||
if result.GetWasSuccessful() {
|
|
||||||
itemHandles = append(itemHandles, result.GetItemHandle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if len(itemHandles) > 0 {
|
|
||||||
_, _ = session.UnsubscribeBulk(context.Background(), serverHandle, itemHandles)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Warm-up: drive identical calls so any first-call JIT / connection-pool
|
|
||||||
// setup is amortised before the measurement window opens. The ctx.Err()
|
|
||||||
// guard short-circuits on Ctrl+C / parent-cancel instead of spinning
|
|
||||||
// failing ReadBulk calls until the wall-clock deadline elapses.
|
|
||||||
warmupDeadline := time.Now().Add(time.Duration(*warmupSeconds) * time.Second)
|
|
||||||
timeout := time.Duration(*timeoutMs) * time.Millisecond
|
|
||||||
for time.Now().Before(warmupDeadline) && ctx.Err() == nil {
|
|
||||||
_, _ = session.ReadBulk(ctx, serverHandle, tags, timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Steady state: per-call latency captured via time.Now() deltas.
|
|
||||||
latenciesMs := make([]float64, 0, 65536)
|
|
||||||
var totalReadResults int64
|
|
||||||
var cachedReadResults int64
|
|
||||||
var successfulCalls, failedCalls int
|
|
||||||
steadyStart := time.Now()
|
|
||||||
steadyDeadline := steadyStart.Add(time.Duration(*durationSeconds) * time.Second)
|
|
||||||
|
|
||||||
for time.Now().Before(steadyDeadline) && ctx.Err() == nil {
|
|
||||||
callStart := time.Now()
|
|
||||||
results, err := session.ReadBulk(ctx, serverHandle, tags, timeout)
|
|
||||||
elapsed := time.Since(callStart)
|
|
||||||
latenciesMs = append(latenciesMs, float64(elapsed.Nanoseconds())/1e6)
|
|
||||||
if err != nil {
|
|
||||||
failedCalls++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
successfulCalls++
|
|
||||||
for _, r := range results {
|
|
||||||
totalReadResults++
|
|
||||||
if r.GetWasCached() {
|
|
||||||
cachedReadResults++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
steadyElapsed := time.Since(steadyStart)
|
|
||||||
totalCalls := successfulCalls + failedCalls
|
|
||||||
|
|
||||||
callsPerSecond := 0.0
|
|
||||||
if steadyElapsed.Seconds() > 0 {
|
|
||||||
callsPerSecond = float64(totalCalls) / steadyElapsed.Seconds()
|
|
||||||
}
|
|
||||||
|
|
||||||
stats := map[string]any{
|
|
||||||
"language": "go",
|
|
||||||
"command": "bench-read-bulk",
|
|
||||||
"endpoint": options.Endpoint,
|
|
||||||
"clientName": *clientName,
|
|
||||||
"bulkSize": *bulkSize,
|
|
||||||
"durationSeconds": *durationSeconds,
|
|
||||||
"warmupSeconds": *warmupSeconds,
|
|
||||||
"durationMs": steadyElapsed.Milliseconds(),
|
|
||||||
"tags": tags,
|
|
||||||
"totalCalls": totalCalls,
|
|
||||||
"successfulCalls": successfulCalls,
|
|
||||||
"failedCalls": failedCalls,
|
|
||||||
"totalReadResults": totalReadResults,
|
|
||||||
"cachedReadResults": cachedReadResults,
|
|
||||||
"callsPerSecond": roundTo(callsPerSecond, 2),
|
|
||||||
"latencyMs": percentileSummary(latenciesMs),
|
|
||||||
}
|
|
||||||
if *jsonOutput {
|
|
||||||
return writeJSON(stdout, stats)
|
|
||||||
}
|
|
||||||
fmt.Fprintln(stdout, callsPerSecond)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// percentileSummary returns the same { p50, p95, p99, max, mean } shape every
|
|
||||||
// language bench emits, rounded to 3 decimal places so the PowerShell driver
|
|
||||||
// sees one schema across all five clients.
|
|
||||||
func percentileSummary(sample []float64) map[string]float64 {
|
|
||||||
if len(sample) == 0 {
|
|
||||||
return map[string]float64{"p50": 0, "p95": 0, "p99": 0, "max": 0, "mean": 0}
|
|
||||||
}
|
|
||||||
sorted := append([]float64(nil), sample...)
|
|
||||||
sort.Float64s(sorted)
|
|
||||||
mean := 0.0
|
|
||||||
maxValue := sorted[len(sorted)-1]
|
|
||||||
for _, v := range sample {
|
|
||||||
mean += v
|
|
||||||
}
|
|
||||||
mean /= float64(len(sample))
|
|
||||||
return map[string]float64{
|
|
||||||
"p50": roundTo(percentile(sorted, 0.50), 3),
|
|
||||||
"p95": roundTo(percentile(sorted, 0.95), 3),
|
|
||||||
"p99": roundTo(percentile(sorted, 0.99), 3),
|
|
||||||
"max": roundTo(maxValue, 3),
|
|
||||||
"mean": roundTo(mean, 3),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// percentile uses nearest-rank with linear interpolation; matches the .NET
|
|
||||||
// implementation so cross-language comparisons are apples-to-apples.
|
|
||||||
func percentile(sorted []float64, quantile float64) float64 {
|
|
||||||
if len(sorted) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if len(sorted) == 1 {
|
|
||||||
return sorted[0]
|
|
||||||
}
|
|
||||||
rank := quantile * float64(len(sorted)-1)
|
|
||||||
lower := int(rank)
|
|
||||||
upper := lower + 1
|
|
||||||
if upper >= len(sorted) {
|
|
||||||
return sorted[lower]
|
|
||||||
}
|
|
||||||
fraction := rank - float64(lower)
|
|
||||||
return sorted[lower] + (sorted[upper]-sorted[lower])*fraction
|
|
||||||
}
|
|
||||||
|
|
||||||
func roundTo(value float64, digits int) float64 {
|
|
||||||
shift := 1.0
|
|
||||||
for i := 0; i < digits; i++ {
|
|
||||||
shift *= 10
|
|
||||||
}
|
|
||||||
return float64(int64(value*shift+0.5)) / shift
|
|
||||||
}
|
|
||||||
|
|
||||||
func runWrite(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
func runWrite(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
flags := flag.NewFlagSet("write", flag.ContinueOnError)
|
flags := flag.NewFlagSet("write", flag.ContinueOnError)
|
||||||
flags.SetOutput(stderr)
|
flags.SetOutput(stderr)
|
||||||
@@ -815,119 +423,6 @@ func runStreamEvents(ctx context.Context, args []string, stdout, stderr io.Write
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runStreamAlarms(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
flags := flag.NewFlagSet("stream-alarms", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
common := bindCommonFlags(flags)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
filterPrefix := flags.String("filter-prefix", "", "alarm-reference prefix scoping the feed; empty means unscoped")
|
|
||||||
limit := flags.Int("limit", 0, "maximum feed messages to read; 0 means unbounded")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
client, _, err := dialForCommand(ctx, common)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
// Mirror runStreamEvents so Ctrl+C on a long-running stream-alarms command
|
|
||||||
// cancels the gRPC stream cleanly (the gateway sees codes.Canceled rather
|
|
||||||
// than a torn TCP connection) and the deferred client.Close() actually runs.
|
|
||||||
signalCtx, stopSignals := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
|
|
||||||
defer stopSignals()
|
|
||||||
|
|
||||||
streamCtx, cancelStream := context.WithCancel(signalCtx)
|
|
||||||
defer cancelStream()
|
|
||||||
stream, err := client.StreamAlarms(streamCtx, &mxgateway.StreamAlarmsRequest{AlarmFilterPrefix: *filterPrefix})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
count := 0
|
|
||||||
for {
|
|
||||||
message, err := stream.Recv()
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *jsonOutput {
|
|
||||||
fmt.Fprintln(stdout, string(mustMarshalProto(message)))
|
|
||||||
} else {
|
|
||||||
fmt.Fprintln(stdout, formatAlarmFeedMessage(message))
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
if *limit > 0 && count >= *limit {
|
|
||||||
cancelStream()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatAlarmFeedMessage renders one AlarmFeedMessage in the CLI's plain-text
|
|
||||||
// output style, distinguishing the active-alarm snapshot, snapshot-complete
|
|
||||||
// sentinel, and transition cases of the message's payload oneof.
|
|
||||||
func formatAlarmFeedMessage(message *mxgateway.AlarmFeedMessage) string {
|
|
||||||
switch {
|
|
||||||
case message.GetActiveAlarm() != nil:
|
|
||||||
alarm := message.GetActiveAlarm()
|
|
||||||
return fmt.Sprintf("active-alarm %s state=%s severity=%d", alarm.GetAlarmFullReference(), alarm.GetCurrentState(), alarm.GetSeverity())
|
|
||||||
case message.GetSnapshotComplete():
|
|
||||||
return "snapshot-complete"
|
|
||||||
case message.GetTransition() != nil:
|
|
||||||
transition := message.GetTransition()
|
|
||||||
return fmt.Sprintf("transition %s kind=%s severity=%d", transition.GetAlarmFullReference(), transition.GetTransitionKind(), transition.GetSeverity())
|
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runAcknowledgeAlarm(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
flags := flag.NewFlagSet("acknowledge-alarm", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
common := bindCommonFlags(flags)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
reference := flags.String("reference", "", "full alarm reference to acknowledge")
|
|
||||||
comment := flags.String("comment", "", "operator acknowledge comment")
|
|
||||||
operator := flags.String("operator", "", "operator user performing the acknowledge")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *reference == "" {
|
|
||||||
return errors.New("reference is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, options, err := dialForCommand(ctx, common)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
reply, err := client.AcknowledgeAlarm(ctx, &mxgateway.AcknowledgeAlarmRequest{
|
|
||||||
AlarmFullReference: *reference,
|
|
||||||
Comment: *comment,
|
|
||||||
OperatorUser: *operator,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *jsonOutput {
|
|
||||||
return writeJSON(stdout, commandReplyOutput{
|
|
||||||
Command: "acknowledge-alarm",
|
|
||||||
Options: options,
|
|
||||||
Reply: mustMarshalProto(reply),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(stdout, reply.GetHresult())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runSmoke(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
func runSmoke(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
flags := flag.NewFlagSet("smoke", flag.ContinueOnError)
|
flags := flag.NewFlagSet("smoke", flag.ContinueOnError)
|
||||||
flags.SetOutput(stderr)
|
flags.SetOutput(stderr)
|
||||||
@@ -1014,7 +509,7 @@ func parseStringList(value string) []string {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseInt32List(value string) ([]int32, error) {
|
func parseInt32List(value string) []int32 {
|
||||||
parts := strings.Split(value, ",")
|
parts := strings.Split(value, ",")
|
||||||
items := make([]int32, 0, len(parts))
|
items := make([]int32, 0, len(parts))
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
@@ -1024,11 +519,11 @@ func parseInt32List(value string) ([]int32, error) {
|
|||||||
}
|
}
|
||||||
parsed, err := strconv.ParseInt(item, 10, 32)
|
parsed, err := strconv.ParseInt(item, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid item handle %q: %w", item, err)
|
panic(err)
|
||||||
}
|
}
|
||||||
items = append(items, int32(parsed))
|
items = append(items, int32(parsed))
|
||||||
}
|
}
|
||||||
return items, nil
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
func bindCommonFlags(flags *flag.FlagSet) *commonOptions {
|
func bindCommonFlags(flags *flag.FlagSet) *commonOptions {
|
||||||
@@ -1147,36 +642,6 @@ func writeBulkOutput(stdout io.Writer, jsonOutput bool, command string, options
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeWriteBulkOutput(stdout io.Writer, jsonOutput bool, command string, options commonOptions, results []*mxgateway.BulkWriteResult, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if jsonOutput {
|
|
||||||
return writeJSON(stdout, map[string]any{
|
|
||||||
"command": command,
|
|
||||||
"options": options,
|
|
||||||
"results": results,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fmt.Fprintln(stdout, len(results))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeReadBulkOutput(stdout io.Writer, jsonOutput bool, command string, options commonOptions, results []*mxgateway.BulkReadResult, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if jsonOutput {
|
|
||||||
return writeJSON(stdout, map[string]any{
|
|
||||||
"command": command,
|
|
||||||
"options": options,
|
|
||||||
"results": results,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fmt.Fprintln(stdout, len(results))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeJSON(writer io.Writer, value any) error {
|
func writeJSON(writer io.Writer, value any) error {
|
||||||
encoder := json.NewEncoder(writer)
|
encoder := json.NewEncoder(writer)
|
||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
@@ -1196,67 +661,7 @@ type protojsonMessage interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeUsage(writer io.Writer) {
|
func writeUsage(writer io.Writer) {
|
||||||
fmt.Fprintln(writer, "usage: mxgw-go <version|open-session|close-session|register|add-item|advise|subscribe-bulk|unsubscribe-bulk|read-bulk|write-bulk|write2-bulk|write-secured-bulk|write-secured2-bulk|bench-read-bulk|write|stream-events|stream-alarms|acknowledge-alarm|smoke|galaxy-test-connection|galaxy-last-deploy|galaxy-discover|galaxy-watch|batch>")
|
fmt.Fprintln(writer, "usage: mxgw-go <version|open-session|close-session|register|add-item|advise|subscribe-bulk|unsubscribe-bulk|write|stream-events|smoke|galaxy-test-connection|galaxy-last-deploy|galaxy-discover|galaxy-watch>")
|
||||||
}
|
|
||||||
|
|
||||||
// batchEOR is the end-of-result sentinel emitted to stdout after every command
|
|
||||||
// in batch mode, regardless of success or failure.
|
|
||||||
const batchEOR = "__MXGW_BATCH_EOR__"
|
|
||||||
|
|
||||||
// runBatch reads one command line at a time from in, dispatches each via the
|
|
||||||
// normal runWithIO routing, and writes a batchEOR sentinel to stdout after
|
|
||||||
// every result. Errors are serialised as JSON to stdout (not stderr) so the
|
|
||||||
// harness can parse them without interleaving stderr. Blank lines are
|
|
||||||
// skipped; only stdin EOF ends the session.
|
|
||||||
//
|
|
||||||
// The scanner buffer is widened to 16 MiB so a single long command line
|
|
||||||
// (e.g. a bulk-write with several thousand handles) does not trip the
|
|
||||||
// default 64 KiB bufio.Scanner token-too-long error and abort the session.
|
|
||||||
// If a line still exceeds the cap, the error is surfaced as a per-command
|
|
||||||
// error-with-sentinel and the session continues.
|
|
||||||
func runBatch(ctx context.Context, in io.Reader, stdout, stderr io.Writer) error {
|
|
||||||
bw := bufio.NewWriter(stdout)
|
|
||||||
scanner := bufio.NewScanner(in)
|
|
||||||
scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
|
|
||||||
for {
|
|
||||||
if !scanner.Scan() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
line := scanner.Text()
|
|
||||||
args := strings.Fields(line)
|
|
||||||
if len(args) == 0 {
|
|
||||||
// Skip blank / whitespace-only lines; do NOT terminate. The
|
|
||||||
// session ends only on stdin EOF so a stray blank line in a
|
|
||||||
// PowerShell here-string does not silently drop later commands.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := runWithIO(ctx, args, bw, stderr); err != nil {
|
|
||||||
// Write error as JSON to stdout (bw) so the harness sees it in the
|
|
||||||
// same stream as normal output, framed by the EOR sentinel.
|
|
||||||
errPayload := map[string]string{
|
|
||||||
"error": err.Error(),
|
|
||||||
"type": "error",
|
|
||||||
}
|
|
||||||
_ = writeJSON(bw, errPayload)
|
|
||||||
}
|
|
||||||
_, _ = fmt.Fprintln(bw, batchEOR)
|
|
||||||
_ = bw.Flush()
|
|
||||||
}
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
// Emit the scanner failure as a final error-with-sentinel so the
|
|
||||||
// harness sees the failure framed, then return the error so the
|
|
||||||
// process exit reflects it. This handles bufio.ErrTooLong for any
|
|
||||||
// pathological line above the 16 MiB cap.
|
|
||||||
errPayload := map[string]string{
|
|
||||||
"error": err.Error(),
|
|
||||||
"type": "error",
|
|
||||||
}
|
|
||||||
_ = writeJSON(bw, errPayload)
|
|
||||||
_, _ = fmt.Fprintln(bw, batchEOR)
|
|
||||||
_ = bw.Flush()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func dialGalaxyForCommand(ctx context.Context, common *commonOptions) (*mxgateway.GalaxyClient, commonOptions, error) {
|
func dialGalaxyForCommand(ctx context.Context, common *commonOptions) (*mxgateway.GalaxyClient, commonOptions, error) {
|
||||||
|
|||||||
@@ -2,15 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRunVersionJSON(t *testing.T) {
|
func TestRunVersionJSON(t *testing.T) {
|
||||||
@@ -53,34 +47,6 @@ func TestCommonOptionsRedactsAPIKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunBatchEmitsEORAfterVersion(t *testing.T) {
|
|
||||||
var stdout bytes.Buffer
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
|
|
||||||
in := strings.NewReader("version --json\n")
|
|
||||||
if err := runBatch(t.Context(), in, &stdout, &stderr); err != nil {
|
|
||||||
t.Fatalf("runBatch() error = %v; stderr = %s", err, stderr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
out := stdout.String()
|
|
||||||
if !strings.Contains(out, "\n"+batchEOR+"\n") && !strings.HasSuffix(out, batchEOR+"\n") {
|
|
||||||
t.Fatalf("expected EOR marker %q in stdout; got: %q", batchEOR, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
idx := strings.Index(out, batchEOR)
|
|
||||||
if idx <= 0 {
|
|
||||||
t.Fatalf("EOR marker not found or appeared before any output: %q", out)
|
|
||||||
}
|
|
||||||
payload := out[:idx]
|
|
||||||
var output versionOutput
|
|
||||||
if err := json.Unmarshal([]byte(payload), &output); err != nil {
|
|
||||||
t.Fatalf("parse JSON block before EOR: %v (payload=%q)", err, payload)
|
|
||||||
}
|
|
||||||
if output.GatewayProtocolVersion == 0 || output.WorkerProtocolVersion == 0 {
|
|
||||||
t.Fatalf("protocol versions were not populated: %+v", output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseValueBuildsTypedValue(t *testing.T) {
|
func TestParseValueBuildsTypedValue(t *testing.T) {
|
||||||
value, err := parseValue("int32", "123")
|
value, err := parseValue("int32", "123")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -90,207 +56,3 @@ func TestParseValueBuildsTypedValue(t *testing.T) {
|
|||||||
t.Fatalf("int32 value = %d, want 123", got)
|
t.Fatalf("int32 value = %d, want 123", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRunWriteBulkVariantGatesSecuredFlags pins the Client.Go-022 fix:
|
|
||||||
// secured-only flags must be unavailable on non-secured variants, and
|
|
||||||
// vice-versa, so a wrong-variant flag fails with a clean "flag provided
|
|
||||||
// but not defined" error instead of silently no-op'ing.
|
|
||||||
func TestRunWriteBulkVariantGatesSecuredFlags(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "write-bulk-rejects-current-user-id",
|
|
||||||
args: []string{"write-bulk", "-current-user-id", "5", "-item-handles", "1", "-values", "1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "write-bulk-rejects-verifier-user-id",
|
|
||||||
args: []string{"write-bulk", "-verifier-user-id", "5", "-item-handles", "1", "-values", "1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "write2-bulk-rejects-current-user-id",
|
|
||||||
args: []string{"write2-bulk", "-current-user-id", "5", "-item-handles", "1", "-values", "1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "write-secured-bulk-rejects-user-id",
|
|
||||||
args: []string{"write-secured-bulk", "-user-id", "5", "-item-handles", "1", "-values", "1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "write-secured2-bulk-rejects-user-id",
|
|
||||||
args: []string{"write-secured2-bulk", "-user-id", "5", "-item-handles", "1", "-values", "1"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
err := runWithIO(t.Context(), tc.args, &stdout, &stderr)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("runWithIO(%v) returned no error", tc.args)
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "flag provided but not defined") {
|
|
||||||
t.Fatalf("runWithIO(%v) error = %v; want 'flag provided but not defined'", tc.args, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRunBenchReadBulkRespectsContextCancellation pins the Client.Go-023
|
|
||||||
// fix: the warm-up and steady-state wall-clock loops must honour ctx.Err()
|
|
||||||
// so an external cancel (Ctrl+C, parent-cancel from a cross-language bench
|
|
||||||
// driver) short-circuits the bench instead of spinning failing ReadBulk
|
|
||||||
// calls until the wall-clock deadline elapses.
|
|
||||||
func TestRunBenchReadBulkRespectsContextCancellation(t *testing.T) {
|
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("listen: %v", err)
|
|
||||||
}
|
|
||||||
server := grpc.NewServer()
|
|
||||||
fake := &benchFakeGateway{}
|
|
||||||
pb.RegisterMxAccessGatewayServer(server, fake)
|
|
||||||
go func() {
|
|
||||||
_ = server.Serve(listener)
|
|
||||||
}()
|
|
||||||
defer server.Stop()
|
|
||||||
defer listener.Close()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Long warm-up + duration, so if the ctx.Err() guard were missing the
|
|
||||||
// loops would run for ~10s. With the guard, the cancel below short-
|
|
||||||
// circuits both loops within ~one ReadBulk iteration.
|
|
||||||
args := []string{
|
|
||||||
"bench-read-bulk",
|
|
||||||
"-endpoint", listener.Addr().String(),
|
|
||||||
"-plaintext",
|
|
||||||
"-api-key", "test",
|
|
||||||
"-warmup-seconds", "5",
|
|
||||||
"-duration-seconds", "5",
|
|
||||||
"-bulk-size", "1",
|
|
||||||
"-timeout-ms", "100",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel after a brief delay — far less than warmup+duration (10s).
|
|
||||||
go func() {
|
|
||||||
time.Sleep(150 * time.Millisecond)
|
|
||||||
cancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
start := time.Now()
|
|
||||||
err = runWithIO(ctx, args, &stdout, &stderr)
|
|
||||||
elapsed := time.Since(start)
|
|
||||||
|
|
||||||
// With the ctx.Err() guard, the loops exit well before the wall-clock
|
|
||||||
// deadlines (warmup=5s + duration=5s = 10s). Allow generous slack for
|
|
||||||
// CI noise but assert clearly less than the un-guarded worst case.
|
|
||||||
if elapsed > 4*time.Second {
|
|
||||||
t.Fatalf("bench-read-bulk took %s after ctx cancel; want <4s (ctx.Err() guard missing?). err=%v stderr=%s", elapsed, err, stderr.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// benchFakeGateway is a minimal MxAccessGatewayServer that satisfies the
|
|
||||||
// bench-read-bulk session-setup sequence (OpenSession + Invoke for Register
|
|
||||||
// / SubscribeBulk / ReadBulk / UnsubscribeBulk / CloseSession).
|
|
||||||
type benchFakeGateway struct {
|
|
||||||
pb.UnimplementedMxAccessGatewayServer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *benchFakeGateway) OpenSession(_ context.Context, _ *pb.OpenSessionRequest) (*pb.OpenSessionReply, error) {
|
|
||||||
return &pb.OpenSessionReply{
|
|
||||||
SessionId: "bench-session",
|
|
||||||
ProtocolStatus: &pb.ProtocolStatus{Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *benchFakeGateway) CloseSession(_ context.Context, req *pb.CloseSessionRequest) (*pb.CloseSessionReply, error) {
|
|
||||||
return &pb.CloseSessionReply{
|
|
||||||
SessionId: req.GetSessionId(),
|
|
||||||
ProtocolStatus: &pb.ProtocolStatus{Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *benchFakeGateway) Invoke(_ context.Context, req *pb.MxCommandRequest) (*pb.MxCommandReply, error) {
|
|
||||||
kind := req.GetCommand().GetKind()
|
|
||||||
reply := &pb.MxCommandReply{
|
|
||||||
SessionId: req.GetSessionId(),
|
|
||||||
Kind: kind,
|
|
||||||
ProtocolStatus: &pb.ProtocolStatus{Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK},
|
|
||||||
}
|
|
||||||
switch kind {
|
|
||||||
case pb.MxCommandKind_MX_COMMAND_KIND_REGISTER:
|
|
||||||
reply.Payload = &pb.MxCommandReply_Register{Register: &pb.RegisterReply{ServerHandle: 1}}
|
|
||||||
case pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK:
|
|
||||||
reply.Payload = &pb.MxCommandReply_SubscribeBulk{SubscribeBulk: &pb.BulkSubscribeReply{
|
|
||||||
Results: []*pb.SubscribeResult{{ServerHandle: 1, ItemHandle: 1, WasSuccessful: true}},
|
|
||||||
}}
|
|
||||||
case pb.MxCommandKind_MX_COMMAND_KIND_READ_BULK:
|
|
||||||
reply.Payload = &pb.MxCommandReply_ReadBulk{ReadBulk: &pb.BulkReadReply{
|
|
||||||
Results: []*pb.BulkReadResult{{ItemHandle: 1, WasSuccessful: true, WasCached: true}},
|
|
||||||
}}
|
|
||||||
case pb.MxCommandKind_MX_COMMAND_KIND_UNSUBSCRIBE_BULK:
|
|
||||||
reply.Payload = &pb.MxCommandReply_UnsubscribeBulk{UnsubscribeBulk: &pb.BulkSubscribeReply{}}
|
|
||||||
}
|
|
||||||
return reply, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRunBenchReadBulkRejectsNonPositiveBulkSize pins the Client.Go-023-adjacent
|
|
||||||
// positivity checks so they cannot drift while resolving the cancellation finding.
|
|
||||||
func TestRunBenchReadBulkRejectsNonPositiveBulkSize(t *testing.T) {
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
err := runWithIO(t.Context(), []string{"bench-read-bulk", "-bulk-size", "0"}, &stdout, &stderr)
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "bulk-size must be positive") {
|
|
||||||
t.Fatalf("bench-read-bulk -bulk-size 0 error = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRunBatchSkipsBlankLinesAndContinuesUntilEOF pins the Client.Go-027 fix:
|
|
||||||
// a blank line in the middle of a batch session must NOT terminate the loop —
|
|
||||||
// only stdin EOF ends the session.
|
|
||||||
func TestRunBatchSkipsBlankLinesAndContinuesUntilEOF(t *testing.T) {
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
|
|
||||||
// version -> blank -> version (a stray blank line in the middle of a
|
|
||||||
// programmatic session).
|
|
||||||
in := strings.NewReader("version --json\n\nversion --json\n")
|
|
||||||
if err := runBatch(t.Context(), in, &stdout, &stderr); err != nil {
|
|
||||||
t.Fatalf("runBatch() error = %v; stderr = %s", err, stderr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
out := stdout.String()
|
|
||||||
// Both version commands must have produced a result before the EOR sentinel.
|
|
||||||
if count := strings.Count(out, batchEOR); count != 2 {
|
|
||||||
t.Fatalf("EOR sentinel count = %d, want 2 (one per command, blank line skipped); out = %q", count, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRunBatchHandlesLongCommandLine pins the Client.Go-026 fix: a command
|
|
||||||
// line longer than the default bufio.Scanner token size (64 KiB) must not
|
|
||||||
// abort the batch session.
|
|
||||||
func TestRunBatchHandlesLongCommandLine(t *testing.T) {
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
|
|
||||||
// Build a single command line larger than 64 KiB. The command itself is
|
|
||||||
// invalid (no real session) but runBatch must still emit an EOR sentinel
|
|
||||||
// and continue to the next command rather than dropping the line on the
|
|
||||||
// floor with a bufio.ErrTooLong from the outer return.
|
|
||||||
huge := strings.Repeat("tag-with-a-reasonably-long-name-and-suffix,", 2000) + "trailing"
|
|
||||||
line := "subscribe-bulk -session-id none -items " + huge
|
|
||||||
if len(line) <= 64*1024 {
|
|
||||||
t.Fatalf("test setup error: long line length = %d, want > 64KiB", len(line))
|
|
||||||
}
|
|
||||||
in := strings.NewReader(line + "\nversion --json\n")
|
|
||||||
|
|
||||||
if err := runBatch(t.Context(), in, &stdout, &stderr); err != nil {
|
|
||||||
t.Fatalf("runBatch() error = %v; stderr = %s", err, stderr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
out := stdout.String()
|
|
||||||
// Both commands must produce an EOR sentinel — the long line should be a
|
|
||||||
// per-command error (still emitted with EOR), then the version command
|
|
||||||
// should run normally.
|
|
||||||
if count := strings.Count(out, batchEOR); count != 2 {
|
|
||||||
t.Fatalf("EOR sentinel count = %d, want 2 (one per command, even when first is too long); out length = %d", count, len(out))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,20 +2,20 @@ Set-StrictMode -Version Latest
|
|||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..')
|
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..')
|
||||||
$protoRoot = Join-Path $repoRoot 'src\ZB.MOM.WW.MxGateway.Contracts\Protos'
|
$protoRoot = Join-Path $repoRoot 'src\MxGateway.Contracts\Protos'
|
||||||
$outputRoot = Join-Path $PSScriptRoot 'internal\generated'
|
$outputRoot = Join-Path $PSScriptRoot 'internal\generated'
|
||||||
$modulePath = 'gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated'
|
$modulePath = 'gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated'
|
||||||
$protoc = 'C:\Users\dohertj2\AppData\Local\Microsoft\WinGet\Packages\Google.Protobuf_Microsoft.Winget.Source_8wekyb3d8bbwe\bin\protoc.exe'
|
$protoc = 'C:\Users\dohertj2\AppData\Local\Microsoft\WinGet\Packages\Google.Protobuf_Microsoft.Winget.Source_8wekyb3d8bbwe\bin\protoc.exe'
|
||||||
$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/ToolchainLinks.md."
|
throw "protoc was not found at $protoc. See docs/toolchain-links.md."
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($pluginName in @('protoc-gen-go.exe', 'protoc-gen-go-grpc.exe')) {
|
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/ToolchainLinks.md."
|
throw "$pluginName was not found at $pluginPath. See docs/toolchain-links.md."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -690,26 +690,12 @@ type GalaxyAttribute struct {
|
|||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
AttributeName string `protobuf:"bytes,1,opt,name=attribute_name,json=attributeName,proto3" json:"attribute_name,omitempty"`
|
AttributeName string `protobuf:"bytes,1,opt,name=attribute_name,json=attributeName,proto3" json:"attribute_name,omitempty"`
|
||||||
FullTagReference string `protobuf:"bytes,2,opt,name=full_tag_reference,json=fullTagReference,proto3" json:"full_tag_reference,omitempty"`
|
FullTagReference string `protobuf:"bytes,2,opt,name=full_tag_reference,json=fullTagReference,proto3" json:"full_tag_reference,omitempty"`
|
||||||
// Raw Galaxy SQL `dbo.data_type` identifier, passed through unchanged.
|
|
||||||
// This is NOT a member of `mxaccess_gateway.v1.MxDataType` — Galaxy's
|
|
||||||
// type enumeration is distinct from MXAccess's wire data-type enum and
|
|
||||||
// the two must not be cast or compared. The GalaxyRepository service is
|
|
||||||
// metadata-only and deliberately does not share types with
|
|
||||||
// mxaccess_gateway.proto. See docs/GalaxyRepository.md.
|
|
||||||
MxDataType int32 `protobuf:"varint,3,opt,name=mx_data_type,json=mxDataType,proto3" json:"mx_data_type,omitempty"`
|
MxDataType int32 `protobuf:"varint,3,opt,name=mx_data_type,json=mxDataType,proto3" json:"mx_data_type,omitempty"`
|
||||||
// Human-readable name from Galaxy's `dbo.data_type` table (e.g. "Float",
|
|
||||||
// "Integer", "Boolean"). Free-form Galaxy text; not a stable enum.
|
|
||||||
DataTypeName string `protobuf:"bytes,4,opt,name=data_type_name,json=dataTypeName,proto3" json:"data_type_name,omitempty"`
|
DataTypeName string `protobuf:"bytes,4,opt,name=data_type_name,json=dataTypeName,proto3" json:"data_type_name,omitempty"`
|
||||||
IsArray bool `protobuf:"varint,5,opt,name=is_array,json=isArray,proto3" json:"is_array,omitempty"`
|
IsArray bool `protobuf:"varint,5,opt,name=is_array,json=isArray,proto3" json:"is_array,omitempty"`
|
||||||
ArrayDimension int32 `protobuf:"varint,6,opt,name=array_dimension,json=arrayDimension,proto3" json:"array_dimension,omitempty"`
|
ArrayDimension int32 `protobuf:"varint,6,opt,name=array_dimension,json=arrayDimension,proto3" json:"array_dimension,omitempty"`
|
||||||
ArrayDimensionPresent bool `protobuf:"varint,7,opt,name=array_dimension_present,json=arrayDimensionPresent,proto3" json:"array_dimension_present,omitempty"`
|
ArrayDimensionPresent bool `protobuf:"varint,7,opt,name=array_dimension_present,json=arrayDimensionPresent,proto3" json:"array_dimension_present,omitempty"`
|
||||||
// Raw Galaxy SQL attribute-category identifier, passed through unchanged.
|
|
||||||
// Galaxy-specific; not mapped to any gateway enum. See
|
|
||||||
// docs/GalaxyRepository.md.
|
|
||||||
MxAttributeCategory int32 `protobuf:"varint,8,opt,name=mx_attribute_category,json=mxAttributeCategory,proto3" json:"mx_attribute_category,omitempty"`
|
MxAttributeCategory int32 `protobuf:"varint,8,opt,name=mx_attribute_category,json=mxAttributeCategory,proto3" json:"mx_attribute_category,omitempty"`
|
||||||
// Raw Galaxy SQL security-classification identifier, passed through
|
|
||||||
// unchanged. Galaxy-specific; not mapped to any gateway enum. See
|
|
||||||
// docs/GalaxyRepository.md.
|
|
||||||
SecurityClassification int32 `protobuf:"varint,9,opt,name=security_classification,json=securityClassification,proto3" json:"security_classification,omitempty"`
|
SecurityClassification int32 `protobuf:"varint,9,opt,name=security_classification,json=securityClassification,proto3" json:"security_classification,omitempty"`
|
||||||
IsHistorized bool `protobuf:"varint,10,opt,name=is_historized,json=isHistorized,proto3" json:"is_historized,omitempty"`
|
IsHistorized bool `protobuf:"varint,10,opt,name=is_historized,json=isHistorized,proto3" json:"is_historized,omitempty"`
|
||||||
IsAlarm bool `protobuf:"varint,11,opt,name=is_alarm,json=isAlarm,proto3" json:"is_alarm,omitempty"`
|
IsAlarm bool `protobuf:"varint,11,opt,name=is_alarm,json=isAlarm,proto3" json:"is_alarm,omitempty"`
|
||||||
@@ -824,260 +810,6 @@ func (x *GalaxyAttribute) GetIsAlarm() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type BrowseChildrenRequest struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
// Parent selector. Empty oneof returns root objects (parent_gobject_id == 0).
|
|
||||||
//
|
|
||||||
// Types that are valid to be assigned to Parent:
|
|
||||||
//
|
|
||||||
// *BrowseChildrenRequest_ParentGobjectId
|
|
||||||
// *BrowseChildrenRequest_ParentTagName
|
|
||||||
// *BrowseChildrenRequest_ParentContainedPath
|
|
||||||
Parent isBrowseChildrenRequest_Parent `protobuf_oneof:"parent"`
|
|
||||||
// Maximum number of direct children to return. Server default 500; cap 5000.
|
|
||||||
PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
|
|
||||||
// Opaque token returned by a previous BrowseChildren response. Bound to the
|
|
||||||
// cache sequence, parent selector, and the filter set; a mismatch returns
|
|
||||||
// InvalidArgument.
|
|
||||||
PageToken string `protobuf:"bytes,5,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"`
|
|
||||||
// --- Filter parity with DiscoverHierarchy. AND-combined. ---
|
|
||||||
CategoryIds []int32 `protobuf:"varint,6,rep,packed,name=category_ids,json=categoryIds,proto3" json:"category_ids,omitempty"`
|
|
||||||
TemplateChainContains []string `protobuf:"bytes,7,rep,name=template_chain_contains,json=templateChainContains,proto3" json:"template_chain_contains,omitempty"`
|
|
||||||
TagNameGlob string `protobuf:"bytes,8,opt,name=tag_name_glob,json=tagNameGlob,proto3" json:"tag_name_glob,omitempty"`
|
|
||||||
IncludeAttributes *bool `protobuf:"varint,9,opt,name=include_attributes,json=includeAttributes,proto3,oneof" json:"include_attributes,omitempty"`
|
|
||||||
AlarmBearingOnly bool `protobuf:"varint,10,opt,name=alarm_bearing_only,json=alarmBearingOnly,proto3" json:"alarm_bearing_only,omitempty"`
|
|
||||||
HistorizedOnly bool `protobuf:"varint,11,opt,name=historized_only,json=historizedOnly,proto3" json:"historized_only,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) Reset() {
|
|
||||||
*x = BrowseChildrenRequest{}
|
|
||||||
mi := &file_galaxy_repository_proto_msgTypes[10]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*BrowseChildrenRequest) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_galaxy_repository_proto_msgTypes[10]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use BrowseChildrenRequest.ProtoReflect.Descriptor instead.
|
|
||||||
func (*BrowseChildrenRequest) Descriptor() ([]byte, []int) {
|
|
||||||
return file_galaxy_repository_proto_rawDescGZIP(), []int{10}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) GetParent() isBrowseChildrenRequest_Parent {
|
|
||||||
if x != nil {
|
|
||||||
return x.Parent
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) GetParentGobjectId() int32 {
|
|
||||||
if x != nil {
|
|
||||||
if x, ok := x.Parent.(*BrowseChildrenRequest_ParentGobjectId); ok {
|
|
||||||
return x.ParentGobjectId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) GetParentTagName() string {
|
|
||||||
if x != nil {
|
|
||||||
if x, ok := x.Parent.(*BrowseChildrenRequest_ParentTagName); ok {
|
|
||||||
return x.ParentTagName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) GetParentContainedPath() string {
|
|
||||||
if x != nil {
|
|
||||||
if x, ok := x.Parent.(*BrowseChildrenRequest_ParentContainedPath); ok {
|
|
||||||
return x.ParentContainedPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) GetPageSize() int32 {
|
|
||||||
if x != nil {
|
|
||||||
return x.PageSize
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) GetPageToken() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.PageToken
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) GetCategoryIds() []int32 {
|
|
||||||
if x != nil {
|
|
||||||
return x.CategoryIds
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) GetTemplateChainContains() []string {
|
|
||||||
if x != nil {
|
|
||||||
return x.TemplateChainContains
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) GetTagNameGlob() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.TagNameGlob
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) GetIncludeAttributes() bool {
|
|
||||||
if x != nil && x.IncludeAttributes != nil {
|
|
||||||
return *x.IncludeAttributes
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) GetAlarmBearingOnly() bool {
|
|
||||||
if x != nil {
|
|
||||||
return x.AlarmBearingOnly
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenRequest) GetHistorizedOnly() bool {
|
|
||||||
if x != nil {
|
|
||||||
return x.HistorizedOnly
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type isBrowseChildrenRequest_Parent interface {
|
|
||||||
isBrowseChildrenRequest_Parent()
|
|
||||||
}
|
|
||||||
|
|
||||||
type BrowseChildrenRequest_ParentGobjectId struct {
|
|
||||||
ParentGobjectId int32 `protobuf:"varint,1,opt,name=parent_gobject_id,json=parentGobjectId,proto3,oneof"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BrowseChildrenRequest_ParentTagName struct {
|
|
||||||
ParentTagName string `protobuf:"bytes,2,opt,name=parent_tag_name,json=parentTagName,proto3,oneof"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BrowseChildrenRequest_ParentContainedPath struct {
|
|
||||||
ParentContainedPath string `protobuf:"bytes,3,opt,name=parent_contained_path,json=parentContainedPath,proto3,oneof"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*BrowseChildrenRequest_ParentGobjectId) isBrowseChildrenRequest_Parent() {}
|
|
||||||
|
|
||||||
func (*BrowseChildrenRequest_ParentTagName) isBrowseChildrenRequest_Parent() {}
|
|
||||||
|
|
||||||
func (*BrowseChildrenRequest_ParentContainedPath) isBrowseChildrenRequest_Parent() {}
|
|
||||||
|
|
||||||
type BrowseChildrenReply struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
// Direct children matching the filter, sorted areas-first then by
|
|
||||||
// case-insensitive display name (same order as the dashboard tree).
|
|
||||||
Children []*GalaxyObject `protobuf:"bytes,1,rep,name=children,proto3" json:"children,omitempty"`
|
|
||||||
// Non-empty when another page of siblings is available.
|
|
||||||
NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"`
|
|
||||||
// Total matching direct children of the parent (post-filter).
|
|
||||||
TotalChildCount int32 `protobuf:"varint,3,opt,name=total_child_count,json=totalChildCount,proto3" json:"total_child_count,omitempty"`
|
|
||||||
// Parallel array, indexed with `children`. True when the child has at least
|
|
||||||
// one matching descendant under the same filter set. Lets a UI choose
|
|
||||||
// whether to draw an expand triangle without an extra round trip.
|
|
||||||
ChildHasChildren []bool `protobuf:"varint,4,rep,packed,name=child_has_children,json=childHasChildren,proto3" json:"child_has_children,omitempty"`
|
|
||||||
// Cache sequence this reply was projected from. Clients may pass it back as
|
|
||||||
// part of the page_token contract. Mismatch on the next page -> InvalidArgument.
|
|
||||||
CacheSequence uint64 `protobuf:"varint,5,opt,name=cache_sequence,json=cacheSequence,proto3" json:"cache_sequence,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenReply) Reset() {
|
|
||||||
*x = BrowseChildrenReply{}
|
|
||||||
mi := &file_galaxy_repository_proto_msgTypes[11]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenReply) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*BrowseChildrenReply) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenReply) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_galaxy_repository_proto_msgTypes[11]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use BrowseChildrenReply.ProtoReflect.Descriptor instead.
|
|
||||||
func (*BrowseChildrenReply) Descriptor() ([]byte, []int) {
|
|
||||||
return file_galaxy_repository_proto_rawDescGZIP(), []int{11}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenReply) GetChildren() []*GalaxyObject {
|
|
||||||
if x != nil {
|
|
||||||
return x.Children
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenReply) GetNextPageToken() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.NextPageToken
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenReply) GetTotalChildCount() int32 {
|
|
||||||
if x != nil {
|
|
||||||
return x.TotalChildCount
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenReply) GetChildHasChildren() []bool {
|
|
||||||
if x != nil {
|
|
||||||
return x.ChildHasChildren
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BrowseChildrenReply) GetCacheSequence() uint64 {
|
|
||||||
if x != nil {
|
|
||||||
return x.CacheSequence
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var File_galaxy_repository_proto protoreflect.FileDescriptor
|
var File_galaxy_repository_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
const file_galaxy_repository_proto_rawDesc = "" +
|
const file_galaxy_repository_proto_rawDesc = "" +
|
||||||
@@ -1151,35 +883,12 @@ const file_galaxy_repository_proto_rawDesc = "" +
|
|||||||
"\x17security_classification\x18\t \x01(\x05R\x16securityClassification\x12#\n" +
|
"\x17security_classification\x18\t \x01(\x05R\x16securityClassification\x12#\n" +
|
||||||
"\ris_historized\x18\n" +
|
"\ris_historized\x18\n" +
|
||||||
" \x01(\bR\fisHistorized\x12\x19\n" +
|
" \x01(\bR\fisHistorized\x12\x19\n" +
|
||||||
"\bis_alarm\x18\v \x01(\bR\aisAlarm\"\x8c\x04\n" +
|
"\bis_alarm\x18\v \x01(\bR\aisAlarm2\xcc\x03\n" +
|
||||||
"\x15BrowseChildrenRequest\x12,\n" +
|
|
||||||
"\x11parent_gobject_id\x18\x01 \x01(\x05H\x00R\x0fparentGobjectId\x12(\n" +
|
|
||||||
"\x0fparent_tag_name\x18\x02 \x01(\tH\x00R\rparentTagName\x124\n" +
|
|
||||||
"\x15parent_contained_path\x18\x03 \x01(\tH\x00R\x13parentContainedPath\x12\x1b\n" +
|
|
||||||
"\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12\x1d\n" +
|
|
||||||
"\n" +
|
|
||||||
"page_token\x18\x05 \x01(\tR\tpageToken\x12!\n" +
|
|
||||||
"\fcategory_ids\x18\x06 \x03(\x05R\vcategoryIds\x126\n" +
|
|
||||||
"\x17template_chain_contains\x18\a \x03(\tR\x15templateChainContains\x12\"\n" +
|
|
||||||
"\rtag_name_glob\x18\b \x01(\tR\vtagNameGlob\x122\n" +
|
|
||||||
"\x12include_attributes\x18\t \x01(\bH\x01R\x11includeAttributes\x88\x01\x01\x12,\n" +
|
|
||||||
"\x12alarm_bearing_only\x18\n" +
|
|
||||||
" \x01(\bR\x10alarmBearingOnly\x12'\n" +
|
|
||||||
"\x0fhistorized_only\x18\v \x01(\bR\x0ehistorizedOnlyB\b\n" +
|
|
||||||
"\x06parentB\x15\n" +
|
|
||||||
"\x13_include_attributes\"\xfe\x01\n" +
|
|
||||||
"\x13BrowseChildrenReply\x12>\n" +
|
|
||||||
"\bchildren\x18\x01 \x03(\v2\".galaxy_repository.v1.GalaxyObjectR\bchildren\x12&\n" +
|
|
||||||
"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12*\n" +
|
|
||||||
"\x11total_child_count\x18\x03 \x01(\x05R\x0ftotalChildCount\x12,\n" +
|
|
||||||
"\x12child_has_children\x18\x04 \x03(\bR\x10childHasChildren\x12%\n" +
|
|
||||||
"\x0ecache_sequence\x18\x05 \x01(\x04R\rcacheSequence2\xb6\x04\n" +
|
|
||||||
"\x10GalaxyRepository\x12h\n" +
|
"\x10GalaxyRepository\x12h\n" +
|
||||||
"\x0eTestConnection\x12+.galaxy_repository.v1.TestConnectionRequest\x1a).galaxy_repository.v1.TestConnectionReply\x12q\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" +
|
"\x11GetLastDeployTime\x12..galaxy_repository.v1.GetLastDeployTimeRequest\x1a,.galaxy_repository.v1.GetLastDeployTimeReply\x12q\n" +
|
||||||
"\x11DiscoverHierarchy\x12..galaxy_repository.v1.DiscoverHierarchyRequest\x1a,.galaxy_repository.v1.DiscoverHierarchyReply\x12h\n" +
|
"\x11DiscoverHierarchy\x12..galaxy_repository.v1.DiscoverHierarchyRequest\x1a,.galaxy_repository.v1.DiscoverHierarchyReply\x12h\n" +
|
||||||
"\x11WatchDeployEvents\x12..galaxy_repository.v1.WatchDeployEventsRequest\x1a!.galaxy_repository.v1.DeployEvent0\x01\x12h\n" +
|
"\x11WatchDeployEvents\x12..galaxy_repository.v1.WatchDeployEventsRequest\x1a!.galaxy_repository.v1.DeployEvent0\x01B#\xaa\x02 MxGateway.Contracts.Proto.Galaxyb\x06proto3"
|
||||||
"\x0eBrowseChildren\x12+.galaxy_repository.v1.BrowseChildrenRequest\x1a).galaxy_repository.v1.BrowseChildrenReplyB-\xaa\x02*ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxyb\x06proto3"
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_galaxy_repository_proto_rawDescOnce sync.Once
|
file_galaxy_repository_proto_rawDescOnce sync.Once
|
||||||
@@ -1193,7 +902,7 @@ func file_galaxy_repository_proto_rawDescGZIP() []byte {
|
|||||||
return file_galaxy_repository_proto_rawDescData
|
return file_galaxy_repository_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_galaxy_repository_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
var file_galaxy_repository_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
|
||||||
var file_galaxy_repository_proto_goTypes = []any{
|
var file_galaxy_repository_proto_goTypes = []any{
|
||||||
(*TestConnectionRequest)(nil), // 0: galaxy_repository.v1.TestConnectionRequest
|
(*TestConnectionRequest)(nil), // 0: galaxy_repository.v1.TestConnectionRequest
|
||||||
(*TestConnectionReply)(nil), // 1: galaxy_repository.v1.TestConnectionReply
|
(*TestConnectionReply)(nil), // 1: galaxy_repository.v1.TestConnectionReply
|
||||||
@@ -1205,35 +914,30 @@ var file_galaxy_repository_proto_goTypes = []any{
|
|||||||
(*DeployEvent)(nil), // 7: galaxy_repository.v1.DeployEvent
|
(*DeployEvent)(nil), // 7: galaxy_repository.v1.DeployEvent
|
||||||
(*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
|
||||||
(*BrowseChildrenRequest)(nil), // 10: galaxy_repository.v1.BrowseChildrenRequest
|
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
|
||||||
(*BrowseChildrenReply)(nil), // 11: galaxy_repository.v1.BrowseChildrenReply
|
(*wrapperspb.Int32Value)(nil), // 11: google.protobuf.Int32Value
|
||||||
(*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp
|
|
||||||
(*wrapperspb.Int32Value)(nil), // 13: google.protobuf.Int32Value
|
|
||||||
}
|
}
|
||||||
var file_galaxy_repository_proto_depIdxs = []int32{
|
var file_galaxy_repository_proto_depIdxs = []int32{
|
||||||
12, // 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
|
||||||
13, // 1: galaxy_repository.v1.DiscoverHierarchyRequest.max_depth:type_name -> google.protobuf.Int32Value
|
11, // 1: galaxy_repository.v1.DiscoverHierarchyRequest.max_depth:type_name -> google.protobuf.Int32Value
|
||||||
8, // 2: galaxy_repository.v1.DiscoverHierarchyReply.objects:type_name -> galaxy_repository.v1.GalaxyObject
|
8, // 2: galaxy_repository.v1.DiscoverHierarchyReply.objects:type_name -> galaxy_repository.v1.GalaxyObject
|
||||||
12, // 3: galaxy_repository.v1.WatchDeployEventsRequest.last_seen_deploy_time:type_name -> google.protobuf.Timestamp
|
10, // 3: galaxy_repository.v1.WatchDeployEventsRequest.last_seen_deploy_time:type_name -> google.protobuf.Timestamp
|
||||||
12, // 4: galaxy_repository.v1.DeployEvent.observed_at:type_name -> google.protobuf.Timestamp
|
10, // 4: galaxy_repository.v1.DeployEvent.observed_at:type_name -> google.protobuf.Timestamp
|
||||||
12, // 5: galaxy_repository.v1.DeployEvent.time_of_last_deploy:type_name -> google.protobuf.Timestamp
|
10, // 5: galaxy_repository.v1.DeployEvent.time_of_last_deploy:type_name -> google.protobuf.Timestamp
|
||||||
9, // 6: galaxy_repository.v1.GalaxyObject.attributes:type_name -> galaxy_repository.v1.GalaxyAttribute
|
9, // 6: galaxy_repository.v1.GalaxyObject.attributes:type_name -> galaxy_repository.v1.GalaxyAttribute
|
||||||
8, // 7: galaxy_repository.v1.BrowseChildrenReply.children:type_name -> galaxy_repository.v1.GalaxyObject
|
0, // 7: galaxy_repository.v1.GalaxyRepository.TestConnection:input_type -> galaxy_repository.v1.TestConnectionRequest
|
||||||
0, // 8: galaxy_repository.v1.GalaxyRepository.TestConnection:input_type -> galaxy_repository.v1.TestConnectionRequest
|
2, // 8: galaxy_repository.v1.GalaxyRepository.GetLastDeployTime:input_type -> galaxy_repository.v1.GetLastDeployTimeRequest
|
||||||
2, // 9: galaxy_repository.v1.GalaxyRepository.GetLastDeployTime:input_type -> galaxy_repository.v1.GetLastDeployTimeRequest
|
4, // 9: galaxy_repository.v1.GalaxyRepository.DiscoverHierarchy:input_type -> galaxy_repository.v1.DiscoverHierarchyRequest
|
||||||
4, // 10: galaxy_repository.v1.GalaxyRepository.DiscoverHierarchy:input_type -> galaxy_repository.v1.DiscoverHierarchyRequest
|
6, // 10: galaxy_repository.v1.GalaxyRepository.WatchDeployEvents:input_type -> galaxy_repository.v1.WatchDeployEventsRequest
|
||||||
6, // 11: galaxy_repository.v1.GalaxyRepository.WatchDeployEvents:input_type -> galaxy_repository.v1.WatchDeployEventsRequest
|
1, // 11: galaxy_repository.v1.GalaxyRepository.TestConnection:output_type -> galaxy_repository.v1.TestConnectionReply
|
||||||
10, // 12: galaxy_repository.v1.GalaxyRepository.BrowseChildren:input_type -> galaxy_repository.v1.BrowseChildrenRequest
|
3, // 12: galaxy_repository.v1.GalaxyRepository.GetLastDeployTime:output_type -> galaxy_repository.v1.GetLastDeployTimeReply
|
||||||
1, // 13: galaxy_repository.v1.GalaxyRepository.TestConnection:output_type -> galaxy_repository.v1.TestConnectionReply
|
5, // 13: galaxy_repository.v1.GalaxyRepository.DiscoverHierarchy:output_type -> galaxy_repository.v1.DiscoverHierarchyReply
|
||||||
3, // 14: galaxy_repository.v1.GalaxyRepository.GetLastDeployTime:output_type -> galaxy_repository.v1.GetLastDeployTimeReply
|
7, // 14: galaxy_repository.v1.GalaxyRepository.WatchDeployEvents:output_type -> galaxy_repository.v1.DeployEvent
|
||||||
5, // 15: galaxy_repository.v1.GalaxyRepository.DiscoverHierarchy:output_type -> galaxy_repository.v1.DiscoverHierarchyReply
|
11, // [11:15] is the sub-list for method output_type
|
||||||
7, // 16: galaxy_repository.v1.GalaxyRepository.WatchDeployEvents:output_type -> galaxy_repository.v1.DeployEvent
|
7, // [7:11] is the sub-list for method input_type
|
||||||
11, // 17: galaxy_repository.v1.GalaxyRepository.BrowseChildren:output_type -> galaxy_repository.v1.BrowseChildrenReply
|
7, // [7:7] is the sub-list for extension type_name
|
||||||
13, // [13:18] is the sub-list for method output_type
|
7, // [7:7] is the sub-list for extension extendee
|
||||||
8, // [8:13] is the sub-list for method input_type
|
0, // [0:7] is the sub-list for field type_name
|
||||||
8, // [8:8] is the sub-list for extension type_name
|
|
||||||
8, // [8:8] is the sub-list for extension extendee
|
|
||||||
0, // [0:8] is the sub-list for field type_name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_galaxy_repository_proto_init() }
|
func init() { file_galaxy_repository_proto_init() }
|
||||||
@@ -1246,18 +950,13 @@ func file_galaxy_repository_proto_init() {
|
|||||||
(*DiscoverHierarchyRequest_RootTagName)(nil),
|
(*DiscoverHierarchyRequest_RootTagName)(nil),
|
||||||
(*DiscoverHierarchyRequest_RootContainedPath)(nil),
|
(*DiscoverHierarchyRequest_RootContainedPath)(nil),
|
||||||
}
|
}
|
||||||
file_galaxy_repository_proto_msgTypes[10].OneofWrappers = []any{
|
|
||||||
(*BrowseChildrenRequest_ParentGobjectId)(nil),
|
|
||||||
(*BrowseChildrenRequest_ParentTagName)(nil),
|
|
||||||
(*BrowseChildrenRequest_ParentContainedPath)(nil),
|
|
||||||
}
|
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_galaxy_repository_proto_rawDesc), len(file_galaxy_repository_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_galaxy_repository_proto_rawDesc), len(file_galaxy_repository_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 12,
|
NumMessages: 10,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.2
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
// - protoc v7.34.1
|
// - protoc v7.34.1
|
||||||
// source: galaxy_repository.proto
|
// source: galaxy_repository.proto
|
||||||
|
|
||||||
@@ -23,7 +23,6 @@ const (
|
|||||||
GalaxyRepository_GetLastDeployTime_FullMethodName = "/galaxy_repository.v1.GalaxyRepository/GetLastDeployTime"
|
GalaxyRepository_GetLastDeployTime_FullMethodName = "/galaxy_repository.v1.GalaxyRepository/GetLastDeployTime"
|
||||||
GalaxyRepository_DiscoverHierarchy_FullMethodName = "/galaxy_repository.v1.GalaxyRepository/DiscoverHierarchy"
|
GalaxyRepository_DiscoverHierarchy_FullMethodName = "/galaxy_repository.v1.GalaxyRepository/DiscoverHierarchy"
|
||||||
GalaxyRepository_WatchDeployEvents_FullMethodName = "/galaxy_repository.v1.GalaxyRepository/WatchDeployEvents"
|
GalaxyRepository_WatchDeployEvents_FullMethodName = "/galaxy_repository.v1.GalaxyRepository/WatchDeployEvents"
|
||||||
GalaxyRepository_BrowseChildren_FullMethodName = "/galaxy_repository.v1.GalaxyRepository/BrowseChildren"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GalaxyRepositoryClient is the client API for GalaxyRepository service.
|
// GalaxyRepositoryClient is the client API for GalaxyRepository service.
|
||||||
@@ -45,11 +44,6 @@ type GalaxyRepositoryClient interface {
|
|||||||
// increasing per server start; gaps indicate the per-subscriber buffer dropped
|
// increasing per server start; gaps indicate the per-subscriber buffer dropped
|
||||||
// older events because the client was too slow.
|
// older events because the client was too slow.
|
||||||
WatchDeployEvents(ctx context.Context, in *WatchDeployEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[DeployEvent], error)
|
WatchDeployEvents(ctx context.Context, in *WatchDeployEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[DeployEvent], error)
|
||||||
// Returns the direct children of a parent object (or the root objects when
|
|
||||||
// `parent` is unset). Designed for OPC UA-style lazy expand: clients walk
|
|
||||||
// one level at a time instead of paging the full hierarchy. Filters mirror
|
|
||||||
// DiscoverHierarchy exactly. Backed by the same shared hierarchy cache.
|
|
||||||
BrowseChildren(ctx context.Context, in *BrowseChildrenRequest, opts ...grpc.CallOption) (*BrowseChildrenReply, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type galaxyRepositoryClient struct {
|
type galaxyRepositoryClient struct {
|
||||||
@@ -109,16 +103,6 @@ func (c *galaxyRepositoryClient) WatchDeployEvents(ctx context.Context, in *Watc
|
|||||||
// 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 GalaxyRepository_WatchDeployEventsClient = grpc.ServerStreamingClient[DeployEvent]
|
type GalaxyRepository_WatchDeployEventsClient = grpc.ServerStreamingClient[DeployEvent]
|
||||||
|
|
||||||
func (c *galaxyRepositoryClient) BrowseChildren(ctx context.Context, in *BrowseChildrenRequest, opts ...grpc.CallOption) (*BrowseChildrenReply, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(BrowseChildrenReply)
|
|
||||||
err := c.cc.Invoke(ctx, GalaxyRepository_BrowseChildren_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GalaxyRepositoryServer is the server API for GalaxyRepository service.
|
// GalaxyRepositoryServer is the server API for GalaxyRepository service.
|
||||||
// All implementations must embed UnimplementedGalaxyRepositoryServer
|
// All implementations must embed UnimplementedGalaxyRepositoryServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@@ -138,11 +122,6 @@ type GalaxyRepositoryServer interface {
|
|||||||
// increasing per server start; gaps indicate the per-subscriber buffer dropped
|
// increasing per server start; gaps indicate the per-subscriber buffer dropped
|
||||||
// older events because the client was too slow.
|
// older events because the client was too slow.
|
||||||
WatchDeployEvents(*WatchDeployEventsRequest, grpc.ServerStreamingServer[DeployEvent]) error
|
WatchDeployEvents(*WatchDeployEventsRequest, grpc.ServerStreamingServer[DeployEvent]) error
|
||||||
// Returns the direct children of a parent object (or the root objects when
|
|
||||||
// `parent` is unset). Designed for OPC UA-style lazy expand: clients walk
|
|
||||||
// one level at a time instead of paging the full hierarchy. Filters mirror
|
|
||||||
// DiscoverHierarchy exactly. Backed by the same shared hierarchy cache.
|
|
||||||
BrowseChildren(context.Context, *BrowseChildrenRequest) (*BrowseChildrenReply, error)
|
|
||||||
mustEmbedUnimplementedGalaxyRepositoryServer()
|
mustEmbedUnimplementedGalaxyRepositoryServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,9 +144,6 @@ func (UnimplementedGalaxyRepositoryServer) DiscoverHierarchy(context.Context, *D
|
|||||||
func (UnimplementedGalaxyRepositoryServer) WatchDeployEvents(*WatchDeployEventsRequest, grpc.ServerStreamingServer[DeployEvent]) error {
|
func (UnimplementedGalaxyRepositoryServer) WatchDeployEvents(*WatchDeployEventsRequest, grpc.ServerStreamingServer[DeployEvent]) error {
|
||||||
return status.Error(codes.Unimplemented, "method WatchDeployEvents not implemented")
|
return status.Error(codes.Unimplemented, "method WatchDeployEvents not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedGalaxyRepositoryServer) BrowseChildren(context.Context, *BrowseChildrenRequest) (*BrowseChildrenReply, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method BrowseChildren not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedGalaxyRepositoryServer) mustEmbedUnimplementedGalaxyRepositoryServer() {}
|
func (UnimplementedGalaxyRepositoryServer) mustEmbedUnimplementedGalaxyRepositoryServer() {}
|
||||||
func (UnimplementedGalaxyRepositoryServer) testEmbeddedByValue() {}
|
func (UnimplementedGalaxyRepositoryServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -254,24 +230,6 @@ func _GalaxyRepository_WatchDeployEvents_Handler(srv interface{}, stream grpc.Se
|
|||||||
// 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 GalaxyRepository_WatchDeployEventsServer = grpc.ServerStreamingServer[DeployEvent]
|
type GalaxyRepository_WatchDeployEventsServer = grpc.ServerStreamingServer[DeployEvent]
|
||||||
|
|
||||||
func _GalaxyRepository_BrowseChildren_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(BrowseChildrenRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(GalaxyRepositoryServer).BrowseChildren(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: GalaxyRepository_BrowseChildren_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(GalaxyRepositoryServer).BrowseChildren(ctx, req.(*BrowseChildrenRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GalaxyRepository_ServiceDesc is the grpc.ServiceDesc for GalaxyRepository service.
|
// GalaxyRepository_ServiceDesc is the grpc.ServiceDesc for GalaxyRepository 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)
|
||||||
@@ -291,10 +249,6 @@ var GalaxyRepository_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "DiscoverHierarchy",
|
MethodName: "DiscoverHierarchy",
|
||||||
Handler: _GalaxyRepository_DiscoverHierarchy_Handler,
|
Handler: _GalaxyRepository_DiscoverHierarchy_Handler,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
MethodName: "BrowseChildren",
|
|
||||||
Handler: _GalaxyRepository_BrowseChildren_Handler,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{
|
Streams: []grpc.StreamDesc{
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.2
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
// - protoc v7.34.1
|
// - protoc v7.34.1
|
||||||
// source: mxaccess_gateway.proto
|
// source: mxaccess_gateway.proto
|
||||||
|
|
||||||
@@ -23,9 +23,6 @@ const (
|
|||||||
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_StreamAlarms_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/StreamAlarms"
|
|
||||||
MxAccessGateway_QueryActiveAlarms_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/QueryActiveAlarms"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MxAccessGatewayClient is the client API for MxAccessGateway service.
|
// MxAccessGatewayClient is the client API for MxAccessGateway service.
|
||||||
@@ -38,22 +35,6 @@ 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)
|
|
||||||
// Session-less central alarm feed. The stream opens with the current
|
|
||||||
// active-alarm snapshot (one `active_alarm` per alarm), then a single
|
|
||||||
// `snapshot_complete`, then a `transition` for every subsequent change.
|
|
||||||
// Served by the gateway's always-on alarm monitor; any number of clients
|
|
||||||
// fan out from the single monitor without opening a worker session.
|
|
||||||
StreamAlarms(ctx context.Context, in *StreamAlarmsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[AlarmFeedMessage], error)
|
|
||||||
// Point-in-time snapshot of the currently-active alarm set served from the
|
|
||||||
// gateway's always-on alarm monitor cache (session-less). Used after a
|
|
||||||
// reconnect to seed Part 9 client state, or to reconcile alarms that may
|
|
||||||
// have been missed during a transport blip. Streamed so callers can
|
|
||||||
// begin processing without buffering the full set.
|
|
||||||
// `QueryActiveAlarmsRequest.alarm_filter_prefix` optionally narrows the
|
|
||||||
// snapshot to alarms whose `alarm_full_reference` starts with the given
|
|
||||||
// prefix; an empty prefix returns the full set.
|
|
||||||
QueryActiveAlarms(ctx context.Context, in *QueryActiveAlarmsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ActiveAlarmSnapshot], error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type mxAccessGatewayClient struct {
|
type mxAccessGatewayClient struct {
|
||||||
@@ -113,54 +94,6 @@ func (c *mxAccessGatewayClient) StreamEvents(ctx context.Context, in *StreamEven
|
|||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
// 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) StreamAlarms(ctx context.Context, in *StreamAlarmsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[AlarmFeedMessage], error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
stream, err := c.cc.NewStream(ctx, &MxAccessGateway_ServiceDesc.Streams[1], MxAccessGateway_StreamAlarms_FullMethodName, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
x := &grpc.GenericClientStream[StreamAlarmsRequest, AlarmFeedMessage]{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_StreamAlarmsClient = grpc.ServerStreamingClient[AlarmFeedMessage]
|
|
||||||
|
|
||||||
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[2], 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.
|
||||||
@@ -171,22 +104,6 @@ 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)
|
|
||||||
// Session-less central alarm feed. The stream opens with the current
|
|
||||||
// active-alarm snapshot (one `active_alarm` per alarm), then a single
|
|
||||||
// `snapshot_complete`, then a `transition` for every subsequent change.
|
|
||||||
// Served by the gateway's always-on alarm monitor; any number of clients
|
|
||||||
// fan out from the single monitor without opening a worker session.
|
|
||||||
StreamAlarms(*StreamAlarmsRequest, grpc.ServerStreamingServer[AlarmFeedMessage]) error
|
|
||||||
// Point-in-time snapshot of the currently-active alarm set served from the
|
|
||||||
// gateway's always-on alarm monitor cache (session-less). Used after a
|
|
||||||
// reconnect to seed Part 9 client state, or to reconcile alarms that may
|
|
||||||
// have been missed during a transport blip. Streamed so callers can
|
|
||||||
// begin processing without buffering the full set.
|
|
||||||
// `QueryActiveAlarmsRequest.alarm_filter_prefix` optionally narrows the
|
|
||||||
// snapshot to alarms whose `alarm_full_reference` starts with the given
|
|
||||||
// prefix; an empty prefix returns the full set.
|
|
||||||
QueryActiveAlarms(*QueryActiveAlarmsRequest, grpc.ServerStreamingServer[ActiveAlarmSnapshot]) error
|
|
||||||
mustEmbedUnimplementedMxAccessGatewayServer()
|
mustEmbedUnimplementedMxAccessGatewayServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,15 +126,6 @@ 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) StreamAlarms(*StreamAlarmsRequest, grpc.ServerStreamingServer[AlarmFeedMessage]) error {
|
|
||||||
return status.Error(codes.Unimplemented, "method StreamAlarms 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() {}
|
||||||
|
|
||||||
@@ -304,46 +212,6 @@ func _MxAccessGateway_StreamEvents_Handler(srv interface{}, stream grpc.ServerSt
|
|||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
// 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_StreamAlarms_Handler(srv interface{}, stream grpc.ServerStream) error {
|
|
||||||
m := new(StreamAlarmsRequest)
|
|
||||||
if err := stream.RecvMsg(m); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return srv.(MxAccessGatewayServer).StreamAlarms(m, &grpc.GenericServerStream[StreamAlarmsRequest, AlarmFeedMessage]{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_StreamAlarmsServer = grpc.ServerStreamingServer[AlarmFeedMessage]
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -363,10 +231,6 @@ 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{
|
||||||
{
|
{
|
||||||
@@ -374,16 +238,6 @@ var MxAccessGateway_ServiceDesc = grpc.ServiceDesc{
|
|||||||
Handler: _MxAccessGateway_StreamEvents_Handler,
|
Handler: _MxAccessGateway_StreamEvents_Handler,
|
||||||
ServerStreams: true,
|
ServerStreams: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
StreamName: "StreamAlarms",
|
|
||||||
Handler: _MxAccessGateway_StreamAlarms_Handler,
|
|
||||||
ServerStreams: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
StreamName: "QueryActiveAlarms",
|
|
||||||
Handler: _MxAccessGateway_QueryActiveAlarms_Handler,
|
|
||||||
ServerStreams: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Metadata: "mxaccess_gateway.proto",
|
Metadata: "mxaccess_gateway.proto",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1179,7 +1179,7 @@ const file_mxaccess_worker_proto_rawDesc = "" +
|
|||||||
"\x1eWORKER_FAULT_CATEGORY_STA_HUNG\x10\t\x12(\n" +
|
"\x1eWORKER_FAULT_CATEGORY_STA_HUNG\x10\t\x12(\n" +
|
||||||
"$WORKER_FAULT_CATEGORY_QUEUE_OVERFLOW\x10\n" +
|
"$WORKER_FAULT_CATEGORY_QUEUE_OVERFLOW\x10\n" +
|
||||||
"\x12*\n" +
|
"\x12*\n" +
|
||||||
"&WORKER_FAULT_CATEGORY_SHUTDOWN_TIMEOUT\x10\vB&\xaa\x02#ZB.MOM.WW.MxGateway.Contracts.Protob\x06proto3"
|
"&WORKER_FAULT_CATEGORY_SHUTDOWN_TIMEOUT\x10\vB\x1c\xaa\x02\x19MxGateway.Contracts.Protob\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_mxaccess_worker_proto_rawDescOnce sync.Once
|
file_mxaccess_worker_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AcknowledgeAlarm acknowledges an active MXAccess alarm condition through the
|
|
||||||
// gateway. The gateway authenticates the request against the API key's
|
|
||||||
// invoke:alarm-ack scope and forwards the acknowledge to the worker's MXAccess
|
|
||||||
// session; the resulting native MxStatus is returned in the reply.
|
|
||||||
//
|
|
||||||
// Acks are idempotent — re-acking an already-acked condition is a no-op at
|
|
||||||
// the MxAccess layer.
|
|
||||||
func (c *Client) AcknowledgeAlarm(ctx context.Context, req *AcknowledgeAlarmRequest) (*AcknowledgeAlarmReply, error) {
|
|
||||||
if req == nil {
|
|
||||||
return nil, errors.New("mxgateway: acknowledge alarm request is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
callCtx, cancel := c.callContext(ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
reply, err := c.raw.AcknowledgeAlarm(callCtx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &GatewayError{Op: "acknowledge alarm", Err: err}
|
|
||||||
}
|
|
||||||
if err := EnsureProtocolSuccess("acknowledge alarm", reply.GetProtocolStatus(), nil); err != nil {
|
|
||||||
return reply, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return reply, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryActiveAlarms streams a snapshot of all alarms currently Active or
|
|
||||||
// ActiveAcked — the gateway's ConditionRefresh equivalent. Used after reconnect
|
|
||||||
// to seed local Part 9 state, or to reconcile alarms that may have been missed
|
|
||||||
// during a transport blip.
|
|
||||||
//
|
|
||||||
// The returned stream is owned by the caller; cancel ctx to release it.
|
|
||||||
// Optional alarm-reference prefix scoping (req.AlarmFilterPrefix) limits the
|
|
||||||
// stream to a sub-tree.
|
|
||||||
func (c *Client) QueryActiveAlarms(ctx context.Context, req *QueryActiveAlarmsRequest) (QueryActiveAlarmsClient, error) {
|
|
||||||
if req == nil {
|
|
||||||
return nil, errors.New("mxgateway: query active alarms request is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
stream, err := c.raw.QueryActiveAlarms(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &GatewayError{Op: "query active alarms", Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StreamAlarms attaches to the gateway's central alarm feed. The stream opens
|
|
||||||
// with one AlarmFeedMessage per currently-active alarm (the ConditionRefresh
|
|
||||||
// snapshot), then a single snapshot-complete sentinel, then a transition for
|
|
||||||
// every subsequent raise / acknowledge / clear. It is served by the gateway's
|
|
||||||
// always-on alarm monitor — no worker session is opened — so any number of
|
|
||||||
// clients may attach.
|
|
||||||
//
|
|
||||||
// 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) StreamAlarms(ctx context.Context, req *StreamAlarmsRequest) (StreamAlarmsClient, error) {
|
|
||||||
if req == nil {
|
|
||||||
return nil, errors.New("mxgateway: stream alarms request is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
stream, err := c.raw.StreamAlarms(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &GatewayError{Op: "stream alarms", Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream, nil
|
|
||||||
}
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
"google.golang.org/grpc/test/bufconn"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PR E.4 — pins the Go SDK surface for the new alarm RPCs:
|
|
||||||
// AcknowledgeAlarm + QueryActiveAlarms.
|
|
||||||
|
|
||||||
func TestAcknowledgeAlarmSendsRequestAndReturnsReply(t *testing.T) {
|
|
||||||
fake := &fakeGatewayWithAlarms{
|
|
||||||
acknowledgeReply: &pb.AcknowledgeAlarmReply{
|
|
||||||
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{
|
|
||||||
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{
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStreamAlarmsPassesFilterPrefixAndReceivesFeedMessages(t *testing.T) {
|
|
||||||
fake := &fakeGatewayWithAlarms{
|
|
||||||
feedMessages: []*pb.AlarmFeedMessage{
|
|
||||||
{
|
|
||||||
Payload: &pb.AlarmFeedMessage_ActiveAlarm{
|
|
||||||
ActiveAlarm: &pb.ActiveAlarmSnapshot{
|
|
||||||
AlarmFullReference: "Tank01.Level.HiHi",
|
|
||||||
CurrentState: pb.AlarmConditionState_ALARM_CONDITION_STATE_ACTIVE,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Payload: &pb.AlarmFeedMessage_SnapshotComplete{
|
|
||||||
SnapshotComplete: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client, cleanup := newBufconnClientWithAlarms(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
stream, err := client.StreamAlarms(context.Background(), &pb.StreamAlarmsRequest{
|
|
||||||
AlarmFilterPrefix: "Tank01.",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("StreamAlarms() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var received []*pb.AlarmFeedMessage
|
|
||||||
for {
|
|
||||||
msg, err := stream.Recv()
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("stream.Recv() error = %v", err)
|
|
||||||
}
|
|
||||||
received = append(received, msg)
|
|
||||||
}
|
|
||||||
if len(received) != 2 {
|
|
||||||
t.Fatalf("received count = %d, want 2", len(received))
|
|
||||||
}
|
|
||||||
if got := fake.streamRequest.GetAlarmFilterPrefix(); got != "Tank01." {
|
|
||||||
t.Fatalf("captured filter prefix = %q", got)
|
|
||||||
}
|
|
||||||
if got := fake.streamAuth; got != "Bearer test-api-key" {
|
|
||||||
t.Fatalf("stream authorization metadata = %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStreamAlarmsRejectsNilRequest(t *testing.T) {
|
|
||||||
fake := &fakeGatewayWithAlarms{}
|
|
||||||
client, cleanup := newBufconnClientWithAlarms(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
if _, err := client.StreamAlarms(context.Background(), nil); err == nil {
|
|
||||||
t.Fatal("StreamAlarms(nil) returned no error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeGatewayWithAlarms struct {
|
|
||||||
pb.UnimplementedMxAccessGatewayServer
|
|
||||||
|
|
||||||
acknowledgeRequest *pb.AcknowledgeAlarmRequest
|
|
||||||
acknowledgeReply *pb.AcknowledgeAlarmReply
|
|
||||||
acknowledgeError error
|
|
||||||
acknowledgeAuth string
|
|
||||||
|
|
||||||
queryRequest *pb.QueryActiveAlarmsRequest
|
|
||||||
activeSnapshots []*pb.ActiveAlarmSnapshot
|
|
||||||
|
|
||||||
streamRequest *pb.StreamAlarmsRequest
|
|
||||||
feedMessages []*pb.AlarmFeedMessage
|
|
||||||
streamAuth string
|
|
||||||
}
|
|
||||||
|
|
||||||
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{
|
|
||||||
CorrelationId: req.GetClientCorrelationId(),
|
|
||||||
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 (s *fakeGatewayWithAlarms) StreamAlarms(req *pb.StreamAlarmsRequest, stream grpc.ServerStreamingServer[pb.AlarmFeedMessage]) error {
|
|
||||||
s.streamRequest = req
|
|
||||||
s.streamAuth = authorizationFromContext(stream.Context())
|
|
||||||
for _, msg := range s.feedMessages {
|
|
||||||
if err := stream.Send(msg); 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,14 +1,3 @@
|
|||||||
// Package mxgateway is the Go client for the MXAccess Gateway gRPC service.
|
|
||||||
//
|
|
||||||
// The package wraps the generated gRPC contract with session-oriented helpers
|
|
||||||
// for invoking MXAccess commands, streaming events, and browsing the Galaxy
|
|
||||||
// Repository. Authentication uses an API-key bearer token attached as gRPC
|
|
||||||
// metadata on every call.
|
|
||||||
//
|
|
||||||
// Typical use opens a Client with Dial, opens a Session, invokes commands such
|
|
||||||
// as Register, AddItem, Advise, and Write, and consumes events via
|
|
||||||
// SubscribeEvents. Galaxy Repository browse RPCs are exposed through
|
|
||||||
// GalaxyClient.
|
|
||||||
package mxgateway
|
package mxgateway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -27,6 +16,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
defaultDialTimeout = 10 * time.Second
|
defaultDialTimeout = 10 * time.Second
|
||||||
defaultCallTimeout = 30 * time.Second
|
defaultCallTimeout = 30 * time.Second
|
||||||
|
defaultMaxGrpcMessageBytes = 16 * 1024 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client owns a gateway gRPC connection and exposes session-oriented helpers.
|
// Client owns a gateway gRPC connection and exposes session-oriented helpers.
|
||||||
@@ -61,6 +51,10 @@ func Dial(ctx context.Context, opts Options) (*Client, error) {
|
|||||||
grpc.WithTransportCredentials(transportCredentials),
|
grpc.WithTransportCredentials(transportCredentials),
|
||||||
grpc.WithUnaryInterceptor(unaryAuthInterceptor(opts.APIKey)),
|
grpc.WithUnaryInterceptor(unaryAuthInterceptor(opts.APIKey)),
|
||||||
grpc.WithStreamInterceptor(streamAuthInterceptor(opts.APIKey)),
|
grpc.WithStreamInterceptor(streamAuthInterceptor(opts.APIKey)),
|
||||||
|
grpc.WithDefaultCallOptions(
|
||||||
|
grpc.MaxCallRecvMsgSize(resolveMaxGrpcMessageBytes(opts)),
|
||||||
|
grpc.MaxCallSendMsgSize(resolveMaxGrpcMessageBytes(opts)),
|
||||||
|
),
|
||||||
grpc.WithBlock(),
|
grpc.WithBlock(),
|
||||||
}
|
}
|
||||||
dialOptions = append(dialOptions, opts.DialOptions...)
|
dialOptions = append(dialOptions, opts.DialOptions...)
|
||||||
@@ -73,6 +67,13 @@ func Dial(ctx context.Context, opts Options) (*Client, error) {
|
|||||||
return NewClient(conn, opts), nil
|
return NewClient(conn, opts), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveMaxGrpcMessageBytes(opts Options) int {
|
||||||
|
if opts.MaxGrpcMessageBytes > 0 {
|
||||||
|
return opts.MaxGrpcMessageBytes
|
||||||
|
}
|
||||||
|
return defaultMaxGrpcMessageBytes
|
||||||
|
}
|
||||||
|
|
||||||
// NewClient wraps an existing gRPC connection. The caller owns closing conn
|
// NewClient wraps an existing gRPC connection. The caller owns closing conn
|
||||||
// unless it calls Close on the returned Client.
|
// unless it calls Close on the returned Client.
|
||||||
func NewClient(conn *grpc.ClientConn, opts Options) *Client {
|
func NewClient(conn *grpc.ClientConn, opts Options) *Client {
|
||||||
@@ -222,34 +223,17 @@ func resolveTransportCredentials(opts Options) (credentials.TransportCredentials
|
|||||||
return credentials.NewTLS(cfg), nil
|
return credentials.NewTLS(cfg), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return credentials.NewTLS(tlsConfigForOptions(opts)), nil
|
return credentials.NewTLS(&tls.Config{
|
||||||
}
|
|
||||||
|
|
||||||
// tlsConfigForOptions returns the *tls.Config for the no-CA, no-custom-config TLS path.
|
|
||||||
// It returns nil when the caller should use a different credentials path (CA file or custom TLSConfig).
|
|
||||||
// Exposed as an internal helper so unit tests can assert the InsecureSkipVerify posture.
|
|
||||||
func tlsConfigForOptions(opts Options) *tls.Config {
|
|
||||||
// CA file and custom TLSConfig take their own paths in resolveTransportCredentials.
|
|
||||||
if opts.CACertFile != "" || opts.TLSConfig != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &tls.Config{
|
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
ServerName: opts.ServerNameOverride,
|
ServerName: opts.ServerNameOverride,
|
||||||
InsecureSkipVerify: !opts.RequireCertificateValidation, //nolint:gosec // internal tool; self-signed gateway cert expected; opt-in strict via RequireCertificateValidation
|
}), nil
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenSessionOptions describes fields used to create an OpenSessionRequest.
|
// OpenSessionOptions describes fields used to create an OpenSessionRequest.
|
||||||
type OpenSessionOptions struct {
|
type OpenSessionOptions struct {
|
||||||
// RequestedBackend selects the gateway worker backend (empty for default).
|
|
||||||
RequestedBackend string
|
RequestedBackend string
|
||||||
// ClientSessionName is a human-readable name recorded on the session.
|
|
||||||
ClientSessionName string
|
ClientSessionName string
|
||||||
// ClientCorrelationID echoes through gateway logs and replies for tracing.
|
|
||||||
ClientCorrelationID string
|
ClientCorrelationID string
|
||||||
// CommandTimeout sets the per-command timeout the gateway forwards to the
|
|
||||||
// worker; zero leaves the gateway default in place.
|
|
||||||
CommandTimeout time.Duration
|
CommandTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,206 +230,6 @@ func TestSubscribeBulkBuildsOneBulkCommandAndReturnsResults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWriteBulkBuildsOneBulkCommandAndReturnsPerEntryResults(t *testing.T) {
|
|
||||||
fake := &fakeGatewayServer{
|
|
||||||
invokeReply: &pb.MxCommandReply{
|
|
||||||
SessionId: "session-1",
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_WRITE_BULK,
|
|
||||||
ProtocolStatus: &pb.ProtocolStatus{
|
|
||||||
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
|
|
||||||
},
|
|
||||||
Payload: &pb.MxCommandReply_WriteBulk{
|
|
||||||
WriteBulk: &pb.BulkWriteReply{
|
|
||||||
Results: []*pb.BulkWriteResult{
|
|
||||||
{ItemHandle: 10, WasSuccessful: true},
|
|
||||||
{ItemHandle: 11, WasSuccessful: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client, cleanup := newBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
session := NewSessionForID(client, "session-1")
|
|
||||||
|
|
||||||
entries := []*WriteBulkEntry{
|
|
||||||
{ItemHandle: 10, Value: Int32Value(7), UserId: 100},
|
|
||||||
{ItemHandle: 11, Value: Int32Value(8), UserId: 100},
|
|
||||||
}
|
|
||||||
results, err := session.WriteBulk(context.Background(), 12, entries)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("WriteBulk() error = %v", err)
|
|
||||||
}
|
|
||||||
if len(results) != 2 {
|
|
||||||
t.Fatalf("results len = %d, want 2", len(results))
|
|
||||||
}
|
|
||||||
req := fake.invokeRequest
|
|
||||||
if req.GetCommand().GetKind() != pb.MxCommandKind_MX_COMMAND_KIND_WRITE_BULK {
|
|
||||||
t.Fatalf("command kind = %s", req.GetCommand().GetKind())
|
|
||||||
}
|
|
||||||
if got := req.GetCommand().GetWriteBulk().GetEntries(); len(got) != 2 {
|
|
||||||
t.Fatalf("entry count = %d, want 2", len(got))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteBulkRejectsNilEntries(t *testing.T) {
|
|
||||||
fake := &fakeGatewayServer{}
|
|
||||||
client, cleanup := newBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
session := NewSessionForID(client, "session-1")
|
|
||||||
|
|
||||||
if _, err := session.WriteBulk(context.Background(), 12, nil); err == nil {
|
|
||||||
t.Fatal("WriteBulk(nil) returned no error")
|
|
||||||
}
|
|
||||||
if _, err := session.Write2Bulk(context.Background(), 12, nil); err == nil {
|
|
||||||
t.Fatal("Write2Bulk(nil) returned no error")
|
|
||||||
}
|
|
||||||
if _, err := session.WriteSecuredBulk(context.Background(), 12, nil); err == nil {
|
|
||||||
t.Fatal("WriteSecuredBulk(nil) returned no error")
|
|
||||||
}
|
|
||||||
if _, err := session.WriteSecured2Bulk(context.Background(), 12, nil); err == nil {
|
|
||||||
t.Fatal("WriteSecured2Bulk(nil) returned no error")
|
|
||||||
}
|
|
||||||
if _, err := session.ReadBulk(context.Background(), 12, nil, 0); err == nil {
|
|
||||||
t.Fatal("ReadBulk(nil) returned no error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBulkMethodsShortCircuitOnEmptySliceWithoutRoundTrip(t *testing.T) {
|
|
||||||
fake := &fakeGatewayServer{
|
|
||||||
invokeReply: &pb.MxCommandReply{
|
|
||||||
ProtocolStatus: &pb.ProtocolStatus{
|
|
||||||
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client, cleanup := newBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
session := NewSessionForID(client, "session-1")
|
|
||||||
|
|
||||||
results, err := session.WriteBulk(context.Background(), 12, []*WriteBulkEntry{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("WriteBulk(empty) error = %v", err)
|
|
||||||
}
|
|
||||||
if len(results) != 0 {
|
|
||||||
t.Fatalf("WriteBulk(empty) results len = %d, want 0", len(results))
|
|
||||||
}
|
|
||||||
if fake.invokeRequest != nil {
|
|
||||||
t.Fatal("WriteBulk(empty) sent a round trip; expected short-circuit")
|
|
||||||
}
|
|
||||||
|
|
||||||
results2, err := session.Write2Bulk(context.Background(), 12, []*Write2BulkEntry{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Write2Bulk(empty) error = %v", err)
|
|
||||||
}
|
|
||||||
if len(results2) != 0 {
|
|
||||||
t.Fatalf("Write2Bulk(empty) results len = %d, want 0", len(results2))
|
|
||||||
}
|
|
||||||
if fake.invokeRequest != nil {
|
|
||||||
t.Fatal("Write2Bulk(empty) sent a round trip; expected short-circuit")
|
|
||||||
}
|
|
||||||
|
|
||||||
results3, err := session.WriteSecuredBulk(context.Background(), 12, []*WriteSecuredBulkEntry{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("WriteSecuredBulk(empty) error = %v", err)
|
|
||||||
}
|
|
||||||
if len(results3) != 0 {
|
|
||||||
t.Fatalf("WriteSecuredBulk(empty) results len = %d, want 0", len(results3))
|
|
||||||
}
|
|
||||||
if fake.invokeRequest != nil {
|
|
||||||
t.Fatal("WriteSecuredBulk(empty) sent a round trip; expected short-circuit")
|
|
||||||
}
|
|
||||||
|
|
||||||
results4, err := session.WriteSecured2Bulk(context.Background(), 12, []*WriteSecured2BulkEntry{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("WriteSecured2Bulk(empty) error = %v", err)
|
|
||||||
}
|
|
||||||
if len(results4) != 0 {
|
|
||||||
t.Fatalf("WriteSecured2Bulk(empty) results len = %d, want 0", len(results4))
|
|
||||||
}
|
|
||||||
if fake.invokeRequest != nil {
|
|
||||||
t.Fatal("WriteSecured2Bulk(empty) sent a round trip; expected short-circuit")
|
|
||||||
}
|
|
||||||
|
|
||||||
readResults, err := session.ReadBulk(context.Background(), 12, []string{}, 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ReadBulk(empty) error = %v", err)
|
|
||||||
}
|
|
||||||
if len(readResults) != 0 {
|
|
||||||
t.Fatalf("ReadBulk(empty) results len = %d, want 0", len(readResults))
|
|
||||||
}
|
|
||||||
if fake.invokeRequest != nil {
|
|
||||||
t.Fatal("ReadBulk(empty) sent a round trip; expected short-circuit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReadBulkForwardsTimeoutAndUnpacksCachedFlag(t *testing.T) {
|
|
||||||
fake := &fakeGatewayServer{
|
|
||||||
invokeReply: &pb.MxCommandReply{
|
|
||||||
SessionId: "session-1",
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_READ_BULK,
|
|
||||||
ProtocolStatus: &pb.ProtocolStatus{
|
|
||||||
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
|
|
||||||
},
|
|
||||||
Payload: &pb.MxCommandReply_ReadBulk{
|
|
||||||
ReadBulk: &pb.BulkReadReply{
|
|
||||||
Results: []*pb.BulkReadResult{
|
|
||||||
{TagAddress: "Tank01.Level", WasSuccessful: true, WasCached: true},
|
|
||||||
{TagAddress: "Tank02.Level", WasSuccessful: true, WasCached: false},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client, cleanup := newBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
session := NewSessionForID(client, "session-1")
|
|
||||||
|
|
||||||
results, err := session.ReadBulk(context.Background(), 12, []string{"Tank01.Level", "Tank02.Level"}, 250*time.Millisecond)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ReadBulk() error = %v", err)
|
|
||||||
}
|
|
||||||
if len(results) != 2 {
|
|
||||||
t.Fatalf("results len = %d, want 2", len(results))
|
|
||||||
}
|
|
||||||
if !results[0].GetWasCached() || results[1].GetWasCached() {
|
|
||||||
t.Fatalf("WasCached flags = [%v %v], want [true false]", results[0].GetWasCached(), results[1].GetWasCached())
|
|
||||||
}
|
|
||||||
req := fake.invokeRequest
|
|
||||||
if req.GetCommand().GetKind() != pb.MxCommandKind_MX_COMMAND_KIND_READ_BULK {
|
|
||||||
t.Fatalf("command kind = %s", req.GetCommand().GetKind())
|
|
||||||
}
|
|
||||||
if got := req.GetCommand().GetReadBulk().GetTimeoutMs(); got != 250 {
|
|
||||||
t.Fatalf("timeout ms = %d, want 250", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReadBulkSaturatesTimeoutAboveMaxUint32(t *testing.T) {
|
|
||||||
fake := &fakeGatewayServer{
|
|
||||||
invokeReply: &pb.MxCommandReply{
|
|
||||||
SessionId: "session-1",
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_READ_BULK,
|
|
||||||
ProtocolStatus: &pb.ProtocolStatus{
|
|
||||||
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client, cleanup := newBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
session := NewSessionForID(client, "session-1")
|
|
||||||
|
|
||||||
// 100 days in milliseconds exceeds MaxUint32 (~49.7 days).
|
|
||||||
hugeTimeout := 100 * 24 * time.Hour
|
|
||||||
_, err := session.ReadBulk(context.Background(), 12, []string{"Tank01.Level"}, hugeTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ReadBulk() error = %v", err)
|
|
||||||
}
|
|
||||||
got := fake.invokeRequest.GetCommand().GetReadBulk().GetTimeoutMs()
|
|
||||||
if got != ^uint32(0) {
|
|
||||||
t.Fatalf("timeout ms = %d, want %d (MaxUint32)", got, ^uint32(0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvokeReturnsTypedMxAccessErrorWithRawReply(t *testing.T) {
|
func TestInvokeReturnsTypedMxAccessErrorWithRawReply(t *testing.T) {
|
||||||
hresult := int32(-2147467259)
|
hresult := int32(-2147467259)
|
||||||
fake := &fakeGatewayServer{
|
fake := &fakeGatewayServer{
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// tlsConfigFromOptions is the internal helper under test.
|
|
||||||
// It extracts the *tls.Config from the no-CA TLS path of resolveTransportCredentials.
|
|
||||||
// We exercise it directly to avoid needing a real dial target.
|
|
||||||
|
|
||||||
func TestTLSInsecureSkipVerify_DefaultTrue(t *testing.T) {
|
|
||||||
cfg := tlsConfigForOptions(Options{
|
|
||||||
Endpoint: "localhost:5120",
|
|
||||||
})
|
|
||||||
if cfg == nil {
|
|
||||||
t.Fatal("expected non-nil tls.Config")
|
|
||||||
}
|
|
||||||
if !cfg.InsecureSkipVerify {
|
|
||||||
t.Error("InsecureSkipVerify should be true by default when no CA is pinned")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTLSInsecureSkipVerify_FalseWhenRequireCertificateValidation(t *testing.T) {
|
|
||||||
cfg := tlsConfigForOptions(Options{
|
|
||||||
Endpoint: "localhost:5120",
|
|
||||||
RequireCertificateValidation: true,
|
|
||||||
})
|
|
||||||
if cfg == nil {
|
|
||||||
t.Fatal("expected non-nil tls.Config")
|
|
||||||
}
|
|
||||||
if cfg.InsecureSkipVerify {
|
|
||||||
t.Error("InsecureSkipVerify should be false when RequireCertificateValidation is true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTLSInsecureSkipVerify_FalseWhenCACertFileSet(t *testing.T) {
|
|
||||||
// When a CA file is pinned, the CA-verification path is taken instead.
|
|
||||||
// tlsConfigForOptions should return nil (the CA path does not use our helper).
|
|
||||||
cfg := tlsConfigForOptions(Options{
|
|
||||||
Endpoint: "localhost:5120",
|
|
||||||
CACertFile: "/some/ca.pem",
|
|
||||||
})
|
|
||||||
if cfg != nil {
|
|
||||||
t.Error("expected nil tls.Config when CACertFile is set (CA path taken)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTLSInsecureSkipVerify_FalseWhenCustomTLSConfig(t *testing.T) {
|
|
||||||
// When TLSConfig is supplied explicitly, our default skip-verify must not overwrite it.
|
|
||||||
custom := &tls.Config{MinVersion: tls.VersionTLS13}
|
|
||||||
cfg := tlsConfigForOptions(Options{
|
|
||||||
Endpoint: "localhost:5120",
|
|
||||||
TLSConfig: custom,
|
|
||||||
})
|
|
||||||
if cfg != nil {
|
|
||||||
t.Error("expected nil tls.Config when TLSConfig is already set (custom config path taken)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,13 +8,10 @@ import (
|
|||||||
|
|
||||||
// GatewayError wraps transport-level gRPC failures.
|
// GatewayError wraps transport-level gRPC failures.
|
||||||
type GatewayError struct {
|
type GatewayError struct {
|
||||||
// Op names the operation that failed (for example "dial" or "invoke").
|
|
||||||
Op string
|
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 ""
|
||||||
@@ -25,7 +22,6 @@ 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
|
||||||
@@ -36,15 +32,11 @@ 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 names the gateway operation that produced the non-OK status.
|
|
||||||
Op string
|
Op string
|
||||||
// Status carries the gateway-reported protocol status.
|
|
||||||
Status *ProtocolStatus
|
Status *ProtocolStatus
|
||||||
// Reply is the raw command reply, when one was returned alongside the status.
|
|
||||||
Reply *MxCommandReply
|
Reply *MxCommandReply
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error returns the formatted command error message.
|
|
||||||
func (e *CommandError) Error() string {
|
func (e *CommandError) Error() string {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -61,13 +53,10 @@ 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 is the raw MXAccess command reply that surfaced the failure.
|
|
||||||
Reply *MxCommandReply
|
Reply *MxCommandReply
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error returns the formatted MXAccess error message.
|
|
||||||
func (e *MxAccessError) Error() string {
|
func (e *MxAccessError) Error() string {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -84,7 +73,6 @@ 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
|
||||||
|
|||||||
+18
-237
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
||||||
@@ -15,12 +14,6 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// browseChildrenPageSize is the per-request page size used by the lazy walker.
|
|
||||||
const browseChildrenPageSize = 500
|
|
||||||
|
|
||||||
// discoverHierarchyPageSize is the per-request page size used by DiscoverHierarchy.
|
|
||||||
// Mirrors the .NET client constant so large galaxies are not silently truncated
|
|
||||||
// by the server's default page cap.
|
|
||||||
const discoverHierarchyPageSize = 5000
|
const discoverHierarchyPageSize = 5000
|
||||||
|
|
||||||
// RawGalaxyRepositoryClient is the generated gRPC client interface for the
|
// RawGalaxyRepositoryClient is the generated gRPC client interface for the
|
||||||
@@ -30,30 +23,16 @@ type RawGalaxyRepositoryClient = pb.GalaxyRepositoryClient
|
|||||||
|
|
||||||
// Generated protobuf aliases for Galaxy Repository messages.
|
// Generated protobuf aliases for Galaxy Repository messages.
|
||||||
type (
|
type (
|
||||||
// TestConnectionRequest is the request for Galaxy Repository TestConnection.
|
|
||||||
TestConnectionRequest = pb.TestConnectionRequest
|
TestConnectionRequest = pb.TestConnectionRequest
|
||||||
// TestConnectionReply is the reply for Galaxy Repository TestConnection.
|
|
||||||
TestConnectionReply = pb.TestConnectionReply
|
TestConnectionReply = pb.TestConnectionReply
|
||||||
// GetLastDeployTimeRequest is the request for GetLastDeployTime.
|
|
||||||
GetLastDeployTimeRequest = pb.GetLastDeployTimeRequest
|
GetLastDeployTimeRequest = pb.GetLastDeployTimeRequest
|
||||||
// GetLastDeployTimeReply is the reply for GetLastDeployTime.
|
|
||||||
GetLastDeployTimeReply = pb.GetLastDeployTimeReply
|
GetLastDeployTimeReply = pb.GetLastDeployTimeReply
|
||||||
// DiscoverHierarchyRequest is the request for DiscoverHierarchy.
|
|
||||||
DiscoverHierarchyRequest = pb.DiscoverHierarchyRequest
|
DiscoverHierarchyRequest = pb.DiscoverHierarchyRequest
|
||||||
// DiscoverHierarchyReply is the reply for DiscoverHierarchy.
|
|
||||||
DiscoverHierarchyReply = pb.DiscoverHierarchyReply
|
DiscoverHierarchyReply = pb.DiscoverHierarchyReply
|
||||||
// GalaxyObject describes one Galaxy object with its dynamic attributes.
|
|
||||||
GalaxyObject = pb.GalaxyObject
|
GalaxyObject = pb.GalaxyObject
|
||||||
// GalaxyAttribute describes one dynamic attribute on a GalaxyObject.
|
|
||||||
GalaxyAttribute = pb.GalaxyAttribute
|
GalaxyAttribute = pb.GalaxyAttribute
|
||||||
// WatchDeployEventsRequest is the request for WatchDeployEvents.
|
|
||||||
WatchDeployEventsRequest = pb.WatchDeployEventsRequest
|
WatchDeployEventsRequest = pb.WatchDeployEventsRequest
|
||||||
// DeployEvent is one Galaxy Repository deploy event.
|
|
||||||
DeployEvent = pb.DeployEvent
|
DeployEvent = pb.DeployEvent
|
||||||
// BrowseChildrenRequest is the request for BrowseChildren.
|
|
||||||
BrowseChildrenRequest = pb.BrowseChildrenRequest
|
|
||||||
// BrowseChildrenReply is the reply for BrowseChildren.
|
|
||||||
BrowseChildrenReply = pb.BrowseChildrenReply
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RawDeployEventStream is the generated WatchDeployEvents client stream.
|
// RawDeployEventStream is the generated WatchDeployEvents client stream.
|
||||||
@@ -94,6 +73,10 @@ func DialGalaxy(ctx context.Context, opts Options) (*GalaxyClient, error) {
|
|||||||
grpc.WithTransportCredentials(transportCredentials),
|
grpc.WithTransportCredentials(transportCredentials),
|
||||||
grpc.WithUnaryInterceptor(unaryAuthInterceptor(opts.APIKey)),
|
grpc.WithUnaryInterceptor(unaryAuthInterceptor(opts.APIKey)),
|
||||||
grpc.WithStreamInterceptor(streamAuthInterceptor(opts.APIKey)),
|
grpc.WithStreamInterceptor(streamAuthInterceptor(opts.APIKey)),
|
||||||
|
grpc.WithDefaultCallOptions(
|
||||||
|
grpc.MaxCallRecvMsgSize(resolveMaxGrpcMessageBytes(opts)),
|
||||||
|
grpc.MaxCallSendMsgSize(resolveMaxGrpcMessageBytes(opts)),
|
||||||
|
),
|
||||||
grpc.WithBlock(),
|
grpc.WithBlock(),
|
||||||
}
|
}
|
||||||
dialOptions = append(dialOptions, opts.DialOptions...)
|
dialOptions = append(dialOptions, opts.DialOptions...)
|
||||||
@@ -160,36 +143,34 @@ func (c *GalaxyClient) GetLastDeployTime(ctx context.Context) (time.Time, bool,
|
|||||||
|
|
||||||
// DiscoverHierarchy returns the deployed Galaxy object hierarchy with each
|
// DiscoverHierarchy returns the deployed Galaxy object hierarchy with each
|
||||||
// object's dynamic attributes. The objects are returned in the order supplied
|
// object's dynamic attributes. The objects are returned in the order supplied
|
||||||
// by the server. The call pages over the server's NextPageToken until the
|
// by the server.
|
||||||
// server signals it has no more results, matching the .NET client.
|
|
||||||
func (c *GalaxyClient) DiscoverHierarchy(ctx context.Context) ([]*GalaxyObject, error) {
|
func (c *GalaxyClient) DiscoverHierarchy(ctx context.Context) ([]*GalaxyObject, error) {
|
||||||
var objects []*GalaxyObject
|
|
||||||
pageToken := ""
|
|
||||||
seen := map[string]struct{}{}
|
|
||||||
for {
|
|
||||||
callCtx, cancel := c.callContext(ctx)
|
callCtx, cancel := c.callContext(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var objects []*GalaxyObject
|
||||||
|
seenPageTokens := make(map[string]struct{})
|
||||||
|
pageToken := ""
|
||||||
|
for {
|
||||||
reply, err := c.raw.DiscoverHierarchy(callCtx, &pb.DiscoverHierarchyRequest{
|
reply, err := c.raw.DiscoverHierarchy(callCtx, &pb.DiscoverHierarchyRequest{
|
||||||
PageSize: discoverHierarchyPageSize,
|
PageSize: discoverHierarchyPageSize,
|
||||||
PageToken: pageToken,
|
PageToken: pageToken,
|
||||||
})
|
})
|
||||||
cancel()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &GatewayError{Op: "galaxy discover hierarchy", Err: err}
|
return nil, &GatewayError{Op: "galaxy discover hierarchy", Err: err}
|
||||||
}
|
}
|
||||||
objects = append(objects, reply.GetObjects()...)
|
objects = append(objects, reply.GetObjects()...)
|
||||||
pageToken = reply.GetNextPageToken()
|
pageToken = reply.GetNextPageToken()
|
||||||
if pageToken == "" {
|
if pageToken == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, seen := seenPageTokens[pageToken]; seen {
|
||||||
|
return nil, fmt.Errorf("mxgateway: galaxy discover hierarchy returned repeated page token %q", pageToken)
|
||||||
|
}
|
||||||
|
seenPageTokens[pageToken] = struct{}{}
|
||||||
|
}
|
||||||
return objects, nil
|
return objects, nil
|
||||||
}
|
}
|
||||||
if _, dup := seen[pageToken]; dup {
|
|
||||||
return nil, &GatewayError{
|
|
||||||
Op: "galaxy discover hierarchy",
|
|
||||||
Err: fmt.Errorf("repeated page token %q", pageToken),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
seen[pageToken] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WatchDeployEventsRaw starts the generated WatchDeployEvents stream for callers
|
// WatchDeployEventsRaw starts the generated WatchDeployEvents stream for callers
|
||||||
// that want direct control over Recv. The caller owns the returned stream's
|
// that want direct control over Recv. The caller owns the returned stream's
|
||||||
@@ -271,206 +252,6 @@ func (c *GalaxyClient) Close() error {
|
|||||||
return c.conn.Close()
|
return c.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// LazyBrowseNode is one node in a lazy Galaxy hierarchy walk produced by
|
|
||||||
// (*GalaxyClient).Browse. Children are not fetched until Expand is called.
|
|
||||||
// The node is safe for concurrent use; concurrent Expand calls coalesce onto
|
|
||||||
// a single in-flight RPC and do not block snapshot accessors.
|
|
||||||
type LazyBrowseNode struct {
|
|
||||||
client *GalaxyClient
|
|
||||||
object *pb.GalaxyObject
|
|
||||||
hasChildrenHint bool
|
|
||||||
options BrowseChildrenOptions
|
|
||||||
|
|
||||||
// expandLock gates inspection and mutation of expand-coordination state
|
|
||||||
// (expanding, expandDone, expandErr). It is held only briefly; the BrowseChildren
|
|
||||||
// RPC itself runs outside this lock so concurrent readers and waiters are not blocked.
|
|
||||||
expandLock sync.Mutex
|
|
||||||
expanding bool
|
|
||||||
expandDone chan struct{}
|
|
||||||
expandErr error
|
|
||||||
|
|
||||||
// mu protects the children snapshot and isExpanded flag for concurrent
|
|
||||||
// Children() / IsExpanded() readers.
|
|
||||||
mu sync.RWMutex
|
|
||||||
children []*LazyBrowseNode
|
|
||||||
isExpanded bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Object returns the underlying GalaxyObject describing this node.
|
|
||||||
func (n *LazyBrowseNode) Object() *pb.GalaxyObject { return n.object }
|
|
||||||
|
|
||||||
// HasChildrenHint reports the server-supplied hint on whether this node has
|
|
||||||
// matching descendants under the current filter set.
|
|
||||||
func (n *LazyBrowseNode) HasChildrenHint() bool { return n.hasChildrenHint }
|
|
||||||
|
|
||||||
// Children returns a snapshot copy of the currently-loaded child nodes. Returns
|
|
||||||
// an empty slice when Expand has not yet been called.
|
|
||||||
func (n *LazyBrowseNode) Children() []*LazyBrowseNode {
|
|
||||||
n.mu.RLock()
|
|
||||||
defer n.mu.RUnlock()
|
|
||||||
out := make([]*LazyBrowseNode, len(n.children))
|
|
||||||
copy(out, n.children)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsExpanded reports whether Expand has completed successfully on this node.
|
|
||||||
func (n *LazyBrowseNode) IsExpanded() bool {
|
|
||||||
n.mu.RLock()
|
|
||||||
defer n.mu.RUnlock()
|
|
||||||
return n.isExpanded
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expand fetches this node's direct children via BrowseChildren when they have
|
|
||||||
// not yet been loaded. Subsequent calls after a successful Expand are a no-op
|
|
||||||
// and do not issue another RPC.
|
|
||||||
//
|
|
||||||
// Expand is safe to call concurrently from multiple goroutines: callers that
|
|
||||||
// arrive while an expansion is in flight wait on the active RPC and share its
|
|
||||||
// result instead of issuing a second RPC. The RPC itself runs without holding
|
|
||||||
// the snapshot mutex, so concurrent Children() and IsExpanded() callers are
|
|
||||||
// not blocked for the duration of the network round trip.
|
|
||||||
//
|
|
||||||
// Failure semantics: a failed expansion surfaces the same error to every
|
|
||||||
// in-flight waiter, but the node is left in its pre-call state (isExpanded =
|
|
||||||
// false, no in-flight expansion). The next Expand call therefore retries with
|
|
||||||
// a fresh RPC; failures are not sticky.
|
|
||||||
func (n *LazyBrowseNode) Expand(ctx context.Context) error {
|
|
||||||
// Fast path: already expanded.
|
|
||||||
n.mu.RLock()
|
|
||||||
if n.isExpanded {
|
|
||||||
n.mu.RUnlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
n.mu.RUnlock()
|
|
||||||
|
|
||||||
// Either start a new expansion or wait on an existing one.
|
|
||||||
n.expandLock.Lock()
|
|
||||||
n.mu.RLock()
|
|
||||||
alreadyExpanded := n.isExpanded
|
|
||||||
n.mu.RUnlock()
|
|
||||||
if alreadyExpanded {
|
|
||||||
n.expandLock.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if n.expanding {
|
|
||||||
done := n.expandDone
|
|
||||||
n.expandLock.Unlock()
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
n.expandLock.Lock()
|
|
||||||
err := n.expandErr
|
|
||||||
n.expandLock.Unlock()
|
|
||||||
return err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n.expanding = true
|
|
||||||
n.expandDone = make(chan struct{})
|
|
||||||
done := n.expandDone
|
|
||||||
n.expandLock.Unlock()
|
|
||||||
|
|
||||||
// Issue the RPC outside any lock so concurrent readers/waiters are not blocked.
|
|
||||||
parentID := n.object.GetGobjectId()
|
|
||||||
children, err := n.client.browseChildrenInner(ctx, &parentID, n.options)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
n.mu.Lock()
|
|
||||||
n.children = children
|
|
||||||
n.isExpanded = true
|
|
||||||
n.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish result to waiters and clear the in-flight marker so a failed
|
|
||||||
// expansion can be retried by the next Expand call.
|
|
||||||
n.expandLock.Lock()
|
|
||||||
n.expandErr = err
|
|
||||||
n.expanding = false
|
|
||||||
close(done)
|
|
||||||
n.expandLock.Unlock()
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Browse returns the root nodes of the Galaxy hierarchy. The returned nodes
|
|
||||||
// have only their server-supplied hints populated; call Expand on each node to
|
|
||||||
// fetch its direct children. When opts is nil the server defaults apply.
|
|
||||||
func (c *GalaxyClient) Browse(ctx context.Context, opts *BrowseChildrenOptions) ([]*LazyBrowseNode, error) {
|
|
||||||
effective := BrowseChildrenOptions{}
|
|
||||||
if opts != nil {
|
|
||||||
effective = *opts
|
|
||||||
}
|
|
||||||
return c.browseChildrenInner(ctx, nil, effective)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BrowseChildrenRaw issues a single BrowseChildren RPC and returns the raw
|
|
||||||
// reply for callers that need direct page-token control. Transport-level
|
|
||||||
// failures are wrapped in *GatewayError to match the rest of the client.
|
|
||||||
func (c *GalaxyClient) BrowseChildrenRaw(ctx context.Context, req *pb.BrowseChildrenRequest) (*pb.BrowseChildrenReply, error) {
|
|
||||||
callCtx, cancel := c.callContext(ctx)
|
|
||||||
defer cancel()
|
|
||||||
reply, err := c.raw.BrowseChildren(callCtx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &GatewayError{Op: "galaxy browse children", Err: err}
|
|
||||||
}
|
|
||||||
return reply, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GalaxyClient) browseChildrenInner(
|
|
||||||
ctx context.Context,
|
|
||||||
parentGobjectID *int32,
|
|
||||||
opts BrowseChildrenOptions,
|
|
||||||
) ([]*LazyBrowseNode, error) {
|
|
||||||
var nodes []*LazyBrowseNode
|
|
||||||
pageToken := ""
|
|
||||||
seen := map[string]struct{}{}
|
|
||||||
for {
|
|
||||||
req := &pb.BrowseChildrenRequest{
|
|
||||||
PageSize: browseChildrenPageSize,
|
|
||||||
PageToken: pageToken,
|
|
||||||
CategoryIds: opts.CategoryIds,
|
|
||||||
TemplateChainContains: opts.TemplateChainContains,
|
|
||||||
TagNameGlob: opts.TagNameGlob,
|
|
||||||
AlarmBearingOnly: opts.AlarmBearingOnly,
|
|
||||||
HistorizedOnly: opts.HistorizedOnly,
|
|
||||||
}
|
|
||||||
if parentGobjectID != nil {
|
|
||||||
req.Parent = &pb.BrowseChildrenRequest_ParentGobjectId{ParentGobjectId: *parentGobjectID}
|
|
||||||
}
|
|
||||||
if opts.IncludeAttributes != nil {
|
|
||||||
req.IncludeAttributes = opts.IncludeAttributes
|
|
||||||
}
|
|
||||||
|
|
||||||
reply, err := c.BrowseChildrenRaw(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, child := range reply.GetChildren() {
|
|
||||||
hasChildren := reply.GetChildHasChildren()
|
|
||||||
hint := i < len(hasChildren) && hasChildren[i]
|
|
||||||
nodes = append(nodes, &LazyBrowseNode{
|
|
||||||
client: c,
|
|
||||||
object: child,
|
|
||||||
hasChildrenHint: hint,
|
|
||||||
options: opts,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pageToken = reply.GetNextPageToken()
|
|
||||||
if pageToken == "" {
|
|
||||||
return nodes, nil
|
|
||||||
}
|
|
||||||
if _, dup := seen[pageToken]; dup {
|
|
||||||
return nil, &GatewayError{
|
|
||||||
Op: "galaxy browse children",
|
|
||||||
Err: fmt.Errorf("repeated page token %q", pageToken),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
seen[pageToken] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GalaxyClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
func (c *GalaxyClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
timeout := c.opts.CallTimeout
|
timeout := c.opts.CallTimeout
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
"google.golang.org/grpc/test/bufconn"
|
"google.golang.org/grpc/test/bufconn"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
@@ -98,7 +96,9 @@ func TestGalaxyGetLastDeployTimeReturnsAbsentWhenTimestampNil(t *testing.T) {
|
|||||||
|
|
||||||
func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
||||||
fake := &fakeGalaxyServer{
|
fake := &fakeGalaxyServer{
|
||||||
discoverReply: &pb.DiscoverHierarchyReply{
|
discoverReplies: []*pb.DiscoverHierarchyReply{{
|
||||||
|
NextPageToken: "page-2",
|
||||||
|
TotalObjectCount: 2,
|
||||||
Objects: []*pb.GalaxyObject{
|
Objects: []*pb.GalaxyObject{
|
||||||
{
|
{
|
||||||
GobjectId: 1,
|
GobjectId: 1,
|
||||||
@@ -117,6 +117,10 @@ func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TotalObjectCount: 2,
|
||||||
|
Objects: []*pb.GalaxyObject{
|
||||||
{
|
{
|
||||||
GobjectId: 2,
|
GobjectId: 2,
|
||||||
TagName: "TestMachine_002",
|
TagName: "TestMachine_002",
|
||||||
@@ -124,7 +128,7 @@ func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
|||||||
ParentGobjectId: 1,
|
ParentGobjectId: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}},
|
||||||
}
|
}
|
||||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@@ -136,6 +140,15 @@ func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
|||||||
if len(objects) != 2 {
|
if len(objects) != 2 {
|
||||||
t.Fatalf("len(objects) = %d, want 2", len(objects))
|
t.Fatalf("len(objects) = %d, want 2", len(objects))
|
||||||
}
|
}
|
||||||
|
if len(fake.discoverRequests) != 2 {
|
||||||
|
t.Fatalf("len(discoverRequests) = %d, want 2", len(fake.discoverRequests))
|
||||||
|
}
|
||||||
|
if fake.discoverRequests[0].GetPageSize() != 5000 || fake.discoverRequests[0].GetPageToken() != "" {
|
||||||
|
t.Fatalf("first request = %+v", fake.discoverRequests[0])
|
||||||
|
}
|
||||||
|
if fake.discoverRequests[1].GetPageToken() != "page-2" {
|
||||||
|
t.Fatalf("second page_token = %q, want page-2", fake.discoverRequests[1].GetPageToken())
|
||||||
|
}
|
||||||
if objects[0].GetTagName() != "TestMachine_001" {
|
if objects[0].GetTagName() != "TestMachine_001" {
|
||||||
t.Fatalf("objects[0].TagName = %q", objects[0].GetTagName())
|
t.Fatalf("objects[0].TagName = %q", objects[0].GetTagName())
|
||||||
}
|
}
|
||||||
@@ -147,44 +160,22 @@ func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGalaxyDiscoverHierarchyPaginatesAcrossMultiplePages(t *testing.T) {
|
func TestGalaxyDiscoverHierarchyRejectsRepeatedPageToken(t *testing.T) {
|
||||||
page1 := &pb.DiscoverHierarchyReply{
|
|
||||||
Objects: []*pb.GalaxyObject{
|
|
||||||
{GobjectId: 1, TagName: "A"},
|
|
||||||
{GobjectId: 2, TagName: "B"},
|
|
||||||
},
|
|
||||||
NextPageToken: "page-2",
|
|
||||||
TotalObjectCount: 3,
|
|
||||||
}
|
|
||||||
page2 := &pb.DiscoverHierarchyReply{
|
|
||||||
Objects: []*pb.GalaxyObject{
|
|
||||||
{GobjectId: 3, TagName: "C"},
|
|
||||||
},
|
|
||||||
TotalObjectCount: 3,
|
|
||||||
}
|
|
||||||
fake := &fakeGalaxyServer{
|
fake := &fakeGalaxyServer{
|
||||||
discoverHierarchyReplies: []*pb.DiscoverHierarchyReply{page1, page2},
|
discoverReplies: []*pb.DiscoverHierarchyReply{
|
||||||
|
{NextPageToken: "7:1"},
|
||||||
|
{NextPageToken: "7:1"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
objs, err := client.DiscoverHierarchy(context.Background())
|
_, err := client.DiscoverHierarchy(context.Background())
|
||||||
if err != nil {
|
if err == nil {
|
||||||
t.Fatalf("DiscoverHierarchy: %v", err)
|
t.Fatal("DiscoverHierarchy() error = nil, want repeated token error")
|
||||||
}
|
}
|
||||||
if got, want := len(objs), 3; got != want {
|
if !strings.Contains(err.Error(), "repeated page token") {
|
||||||
t.Fatalf("len(objs) = %d, want %d", got, want)
|
t.Fatalf("error = %v, want repeated page token", err)
|
||||||
}
|
|
||||||
if len(fake.discoverHierarchyCalls) != 2 {
|
|
||||||
t.Fatalf("expected 2 RPC calls, got %d", len(fake.discoverHierarchyCalls))
|
|
||||||
}
|
|
||||||
if fake.discoverHierarchyCalls[0].GetPageSize() != discoverHierarchyPageSize {
|
|
||||||
t.Fatalf("first call PageSize = %d, want %d",
|
|
||||||
fake.discoverHierarchyCalls[0].GetPageSize(), discoverHierarchyPageSize)
|
|
||||||
}
|
|
||||||
if fake.discoverHierarchyCalls[1].GetPageToken() != "page-2" {
|
|
||||||
t.Fatalf("second call page token = %q, want %q",
|
|
||||||
fake.discoverHierarchyCalls[1].GetPageToken(), "page-2")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,15 +410,12 @@ type fakeGalaxyServer struct {
|
|||||||
failTest bool
|
failTest bool
|
||||||
deployReply *pb.GetLastDeployTimeReply
|
deployReply *pb.GetLastDeployTimeReply
|
||||||
discoverReply *pb.DiscoverHierarchyReply
|
discoverReply *pb.DiscoverHierarchyReply
|
||||||
discoverHierarchyCalls []*pb.DiscoverHierarchyRequest
|
discoverReplies []*pb.DiscoverHierarchyReply
|
||||||
discoverHierarchyReplies []*pb.DiscoverHierarchyReply
|
discoverRequests []*pb.DiscoverHierarchyRequest
|
||||||
watchEvents []*pb.DeployEvent
|
watchEvents []*pb.DeployEvent
|
||||||
watchRequest *pb.WatchDeployEventsRequest
|
watchRequest *pb.WatchDeployEventsRequest
|
||||||
watchSendInterval time.Duration
|
watchSendInterval time.Duration
|
||||||
watchHoldOpen bool
|
watchHoldOpen bool
|
||||||
browseChildrenCalls []*pb.BrowseChildrenRequest
|
|
||||||
browseChildrenReplies []*pb.BrowseChildrenReply
|
|
||||||
browseChildrenError error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *fakeGalaxyServer) TestConnection(ctx context.Context, req *pb.TestConnectionRequest) (*pb.TestConnectionReply, error) {
|
func (s *fakeGalaxyServer) TestConnection(ctx context.Context, req *pb.TestConnectionRequest) (*pb.TestConnectionReply, error) {
|
||||||
@@ -449,10 +437,10 @@ func (s *fakeGalaxyServer) GetLastDeployTime(ctx context.Context, req *pb.GetLas
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *fakeGalaxyServer) DiscoverHierarchy(ctx context.Context, req *pb.DiscoverHierarchyRequest) (*pb.DiscoverHierarchyReply, error) {
|
func (s *fakeGalaxyServer) DiscoverHierarchy(ctx context.Context, req *pb.DiscoverHierarchyRequest) (*pb.DiscoverHierarchyReply, error) {
|
||||||
s.discoverHierarchyCalls = append(s.discoverHierarchyCalls, req)
|
s.discoverRequests = append(s.discoverRequests, req)
|
||||||
if len(s.discoverHierarchyReplies) > 0 {
|
if len(s.discoverReplies) > 0 {
|
||||||
reply := s.discoverHierarchyReplies[0]
|
reply := s.discoverReplies[0]
|
||||||
s.discoverHierarchyReplies = s.discoverHierarchyReplies[1:]
|
s.discoverReplies = s.discoverReplies[1:]
|
||||||
return reply, nil
|
return reply, nil
|
||||||
}
|
}
|
||||||
if s.discoverReply != nil {
|
if s.discoverReply != nil {
|
||||||
@@ -480,385 +468,3 @@ func (s *fakeGalaxyServer) WatchDeployEvents(req *pb.WatchDeployEventsRequest, s
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *fakeGalaxyServer) BrowseChildren(ctx context.Context, req *pb.BrowseChildrenRequest) (*pb.BrowseChildrenReply, error) {
|
|
||||||
s.browseChildrenCalls = append(s.browseChildrenCalls, req)
|
|
||||||
if s.browseChildrenError != nil {
|
|
||||||
err := s.browseChildrenError
|
|
||||||
s.browseChildrenError = nil
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(s.browseChildrenReplies) == 0 {
|
|
||||||
return &pb.BrowseChildrenReply{}, nil
|
|
||||||
}
|
|
||||||
reply := s.browseChildrenReplies[0]
|
|
||||||
s.browseChildrenReplies = s.browseChildrenReplies[1:]
|
|
||||||
return reply, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func obj(id int32, tag string, isArea bool) *pb.GalaxyObject {
|
|
||||||
return &pb.GalaxyObject{
|
|
||||||
GobjectId: id,
|
|
||||||
TagName: tag,
|
|
||||||
BrowseName: tag,
|
|
||||||
IsArea: isArea,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildBrowseReply(children []*pb.GalaxyObject, hasChildren []bool, seq uint64) *pb.BrowseChildrenReply {
|
|
||||||
return &pb.BrowseChildrenReply{
|
|
||||||
TotalChildCount: int32(len(children)),
|
|
||||||
CacheSequence: seq,
|
|
||||||
Children: children,
|
|
||||||
ChildHasChildren: hasChildren,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGalaxyBrowseNoParentReturnsRoots(t *testing.T) {
|
|
||||||
fake := &fakeGalaxyServer{
|
|
||||||
browseChildrenReplies: []*pb.BrowseChildrenReply{
|
|
||||||
buildBrowseReply(
|
|
||||||
[]*pb.GalaxyObject{obj(1, "Plant", true), obj(99, "Other", false)},
|
|
||||||
[]bool{true, false},
|
|
||||||
7,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
roots, err := client.Browse(context.Background(), nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Browse: %v", err)
|
|
||||||
}
|
|
||||||
if got, want := len(roots), 2; got != want {
|
|
||||||
t.Fatalf("len(roots) = %d, want %d", got, want)
|
|
||||||
}
|
|
||||||
if roots[0].Object().GetTagName() != "Plant" {
|
|
||||||
t.Fatalf("roots[0].TagName = %q", roots[0].Object().GetTagName())
|
|
||||||
}
|
|
||||||
if !roots[0].HasChildrenHint() {
|
|
||||||
t.Fatal("roots[0].HasChildrenHint = false, want true")
|
|
||||||
}
|
|
||||||
if roots[0].IsExpanded() {
|
|
||||||
t.Fatal("roots[0].IsExpanded = true, want false")
|
|
||||||
}
|
|
||||||
if roots[1].HasChildrenHint() {
|
|
||||||
t.Fatal("roots[1].HasChildrenHint = true, want false")
|
|
||||||
}
|
|
||||||
if len(fake.browseChildrenCalls) != 1 {
|
|
||||||
t.Fatalf("BrowseChildren calls = %d, want 1", len(fake.browseChildrenCalls))
|
|
||||||
}
|
|
||||||
if fake.browseChildrenCalls[0].GetParent() != nil {
|
|
||||||
t.Fatalf("root browse should not set Parent oneof, got %T", fake.browseChildrenCalls[0].GetParent())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGalaxyBrowseExpandPopulatesChildrenAndMarksExpanded(t *testing.T) {
|
|
||||||
fake := &fakeGalaxyServer{
|
|
||||||
browseChildrenReplies: []*pb.BrowseChildrenReply{
|
|
||||||
buildBrowseReply(
|
|
||||||
[]*pb.GalaxyObject{obj(1, "Plant", true)},
|
|
||||||
[]bool{true},
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
buildBrowseReply(
|
|
||||||
[]*pb.GalaxyObject{obj(10, "Area1", true), obj(11, "Tank1", false)},
|
|
||||||
[]bool{true, false},
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
roots, err := client.Browse(context.Background(), nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Browse: %v", err)
|
|
||||||
}
|
|
||||||
if len(roots) != 1 {
|
|
||||||
t.Fatalf("len(roots) = %d, want 1", len(roots))
|
|
||||||
}
|
|
||||||
plant := roots[0]
|
|
||||||
if plant.IsExpanded() {
|
|
||||||
t.Fatal("plant.IsExpanded = true before Expand, want false")
|
|
||||||
}
|
|
||||||
if err := plant.Expand(context.Background()); err != nil {
|
|
||||||
t.Fatalf("Expand: %v", err)
|
|
||||||
}
|
|
||||||
if !plant.IsExpanded() {
|
|
||||||
t.Fatal("plant.IsExpanded = false after Expand, want true")
|
|
||||||
}
|
|
||||||
children := plant.Children()
|
|
||||||
if len(children) != 2 {
|
|
||||||
t.Fatalf("len(children) = %d, want 2", len(children))
|
|
||||||
}
|
|
||||||
if children[0].Object().GetTagName() != "Area1" {
|
|
||||||
t.Fatalf("children[0].TagName = %q, want Area1", children[0].Object().GetTagName())
|
|
||||||
}
|
|
||||||
if !children[0].HasChildrenHint() {
|
|
||||||
t.Fatal("children[0].HasChildrenHint = false, want true")
|
|
||||||
}
|
|
||||||
if children[1].HasChildrenHint() {
|
|
||||||
t.Fatal("children[1].HasChildrenHint = true, want false")
|
|
||||||
}
|
|
||||||
if len(fake.browseChildrenCalls) != 2 {
|
|
||||||
t.Fatalf("BrowseChildren calls = %d, want 2", len(fake.browseChildrenCalls))
|
|
||||||
}
|
|
||||||
parent := fake.browseChildrenCalls[1].GetParent()
|
|
||||||
parentGobj, ok := parent.(*pb.BrowseChildrenRequest_ParentGobjectId)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Parent oneof = %T, want *BrowseChildrenRequest_ParentGobjectId", parent)
|
|
||||||
}
|
|
||||||
if parentGobj.ParentGobjectId != 1 {
|
|
||||||
t.Fatalf("ParentGobjectId = %d, want 1", parentGobj.ParentGobjectId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGalaxyBrowseExpandIdempotentNoSecondRpc(t *testing.T) {
|
|
||||||
fake := &fakeGalaxyServer{
|
|
||||||
browseChildrenReplies: []*pb.BrowseChildrenReply{
|
|
||||||
buildBrowseReply(
|
|
||||||
[]*pb.GalaxyObject{obj(1, "Plant", true)},
|
|
||||||
[]bool{true},
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
buildBrowseReply(
|
|
||||||
[]*pb.GalaxyObject{obj(10, "Area1", true)},
|
|
||||||
[]bool{false},
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
roots, err := client.Browse(context.Background(), nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Browse: %v", err)
|
|
||||||
}
|
|
||||||
plant := roots[0]
|
|
||||||
if err := plant.Expand(context.Background()); err != nil {
|
|
||||||
t.Fatalf("Expand #1: %v", err)
|
|
||||||
}
|
|
||||||
callsAfterFirst := len(fake.browseChildrenCalls)
|
|
||||||
if callsAfterFirst != 2 {
|
|
||||||
t.Fatalf("BrowseChildren calls after first Expand = %d, want 2", callsAfterFirst)
|
|
||||||
}
|
|
||||||
if err := plant.Expand(context.Background()); err != nil {
|
|
||||||
t.Fatalf("Expand #2: %v", err)
|
|
||||||
}
|
|
||||||
if got := len(fake.browseChildrenCalls); got != callsAfterFirst {
|
|
||||||
t.Fatalf("BrowseChildren calls after second Expand = %d, want %d (no extra RPC)", got, callsAfterFirst)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGalaxyBrowseExpandUnknownParentReturnsNotFoundError(t *testing.T) {
|
|
||||||
fake := &fakeGalaxyServer{
|
|
||||||
browseChildrenReplies: []*pb.BrowseChildrenReply{
|
|
||||||
buildBrowseReply(
|
|
||||||
[]*pb.GalaxyObject{obj(1, "Plant", true)},
|
|
||||||
[]bool{true},
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
browseChildrenError: status.Error(codes.NotFound, "parent not found"),
|
|
||||||
}
|
|
||||||
// The first Browse() consumes the first reply; the next call (Expand) will
|
|
||||||
// then hit browseChildrenError. We need the error to fire only on the second
|
|
||||||
// call, so seed the reply first and let the call sequence consume them in
|
|
||||||
// order. Because BrowseChildren in the fake consumes browseChildrenError
|
|
||||||
// before falling through to replies, swap the strategy: keep the root reply
|
|
||||||
// but have BrowseChildren return the error on the second call. We do this by
|
|
||||||
// emptying the reply list after the first Browse.
|
|
||||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
// First call returns the error (because browseChildrenError takes precedence).
|
|
||||||
// To avoid that, clear it for the root call by performing a manual setup: we
|
|
||||||
// pre-stage replies first, then set the error after the first call. Easiest:
|
|
||||||
// pre-Browse() with error=nil, then set error before Expand.
|
|
||||||
fake.browseChildrenError = nil
|
|
||||||
roots, err := client.Browse(context.Background(), nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Browse: %v", err)
|
|
||||||
}
|
|
||||||
if len(roots) != 1 {
|
|
||||||
t.Fatalf("len(roots) = %d, want 1", len(roots))
|
|
||||||
}
|
|
||||||
fake.browseChildrenError = status.Error(codes.NotFound, "parent not found")
|
|
||||||
|
|
||||||
err = roots[0].Expand(context.Background())
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Expand: error = nil, want NotFound")
|
|
||||||
}
|
|
||||||
if status.Code(err) != codes.NotFound {
|
|
||||||
t.Fatalf("status.Code = %s, want NotFound", status.Code(err))
|
|
||||||
}
|
|
||||||
if roots[0].IsExpanded() {
|
|
||||||
t.Fatal("roots[0].IsExpanded = true after failed Expand, want false")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGalaxyBrowseExpandMultiPageGathersAllPages(t *testing.T) {
|
|
||||||
firstPage := buildBrowseReply(
|
|
||||||
[]*pb.GalaxyObject{obj(1, "Plant", true)},
|
|
||||||
[]bool{true},
|
|
||||||
7,
|
|
||||||
)
|
|
||||||
|
|
||||||
pageA := buildBrowseReply(
|
|
||||||
[]*pb.GalaxyObject{obj(10, "Child1", false), obj(11, "Child2", false)},
|
|
||||||
[]bool{false, false},
|
|
||||||
7,
|
|
||||||
)
|
|
||||||
pageA.NextPageToken = "7:abc:2"
|
|
||||||
pageB := buildBrowseReply(
|
|
||||||
[]*pb.GalaxyObject{obj(12, "Child3", false)},
|
|
||||||
[]bool{false},
|
|
||||||
7,
|
|
||||||
)
|
|
||||||
|
|
||||||
fake := &fakeGalaxyServer{
|
|
||||||
browseChildrenReplies: []*pb.BrowseChildrenReply{firstPage, pageA, pageB},
|
|
||||||
}
|
|
||||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
roots, err := client.Browse(context.Background(), nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Browse: %v", err)
|
|
||||||
}
|
|
||||||
if err := roots[0].Expand(context.Background()); err != nil {
|
|
||||||
t.Fatalf("Expand: %v", err)
|
|
||||||
}
|
|
||||||
children := roots[0].Children()
|
|
||||||
if len(children) != 3 {
|
|
||||||
t.Fatalf("len(children) = %d, want 3", len(children))
|
|
||||||
}
|
|
||||||
if len(fake.browseChildrenCalls) != 3 {
|
|
||||||
t.Fatalf("BrowseChildren calls = %d, want 3", len(fake.browseChildrenCalls))
|
|
||||||
}
|
|
||||||
if got := fake.browseChildrenCalls[2].GetPageToken(); got != "7:abc:2" {
|
|
||||||
t.Fatalf("third call PageToken = %q, want %q", got, "7:abc:2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGalaxyBrowseWithFilterForwardsToRequest(t *testing.T) {
|
|
||||||
fake := &fakeGalaxyServer{
|
|
||||||
browseChildrenReplies: []*pb.BrowseChildrenReply{
|
|
||||||
buildBrowseReply(nil, nil, 1),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
include := true
|
|
||||||
opts := &BrowseChildrenOptions{
|
|
||||||
CategoryIds: []int32{7, 9},
|
|
||||||
TemplateChainContains: []string{"$AppObject"},
|
|
||||||
TagNameGlob: "Tank*",
|
|
||||||
IncludeAttributes: &include,
|
|
||||||
AlarmBearingOnly: true,
|
|
||||||
HistorizedOnly: true,
|
|
||||||
}
|
|
||||||
if _, err := client.Browse(context.Background(), opts); err != nil {
|
|
||||||
t.Fatalf("Browse: %v", err)
|
|
||||||
}
|
|
||||||
if len(fake.browseChildrenCalls) != 1 {
|
|
||||||
t.Fatalf("BrowseChildren calls = %d, want 1", len(fake.browseChildrenCalls))
|
|
||||||
}
|
|
||||||
got := fake.browseChildrenCalls[0]
|
|
||||||
if want := []int32{7, 9}; len(got.GetCategoryIds()) != 2 || got.GetCategoryIds()[0] != want[0] || got.GetCategoryIds()[1] != want[1] {
|
|
||||||
t.Fatalf("CategoryIds = %v, want %v", got.GetCategoryIds(), want)
|
|
||||||
}
|
|
||||||
if want := []string{"$AppObject"}; len(got.GetTemplateChainContains()) != 1 || got.GetTemplateChainContains()[0] != want[0] {
|
|
||||||
t.Fatalf("TemplateChainContains = %v, want %v", got.GetTemplateChainContains(), want)
|
|
||||||
}
|
|
||||||
if got.GetTagNameGlob() != "Tank*" {
|
|
||||||
t.Fatalf("TagNameGlob = %q, want %q", got.GetTagNameGlob(), "Tank*")
|
|
||||||
}
|
|
||||||
if !got.GetIncludeAttributes() {
|
|
||||||
t.Fatal("IncludeAttributes = false, want true")
|
|
||||||
}
|
|
||||||
if !got.GetAlarmBearingOnly() {
|
|
||||||
t.Fatal("AlarmBearingOnly = false, want true")
|
|
||||||
}
|
|
||||||
if !got.GetHistorizedOnly() {
|
|
||||||
t.Fatal("HistorizedOnly = false, want true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGalaxyBrowseExpandConcurrentCallersOnlyFireOneRpc(t *testing.T) {
|
|
||||||
fake := &fakeGalaxyServer{
|
|
||||||
browseChildrenReplies: []*pb.BrowseChildrenReply{
|
|
||||||
// roots
|
|
||||||
buildBrowseReply([]*pb.GalaxyObject{obj(1, "Plant", true)}, []bool{true}, 7),
|
|
||||||
// one expand: one child
|
|
||||||
buildBrowseReply([]*pb.GalaxyObject{obj(2, "Mixer", false)}, []bool{false}, 7),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
roots, err := client.Browse(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Browse: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
errs := make(chan error, 10)
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
errs <- roots[0].Expand(ctx)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
close(errs)
|
|
||||||
for err := range errs {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("concurrent Expand: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !roots[0].IsExpanded() {
|
|
||||||
t.Fatal("IsExpanded() = false after 10 concurrent expands")
|
|
||||||
}
|
|
||||||
if got, want := len(roots[0].Children()), 1; got != want {
|
|
||||||
t.Fatalf("len(children) = %d, want %d", got, want)
|
|
||||||
}
|
|
||||||
// 1 roots fetch + exactly 1 expand fetch.
|
|
||||||
if got, want := len(fake.browseChildrenCalls), 2; got != want {
|
|
||||||
t.Fatalf("RPC count = %d, want %d", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGalaxyBrowseChildrenRejectsRepeatedPageToken(t *testing.T) {
|
|
||||||
// Build a reply that carries a non-empty NextPageToken so browseChildrenInner
|
|
||||||
// will request a second page. Queue the same reply twice so the second response
|
|
||||||
// returns the same page token, triggering the duplicate-token guard.
|
|
||||||
page := buildBrowseReply(
|
|
||||||
[]*pb.GalaxyObject{obj(1, "Plant", true)},
|
|
||||||
[]bool{true},
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
page.NextPageToken = "1:abc:1"
|
|
||||||
|
|
||||||
fake := &fakeGalaxyServer{
|
|
||||||
browseChildrenReplies: []*pb.BrowseChildrenReply{page, page},
|
|
||||||
}
|
|
||||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
_, err := client.Browse(context.Background(), nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Browse: error = nil, want repeated-page-token error")
|
|
||||||
}
|
|
||||||
var gwErr *GatewayError
|
|
||||||
if !errors.As(err, &gwErr) {
|
|
||||||
t.Fatalf("error type = %T, want *GatewayError; err = %v", err, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,55 +11,17 @@ import (
|
|||||||
|
|
||||||
// Options configures gateway connections.
|
// Options configures gateway connections.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// Endpoint is the gateway host:port address to dial.
|
|
||||||
Endpoint string
|
Endpoint string
|
||||||
// APIKey is the bearer token attached to outgoing gRPC metadata.
|
|
||||||
APIKey string
|
APIKey string
|
||||||
// Plaintext disables TLS and uses insecure credentials when true.
|
|
||||||
Plaintext bool
|
Plaintext bool
|
||||||
// CACertFile points to a PEM file used to verify the gateway certificate.
|
|
||||||
CACertFile string
|
CACertFile string
|
||||||
// ServerNameOverride overrides the TLS SNI/SAN name presented to the gateway.
|
|
||||||
ServerNameOverride string
|
ServerNameOverride string
|
||||||
// DialTimeout bounds the blocking Dial; zero applies a built-in default.
|
|
||||||
DialTimeout time.Duration
|
DialTimeout time.Duration
|
||||||
// CallTimeout bounds each unary RPC; zero applies a built-in default and
|
|
||||||
// negative disables the bound entirely.
|
|
||||||
CallTimeout time.Duration
|
CallTimeout time.Duration
|
||||||
// TLSConfig supplies a custom TLS configuration; takes precedence over
|
MaxGrpcMessageBytes int
|
||||||
// CACertFile when TransportCredentials is unset.
|
|
||||||
TLSConfig *tls.Config
|
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 are appended to the gRPC dial options after the defaults.
|
|
||||||
DialOptions []grpc.DialOption
|
DialOptions []grpc.DialOption
|
||||||
// RequireCertificateValidation forces TLS certificate verification even when
|
|
||||||
// no CACertFile is pinned. Default false: the gateway's self-signed cert is
|
|
||||||
// accepted without verification (internal-tool posture).
|
|
||||||
RequireCertificateValidation bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// BrowseChildrenOptions configures lazy Galaxy hierarchy walks performed by
|
|
||||||
// (*GalaxyClient).Browse and (*LazyBrowseNode).Expand. All fields are optional;
|
|
||||||
// the zero value matches the dashboard default (no filters, all attributes per
|
|
||||||
// the server default).
|
|
||||||
type BrowseChildrenOptions struct {
|
|
||||||
// CategoryIds restricts results to the listed Galaxy category ids when set.
|
|
||||||
CategoryIds []int32
|
|
||||||
// TemplateChainContains restricts results to objects whose template chain
|
|
||||||
// contains any of the listed template tag names.
|
|
||||||
TemplateChainContains []string
|
|
||||||
// TagNameGlob restricts results to objects whose tag name matches the glob
|
|
||||||
// pattern when non-empty.
|
|
||||||
TagNameGlob string
|
|
||||||
// IncludeAttributes overrides the server default for attribute inclusion when
|
|
||||||
// non-nil. The pointer form mirrors the proto's optional field.
|
|
||||||
IncludeAttributes *bool
|
|
||||||
// AlarmBearingOnly limits results to alarm-bearing objects when true.
|
|
||||||
AlarmBearingOnly bool
|
|
||||||
// HistorizedOnly limits results to historized objects when true.
|
|
||||||
HistorizedOnly bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RedactedAPIKey returns a display-safe representation of the configured API
|
// RedactedAPIKey returns a display-safe representation of the configured API
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
@@ -19,9 +18,7 @@ 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 is the terminal stream error; when non-nil no further results follow.
|
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,173 +385,6 @@ func (s *Session) UnsubscribeBulk(ctx context.Context, serverHandle int32, itemH
|
|||||||
return reply.GetUnsubscribeBulk().GetResults(), nil
|
return reply.GetUnsubscribeBulk().GetResults(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteBulk invokes MXAccess Write sequentially for each entry inside one gateway command.
|
|
||||||
// Per-entry failures appear as BulkWriteResult entries with WasSuccessful=false; the call
|
|
||||||
// never returns an error for per-entry MXAccess failures (it returns an error only for
|
|
||||||
// protocol-level failures or transport errors).
|
|
||||||
//
|
|
||||||
// A non-nil but empty entries slice is treated as a no-op and returns an empty result
|
|
||||||
// without a wire round-trip; pass nil to surface a clear "entries are required" error.
|
|
||||||
func (s *Session) WriteBulk(ctx context.Context, serverHandle int32, entries []*WriteBulkEntry) ([]*BulkWriteResult, error) {
|
|
||||||
if entries == nil {
|
|
||||||
return nil, errors.New("mxgateway: write bulk entries are required")
|
|
||||||
}
|
|
||||||
if err := ensureBulkSize("write bulk entries", len(entries)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(entries) == 0 {
|
|
||||||
return []*BulkWriteResult{}, nil
|
|
||||||
}
|
|
||||||
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_WRITE_BULK,
|
|
||||||
Payload: &pb.MxCommand_WriteBulk{
|
|
||||||
WriteBulk: &pb.WriteBulkCommand{
|
|
||||||
ServerHandle: serverHandle,
|
|
||||||
Entries: entries,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return reply.GetWriteBulk().GetResults(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write2Bulk invokes MXAccess Write2 (timestamped) for each entry inside one gateway command.
|
|
||||||
//
|
|
||||||
// A non-nil but empty entries slice is treated as a no-op and returns an empty result
|
|
||||||
// without a wire round-trip; pass nil to surface a clear "entries are required" error.
|
|
||||||
func (s *Session) Write2Bulk(ctx context.Context, serverHandle int32, entries []*Write2BulkEntry) ([]*BulkWriteResult, error) {
|
|
||||||
if entries == nil {
|
|
||||||
return nil, errors.New("mxgateway: write2 bulk entries are required")
|
|
||||||
}
|
|
||||||
if err := ensureBulkSize("write2 bulk entries", len(entries)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(entries) == 0 {
|
|
||||||
return []*BulkWriteResult{}, nil
|
|
||||||
}
|
|
||||||
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_WRITE2_BULK,
|
|
||||||
Payload: &pb.MxCommand_Write2Bulk{
|
|
||||||
Write2Bulk: &pb.Write2BulkCommand{
|
|
||||||
ServerHandle: serverHandle,
|
|
||||||
Entries: entries,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return reply.GetWrite2Bulk().GetResults(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteSecuredBulk invokes MXAccess WriteSecured for each entry. Credential-sensitive
|
|
||||||
// values must not be logged by callers; mirrors the single-item WriteSecured contract.
|
|
||||||
//
|
|
||||||
// A non-nil but empty entries slice is treated as a no-op and returns an empty result
|
|
||||||
// without a wire round-trip; pass nil to surface a clear "entries are required" error.
|
|
||||||
func (s *Session) WriteSecuredBulk(ctx context.Context, serverHandle int32, entries []*WriteSecuredBulkEntry) ([]*BulkWriteResult, error) {
|
|
||||||
if entries == nil {
|
|
||||||
return nil, errors.New("mxgateway: write-secured bulk entries are required")
|
|
||||||
}
|
|
||||||
if err := ensureBulkSize("write-secured bulk entries", len(entries)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(entries) == 0 {
|
|
||||||
return []*BulkWriteResult{}, nil
|
|
||||||
}
|
|
||||||
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_WRITE_SECURED_BULK,
|
|
||||||
Payload: &pb.MxCommand_WriteSecuredBulk{
|
|
||||||
WriteSecuredBulk: &pb.WriteSecuredBulkCommand{
|
|
||||||
ServerHandle: serverHandle,
|
|
||||||
Entries: entries,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return reply.GetWriteSecuredBulk().GetResults(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteSecured2Bulk invokes MXAccess WriteSecured2 (timestamped) for each entry.
|
|
||||||
//
|
|
||||||
// A non-nil but empty entries slice is treated as a no-op and returns an empty result
|
|
||||||
// without a wire round-trip; pass nil to surface a clear "entries are required" error.
|
|
||||||
func (s *Session) WriteSecured2Bulk(ctx context.Context, serverHandle int32, entries []*WriteSecured2BulkEntry) ([]*BulkWriteResult, error) {
|
|
||||||
if entries == nil {
|
|
||||||
return nil, errors.New("mxgateway: write-secured2 bulk entries are required")
|
|
||||||
}
|
|
||||||
if err := ensureBulkSize("write-secured2 bulk entries", len(entries)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(entries) == 0 {
|
|
||||||
return []*BulkWriteResult{}, nil
|
|
||||||
}
|
|
||||||
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_WRITE_SECURED2_BULK,
|
|
||||||
Payload: &pb.MxCommand_WriteSecured2Bulk{
|
|
||||||
WriteSecured2Bulk: &pb.WriteSecured2BulkCommand{
|
|
||||||
ServerHandle: serverHandle,
|
|
||||||
Entries: entries,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return reply.GetWriteSecured2Bulk().GetResults(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBulk snapshots the current value of each requested tag.
|
|
||||||
//
|
|
||||||
// MXAccess COM has no synchronous Read; the worker satisfies this by returning the
|
|
||||||
// most recent cached OnDataChange value when the tag is already advised (WasCached=true),
|
|
||||||
// or by taking a full AddItem + Advise + wait + UnAdvise + RemoveItem snapshot lifecycle
|
|
||||||
// otherwise. timeout bounds the wait per tag in the snapshot case; pass zero to use the
|
|
||||||
// worker default. Per-tag failures (timeout, invalid tag) appear as BulkReadResult entries
|
|
||||||
// with WasSuccessful=false; the call never returns an error for per-tag MXAccess failures.
|
|
||||||
//
|
|
||||||
// A non-nil but empty tagAddresses slice is treated as a no-op and returns an empty
|
|
||||||
// result without a wire round-trip; pass nil to surface a clear "tag addresses are
|
|
||||||
// required" error.
|
|
||||||
func (s *Session) ReadBulk(ctx context.Context, serverHandle int32, tagAddresses []string, timeout time.Duration) ([]*BulkReadResult, error) {
|
|
||||||
if tagAddresses == nil {
|
|
||||||
return nil, errors.New("mxgateway: tag addresses are required")
|
|
||||||
}
|
|
||||||
if err := ensureBulkSize("tag addresses", len(tagAddresses)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(tagAddresses) == 0 {
|
|
||||||
return []*BulkReadResult{}, nil
|
|
||||||
}
|
|
||||||
var timeoutMs uint32
|
|
||||||
if timeout > 0 {
|
|
||||||
ms := timeout.Milliseconds()
|
|
||||||
if ms > int64(^uint32(0)) {
|
|
||||||
timeoutMs = ^uint32(0)
|
|
||||||
} else {
|
|
||||||
timeoutMs = uint32(ms)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_READ_BULK,
|
|
||||||
Payload: &pb.MxCommand_ReadBulk{
|
|
||||||
ReadBulk: &pb.ReadBulkCommand{
|
|
||||||
ServerHandle: serverHandle,
|
|
||||||
TagAddresses: tagAddresses,
|
|
||||||
TimeoutMs: timeoutMs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return reply.GetReadBulk().GetResults(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write invokes MXAccess Write.
|
// Write invokes MXAccess Write.
|
||||||
func (s *Session) Write(ctx context.Context, serverHandle, itemHandle int32, value *MxValue, userID int32) error {
|
func (s *Session) Write(ctx context.Context, serverHandle, itemHandle int32, value *MxValue, userID int32) error {
|
||||||
_, err := s.WriteRaw(ctx, serverHandle, itemHandle, value, userID)
|
_, err := s.WriteRaw(ctx, serverHandle, itemHandle, value, userID)
|
||||||
|
|||||||
@@ -12,202 +12,77 @@ 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 is the gateway OpenSession request message.
|
|
||||||
OpenSessionRequest = pb.OpenSessionRequest
|
OpenSessionRequest = pb.OpenSessionRequest
|
||||||
// OpenSessionReply is the gateway OpenSession reply message.
|
|
||||||
OpenSessionReply = pb.OpenSessionReply
|
OpenSessionReply = pb.OpenSessionReply
|
||||||
// CloseSessionRequest is the gateway CloseSession request message.
|
|
||||||
CloseSessionRequest = pb.CloseSessionRequest
|
CloseSessionRequest = pb.CloseSessionRequest
|
||||||
// CloseSessionReply is the gateway CloseSession reply message.
|
|
||||||
CloseSessionReply = pb.CloseSessionReply
|
CloseSessionReply = pb.CloseSessionReply
|
||||||
// StreamEventsRequest is the gateway StreamEvents request message.
|
|
||||||
StreamEventsRequest = pb.StreamEventsRequest
|
StreamEventsRequest = pb.StreamEventsRequest
|
||||||
// MxCommandRequest carries one MXAccess command for Invoke.
|
|
||||||
MxCommandRequest = pb.MxCommandRequest
|
MxCommandRequest = pb.MxCommandRequest
|
||||||
// MxCommandReply is the reply to an MXAccess command Invoke.
|
|
||||||
MxCommandReply = pb.MxCommandReply
|
MxCommandReply = pb.MxCommandReply
|
||||||
// MxCommand is the discriminated union of MXAccess command payloads.
|
|
||||||
MxCommand = pb.MxCommand
|
MxCommand = pb.MxCommand
|
||||||
// MxEvent is one ordered event delivered on a session event stream.
|
|
||||||
MxEvent = pb.MxEvent
|
MxEvent = pb.MxEvent
|
||||||
// MxValue is the protobuf representation of an MXAccess value.
|
|
||||||
MxValue = pb.MxValue
|
MxValue = pb.MxValue
|
||||||
// Value is an alias for MxValue retained for symmetry with other clients.
|
|
||||||
Value = pb.MxValue
|
Value = pb.MxValue
|
||||||
// MxArray is the protobuf representation of an MXAccess array value.
|
|
||||||
MxArray = pb.MxArray
|
MxArray = pb.MxArray
|
||||||
// MxStatusProxy mirrors the MXAccess MXSTATUS_PROXY structure.
|
|
||||||
MxStatusProxy = pb.MxStatusProxy
|
MxStatusProxy = pb.MxStatusProxy
|
||||||
// ProtocolStatus is the gateway-level status carried on every reply.
|
|
||||||
ProtocolStatus = pb.ProtocolStatus
|
ProtocolStatus = pb.ProtocolStatus
|
||||||
// RegisterCommand is the payload of an MXAccess Register command.
|
|
||||||
RegisterCommand = pb.RegisterCommand
|
RegisterCommand = pb.RegisterCommand
|
||||||
// UnregisterCommand is the payload of an MXAccess Unregister command.
|
|
||||||
UnregisterCommand = pb.UnregisterCommand
|
UnregisterCommand = pb.UnregisterCommand
|
||||||
// AddItemCommand is the payload of an MXAccess AddItem command.
|
|
||||||
AddItemCommand = pb.AddItemCommand
|
AddItemCommand = pb.AddItemCommand
|
||||||
// AddItem2Command is the payload of an MXAccess AddItem2 command.
|
|
||||||
AddItem2Command = pb.AddItem2Command
|
AddItem2Command = pb.AddItem2Command
|
||||||
// RemoveItemCommand is the payload of an MXAccess RemoveItem command.
|
|
||||||
RemoveItemCommand = pb.RemoveItemCommand
|
RemoveItemCommand = pb.RemoveItemCommand
|
||||||
// AdviseCommand is the payload of an MXAccess Advise command.
|
|
||||||
AdviseCommand = pb.AdviseCommand
|
AdviseCommand = pb.AdviseCommand
|
||||||
// UnAdviseCommand is the payload of an MXAccess UnAdvise command.
|
|
||||||
UnAdviseCommand = pb.UnAdviseCommand
|
UnAdviseCommand = pb.UnAdviseCommand
|
||||||
// AddItemBulkCommand is the payload of an AddItem bulk command.
|
|
||||||
AddItemBulkCommand = pb.AddItemBulkCommand
|
AddItemBulkCommand = pb.AddItemBulkCommand
|
||||||
// AdviseItemBulkCommand is the payload of an Advise bulk command.
|
|
||||||
AdviseItemBulkCommand = pb.AdviseItemBulkCommand
|
AdviseItemBulkCommand = pb.AdviseItemBulkCommand
|
||||||
// RemoveItemBulkCommand is the payload of a RemoveItem bulk command.
|
|
||||||
RemoveItemBulkCommand = pb.RemoveItemBulkCommand
|
RemoveItemBulkCommand = pb.RemoveItemBulkCommand
|
||||||
// UnAdviseItemBulkCommand is the payload of an UnAdvise bulk command.
|
|
||||||
UnAdviseItemBulkCommand = pb.UnAdviseItemBulkCommand
|
UnAdviseItemBulkCommand = pb.UnAdviseItemBulkCommand
|
||||||
// SubscribeBulkCommand combines AddItem and Advise for a list of tags.
|
|
||||||
SubscribeBulkCommand = pb.SubscribeBulkCommand
|
SubscribeBulkCommand = pb.SubscribeBulkCommand
|
||||||
// UnsubscribeBulkCommand combines UnAdvise and RemoveItem for a list of items.
|
|
||||||
UnsubscribeBulkCommand = pb.UnsubscribeBulkCommand
|
UnsubscribeBulkCommand = pb.UnsubscribeBulkCommand
|
||||||
// WriteCommand is the payload of an MXAccess Write command.
|
|
||||||
WriteCommand = pb.WriteCommand
|
WriteCommand = pb.WriteCommand
|
||||||
// Write2Command is the payload of an MXAccess Write2 command.
|
|
||||||
Write2Command = pb.Write2Command
|
Write2Command = pb.Write2Command
|
||||||
// WriteBulkCommand is the payload of a bulk Write command.
|
|
||||||
WriteBulkCommand = pb.WriteBulkCommand
|
|
||||||
// WriteBulkEntry is one entry inside a WriteBulkCommand.
|
|
||||||
WriteBulkEntry = pb.WriteBulkEntry
|
|
||||||
// Write2BulkCommand is the payload of a bulk Write2 (timestamped) command.
|
|
||||||
Write2BulkCommand = pb.Write2BulkCommand
|
|
||||||
// Write2BulkEntry is one entry inside a Write2BulkCommand.
|
|
||||||
Write2BulkEntry = pb.Write2BulkEntry
|
|
||||||
// WriteSecuredBulkCommand is the payload of a bulk WriteSecured command.
|
|
||||||
WriteSecuredBulkCommand = pb.WriteSecuredBulkCommand
|
|
||||||
// WriteSecuredBulkEntry is one entry inside a WriteSecuredBulkCommand.
|
|
||||||
WriteSecuredBulkEntry = pb.WriteSecuredBulkEntry
|
|
||||||
// WriteSecured2BulkCommand is the payload of a bulk WriteSecured2 (timestamped) command.
|
|
||||||
WriteSecured2BulkCommand = pb.WriteSecured2BulkCommand
|
|
||||||
// WriteSecured2BulkEntry is one entry inside a WriteSecured2BulkCommand.
|
|
||||||
WriteSecured2BulkEntry = pb.WriteSecured2BulkEntry
|
|
||||||
// ReadBulkCommand is the payload of a bulk Read snapshot command.
|
|
||||||
ReadBulkCommand = pb.ReadBulkCommand
|
|
||||||
// BulkWriteReply aggregates BulkWriteResult entries for a bulk write command.
|
|
||||||
BulkWriteReply = pb.BulkWriteReply
|
|
||||||
// BulkWriteResult is one entry in a bulk write reply list.
|
|
||||||
BulkWriteResult = pb.BulkWriteResult
|
|
||||||
// BulkReadReply aggregates BulkReadResult entries for a bulk read command.
|
|
||||||
BulkReadReply = pb.BulkReadReply
|
|
||||||
// BulkReadResult is one entry in a bulk read reply list.
|
|
||||||
BulkReadResult = pb.BulkReadResult
|
|
||||||
// RegisterReply carries the ServerHandle returned by Register.
|
|
||||||
RegisterReply = pb.RegisterReply
|
RegisterReply = pb.RegisterReply
|
||||||
// AddItemReply carries the ItemHandle returned by AddItem.
|
|
||||||
AddItemReply = pb.AddItemReply
|
AddItemReply = pb.AddItemReply
|
||||||
// AddItem2Reply carries the ItemHandle returned by AddItem2.
|
|
||||||
AddItem2Reply = pb.AddItem2Reply
|
AddItem2Reply = pb.AddItem2Reply
|
||||||
// SubscribeResult is one entry in a bulk command result list.
|
|
||||||
SubscribeResult = pb.SubscribeResult
|
SubscribeResult = pb.SubscribeResult
|
||||||
// BulkSubscribeReply aggregates SubscribeResult entries for a bulk command.
|
|
||||||
BulkSubscribeReply = pb.BulkSubscribeReply
|
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
|
|
||||||
// StreamAlarmsRequest is the gateway StreamAlarms request message.
|
|
||||||
StreamAlarmsRequest = pb.StreamAlarmsRequest
|
|
||||||
// AlarmFeedMessage is one message on the StreamAlarms feed — an
|
|
||||||
// active-alarm snapshot row, a snapshot-complete sentinel, or a transition.
|
|
||||||
AlarmFeedMessage = pb.AlarmFeedMessage
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// StreamAlarmsClient is the generated server-streaming client for the
|
|
||||||
// StreamAlarms RPC.
|
|
||||||
type StreamAlarmsClient = pb.MxAccessGateway_StreamAlarmsClient
|
|
||||||
|
|
||||||
// Enumerations from the generated contract re-exported for client callers.
|
|
||||||
type (
|
type (
|
||||||
// MxCommandKind discriminates which MXAccess command an MxCommand carries.
|
|
||||||
MxCommandKind = pb.MxCommandKind
|
MxCommandKind = pb.MxCommandKind
|
||||||
// MxDataType is the MXAccess data type tag on values and arrays.
|
|
||||||
MxDataType = pb.MxDataType
|
MxDataType = pb.MxDataType
|
||||||
// MxEventFamily groups MXAccess events by source category.
|
|
||||||
MxEventFamily = pb.MxEventFamily
|
MxEventFamily = pb.MxEventFamily
|
||||||
// MxStatusCategory classifies MXSTATUS_PROXY entries.
|
|
||||||
MxStatusCategory = pb.MxStatusCategory
|
MxStatusCategory = pb.MxStatusCategory
|
||||||
// MxStatusSource identifies the originator of a status entry.
|
|
||||||
MxStatusSource = pb.MxStatusSource
|
MxStatusSource = pb.MxStatusSource
|
||||||
// ProtocolStatusCode enumerates gateway-level status codes.
|
|
||||||
ProtocolStatusCode = pb.ProtocolStatusCode
|
ProtocolStatusCode = pb.ProtocolStatusCode
|
||||||
// SessionState enumerates gateway session lifecycle states.
|
|
||||||
SessionState = pb.SessionState
|
SessionState = pb.SessionState
|
||||||
)
|
)
|
||||||
|
|
||||||
// MXAccess command kind, data type, and protocol status constants surfaced
|
|
||||||
// from the generated contract.
|
|
||||||
const (
|
const (
|
||||||
// CommandKindRegister selects the MXAccess Register command.
|
|
||||||
CommandKindRegister = pb.MxCommandKind_MX_COMMAND_KIND_REGISTER
|
CommandKindRegister = pb.MxCommandKind_MX_COMMAND_KIND_REGISTER
|
||||||
// CommandKindUnregister selects the MXAccess Unregister command.
|
|
||||||
CommandKindUnregister = pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER
|
CommandKindUnregister = pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER
|
||||||
// CommandKindAddItem selects the MXAccess AddItem command.
|
|
||||||
CommandKindAddItem = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM
|
CommandKindAddItem = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM
|
||||||
// CommandKindAddItem2 selects the MXAccess AddItem2 command.
|
|
||||||
CommandKindAddItem2 = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2
|
CommandKindAddItem2 = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2
|
||||||
// CommandKindRemoveItem selects the MXAccess RemoveItem command.
|
|
||||||
CommandKindRemoveItem = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM
|
CommandKindRemoveItem = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM
|
||||||
// CommandKindAdvise selects the MXAccess Advise command.
|
|
||||||
CommandKindAdvise = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE
|
CommandKindAdvise = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE
|
||||||
// CommandKindUnAdvise selects the MXAccess UnAdvise command.
|
|
||||||
CommandKindUnAdvise = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE
|
CommandKindUnAdvise = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE
|
||||||
// CommandKindAddItemBulk selects the AddItem bulk command.
|
|
||||||
CommandKindAddItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM_BULK
|
CommandKindAddItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM_BULK
|
||||||
// CommandKindAdviseItemBulk selects the Advise bulk command.
|
|
||||||
CommandKindAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_ITEM_BULK
|
CommandKindAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_ITEM_BULK
|
||||||
// CommandKindRemoveItemBulk selects the RemoveItem bulk command.
|
|
||||||
CommandKindRemoveItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM_BULK
|
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 selects the AddItem+Advise combined bulk command.
|
|
||||||
CommandKindSubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK
|
CommandKindSubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK
|
||||||
// CommandKindUnsubscribeBulk selects the UnAdvise+RemoveItem combined bulk command.
|
|
||||||
CommandKindUnsubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_UNSUBSCRIBE_BULK
|
CommandKindUnsubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_UNSUBSCRIBE_BULK
|
||||||
// CommandKindWrite selects the MXAccess Write command.
|
|
||||||
CommandKindWrite = pb.MxCommandKind_MX_COMMAND_KIND_WRITE
|
CommandKindWrite = pb.MxCommandKind_MX_COMMAND_KIND_WRITE
|
||||||
// CommandKindWrite2 selects the MXAccess Write2 command.
|
|
||||||
CommandKindWrite2 = pb.MxCommandKind_MX_COMMAND_KIND_WRITE2
|
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 denotes an MXAccess Float (single precision) value.
|
|
||||||
DataTypeFloat = pb.MxDataType_MX_DATA_TYPE_FLOAT
|
DataTypeFloat = pb.MxDataType_MX_DATA_TYPE_FLOAT
|
||||||
// DataTypeDouble denotes an MXAccess Double (double precision) value.
|
|
||||||
DataTypeDouble = pb.MxDataType_MX_DATA_TYPE_DOUBLE
|
DataTypeDouble = pb.MxDataType_MX_DATA_TYPE_DOUBLE
|
||||||
// DataTypeString denotes an MXAccess String value.
|
|
||||||
DataTypeString = pb.MxDataType_MX_DATA_TYPE_STRING
|
DataTypeString = pb.MxDataType_MX_DATA_TYPE_STRING
|
||||||
// DataTypeTime denotes an MXAccess timestamp value.
|
|
||||||
DataTypeTime = pb.MxDataType_MX_DATA_TYPE_TIME
|
DataTypeTime = pb.MxDataType_MX_DATA_TYPE_TIME
|
||||||
|
|
||||||
// ProtocolStatusOK indicates the gateway processed the request successfully.
|
|
||||||
ProtocolStatusOK = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK
|
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 = 3
|
GatewayProtocolVersion uint32 = 2
|
||||||
|
|
||||||
// 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.
|
||||||
|
|||||||
+29
-132
@@ -10,23 +10,22 @@ clients/java/
|
|||||||
settings.gradle
|
settings.gradle
|
||||||
build.gradle
|
build.gradle
|
||||||
src/main/generated/
|
src/main/generated/
|
||||||
zb-mom-ww-mxgateway-client/
|
mxgateway-client/
|
||||||
zb-mom-ww-mxgateway-cli/
|
mxgateway-cli/
|
||||||
```
|
```
|
||||||
|
|
||||||
`zb-mom-ww-mxgateway-client` generates Java protobuf and gRPC sources from
|
`mxgateway-client` generates Java protobuf and gRPC sources from
|
||||||
`../../src/ZB.MOM.WW.MxGateway.Contracts/Protos`. The Gradle protobuf plugin writes those
|
`../../src/MxGateway.Contracts/Protos`. The Gradle protobuf plugin writes those
|
||||||
generated sources under `src/main/generated`, which matches the client proto
|
generated sources under `src/main/generated`, which matches the client proto
|
||||||
manifest in `../proto/proto-inputs.json`. Do not edit generated files by hand.
|
manifest in `../proto/proto-inputs.json`. Do not edit generated files by hand.
|
||||||
|
|
||||||
`zb-mom-ww-mxgateway-client` exposes `MxGatewayClientOptions`, `MxGatewayClient`,
|
`mxgateway-client` exposes `MxGatewayClientOptions`, `MxGatewayClient`,
|
||||||
`MxGatewaySession`, value/status helpers, typed gateway exceptions, raw
|
`MxGatewaySession`, value/status helpers, typed gateway exceptions, raw
|
||||||
generated stubs, and generated protobuf messages for parity tests.
|
generated stubs, and generated protobuf messages for parity tests.
|
||||||
|
|
||||||
`zb-mom-ww-mxgateway-cli` depends on `zb-mom-ww-mxgateway-client` and provides
|
`mxgateway-cli` depends on `mxgateway-client` and provides the `mxgw-java`
|
||||||
the `mxgw-java` application entry point. The CLI supports version, session,
|
application entry point. The CLI supports version, session, command, event
|
||||||
command, event streaming, write, and smoke-test commands with deterministic
|
streaming, write, and smoke-test commands with deterministic JSON output.
|
||||||
JSON output.
|
|
||||||
|
|
||||||
## Regenerating Protobuf Bindings
|
## Regenerating Protobuf Bindings
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ Run generation from `clients/java` after the shared `.proto` files or Java
|
|||||||
output path changes:
|
output path changes:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
gradle :zb-mom-ww-mxgateway-client:generateProto
|
gradle :mxgateway-client:generateProto
|
||||||
```
|
```
|
||||||
|
|
||||||
## Client Usage
|
## Client Usage
|
||||||
@@ -57,16 +56,6 @@ try (MxGatewayClient client = MxGatewayClient.connect(options);
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The gateway can auto-generate its own self-signed certificate (it has no PKI), so
|
|
||||||
the client is **lenient by default**: a TLS connection (`plaintext(false)`) with
|
|
||||||
no `caCertificatePath` accepts whatever certificate the gateway presents (via
|
|
||||||
grpc-netty-shaded's `InsecureTrustManagerFactory`). To verify instead, set
|
|
||||||
`caCertificatePath` to pin a CA, or set `requireCertificateValidation(true)` to
|
|
||||||
verify against the JVM trust store without pinning. Use `serverNameOverride` /
|
|
||||||
`--server-name-override` when the dialed host differs from the certificate SAN.
|
|
||||||
See
|
|
||||||
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).
|
|
||||||
|
|
||||||
Use `rawBlockingStub`, `rawFutureStub`, `rawAsyncStub`, `openSessionRaw`,
|
Use `rawBlockingStub`, `rawFutureStub`, `rawAsyncStub`, `openSessionRaw`,
|
||||||
`closeSessionRaw`, `invoke`, and raw session helper methods when tests need the
|
`closeSessionRaw`, `invoke`, and raw session helper methods when tests need the
|
||||||
underlying protobuf messages. `MxGatewayCommandException` and
|
underlying protobuf messages. `MxGatewayCommandException` and
|
||||||
@@ -78,12 +67,6 @@ cancels the underlying gRPC stream. Canceling or timing out a Java client call
|
|||||||
only stops the client from waiting; it does not abort an in-flight MXAccess COM
|
only stops the client from waiting; it does not abort an in-flight MXAccess COM
|
||||||
call on the worker STA.
|
call on the worker STA.
|
||||||
|
|
||||||
For alarms, `MxGatewayClient` exposes `queryActiveAlarms` (one-shot snapshot),
|
|
||||||
`streamAlarms` (returns an `MxGatewayAlarmFeedSubscription` whose iterator
|
|
||||||
yields alarm-feed messages from the gateway's central monitor), and
|
|
||||||
`acknowledgeAlarm` (ack by full alarm reference with an optional comment and
|
|
||||||
ack target). Close the subscription to cancel the underlying gRPC stream.
|
|
||||||
|
|
||||||
## Galaxy Repository Browse
|
## Galaxy Repository Browse
|
||||||
|
|
||||||
The Galaxy Repository service is a separate metadata-only gRPC service exposed
|
The Galaxy Repository service is a separate metadata-only gRPC service exposed
|
||||||
@@ -121,64 +104,11 @@ The CLI exposes matching subcommands: `galaxy-test`, `galaxy-deploy-time`,
|
|||||||
`--timeout`, and `--json` options as the gateway commands.
|
`--timeout`, and `--json` options as the gateway commands.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-test --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
gradle :mxgateway-cli:run --args="galaxy-test --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-deploy-time --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
gradle :mxgateway-cli:run --args="galaxy-deploy-time --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-discover --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
gradle :mxgateway-cli:run --args="galaxy-discover --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Browsing lazily
|
|
||||||
|
|
||||||
For UI trees or OPC UA bridges, use `browseChildren` to walk one level at a
|
|
||||||
time instead of loading the full hierarchy with `discoverHierarchy`. Pass a
|
|
||||||
default request for root objects; subsequent calls set `parentGobjectId`,
|
|
||||||
`parentTagName`, or `parentContainedPath`. Filter fields match
|
|
||||||
`DiscoverHierarchy`. Each response pairs `getChildrenList()` with
|
|
||||||
`getChildHasChildrenList()` so you know which nodes to expand. See
|
|
||||||
[Galaxy Repository](../../docs/GalaxyRepository.md#browsechildren) for full
|
|
||||||
request and filter semantics. This snippet documents the API as it appears once
|
|
||||||
the Java client is regenerated on the Windows host.
|
|
||||||
|
|
||||||
```java
|
|
||||||
BrowseChildrenReply reply = galaxy.browseChildren(
|
|
||||||
BrowseChildrenRequest.newBuilder().build());
|
|
||||||
|
|
||||||
List<GalaxyObject> children = reply.getChildrenList();
|
|
||||||
List<Boolean> hasChildren = reply.getChildHasChildrenList();
|
|
||||||
for (int i = 0; i < children.size(); i++) {
|
|
||||||
System.out.printf("%s expand=%b%n", children.get(i).getTagName(), hasChildren.get(i));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### High-level walker
|
|
||||||
|
|
||||||
For UI trees, the client provides a `LazyBrowseNode` walker that handles
|
|
||||||
sibling pagination and the `child_has_children` hint for you:
|
|
||||||
|
|
||||||
```java
|
|
||||||
MxGatewayClientOptions options = MxGatewayClientOptions.builder()
|
|
||||||
.endpoint("localhost:5000")
|
|
||||||
.apiKey(System.getenv("MXGATEWAY_API_KEY"))
|
|
||||||
.plaintext(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
try (GalaxyRepositoryClient galaxy = GalaxyRepositoryClient.connect(options)) {
|
|
||||||
List<LazyBrowseNode> roots = galaxy.browse();
|
|
||||||
for (LazyBrowseNode root : roots) {
|
|
||||||
if (root.hasChildrenHint()) {
|
|
||||||
root.expand();
|
|
||||||
}
|
|
||||||
for (LazyBrowseNode child : root.getChildren()) {
|
|
||||||
String kind = child.hasChildrenHint() ? "has children" : "leaf";
|
|
||||||
System.out.println(child.getObject().getTagName() + " (" + kind + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`expand` is idempotent — calling it twice fires only one RPC,
|
|
||||||
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
|
|
||||||
`browse` again from the root.
|
|
||||||
|
|
||||||
### Watching deploy events
|
### Watching deploy events
|
||||||
|
|
||||||
`GalaxyRepository.WatchDeployEvents` is a server-streaming RPC: the gateway
|
`GalaxyRepository.WatchDeployEvents` is a server-streaming RPC: the gateway
|
||||||
@@ -226,8 +156,8 @@ The matching CLI subcommand streams events until cancelled (Ctrl+C) and prints
|
|||||||
one line per event in text mode or one JSON object per event with `--json`:
|
one line per event in text mode or one JSON object per event with `--json`:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-watch --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
gradle :mxgateway-cli:run --args="galaxy-watch --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-watch --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --last-seen-deploy-time 2026-04-28T18:30:00Z --limit 5"
|
gradle :mxgateway-cli:run --args="galaxy-watch --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --last-seen-deploy-time 2026-04-28T18:30:00Z --limit 5"
|
||||||
```
|
```
|
||||||
|
|
||||||
## CLI Usage
|
## CLI Usage
|
||||||
@@ -235,16 +165,14 @@ gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-watch --endpoint localhost:50
|
|||||||
Run the CLI through Gradle:
|
Run the CLI through Gradle:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="version --json"
|
gradle :mxgateway-cli:run --args="version --json"
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="open-session --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --client-session-name java-cli --json"
|
gradle :mxgateway-cli:run --args="open-session --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --client-session-name java-cli --json"
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="register --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --client-name java-cli --json"
|
gradle :mxgateway-cli:run --args="register --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --client-name java-cli --json"
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="add-item --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item TestObject.TestInt --json"
|
gradle :mxgateway-cli:run --args="add-item --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item TestObject.TestInt --json"
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="advise --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item-handle 1 --json"
|
gradle :mxgateway-cli:run --args="advise --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item-handle 1 --json"
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="write --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --json"
|
gradle :mxgateway-cli:run --args="write --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --json"
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="stream-events --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --limit 1 --json"
|
gradle :mxgateway-cli:run --args="stream-events --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --limit 1 --json"
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="stream-alarms --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --filter-prefix Galaxy --limit 1 --json"
|
gradle :mxgateway-cli:run --args="smoke --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --item TestObject.TestInt --json"
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="acknowledge-alarm --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --reference \"\\Galaxy\Area001.Pump001.PumpFault\" --json"
|
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="smoke --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --item TestObject.TestInt --json"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The CLI accepts `--api-key`, `--api-key-env`, `--plaintext`, `--ca-file`,
|
The CLI accepts `--api-key`, `--api-key-env`, `--plaintext`, `--ca-file`,
|
||||||
@@ -254,7 +182,7 @@ output redacts API keys.
|
|||||||
Use TLS options for a secured gateway:
|
Use TLS options for a secured gateway:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="smoke --endpoint mxgateway.example.local:5001 --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item TestObject.TestInt --json"
|
gradle :mxgateway-cli:run --args="smoke --endpoint mxgateway.example.local:5001 --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item TestObject.TestInt --json"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build And Test
|
## Build And Test
|
||||||
@@ -274,11 +202,11 @@ in-process gRPC behavior, stream cancellation, and CLI parser/output behavior.
|
|||||||
Create local library and CLI artifacts from `clients/java`:
|
Create local library and CLI artifacts from `clients/java`:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
gradle :zb-mom-ww-mxgateway-client:jar :zb-mom-ww-mxgateway-cli:installDist
|
gradle :mxgateway-client:jar :mxgateway-cli:installDist
|
||||||
```
|
```
|
||||||
|
|
||||||
The library jar is under `zb-mom-ww-mxgateway-client/build/libs`. The installed CLI
|
The library jar is under `mxgateway-client/build/libs`. The installed CLI
|
||||||
distribution is under `zb-mom-ww-mxgateway-cli/build/install/zb-mom-ww-mxgateway-cli`.
|
distribution is under `mxgateway-cli/build/install/mxgateway-cli`.
|
||||||
|
|
||||||
## Integration Checks
|
## Integration Checks
|
||||||
|
|
||||||
@@ -289,43 +217,12 @@ $env:MXGATEWAY_INTEGRATION = '1'
|
|||||||
$env:MXGATEWAY_ENDPOINT = 'localhost:5000'
|
$env:MXGATEWAY_ENDPOINT = 'localhost:5000'
|
||||||
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
|
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
|
||||||
$env:MXGATEWAY_TEST_ITEM = 'TestObject.TestInt'
|
$env:MXGATEWAY_TEST_ITEM = 'TestObject.TestInt'
|
||||||
gradle :zb-mom-ww-mxgateway-cli:run --args="smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json"
|
gradle :mxgateway-cli:run --args="smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installing from the Gitea Maven repository
|
|
||||||
|
|
||||||
The client publishes to the internal Gitea Maven repository at
|
|
||||||
`https://gitea.dohertylan.com/api/packages/dohertj2/maven`.
|
|
||||||
|
|
||||||
In your consumer project's `build.gradle`:
|
|
||||||
|
|
||||||
````groovy
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
url 'https://gitea.dohertylan.com/api/packages/dohertj2/maven'
|
|
||||||
credentials {
|
|
||||||
username = System.getenv('GITEA_USERNAME')
|
|
||||||
password = System.getenv('GITEA_TOKEN')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'com.zb.mom.ww.mxgateway:zb-mom-ww-mxgateway-client:0.1.0'
|
|
||||||
}
|
|
||||||
````
|
|
||||||
|
|
||||||
To publish a new version from this repo:
|
|
||||||
|
|
||||||
````bash
|
|
||||||
export GITEA_USERNAME=dohertj2
|
|
||||||
export GITEA_TOKEN=<your-gitea-token>
|
|
||||||
gradle :zb-mom-ww-mxgateway-client:publish
|
|
||||||
````
|
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Client Packaging](../../docs/ClientPackaging.md)
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
||||||
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
||||||
- [Java Client Detailed Design](./JavaClientDesign.md)
|
- [Java Client Detailed Design](../../docs/clients-java-design.md)
|
||||||
- [Java Style Guide](../../docs/style-guides/JavaStyleGuide.md)
|
- [Java Style Guide](../../docs/style-guides/JavaStyleGuide.md)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
group = 'com.zb.mom.ww.mxgateway'
|
group = 'com.dohertylan.mxgateway'
|
||||||
version = '0.1.0'
|
version = '0.1.0'
|
||||||
|
|
||||||
pluginManager.withPlugin('java') {
|
pluginManager.withPlugin('java') {
|
||||||
@@ -37,44 +37,4 @@ subprojects {
|
|||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginManager.withPlugin('maven-publish') {
|
|
||||||
publishing {
|
|
||||||
publications {
|
|
||||||
maven(MavenPublication) {
|
|
||||||
from components.java
|
|
||||||
pom {
|
|
||||||
url = 'https://gitea.dohertylan.com/dohertj2/mxaccessgw'
|
|
||||||
description = 'MxAccessGateway Java client'
|
|
||||||
scm {
|
|
||||||
url = 'https://gitea.dohertylan.com/dohertj2/mxaccessgw'
|
|
||||||
connection = 'scm:git:https://gitea.dohertylan.com/dohertj2/mxaccessgw.git'
|
|
||||||
}
|
|
||||||
developers {
|
|
||||||
developer {
|
|
||||||
id = 'dohertj2'
|
|
||||||
name = 'Joseph Doherty'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
licenses {
|
|
||||||
license {
|
|
||||||
name = 'Proprietary'
|
|
||||||
distribution = 'repo'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
name = 'GiteaPackages'
|
|
||||||
url = 'https://gitea.dohertylan.com/api/packages/dohertj2/maven'
|
|
||||||
credentials {
|
|
||||||
username = System.getenv('GITEA_USERNAME') ?: ''
|
|
||||||
password = System.getenv('GITEA_TOKEN') ?: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -3,11 +3,11 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':zb-mom-ww-mxgateway-client')
|
implementation project(':mxgateway-client')
|
||||||
implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}"
|
implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}"
|
||||||
implementation "info.picocli:picocli:${picocliVersion}"
|
implementation "info.picocli:picocli:${picocliVersion}"
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
mainClass = 'com.zb.mom.ww.mxgateway.cli.MxGatewayCli'
|
mainClass = 'com.dohertylan.mxgateway.cli.MxGatewayCli'
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user