Files
mxaccess/CHANGELOG.md
Joseph Doherty 8b50c0fd43 CHANGELOG: curate post-F43 work into V1 entry
The CHANGELOG was cut at F43 and didn't reflect the work that landed
afterwards on the same V1 milestone. Update the V1 [Unreleased] entry
to cover:

Added (since F43):
- F45 — recovery replay re-issues RegisterReference for buffered subs
- F47 — unsubscribe skips UnAdvise for buffered subs
- F49 / F50 / F51 — live verification + Suspend/Activate captures +
  ASB type-matrix expansion with new fixture round-trip tests
- F52.{1,2,3} — codec performance optimisations (BytesMut output,
  thread-local name-signature cache, caller-supplied scratch buffer)
- F54 — per-operation correlation + compat OnWriteComplete fan-out
- F55 — DCOM-managed INmxSvcCallback sink (Path A)
- F56 — Connect/AddSubscriberEngine round-trip in subscribe path
- MxStatus synthesizer kernel ported (settles R3/R4)

Known limitations (post-resolution):
- Drop F45 / F46 / R3+R4 — all resolved.
- Add F53 protocol-crate missing-docs deferral.
- F3 entry now links the new docs/F3-cross-domain-ntlm-recipe.md.

Publish-order section keeps the DAG but flags F48 (no crates.io
publish) up front so anyone reading the recipe knows it's hygiene
not release prep.

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

9.7 KiB
Raw Permalink Blame History

Changelog

All notable changes to the mxaccess workspace are documented here. The format follows Keep a Changelog; the workspace as a whole follows SemVer but the 0.0.x line is pre-release / API-unstable.

[Unreleased] — V1 — 2026-05-07

V1 is the first publishable cut. Closes M0 → M6 from design/60-roadmap.md. The workspace stays at version = "0.0.0" indefinitely (F48 — internal usage only, no crates.io publish; consumers depend via path or git).

