c7505f9570e5e44b1b3e1ada93ee0f29c561546d
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2fc327a8d5 |
[F55 Path A] DCOM-managed INmxSvcCallback sink
Replace the hand-rolled CallbackExporter (TCP listener + custom
OBJREF) with a real `windows-rs` `#[implement]` COM class for
INmxSvcCallback, marshalled via CoMarshalInterface. NmxSvc validates
the callback OBJREF by calling IObjectExporter::ResolveOxid against
the local RPCSS at 127.0.0.1:135; hand-rolled OXIDs aren't registered
there, which is why RegisterEngine2 returned RPC_S_SERVER_UNAVAILABLE
(1722) on every live attempt. CoMarshalInterface registers the OXID
with RPCSS automatically, so the SCM-side resolution succeeds.
Mirrors MxNativeSession.CreateRegisteredService (cs:624), which is
the .NET reference's working path:
ComObjRefProvider.MarshalInterfaceObjRef(callback,
INmxSvcCallback, DifferentMachine)
Layout:
- mxaccess-callback::dcom_sink — INmxSvcCallback + DcomCallbackSink
+ create_dcom_callback_sink_objref. Forwards inbound calls into
the same CallbackEvent::CallbackInvoked { opnum, body } shape the
legacy exporter produces, so callback_router stays path-agnostic.
- Session::from_nmx_client — branched on `windows-com`. Real DCOM
sink when on; legacy CallbackExporter when off (kept for unit
tests that run against an in-process fake NMX peer).
- SessionInner.dcom_sink_holder: Option<IUnknownHolder> — keeps the
COM ref alive for the session's lifetime; shutdown_nmx drops it.
- mxaccess-rpc + mxaccess-callback: windows-rs 0.59 → 0.62. The 0.59
#[implement] macro generates code that doesn't compile under
edition 2024; 0.62 is fixed.
Live result: cargo test -p mxaccess-compat --features
live-windows-com --test lmx_write_complete_live -- --ignored
--nocapture passes end-to-end. RegisterEngine2 OK, write
round-trips, OnWriteComplete fires with the captured MxStatus shape.
Unblocks F49 step 5; F55 marked Resolved in design/followups.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f0c9dd2214 | rust: add version specifiers to workspace path deps for cargo publish | ||
|
|
ad1cf2351c |
[F36 + F40 + F44] M6 wave 1: subscribe_buffered (NMX) + metrics + evidence
Three M6 sub-followups landed in this wave (sub-agent worktrees +
manual reconciliation in main):
**F36 — Session::subscribe_buffered (NMX) per R2 single-sample**
- `BufferedOptions::rounded_update_interval_ms()` — 100ms rounding
helper mirroring MxNativeCompatibilityServer.cs:638
((updateInterval + 99) / 100) * 100, saturating on overflow.
- `Session::subscribe_buffered` (public, lib.rs:604) delegates to
the new private `subscribe_buffered_nmx` which uses the buffered
RegisterReference path: item_definition suffixed with
`.property(buffer)`, subscribe=true (no separate
AdviseSupervisory follow-up — verified against capture 082).
- Per R2 verified at wwtools/mxaccesscli/docs/api-notes.md the wire
semantic is single-sample-per-event with a server-side cadence
knob; rounded_ms is held client-side only (native MXAccess does
not emit a separate SetBufferedUpdateInterval RPC, verified by
absence in 079/082 captures).
- New crates/mxaccess/examples/subscribe-buffered.rs.
- New crates/mxaccess-codec/tests/buffered_register_reference_parity.rs:
4 tests (capture 079/082 round-trip, suffix helper, constructive
forward-build vs capture 082).
**F40 — Optional metrics feature**
- New crates/mxaccess/src/metrics.rs (275 lines): `pub(crate)`
thin wrappers (`record_write_latency`, `record_read_latency`,
`inc_writes`, `inc_reads`, `inc_advises`, `inc_recovery_*`,
`set_active_subscriptions`, etc.) that compile to no-ops under
`#[cfg(not(feature = "metrics"))]`. Call sites in session.rs +
asb_session.rs invoke them unconditionally; the gate is inside
the wrapper.
- `metrics = { version = "0.24", optional = true }` added to
workspace + mxaccess crate Cargo.toml.
- Default build: zero metrics dep, zero runtime cost.
**F44 — Buffered batch + suspend capture decode evidence**
- New docs/M6-buffered-evidence.md: per-capture summary for
077, 079, 080, 081, 082, 094 — call sequence, key wire bytes,
R2/R5 verdict.
- R2 confirmed silently as "not a real risk" — single-sample
observed across 079/080/082/094.
- R5 trigger conditions documented from capture 077: AdviseSupervisory
+ Suspend pair, 1-second intervals, succeeds on enum attributes.
- design/70-risks-and-open-questions.md R2/R5 status updated.
Workspace: 759 → 792 tests, clippy clean, rustdoc -D warnings clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8e695b9347 |
[F12 wrapper + F32 close] Session::connect_nmx_auto + close M5 type-matrix DoD
rust / build / test / clippy / fmt (push) Has been cancelled
Two related closures in one commit:
1. Session-level wrapper around F12: new
`mxaccess::Session::connect_nmx_auto(ntlm_factory, options,
resolver, recovery)` gated on a new `mxaccess/windows-com` feature
(which propagates `mxaccess-nmx/windows-com`). Drives
`NmxClient::create` (the F12 COM-activation factory) for the
`(host, port, service_ipid)` discovery, then funnels into the
shared post-NMX-bind orchestration. Refactored `connect_nmx` to
extract steps 1+2+4+5 into a private `from_nmx_client` helper —
both `connect_nmx` and `connect_nmx_auto` reuse it so the
`CallbackExporter` + router + `RegisterEngine2` + heartbeat policy
stays in one place. The .NET `MxNativeSession.Open` shape
(`MxNativeSession.cs:127-147`) is now reproduced end-to-end on
Windows with `windows-com` on — callers no longer pre-resolve
`(addr, service_ipid)` by hand.
`connect_nmx`'s doc comment updated to drop the stale "F12 not yet
wired" note. `parse_bracketed_host_port` in mxaccess-nmx gets a
`cfg_attr(not(...), allow(dead_code))` so the default-feature
build stays warning-clean.
2. F32 closed via option (b) of its own resolve criterion: the four
missing types (Float / Double / DateTime / Duration) are gated on
Galaxy-side template provisioning that's outside the Rust port's
scope. The deployed test Galaxy on this host only has
mx_data_type ∈ {1=Bool, 2=Int32, 5=String}; we cannot exercise
the missing types without authoring new template attributes in
the Aveva console (a manual platform-engineering task). The
three-type live verification at commit
|
||
|
|
8a0f92b6bc |
[M5] mxaccess: F26 step 1 — AsbTransport bridges AsbClient into Transport trait
First slice of F26. Bridges F25's working AsbClient into the M0
`mxaccess::Transport` trait that Session uses to discriminate
operations across NMX and ASB transports.
API additions:
* `mxaccess::AsbTransport<T>` — generic over the same
AsyncRead+AsyncWrite+Unpin+Send+Sync+'static bound that AsbClient
takes. Owns an AsbClient and exposes it via `client_mut()` /
`into_client()`.
* `impl Transport for AsbTransport<T>`:
- `capabilities()` — `buffered_subscribe = false`,
`activate_suspend = false`, `operation_complete_frame = false`
per `design/60-roadmap.md` M5 (no NMX-specific extensions on
ASB).
- `kind()` — `TransportKind::Asb`.
Path-dep wiring: `mxaccess` now imports `mxaccess-asb` +
`mxaccess-asb-nettcp` directly.
Compile-time `Send + Sync + 'static` assertion guards the
trait-bound contract.
2 new tests:
* `asb_transport_kind_is_asb`.
* `asb_transport_capabilities_disable_buffered_and_activate_suspend`.
Stubbed for F26 step 2:
* `Session::connect_asb` constructor that owns TCP open +
preamble + DH handshake orchestration.
* Operation routing that maps ASB types (ItemStatus, RuntimeValue)
back to mxaccess types (MxStatus, DataChange, MxValue).
Stubbed for F26 step 3:
* Subscription routing — Session::subscribe on ASB needs F25
subscription operations (CreateSubscription / AddMonitoredItems
/ Publish), which are not yet implemented.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a31237d1d0 |
[M4] mxaccess: Subscription impls Stream<Item = DataChange> (resolves F15)
F15 step 2/2 lands the per-Subscription routing on top of step 1's broadcast layer. Subscription is now a working data-change stream. Subscription type - Now impls futures_util::Stream<Item = Result<DataChange, Error>> via tokio_stream::wrappers::BroadcastStream + a per-message filter. - No longer Clone (broadcast::Receiver isn't Clone). Consumers that want fanout subscribe twice or share via Arc<Mutex<...>>. - Holds the broadcast::Receiver subscribed BEFORE AdviseSupervisory fires — guarantees no updates between advise and stream-creation are dropped. - pending VecDeque buffers records from the current message so each poll_next yields at most one DataChange (Stream contract). Filter logic (records_to_data_changes, mirrors cs:333-343) - 0x32 SubscriptionStatus: keep when msg.item_correlation_id == subscription.correlation_id; drop otherwise. - 0x33 DataUpdate: keep ALL — codec exposes no per-record correlation field, and the .NET filter only checks item_correlation_id (which 0x33 doesn't carry), so DataUpdates fan out to every active subscription. Matches .NET behavior verbatim. - Records with value: None drop silently (mirrors evt.Record.Value is null filter at cs:337). - BroadcastStream Lagged(n) maps to Error::Configuration with the lag count in the detail string. Helpers - filetime_to_system_time(i64) -> SystemTime: inverse of system_time_to_filetime; saturates at Unix epoch for FILETIMEs before 1970 since SystemTime can't portably represent pre-epoch. - record_to_data_change(record, reference) -> Option<DataChange>: builds DataChange from one record, returns None for unparseable value (the codec couldn't decode the wire kind). - Status currently hardcoded to MxStatus::DATA_CHANGE_OK (mirrors NmxSubscriptionRecord.ToDataChangeStatus at NmxSubscriptionMessage.cs:22-25 which the .NET reference itself stubs to the OK constant). Cargo.toml additions: futures-util (workspace) + tokio-stream (0.1 with sync feature for BroadcastStream). Tests (5 new in mxaccess; total 40) - subscription_stream_yields_data_change_for_matching_correlation: build a 0x32 SubscriptionStatus with one Int32 record and the subscription's correlation id, inject through test_inject_sender, observe the DataChange (reference, value, quality match) on the Stream. - subscription_stream_filters_out_mismatched_correlation_for_status: inject 0x32 with wrong correlation id, assert the stream stays pending (timeout-as-success). - subscription_stream_keeps_data_update_regardless_of_correlation: inject 0x33 DataUpdate with one Int32 record (no correlation field on the message); stream still yields the DataChange. - filetime_to_system_time_round_trip: build a SystemTime with .005s precision, round-trip through both helpers, assert equality. - filetime_to_system_time_pre_unix_epoch_saturates: FILETIME 0 (year 1601) → SystemTime::UNIX_EPOCH (saturating clamp). design/followups.md: F15 moved to Resolved with both step commits referenced. Open list: 9 items (was 10). Test count delta: 511 -> 516 (+5). All four DoD gates green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2b849aed7a |
[M4] mxaccess: wire CallbackExporter + spawn callback router (F15 step 1/2)
Lands the broadcast layer of F15. Session::connect_nmx now starts a local CallbackExporter on an OS-assigned ephemeral port, builds a callback OBJREF advertising it (using local_hostname() with a 127.0.0.1 fallback), and registers that OBJREF with NmxClient::register_engine_2 (was register_engine_2_without_callback). A router task drains the exporter's CallbackEvent stream, decodes each CallbackInvoked body as NmxSubscriptionMessage, and broadcasts parsed messages on a tokio::sync::broadcast channel. Per-subscription correlation routing — turning these raw messages into per-Subscription DataChange streams — is the next iteration's work. F15 stays open until that lands. New Session API - Session::callbacks() -> broadcast::Receiver<Arc<NmxSubscriptionMessage>>: raw observable of every parsed callback message. Test seam + escape hatch for consumers that need raw access today. - Session::callback_exporter_addr() -> Option<SocketAddr>: returns the exporter's local addr (Some until shutdown_nmx, None after). SessionInner additions - callback_exporter: Mutex<Option<CallbackExporter>> — taken in shutdown. - callback_tx: broadcast::Sender<Arc<NmxSubscriptionMessage>>. - router_handle: std::sync::Mutex<Option<JoinHandle<()>>>. shutdown_nmx now performs the full cleanup chain: 1. UnregisterEngine over the live NMX transport. 2. CallbackExporter::shutdown (cancels accept loop). 3. Wait for router task — exits naturally once exporter's mpsc sender side closes. Std::sync::Mutex guard taken-out-then-dropped before await to avoid clippy::await_holding_lock. Routing rationale (callback_router fn) - CallbackEvent::CallbackInvoked → parse via NmxSubscriptionMessage::parse_inner → broadcast Arc<msg>. - Other event variants (Bind / Auth3Ignored / ProtocolError / etc.) silently dropped at this layer; consumers needing them can listen to a future diagnostic-channel hook (no followup yet). - Parse failures silent — the .NET reference fires a separate UnparsedCallbackReceived event we don't model yet. Cargo.toml: added mxaccess-callback as a direct dep on mxaccess. Tests (5 new in mxaccess; total 35) - callbacks receiver observes injected NmxSubscriptionMessage. - multi-subscriber broadcast hands out the same Arc to each receiver. - callback_exporter_addr is Some before shutdown, None after. - router_task end-to-end: feed a hand-built CallbackInvoked event with a 39-byte 0x32 SubscriptionStatus body, observe the parsed message on the broadcast. - router silently drops non-CallbackInvoked events (e.g. Bind). Test count delta: 506 -> 511 (+5). All four DoD gates green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
70feb63ea5 |
[M4] mxaccess: Session::subscribe + unsubscribe + Subscription handle
Lands the subscribe-path lifecycle: AdviseSupervisory + UnAdvise
round-trip via a Subscription handle. The actual DataChange stream
routing is deferred to F15.
New
- Session::subscribe(reference) -> Result<Subscription, Error> —
resolves the tag, generates a 16-byte correlation_id via
rand::random(), calls NmxClient::advise_supervisory. Mirrors
MxNativeSession.SubscribeAsync (cs:250-270) minus the publisher
Connect dance (will land alongside F15's callback routing).
- Session::unsubscribe(subscription) -> Result<(), Error> — consumes
the handle and calls NmxClient::un_advise. Mirrors
MxNativeSession.Unsubscribe (cs:361-381).
- Subscription { correlation_id, reference, metadata } public type
with accessor methods. Currently a pure lifecycle handle — no
Stream impl yet; the Stream<Item=DataChange> shape lands when F15
wires CallbackExporter routing.
- Removed the old subscribe stub from lib.rs (was Err(Unsupported)).
Drop hazard note
- Subscription deliberately does NOT impl Drop to fire UnAdvise. The
spawn-from-Drop pattern is the R15 hazard tracked in
design/70-risks-and-open-questions.md. Callers must call
Session::unsubscribe(sub).await explicitly. F15's wave-2 long-lived
connection task will support best-effort drop-time cleanup without
the spawn-from-Drop hazard.
Cargo.toml: added rand (for correlation_id generation).
design/followups.md: F15 added (P1, M4 wave 2 callback router).
Open followups now at 11 — slightly over the soft 10-item threshold
but no drift (F13 just resolved last iteration). Next iteration's
Step 0 triage will check whether F15 is actionable.
Tests (4 new in mxaccess; total 30)
- subscribe_then_unsubscribe round-trip via in-memory resolver +
hand-rolled server (2 RPCs: AdviseSupervisory + UnAdvise).
- subscribe propagates non-zero AdviseSupervisory HRESULT.
- subscribe after shutdown returns EngineNotRegistered.
- two_subscribes_produce_distinct_correlation_ids — verifies the
rand::random() correlation id generation differentiates handles.
Test count delta: 494 -> 498 (+4). All four DoD gates green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
12cb10c3a1 |
[M4] mxaccess: Session::connect_nmx + write_value + shutdown (wave 1 main)
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>
|
||
|
|
fe2a6db786 |
Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled
Layout:
- src/ .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
MxAsbClient, probes, tests, harnesses. Executable spec.
- design/ Architectural plan for the Rust port (M0–M6), error
model, protocol invariants, risks (R1–R16), adversarial
review log (review.md).
- rust/ Rust workspace. M0 skeleton + M1 codec parity.
mxaccess-codec: 215 unit tests + 2 cross-implementation
parity tests (byte-identical against .NET reference).
Other crates are M0 stubs awaiting M2+.
- captures/ Frida + netsh + pcap evidence per CLAUDE.md
("captures are evidence, not throwaway logs").
- analysis/ Decompiled C# (frida/proxy/decompiled-*),
Ghidra exports for native DLLs (`exports/` only —
working state at `projects/` and AVEVA's input
binaries at `input/` are gitignored).
- docs/ Reverse-engineering reference docs.
- tools/ Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/ Rust CI: fmt + build + test + clippy on Windows.
- LICENSE MIT (Joseph Doherty, 2026).
Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly
Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|