Commit Graph

3 Commits

Author SHA1 Message Date
Joseph Doherty 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>
2026-05-06 09:25:44 -04:00
Joseph Doherty c5d611d6fa [F12 partial + F55] hold IUnknown for client lifetime + diagnose RegisterEngine2 1722
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
**F12 partial improvement** (`mxaccess-rpc::IUnknownHolder` + `mxaccess-nmx`):

- New `IUnknownHolder` newtype that owns an MTA-resident COM proxy
  with `unsafe impl Send + Sync`. Mirrors the .NET reference's
  `ManagedNmxService2Client._activatedComObject` private field
  (`cs:15`).
- New `activate_and_marshal_iunknown_objref(prog_id, ctx)` returns
  `(Vec<u8>, IUnknownHolder)`. Existing
  `marshal_activated_iunknown_objref` retained as a wrapper that
  drops the holder (kept for inline-use callers).
- `NmxClient` gains an `activated_com_object: Option<IUnknownHolder>`
  field, populated by `Self::create` from the new helper.
  `Self::connect` / `Self::from_bound_transport` set it `None` (no
  COM activation in those paths).
- Holding the IUnknown for the client's lifetime keeps the
  SCM-tracked OXID valid; without it the COM ref count drops to
  zero and the SCM may release the activated server-side instance,
  making subsequent `ResolveOxid` / `RemQueryInterface` calls
  return `RPC_S_SERVER_UNAVAILABLE`.

**F55 (new) — hand-rolled callback exporter rejected by RegisterEngine2**

Five-step instrumentation of `Session::connect_nmx_auto` proves all
six COM-activation / RemQI / final-bind steps succeed. The 1722
fault originates at `RegisterEngine2` itself:

```
from_nmx_client: callback hostname="DESKTOP-6JL3KKO" port=57886 obj_ref_len=162
from_nmx_client: callback obj_ref hex: 4d454f57010000...
from_nmx_client: RegisterEngine2 (31112, mxaccess.31112)
from_nmx_client: RegisterEngine2 FAIL: Transport(Fault { status: 2147944122 })
```

Status `0x800706BA` = `RPC_S_SERVER_UNAVAILABLE` wrapped as Win32
HRESULT.

**Critical finding: the .NET reference's `--probe-register-managed-callback`
(which uses the same hand-rolled `ManagedCallbackExporter` approach
as the Rust port) ALSO fails with the same `0x800706BA` fault.**
Only `--probe-session-write`, which uses
`ComObjRefProvider.MarshalInterfaceObjRef(callback, ...)` to build
the OBJREF via Windows DCOM proxy/stub marshalling, succeeds. So
this is an architectural artifact of the hand-rolled-callback
design, not a Rust port regression.

`design/followups.md` F55 entry documents the three resolution
paths (switch to DCOM-marshalled callback / hybrid / continue
investigating OBJREF rejection at NmxSvc).

F49 stays open with a refined diagnostic — the per-feature live
verification is gated on F55's resolution.

Workspace tests still 824 passing; clippy `-D warnings` clean
across both feature configurations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 08:50:30 -04:00
Joseph Doherty cf9dbaf568 [F6] mxaccess-rpc: ComObjRefProvider port via windows-rs (CoMarshalInterface)
rust / build / test / clippy / fmt (push) Has been cancelled
New module crates/mxaccess-rpc/src/com_objref_provider.rs gated on
cfg(all(windows, feature = "windows-com")). Pulls windows = "0.59"
(features Win32_Foundation + Win32_System_Com +
Win32_System_Com_Marshal + Win32_System_Com_StructuredStorage +
Win32_System_Memory) as an optional dep behind the existing
windows-com feature; default footprint stays slim.

Public API mirrors ComObjRefProvider.cs 1:1: MarshalContext enum
(InProcess / Local / DifferentMachine wrapping the MSHCTX_* newtype
constants), clsid_from_prog_id, marshal_activated_iunknown_objref
(activates via CoCreateInstance with INPROC | LOCAL | REMOTE then
marshals), marshal_iunknown_objref (uses IUnknown::IID),
marshal_interface_objref (CoMarshalInterface over an HGlobal-backed
IStream).

All `unsafe` is internal to the module — public API exposes only
typed Rust values (Vec<u8>, GUID, ProviderError), no raw pointers /
HRESULTs / lifetime-bound interface pointers leak. Each unsafe block
carries an inline SAFETY comment naming the invariants being upheld.

Per-thread COM init via thread-local OnceLock<()>: lazy
CoInitializeEx(MULTITHREADED) on first call; S_FALSE (already
initialised) and RPC_E_CHANGED_MODE (thread is STA) treated as
success — matches the .NET runtime's tolerant apartment behaviour.

ProviderError enumerates the four documented failure modes plus the
apartment-init pre-check: UnknownProgId / ActivationFailed /
MarshalFailed / GlobalLockFailed / ApartmentInitFailed.

4 offline tests: MarshalContext → MSHCTX_* mapping, ensure_apartment
idempotence, clsid_from_prog_id returns UnknownProgId for fake
ProgIDs, marshal_activated short-circuits at the resolution stage.

1 live test (#[ignore], gated on MX_LIVE): activates the real
NmxSvc.NmxService, marshals the proxy's IUnknown via
CoMarshalInterface, then parses the resulting blob via
ComObjRef::parse and asserts non-zero OXID + IPID. Passes against
the AVEVA install on this host.

Workspace tests: mxaccess-rpc went 179 → 183 (+4). All other crates
unchanged.

Unblocks F12 (NmxClient::create — the auto-resolving
COM-activation factory): the underlying primitive
(marshal_activated_iunknown_objref) now exists; remaining work is
threading the windows-com feature through mxaccess-nmx and chaining
ComObjRef::parse → resolve_oxid_with_managed_ntlm_packet_integrity →
RemQueryInterface. design/followups.md F12 updated with a revised
"Resolves when" reflecting that F6's blocker is gone.

Closes F6 in design/followups.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:11:33 -04:00