12cb10c3a1
First working M4 wave 1 slice. Adds session.rs with the connect /
write / shutdown path on top of NmxClient + Resolver, plus a tokio
test that exercises a full round-trip against a hand-rolled server.
Read, subscribe, recovery, and the long-lived connection task land
in wave 2.
Architecture
- Session holds Arc<SessionInner>; SessionInner wraps NmxClient
behind a tokio::sync::Mutex. All RPC ops serialize on that mutex.
Wave 2 will replace it with an mpsc::channel<Op> + dispatcher task
per design/70-risks-and-open-questions.md R15 (drop-time async
cleanup hazards).
- ensure_connected gate stops post-shutdown ops with
Connection::EngineNotRegistered. Shutdown is idempotent via
AtomicBool::swap.
- Manual Debug impl on SessionInner — neither dyn Resolver nor
NmxClient impl Debug.
Public API
- Session::connect_nmx(addr, options, ntlm, service_ipid, resolver,
recovery): validates the policy, opens NmxClient, runs
RegisterEngine2 (no callback yet — wave 2), optionally configures
heartbeat. Returns Error::Connection on non-zero HRESULT.
- Session::write_value(reference, value: WriteValue): resolves the
tag through the configured Resolver, dispatches NmxClient::write.
- Session::resolve_write_kind / resolve_tag: convenience accessors.
- Session::shutdown_nmx: calls UnregisterEngine, idempotent.
Error mapping
- map_nmx / map_transport / map_resolver bridge the inner crate
errors into the public Error enum. NonZeroHresult → InvalidArgument
with the hex code; transport Fault → Status-shaped error;
ResolverError::NotFound → Galaxy { reason: "tag not found: ..." }.
- All three matchers handle their #[non_exhaustive] sources with a
generic catch-all so future variants don't silently break the map.
Tests (8 new in mxaccess; total mxaccess: 19)
- write_value round-trip via in-memory StaticResolver + hand-rolled
unauthenticated DCE/RPC server.
- write_value propagates resolver not-found → Galaxy error.
- write_value propagates non-zero HRESULT → InvalidArgument.
- shutdown is idempotent (second call is a no-op).
- write after shutdown returns EngineNotRegistered.
- resolve_tag and resolve_write_kind work without RPC.
- envelope-kind constants used by Session match codec exports
(sanity guard against codec rename).
mxaccess-nmx: WriteValue now re-exported at crate root.
mxaccess: deps gained mxaccess-nmx/galaxy/rpc + tokio + tracing,
plus async-trait as a dev-dep for the test resolver impl.
Test count delta: 479 -> 487 (+8). All four DoD gates green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mxaccess (Rust port)
Native Rust replacement for AVEVA / Wonderware MXAccess. See ../design/ for
the architectural specification, ../src/ for the .NET reference (the
executable spec), and ../CLAUDE.md for project-wide rules.
Status
M0 — Workspace skeleton. Stub types compile; nothing is implemented yet.
See ../design/60-roadmap.md for the M0–M6 milestone plan.
Layout
rust/
Cargo.toml workspace root
rust-toolchain.toml 1.85 stable
crates/
mxaccess-codec/ pure protocol codec, no I/O
mxaccess-galaxy/ Galaxy SQL resolver (tiberius)
mxaccess-rpc/ DCE/RPC + NTLMv2 + OXID + OBJREF
mxaccess-callback/ INmxSvcCallback RPC server
mxaccess-nmx/ INmxService2 client
mxaccess-asb-nettcp/ net.tcp framing (MC-NMF + MC-NBFX/NBFS)
mxaccess-asb/ IASBIDataV2 client
mxaccess/ async session + Transport trait + public API
mxaccess-compat/ LMXProxyServer-shaped facade
Build
cargo build --workspace
cargo test --workspace
cargo clippy --workspace -- -D warnings
cargo fmt --check
Live probes
. ..\tools\Setup-LiveProbeEnv.ps1
cargo test -p mxaccess --features live -- --ignored
The setup script fetches credentials from Infisical via
wwtools/secrets/Get-Secret.ps1. Never inline plaintext credentials.
License
MIT — see ../LICENSE.