scripts/run-client-e2e-tests.ps1 expects each language CLI to expose a `batch` subcommand that reads command lines from stdin, runs each through the normal subcommand dispatch, writes the JSON result, then a sentinel line `__MXGW_BATCH_EOR__`. The implementation lived on a divergent branch (commit6126099) that was never merged into main — this commit ports the same protocol to HEAD's renamed CLIs so the existing matrix script runs end-to-end. The protocol: - one line of stdin = one full CLI invocation - successful output → stdout, then __MXGW_BATCH_EOR__ - failure → {"error":"...","type":"error"} JSON on stdout, then __MXGW_BATCH_EOR__ (errors do NOT exit the loop) - empty line or EOF terminates the loop Per-CLI additions: .NET: RunBatchAsync + per-line StringWriter capture, JSON error envelope when forceJsonErrors is true. Two new tests in MxGatewayClientCliTests covering the success and error paths. Go: runBatch with bufio.Scanner, runs each line through the existing runWithIO switch with a buffered stdout writer. One new test pinning the EOR sentinel. Rust: new `Batch` variant on the clap Command enum, run_batch re-parses each line via Cli::try_parse_from. Two new tests in the inline mod tests block. Python: new `batch` click command in commands.py that uses CliRunner to dispatch each line; synthesises {"error",..."type"} JSON from click error messages when the captured output isn't already JSON-shaped. Three new tests in test_cli.py. Java: BatchCommand inner @Command with BufferedReader stdin loop, fresh commandLine() per dispatch with captured stdout/stderr PrintWriters; non-zero exit codes and uncaught exceptions both surface as JSON-error blocks. Two new tests. Also fixes scripts/run-client-e2e-tests.ps1 line 705: the Python invocation was still passing the old module name `mxgateway_cli` to `python -m`; the client SDK rename in397d3c5moved it to `zb_mom_ww_mxgateway_cli`. Without the fix the Python leg fails with "No module named mxgateway_cli" before reaching open-session. Verification: full matrix at the redeployed gateway (localhost:5120, running ZB.MOM.WW.MxGateway.Server.exe / ZB.MOM.WW.MxGateway.Worker.exe) with -SkipBulk -SkipReadWriteBulk -SkipParity -SkipAuth (those phases exercise bulk read/write CLI subcommands that also live on the divergent branch — porting those is a follow-up). All five clients report `closed=true, addedItems=120, eventCount=5` and overall `success=true`. Per-language unit tests pass: - dotnet: 59/59 - go: all packages clean - rust: cargo test --workspace clean - python: 42/42 - java: gradle build SUCCESSFUL Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rust Client Workspace
The Rust client workspace contains the MXAccess Gateway client library, a
test CLI, and tests for generated contract wiring plus wrapper behavior. The
library uses
the shared protobuf inputs documented in
../../docs/ClientProtoGeneration.md so the Rust bindings compile against
the same public gateway and worker contracts as the server.
Layout
clients/rust/
Cargo.toml
build.rs
src/
tests/
crates/mxgw-cli/
build.rs reads the .proto files from
../../src/ZB.MOM.WW.MxGateway.Contracts/Protos and generates tonic/prost bindings
into Cargo build output. src/generated.rs declares the Rust modules that
include those generated files. src/generated remains reserved for checked-in
generator output if the crate later changes to source-tree generation.
Build And Test
Run the Rust workspace checks from clients/rust:
cargo fmt --all --check
cargo test --workspace
cargo check --workspace
cargo clippy --workspace --all-targets -- -D warnings
The build script uses protoc from PATH or the Windows path recorded in
../../docs/ToolchainLinks.md.
Packaging
Create local release artifacts from clients/rust:
cargo build --workspace --release
cargo install --path crates/mxgw-cli --locked --force
cargo check --workspace regenerates the tonic and prost modules into
Cargo build output through build.rs.
CLI
The CLI exposes version, session, command, event stream, write, and smoke commands over the same client wrapper used by tests:
cargo run -p mxgw-cli -- version --json
cargo run -p mxgw-cli -- open-session --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
cargo run -p mxgw-cli -- register --session-id <session-id> --client-name mxgw-rust-cli --json
cargo run -p mxgw-cli -- add-item --session-id <session-id> --server-handle 1 --item TestChildObject.TestInt --json
cargo run -p mxgw-cli -- advise --session-id <session-id> --server-handle 1 --item-handle 1 --json
cargo run -p mxgw-cli -- stream-events --session-id <session-id> --max-events 1 --json
cargo run -p mxgw-cli -- write --session-id <session-id> --server-handle 1 --item-handle 1 --value-type int32 --value 123 --json
Use --tls, --ca-file, and --server-name-override for TLS endpoints. The
CLI reads the API key from --api-key or from --api-key-env, which defaults
to MXGATEWAY_API_KEY. API keys are redacted by the library option and secret
types.
cargo run -p mxgw-cli -- smoke --endpoint https://mxgateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt --json
Library Surface
ClientOptions configures endpoint, API key, plaintext or TLS transport,
timeouts, custom CA files, and server name override. GatewayClient::connect
creates an authenticated tonic client and attaches authorization: Bearer <api-key> metadata to unary and streaming calls.
GatewayClient exposes raw generated calls through open_session_raw,
close_session_raw, invoke_raw, stream_events, and raw_client. The
session helpers keep MXAccess handles visible:
let session = client.open_session(request).await?;
let server_handle = session.register("mxgw-rust").await?;
let item_handle = session.add_item(server_handle, "TestChildObject.TestInt").await?;
session.advise(server_handle, item_handle).await?;
let mut events = session.events().await?;
session.close().await?;
MxValue, MxArrayValue, and MxStatus wrap generated protobuf messages while
preserving the raw message for parity diagnostics. Command replies whose
protocol status is not PROTOCOL_STATUS_CODE_OK become Error::Command and
retain the raw MxCommandReply.
Galaxy Repository browse
The Galaxy Repository service exposes a read-only browse over the AVEVA System
Platform Galaxy Repository (ZB SQL database). It uses the same API-key auth as
the gateway service but requires the metadata:read scope on the server.
GalaxyClient wraps the generated Galaxy bindings the same
way GatewayClient wraps the gateway bindings:
let mut galaxy = GalaxyClient::connect(
ClientOptions::new("http://localhost:5000")
.with_api_key(ApiKey::new(api_key)),
).await?;
let ok = galaxy.test_connection().await?;
let last_deploy = galaxy.get_last_deploy_time().await?; // Option<prost_types::Timestamp>
let objects = galaxy.discover_hierarchy().await?; // Vec<GalaxyObject>
get_last_deploy_time returns None when the server reports
present = false. discover_hierarchy returns the generated
GalaxyObject proto type (re-exported via
zb_mom_ww_mxgateway_client::generated::galaxy_repository::v1) with all attributes
attached.
The CLI ships matching subcommands under galaxy:
cargo run -p mxgw-cli -- galaxy test-connection --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
cargo run -p mxgw-cli -- galaxy last-deploy-time --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
cargo run -p mxgw-cli -- galaxy discover-hierarchy --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
Watching deploy events
watch_deploy_events opens the WatchDeployEvents server stream. The
server emits a bootstrap DeployEvent describing the
current cache state on subscribe, then one event each time the cached
galaxy.time_of_last_deploy changes. sequence is monotonic per server
start; gaps signal that the per-subscriber buffer dropped older events.
Pass last_seen_deploy_time to suppress the bootstrap event when the
client's cached deploy time matches the server's.
use futures_util::StreamExt;
let mut stream = galaxy.watch_deploy_events(None).await?;
while let Some(event) = stream.next().await {
let event = event?;
println!(
"seq={} objects={} attributes={}",
event.sequence, event.object_count, event.attribute_count,
);
}
// Drop the stream to cancel the gRPC call.
The matching CLI subcommand prints one line per event (--json switches to
one JSON object per event). --last-seen-deploy-time accepts an RFC3339
timestamp and is forwarded to the server. --max-events (default 0 = no
cap) lets you stop after a fixed number of events; otherwise the command
runs until the stream ends or Ctrl+C is pressed.
cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY
cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --last-seen-deploy-time 2026-04-28T15:30:00Z
Integration Checks
Run live checks only when a gateway and MXAccess-backed worker are available:
$env:MXGATEWAY_INTEGRATION = '1'
$env:MXGATEWAY_ENDPOINT = 'http://127.0.0.1:5000'
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
$env:MXGATEWAY_TEST_ITEM = 'TestChildObject.TestInt'
cargo run -p mxgw-cli -- smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json