8b50c0fd43
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>
179 lines
9.7 KiB
Markdown
179 lines
9.7 KiB
Markdown
# Changelog
|
||
|
||
All notable changes to the `mxaccess` workspace are documented here. The
|
||
format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
||
the workspace as a whole follows [SemVer](https://semver.org/) 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 1–4 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-nmx`** — `INmxService2` client (`RegisterEngine2`,
|
||
`TransferData`, `AddSubscriberEngine`, `SetHeartbeatSendInterval`,
|
||
etc.) plus auto-resolving `NmxClient::create` factory (F12, gated by
|
||
`windows-com`).
|
||
- **`mxaccess-galaxy`** — `tiberius`-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-asb`** — `IASBIDataV2` 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-compat`** — `LMXProxyServer`-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).
|
||
- **Examples** — `connect-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.
|
||
- **Tooling** — `cargo 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.
|