New constructor NmxClient::create(ntlm_factory) gated on
cfg(all(windows, feature = "windows-com")). New crate feature
mxaccess-nmx/windows-com propagates to mxaccess-rpc/windows-com.
Mirrors ManagedNmxService2Client.Create() (cs:30-64) plus
ResolveService (cs:491-523).
Six-step bring-up:
1. com_objref_provider::marshal_activated_iunknown_objref(
"NmxSvc.NmxService", MarshalContext::DifferentMachine)
activates and emits the OBJREF.
2. ComObjRef::parse extracts oxid + the activated server's IUnknown
IPID.
3. resolve_oxid_with_managed_ntlm_packet_integrity against
127.0.0.1:135 (RPCSS endpoint mapper) returns the server's
(host, port) bindings + IRemUnknown IPID.
4. parse_bracketed_host_port pulls the host + port out of the
ncacn_ip_tcp binding's `host[port]` text. Uses rfind for the
rightmost brackets so FQDN forms (foo.example.com[1234])
round-trip — matches the .NET ParseBracketedHost/Port shape at
cs:540-561.
5. A fresh DceRpcTcpClient binds to IRemUnknown and calls
RemQueryInterface(iunknown_ipid, INmxService2_IID,
fresh_causality_id, public_refs=5).
6. A second fresh transport binds to INmxService2 via Self::connect.
The ntlm_factory: impl FnMut() -> NtlmClientContext closure is
invoked three times (one per bind); each NtlmClientContext is
consumed by its bind, so the factory must produce fresh contexts.
New NmxClientError variants:
- Activation(ProviderError) — only emitted with windows-com on.
- EndpointResolution { reason } — covers no ncacn_ip_tcp binding,
malformed host[port], non-zero RemQueryInterface HRESULT.
6 offline tests on parse_bracketed_host_port: FQDN host extraction,
rfind for rightmost brackets, rejection of missing '[' / missing
']' / non-numeric port / port overflow.
1 live test (#[ignore], gated on MX_LIVE + MX_TEST_USER /
MX_TEST_PASSWORD / MX_TEST_DOMAIN populated by
tools/Setup-LiveProbeEnv.ps1): round-trips the full chain against
the AVEVA install on this host. Resolved INmxService2 IPID is
non-zero — verified end-to-end.
Workspace: mxaccess-nmx 17 → 23 (+6). All other crates unchanged.
Closes F12 in design/followups.md. F6 (ComObjRefProvider port) was
the prior blocker; with both landed, the COM-activation path is
end-to-end functional.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seven new high-level methods on NmxClient (port of cs:303-466). Each
takes a GalaxyTagMetadata + typed WriteValue (re-exported from
mxaccess-codec), builds the inner NMX body, wraps in NmxTransferEnvelope,
and dispatches via the existing transfer_data opnum.
Methods landed
- write (cs:303-324)
- write2 (cs:326-349, with explicit FILETIME timestamp)
- write_secured2 (cs:351-380, dual user tokens via
secured_write::resolve_observed_user_token; single-user secured = same id)
- advise_supervisory (cs:382-399, ItemControl envelope)
- send_observed_pre_advise_metadata (cs:401-420, hardcoded target
platform/engine = (1, 1) per the .NET reference)
- register_reference (cs:422-441, accepts caller-built
NmxReferenceRegistrationMessage)
- un_advise (cs:443-466, deliberately uses
NmxTransferMessageKind::Write per cs:457 — the .NET reference's
divergence from AdviseSupervisory's ItemControl envelope, preserved
verbatim per CLAUDE.md unknown-bytes rule)
Internal encode_*_transfer_body helpers extracted as pub(crate) fn for
testability — mirrors the .NET reference's `internal static` shape.
NmxClientError gained two new variants: Codec(CodecError) for
metadata->reference-handle and value-encode failures, and
UnsupportedDataType for the kind-resolution path.
Cargo.toml: added mxaccess-galaxy as a dep on mxaccess-nmx.
design/followups.md: F13 moved to Resolved.
Test count delta: 459 -> 468 (+9 in mxaccess-nmx; 8 -> 17). Tests cover
each encode helper standalone (envelope-kind + length checks) plus
real-socket round-trip tests for write / advise_supervisory /
send_observed_pre_advise_metadata.
All four DoD gates green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands M3 stream B raw opnum surface: an async NmxClient over the
mxaccess-rpc transport that dispatches all 9 INmxService2 procedures
(GetPartnerVersion, RegisterEngine2 + WithoutCallback, UnregisterEngine,
Connect, AddSubscriberEngine, RemoveSubscriberEngine,
SetHeartbeatSendInterval, TransferData) plus a NonZeroHresult error
variant that mirrors ThrowIfFailed (cs:563-574).
New
- crates/mxaccess-nmx/src/client.rs (~580 LoC, 8 tests including 5
real-socket tokio tests against a hand-rolled DCE/RPC server) — port
of the raw opnum surface from ManagedNmxService2Client.cs.
- NmxClient::connect builds the NTLM-packet-integrity bind path; for
tests, NmxClient::from_bound_transport accepts a transport bound any
way the caller likes (the test server doesn't validate signatures).
- fresh_orpc_this generates a per-call Cid via rand::random(), mirroring
the .NET reference's Guid.NewGuid() at every call site.
- NmxClientError::NonZeroHresult unifies the .NET reference's
Marshal.ThrowExceptionForHR + InvalidOperationException branches so
callers see one typed surface for "transport-OK + LMX rejected".
Cargo.toml: added tokio, tracing, thiserror, rand to mxaccess-nmx.
Two layers of the .NET reference are deliberately out of scope this
iteration; both logged as new followups in design/followups.md:
- F12 (P1): the auto-resolving Create() factory, which needs windows-rs
COM activation (gated by F6) + ComObjRefProvider port.
- F13 (P1): the high-level Write*/Advise*/UnAdvise/RegisterReference
helpers, which depend on GalaxyTagMetadata from M3 stream A (the
Galaxy SQL resolver crate, not yet started).
Test count delta: 389 -> 397 (+8). All four DoD gates green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>