Added

  • mxaccess-codec — pure protocol codec covering MxReferenceHandle, NmxTransferEnvelope, NmxItemControlMessage, NmxWriteMessage (scalar
    • array, normal + timestamped), NmxSecuredWrite2Message, NmxSubscriptionMessage (single + multi-record DataUpdate per F44), NmxReferenceRegistrationMessage, NmxMetadataQueryMessage, NmxOperationStatusMessage, ObservedWriteBodyTemplate, ASB Variant + AsbStatus + RuntimeValue, MxStatus, MxValueKind, MxDataType, MxValue. Counting-allocator bench harness in benches/alloc_count.rs (F38) reports 14 allocs per write across the proven matrix, well under the R12 < 5/write target.
  • mxaccess-rpc — DCE/RPC PDU codec, NTLMv2 client + server-direction packet-integrity verify (F2 with subtle::ConstantTimeEq), TCP transport, OBJREF parser + Win32 CoMarshalInterface emitter (F6), IObjectExporter::ResolveOxid + ResolveOxid2 (F10), IRemUnknown::RemQueryInterface + RemAddRef/RemRelease (F11).
  • mxaccess-callback — RPC server hosting INmxSvcCallback + IRemUnknown for inbound DataReceived / StatusReceived frames. dcom_sink (F55 Path A, gated by windows-com) hosts the callback as a DCOM-managed object so RegisterEngine2 accepts it on AVEVA installs that do SCM-side OXID resolution against RPCSS; the hand-rolled CallbackExporter is retained for unit tests.
  • mxaccess-nmxINmxService2 client (RegisterEngine2, TransferData, AddSubscriberEngine, SetHeartbeatSendInterval, etc.) plus auto-resolving NmxClient::create factory (F12, gated by windows-com).
  • mxaccess-galaxytiberius-backed Resolver + UserResolver (F14, gated by galaxy-resolver).
  • mxaccess-asb-nettcp[MS-NMF] framing + [MC-NBFX] binary-XML
    • [MC-NBFS] static dictionary + DH/HMAC/AES auth crypto with constant-time mod_exp via crypto-bigint::DynResidue (F27).
  • mxaccess-asbIASBIDataV2 client (Connect, RegisterItems, Read, Write, PublishWriteComplete, CreateSubscription, AddMonitoredItems, Publish, DeleteMonitoredItems, DeleteSubscription, Disconnect) with canonical-XML HMAC signing for all 13 ConnectedRequest shapes (F28) and DataContract field-suffix names on the binary MonitoredItem body (F34).
  • mxaccess — async Tokio façade exposing Session, AsbSession, Subscription (Stream<Item = Result<DataChange, Error>>), subscribe_buffered per R2 single-sample-with-cadence-knob semantics (F36), recover_connection reconnect loop (F16), recovery events (RecoveryEvent::Started/Recovered/Failed), and a typed Error taxonomy. Recovery replay re-issues RegisterReference (not AdviseSupervisory) for buffered subscriptions so the .property(buffer) shape survives transport rebuild (F45); unsubscribe skips the UnAdvise wire frame for buffered subscriptions to match the .NET reference's IsBuffered guard (F47). Session::ensure_publisher_connected issues the INmxService2::Connect + AddSubscriberEngine round-trip before the first advise against each publishing engine, so 0x33 DataUpdate frames flow on this AVEVA install (F56). New WriteHandle { correlation_id } returned by *_with_handle write variants for per-operation correlation; OperationStatus.context carries the originating OperationContext (F54). Optional metrics feature emits per-op counters, latency histograms, and connection-state gauges (F40).
  • mxaccess-compatLMXProxyServer-shaped Rust facade exposing the 18-method ILMXProxyServer5 surface as async fns over mxaccess::Session / AsbSession with a Mutex<HashMap<i32, ItemRef>> handle table and Stream-based event channels (F35). LmxClient spawns an operation_status_drain fan-out task that routes Write / WriteSecured events to on_write_complete and every other op kind to on_operation_complete, dropping events with unknown correlation ids silently (F54).
  • Examplesconnect-write-read.rs, subscribe.rs, subscribe-buffered.rs, asb-subscribe.rs, multi-tag.rs, recovery.rs, secured-write.rs, plus diagnostic asb-relay.rs. Live-probe DoD verified end-to-end against the AVEVA install.
  • Toolingcargo public-api baselines under design/public-api/{crate}.txt with CI drift check (F41). design/M6-bench-baseline.md records the alloc-count baseline.
  • Performance (post-baseline) — F52. Three codec optimisations measured against the F38 alloc-count harness:
    • write_message::encode_to_bytes_mut (F52.1) — BytesMut output so consumers can split_to / freeze and forward to a wire-level sink without copying. Same alloc count as encode.
    • Thread-local name-signature cache (F52.2) — repeated MxReferenceHandle::from_names calls with the same names skip the to_lowercase + CRC walk. from_names drops 2 → 0 allocs/op once warm; bounded at 1024 entries per thread.
    • write_message::encode_into_bytes_mut (F52.3) — caller-supplied BytesMut scratch buffer; reusing across writes drops fixed-width scalars from 2 → 1 alloc/op and Boolean from 1 → 0. Bench deltas pinned in design/M6-bench-baseline.md § F52.{1,2,3}.
  • Live evidence — F49 / F50 / F51. F49 step 5 (LmxClient OnWriteComplete round-trip) verified live against AVEVA via cargo test -p mxaccess-compat --features live-windows-com --test lmx_write_complete_live. F50 captured Suspend (NMX opcode 0x2D, server-side) + Activate (client-side, no wire traffic) under captures/123-frida-suspend-advised-instrumented/ + captures/124-frida-activate-advised-instrumented/; R5 settled. F51 provisioned 7 UDAs on $TestMachine via wwtools/graccesscli (TestFloat / TestDouble / TestDateTime / TestDuration + array variants), captured live AsbVariant wire bytes for each scalar type, and pinned them via crates/mxaccess-codec/tests/f51_type_matrix_parity.rs.
  • MxStatus synthesizer kernel — Path A from Lmx.dll FUN_10100ce0 ported into MxStatus::from_packed_u32. Settles R3
    • R4 (OperationComplete trigger conditions and completion-only byte mappings: the .NET reference's WriteCompleted is itself a half-implementation; the Rust port preserves the wire bytes verbatim and routes them through the synthesizer kernel).

Changed (vs the .NET reference)

  • NmxSubscriptionMessage::parse_data_update accepts record_count >= 1; the .NET reference hard-throws on record_count != 1. F44 evidence walk against captures/094-frida-buffered-separate-writer/ documents the multi-record observation that drove the divergence.
  • subscribe_buffered returns a Stream<Item = DataChange> (single-sample-per-event); per R2 verification the cadence is a server-side delivery rate knob, not a multi-sample payload.

Known limitations

  • F3 — cross-domain NTLM Type1/2/3 fixture is permanently out-of-scope on the dev host (single-domain only). Single-domain wire parity is verified; cross-domain rounds-trip through the same shape-agnostic AV-pair codec but no live fixture pins it. Self- contained provisioning recipe (lab topology, capture procedure, fixture layout, round-trip test skeleton) at docs/F3-cross-domain-ntlm-recipe.md for whoever has access to a two-forest Windows lab.
  • F53 (protocol crates only)#![warn(missing_docs)] is enabled and warning-clean on the consumer-facing mxaccess + mxaccess-compat lib roots. Protocol crates measure 1883 missing-docs warnings (mostly struct-field-level wire-shape records); enabling the lint there would add per-field one-liners without consumer value. Lint stays off on protocol crates indefinitely. Per-module #![allow(missing_docs)] opt-out is the re-introduction path if a contributor wants per-crate enforcement.

Publish order

Note (2026-05-06, F48): the workspace will not be published to crates.io. Internal usage only; consumers depend via path or git. The dependency DAG below is retained as a workspace-hygiene check (design/F48-publish-dry-run.md validates each crate's cargo package --list produces a clean tarball with no accidental captures or large files) and as the publish recipe if the policy ever changes (e.g. an internal contributor wants registry-style versioning via a private cargo registry).

Workspace crates form a dependency DAG; cargo publish requires already-published deps to exist on crates.io, so the order matters:

  1. mxaccess-codec (no internal deps)
  2. mxaccess-rpc (no internal deps)
  3. mxaccess-asb-nettcp (no internal deps)
  4. mxaccess-galaxy (depends on codec)
  5. mxaccess-callback (depends on rpc + codec)
  6. mxaccess-asb (depends on codec + asb-nettcp)
  7. mxaccess-nmx (depends on codec + galaxy + rpc + callback)
  8. mxaccess (depends on all the above)
  9. mxaccess-compat (depends on mxaccess)

cargo publish --dry-run validates each crate's metadata + tarball in isolation; the dependent crates' dry-runs require the leaf crates to actually exist on crates.io (the registry lookup happens regardless of --no-verify). For pre-publish verification: leaf crates dry-run in CI; dependent crates are validated by the public-api baseline + build-test-clippy matrix.