[R3/R4 Path A] mxaccess: port Lmx.dll FUN_10100ce0 synthesizer kernel
Path A landed for R3/R4. The byte->MxStatus synthesizer in Lmx.dll is
FUN_10100ce0 (`analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md`),
a 4-byte u32 LE -> 4-tuple MxStatus decoder used by every NMX-frame
parser in Lmx.dll. The kernel is byte-deterministic and context-free,
so it ports as a pure function -- the operation-tracking state
machine the original verdict deferred is NOT required for synthesis.
Bit layout (per FUN_10100ce0 lines 21-24):
bit 31: success (-1 if set, 0 if clear)
bits 27..24: category (4 bits)
bits 23..20: detected_by (4 bits)
bits 15..0: detail (i16 -- low 16 bits, signed)
bits 30..28, 19..16: reserved/padding
Codec changes:
- MxStatus::from_packed_u32() / ::to_packed_u32() -- the kernel +
inverse for round-trip parity.
- MxStatus::from_nmx_response_code() -- the constructed-from-response-
code switch in FUN_1010bd10:741-770 (six proven mappings: 0x01, 0x02
-> CommunicationError + RequestingNmx; 0x03 -> ConfigurationError +
RequestingNmx; 0x04 -> ConfigurationError + RespondingNmx; 0x05 ->
CommunicationError + RespondingNmx; 0x1A -> CommunicationError +
RequestingNmx).
- MxStatusCategory / MxStatusSource: from_i16/to_i16 promoted to const
fn so MxStatus::from_packed_u32 can be const.
- NmxOperationStatusMessage::try_parse_process_data_received_body() --
thin wrapper that peels the outer NmxObservedEnvelope before
delegating to try_parse_inner. Mirrors
NmxOperationStatusMessage.TryParseProcessDataReceivedBody (.NET cs:20-32).
- NmxOperationStatusMessage::promote_to_typed() -- entry point that
returns the existing Status field. Documented as a no-op pass-through
for now (the 5-byte inner-body wire shape is NOT the same field as
the 4-byte packed-u32 the kernel decodes); kept for API symmetry.
- 22 new round-trip tests covering the kernel, the response-code
switch, the proven 0x00/0x41/0xEF completion bytes, and round-trip
for every canonical sentinel.
mxaccess (Session) changes:
- New OperationKind enum (Write/WriteSecured/Read/Subscribe/
Unsubscribe/Activate/Suspend/Other).
- New OperationContext struct (correlation_id, op_kind, reference,
retry_count) -- ground for the F54 follow-on per-operation
correlation work.
- New OperationStatus event type {raw, status, context,
is_during_recovery}, mirroring MxNativeOperationStatusEvent (cs:73-78)
with the typed-MxStatus addition.
- Session::operation_status_events() -> broadcast::Receiver<Arc<
OperationStatus>> + operation_status_stream() Stream variant.
- callback_router() now tries operation-status parsing first, falling
through to subscription messages -- matches MxNativeSession
.OnCallbackReceived dispatch order (cs:574,582,590).
- recover_connection() flips a recovery_active counter (Arc<AtomicU32>
shared with the router) so OperationStatus.is_during_recovery is
populated correctly. Mirrors MxNativeSession._recoveryActive
Volatile.Read at cs:573.
- 3 new router tests covering: status-word frame dispatch + typed
promotion to WriteCompleteOk; completion-only frames stay verbatim;
is_during_recovery is stamped from the live counter.
Per-operation context tracking (correlating completion frames back to
outstanding writes/subscribes via the correlation_id) is filed as F54
in design/followups.md. The synthesizer kernel itself is byte-
deterministic, so the kernel and the correlation work are decoupled.
Ghidra evidence (the next-ring xref walk beyond FUN_10114a90):
- analysis/ghidra/exports/Lmx.dll.set-attribute-result-xrefs.md --
xrefs to OnSetAttributeResult / CancelWithStatus / OperationComplete.
- analysis/ghidra/exports/Lmx.dll.vtable-data-xrefs.md -- vtable-slot
data xrefs for the virtual-dispatch path.
- analysis/ghidra/exports/Lmx.dll.synthesizer-decompile.md --
ScanOnDemandCallback::OperationComplete/MultipleOperationComplete
(FUN_1010b990), RemotePlatformResolver::OperationComplete
(FUN_1010dc80), and the constructed-from-responseCode synthesizer
in FUN_1010bd10 (lines 698-770). FUN_1010bd10 is the wire-frame
receiver that drives the synthesis.
- analysis/ghidra/exports/Lmx.dll.synthesizer-helpers-decompile.md --
FUN_10003fc0 (the <success %d category %d ...> formatter; confirms
the 4-tuple layout), FUN_1008f150 (dispatch helper).
- analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md --
FUN_10100ce0 (the kernel itself), FUN_10100bc0 (3xu16 reader),
FUN_1005e580 (4-byte stream reader), FUN_1010ee00 (sister NMX-frame
parser using the same kernel).
- analysis/ghidra/exports/Lmx.dll.synthesizer-callers-xrefs.md --
caller graph; confirms the kernel is called from many wire-frame
parsers but each parser shares the single 4-byte decoder.
R3/R4 verdict updated in design/70-risks-and-open-questions.md from
"settled at verbatim-preserve" to "settled per Path A". F54 filed in
design/followups.md for the per-operation correlation work.
cargo build / test / clippy -D warnings / RUSTDOCFLAGS=-D warnings doc
all clean. cargo public-api baselines regenerated for mxaccess and
mxaccess-codec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
# Lmx.dll xrefs
|
||||||
|
|
||||||
|
## 0x114a90 at 10114a90
|
||||||
|
|
||||||
|
Target function: `FUN_10114a90`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `10196410` | `DATA` | `` |
|
||||||
|
|
||||||
|
## 0x100dc750 at 100dc750
|
||||||
|
|
||||||
|
Target function: `FUN_100dc750`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `1018f268` | `DATA` | `` |
|
||||||
|
|
||||||
|
## 0x1010b990 at 1010b990
|
||||||
|
|
||||||
|
Target function: `FUN_1010b990`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `1010cf49` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010e440` | `UNCONDITIONAL_CALL` | `FUN_1010e410` |
|
||||||
|
|
||||||
|
## 0x1010dc80 at 1010dc80
|
||||||
|
|
||||||
|
Target function: `FUN_1010dc80`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `10195488` | `DATA` | `` |
|
||||||
|
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Lmx.dll xrefs
|
||||||
|
|
||||||
|
## 0x1010bd10 at 1010bd10
|
||||||
|
|
||||||
|
Target function: `FUN_1010bd10`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `1010d89b` | `UNCONDITIONAL_CALL` | `FUN_1010d4a0` |
|
||||||
|
|
||||||
|
## 0x1010e410 at 1010e410
|
||||||
|
|
||||||
|
Target function: `FUN_1010e410`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `101956a8` | `DATA` | `` |
|
||||||
|
|
||||||
|
## 0x10101360 at 10101360
|
||||||
|
|
||||||
|
Target function: `FUN_10101360`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `10061e82` | `UNCONDITIONAL_CALL` | `FUN_10061c60` |
|
||||||
|
| `10110335` | `UNCONDITIONAL_CALL` | `` |
|
||||||
|
|
||||||
|
## 0x10100ce0 at 10100ce0
|
||||||
|
|
||||||
|
Target function: `FUN_10100ce0`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `1010c2ea` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010c474` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010c50d` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010c5fb` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010c8ac` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010ca5f` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010cb16` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010cd61` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010f27d` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
|
||||||
|
| `1010f365` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
|
||||||
|
| `1010fa8d` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
|
||||||
|
| `1010facf` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
|
||||||
|
|
||||||
|
## 0x10100bc0 at 10100bc0
|
||||||
|
|
||||||
|
Target function: `FUN_10100bc0`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `1010c47e` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010c605` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010ca69` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010cb20` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
|
||||||
|
## 0x1005e580 at 1005e580
|
||||||
|
|
||||||
|
Target function: `FUN_1005e580`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `1010c612` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010ca76` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010f51a` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
|
||||||
|
| `1010f8bb` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
|
||||||
|
| `1010fa76` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
|
||||||
|
| `1010fab8` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
|
||||||
|
| `10110415` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
|
||||||
|
| `10110440` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
|
||||||
|
| `10110513` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
|
||||||
|
| `101116fc` | `UNCONDITIONAL_CALL` | `` |
|
||||||
|
| `10111ab6` | `UNCONDITIONAL_CALL` | `` |
|
||||||
|
| `10111d51` | `UNCONDITIONAL_CALL` | `` |
|
||||||
|
| `10111884` | `UNCONDITIONAL_CALL` | `` |
|
||||||
|
| `101118d2` | `UNCONDITIONAL_CALL` | `` |
|
||||||
|
| `10111b52` | `UNCONDITIONAL_CALL` | `` |
|
||||||
|
| `10110be2` | `UNCONDITIONAL_CALL` | `FUN_10110986` |
|
||||||
|
| `10110c03` | `UNCONDITIONAL_CALL` | `FUN_10110986` |
|
||||||
|
|
||||||
|
## 0x10067aa0 at 10067aa0
|
||||||
|
|
||||||
|
Target function: `FUN_10067aa0`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `1006afb2` | `UNCONDITIONAL_CALL` | `FUN_10069c30` |
|
||||||
|
| `101044a9` | `UNCONDITIONAL_CALL` | `Catch@10104467` |
|
||||||
|
| `100fd351` | `UNCONDITIONAL_CALL` | `FUN_100fd200` |
|
||||||
|
| `10067d89` | `UNCONDITIONAL_CALL` | `FUN_10067d30` |
|
||||||
|
| `100fd560` | `UNCONDITIONAL_CALL` | `FUN_100fd400` |
|
||||||
|
| `100ffe3a` | `UNCONDITIONAL_CALL` | `FUN_100ffc90` |
|
||||||
|
| `1010951d` | `UNCONDITIONAL_CALL` | `FUN_10107880` |
|
||||||
|
| `1010bfcc` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010c250` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010c2bb` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010c7a4` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010d27a` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010f497` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
|
||||||
|
| `10070743` | `UNCONDITIONAL_CALL` | `FUN_10070360` |
|
||||||
|
| `10070869` | `UNCONDITIONAL_CALL` | `FUN_10070360` |
|
||||||
|
| `10070a01` | `UNCONDITIONAL_CALL` | `FUN_10070360` |
|
||||||
|
|
||||||
|
## 0x100860c0 at 100860c0
|
||||||
|
|
||||||
|
Target function: `FUN_100860c0`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `10069f19` | `UNCONDITIONAL_CALL` | `FUN_10069c30` |
|
||||||
|
| `1006a588` | `UNCONDITIONAL_CALL` | `FUN_10069c30` |
|
||||||
|
| `10138a4b` | `UNCONDITIONAL_CALL` | `FUN_101389c0` |
|
||||||
|
| `1010c158` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010c206` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010c5ba` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010d0b4` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
| `1010d167` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,574 @@
|
|||||||
|
# Lmx.dll selected decompile
|
||||||
|
|
||||||
|
## FUN_10003fc0 at 10003fc0
|
||||||
|
|
||||||
|
Signature: `undefined FUN_10003fc0(void)`
|
||||||
|
|
||||||
|
```c
|
||||||
|
|
||||||
|
wchar_t * __thiscall
|
||||||
|
FUN_10003fc0(wchar_t *param_1,short param_2,undefined4 param_3,undefined4 param_4,short param_5)
|
||||||
|
|
||||||
|
{
|
||||||
|
swprintf_s(param_1,0x104,L"<success %d category %d detectedBy %d detail %d>",(int)param_2,param_3,
|
||||||
|
param_4,(int)param_5);
|
||||||
|
return param_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## FUN_10016fd0 at 10016fd0
|
||||||
|
|
||||||
|
Signature: `undefined FUN_10016fd0(void)`
|
||||||
|
|
||||||
|
```c
|
||||||
|
|
||||||
|
void __fastcall FUN_10016fd0(int *param_1)
|
||||||
|
|
||||||
|
{
|
||||||
|
byte bStack_17;
|
||||||
|
|
||||||
|
*param_1 = (uint)bStack_17 << 8;
|
||||||
|
param_1[1] = 0;
|
||||||
|
param_1[2] = 0;
|
||||||
|
param_1[3] = 0;
|
||||||
|
param_1[4] = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## FUN_1008f150 at 1008f150
|
||||||
|
|
||||||
|
Signature: `undefined FUN_1008f150(void)`
|
||||||
|
|
||||||
|
```c
|
||||||
|
|
||||||
|
void __thiscall
|
||||||
|
FUN_1008f150(int param_1,int *param_2,undefined4 param_3,undefined4 param_4,undefined4 param_5,
|
||||||
|
undefined4 param_6,undefined4 param_7,undefined4 param_8,undefined4 param_9,
|
||||||
|
undefined4 param_10,undefined4 param_11)
|
||||||
|
|
||||||
|
{
|
||||||
|
int iVar1;
|
||||||
|
char cVar2;
|
||||||
|
undefined4 uVar3;
|
||||||
|
undefined4 uVar4;
|
||||||
|
basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *pbVar5;
|
||||||
|
int iVar6;
|
||||||
|
int *piVar7;
|
||||||
|
_func_basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>_ptr_basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>_ptr
|
||||||
|
*p_Var8;
|
||||||
|
int *local_258;
|
||||||
|
undefined4 local_254;
|
||||||
|
ulong local_250;
|
||||||
|
ushort local_24c;
|
||||||
|
ushort uStack_24a;
|
||||||
|
uchar local_248 [4];
|
||||||
|
uchar local_244 [4];
|
||||||
|
undefined4 local_240;
|
||||||
|
undefined4 local_23c;
|
||||||
|
undefined4 local_238;
|
||||||
|
undefined4 local_234;
|
||||||
|
undefined4 local_230;
|
||||||
|
undefined4 local_22c;
|
||||||
|
undefined4 local_228;
|
||||||
|
undefined4 local_224;
|
||||||
|
GUID local_220 [33];
|
||||||
|
uint local_8;
|
||||||
|
|
||||||
|
local_8 = DAT_101d60b8 ^ (uint)&stack0xfffffffc;
|
||||||
|
CoCreateGuid(local_220);
|
||||||
|
cVar2 = FUN_100408d0();
|
||||||
|
if (cVar2 != '\0') {
|
||||||
|
uVar3 = FUN_10047fe0(local_220[0].Data1,local_220[0]._4_4_,local_220[0].Data4._0_4_,
|
||||||
|
local_220[0].Data4._4_4_);
|
||||||
|
p_Var8 = endl_exref;
|
||||||
|
uVar4 = (**(code **)(*param_2 + 8))();
|
||||||
|
piVar7 = param_2;
|
||||||
|
pbVar5 = (basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *)
|
||||||
|
FUN_1001a0e0(*(undefined4 *)(DAT_101d6474 + 0x38),
|
||||||
|
L"CReferenceStringResolver::ResolveReference - reference ",param_2,
|
||||||
|
L" guid ",uVar3,L" ref ",uVar4);
|
||||||
|
pbVar5 = std::basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>::operator<<
|
||||||
|
(pbVar5,piVar7);
|
||||||
|
uVar3 = FUN_1001a0e0(pbVar5);
|
||||||
|
uVar3 = FUN_1001a0e0(uVar3);
|
||||||
|
uVar3 = FUN_1001a0e0(uVar3);
|
||||||
|
pbVar5 = (basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *)
|
||||||
|
FUN_1001a0e0(uVar3);
|
||||||
|
std::basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>::operator<<
|
||||||
|
(pbVar5,p_Var8);
|
||||||
|
}
|
||||||
|
local_250 = local_220[0].Data1;
|
||||||
|
local_24c = local_220[0].Data2;
|
||||||
|
uStack_24a = local_220[0].Data3;
|
||||||
|
local_244[0] = local_220[0].Data4[4];
|
||||||
|
local_244[1] = local_220[0].Data4[5];
|
||||||
|
local_244[2] = local_220[0].Data4[6];
|
||||||
|
local_244[3] = local_220[0].Data4[7];
|
||||||
|
local_254 = param_11;
|
||||||
|
local_240 = param_7;
|
||||||
|
local_238 = param_9;
|
||||||
|
local_248[0] = local_220[0].Data4[0];
|
||||||
|
local_248[1] = local_220[0].Data4[1];
|
||||||
|
local_248[2] = local_220[0].Data4[2];
|
||||||
|
local_248[3] = local_220[0].Data4[3];
|
||||||
|
local_234 = param_10;
|
||||||
|
local_258 = param_2;
|
||||||
|
iVar1 = *(int *)(param_1 + 0x24);
|
||||||
|
local_22c = param_4;
|
||||||
|
local_23c = param_8;
|
||||||
|
local_228 = param_5;
|
||||||
|
local_230 = param_3;
|
||||||
|
local_224 = param_6;
|
||||||
|
iVar6 = FUN_1008e910(iVar1,*(undefined4 *)(iVar1 + 4),&local_258);
|
||||||
|
if (*(int *)(param_1 + 0x28) == 0x4924923) {
|
||||||
|
/* WARNING: Subroutine does not return */
|
||||||
|
std::_Xlength_error("list<T> too long");
|
||||||
|
}
|
||||||
|
*(int *)(param_1 + 0x28) = *(int *)(param_1 + 0x28) + 1;
|
||||||
|
*(int *)(iVar1 + 4) = iVar6;
|
||||||
|
**(int **)(iVar6 + 4) = iVar6;
|
||||||
|
__security_check_cookie(local_8 ^ (uint)&stack0xfffffffc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## FUN_10112f20 at 10112f20
|
||||||
|
|
||||||
|
Signature: `undefined FUN_10112f20(void)`
|
||||||
|
|
||||||
|
```c
|
||||||
|
|
||||||
|
int * __thiscall FUN_10112f20(int *param_1,int *param_2)
|
||||||
|
|
||||||
|
{
|
||||||
|
UINT UVar1;
|
||||||
|
BSTR pOVar2;
|
||||||
|
|
||||||
|
if (*param_1 != *param_2) {
|
||||||
|
AtlComPtrAssign(param_1,*param_2);
|
||||||
|
}
|
||||||
|
if ((BSTR)param_1[1] != (BSTR)param_2[1]) {
|
||||||
|
SysFreeString((BSTR)param_1[1]);
|
||||||
|
pOVar2 = (BSTR)0x0;
|
||||||
|
if ((BSTR)param_2[1] != (BSTR)0x0) {
|
||||||
|
UVar1 = SysStringByteLen((BSTR)param_2[1]);
|
||||||
|
pOVar2 = SysAllocStringByteLen((LPCSTR)param_2[1],UVar1);
|
||||||
|
}
|
||||||
|
param_1[1] = (int)pOVar2;
|
||||||
|
if ((param_2[1] != 0) && (pOVar2 == (BSTR)0x0)) {
|
||||||
|
/* WARNING: Subroutine does not return */
|
||||||
|
FUN_100013e0(0x8007000e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((BSTR)param_1[2] != (BSTR)param_2[2]) {
|
||||||
|
SysFreeString((BSTR)param_1[2]);
|
||||||
|
pOVar2 = (BSTR)0x0;
|
||||||
|
if ((BSTR)param_2[2] != (BSTR)0x0) {
|
||||||
|
UVar1 = SysStringByteLen((BSTR)param_2[2]);
|
||||||
|
pOVar2 = SysAllocStringByteLen((LPCSTR)param_2[2],UVar1);
|
||||||
|
}
|
||||||
|
param_1[2] = (int)pOVar2;
|
||||||
|
if ((param_2[2] != 0) && (pOVar2 == (BSTR)0x0)) {
|
||||||
|
/* WARNING: Subroutine does not return */
|
||||||
|
FUN_100013e0(0x8007000e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((BSTR)param_1[3] != (BSTR)param_2[3]) {
|
||||||
|
SysFreeString((BSTR)param_1[3]);
|
||||||
|
pOVar2 = (BSTR)0x0;
|
||||||
|
if ((BSTR)param_2[3] != (BSTR)0x0) {
|
||||||
|
UVar1 = SysStringByteLen((BSTR)param_2[3]);
|
||||||
|
pOVar2 = SysAllocStringByteLen((LPCSTR)param_2[3],UVar1);
|
||||||
|
}
|
||||||
|
param_1[3] = (int)pOVar2;
|
||||||
|
if ((param_2[3] != 0) && (pOVar2 == (BSTR)0x0)) {
|
||||||
|
/* WARNING: Subroutine does not return */
|
||||||
|
FUN_100013e0(0x8007000e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((BSTR)param_1[4] != (BSTR)param_2[4]) {
|
||||||
|
SysFreeString((BSTR)param_1[4]);
|
||||||
|
pOVar2 = (BSTR)0x0;
|
||||||
|
if ((BSTR)param_2[4] != (BSTR)0x0) {
|
||||||
|
UVar1 = SysStringByteLen((BSTR)param_2[4]);
|
||||||
|
pOVar2 = SysAllocStringByteLen((LPCSTR)param_2[4],UVar1);
|
||||||
|
}
|
||||||
|
param_1[4] = (int)pOVar2;
|
||||||
|
if ((param_2[4] != 0) && (pOVar2 == (BSTR)0x0)) {
|
||||||
|
/* WARNING: Subroutine does not return */
|
||||||
|
FUN_100013e0(0x8007000e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return param_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## FUN_10112da0 at 10112da0
|
||||||
|
|
||||||
|
Signature: `undefined FUN_10112da0(void)`
|
||||||
|
|
||||||
|
```c
|
||||||
|
|
||||||
|
void __fastcall FUN_10112da0(undefined4 *param_1)
|
||||||
|
|
||||||
|
{
|
||||||
|
int *piVar1;
|
||||||
|
uint uVar2;
|
||||||
|
void *local_10;
|
||||||
|
undefined1 *puStack_c;
|
||||||
|
int local_8;
|
||||||
|
|
||||||
|
puStack_c = &LAB_1017253f;
|
||||||
|
local_10 = ExceptionList;
|
||||||
|
uVar2 = DAT_101d60b8 ^ (uint)&stack0xfffffffc;
|
||||||
|
ExceptionList = &local_10;
|
||||||
|
*param_1 = PreboundReference::vftable;
|
||||||
|
param_1[1] = PreboundReference::vftable;
|
||||||
|
param_1[3] = PreboundReference::vftable;
|
||||||
|
local_8 = 7;
|
||||||
|
piVar1 = (int *)param_1[0x19];
|
||||||
|
if (piVar1 != (int *)0x0) {
|
||||||
|
(**(code **)(*piVar1 + 8))(piVar1,uVar2);
|
||||||
|
}
|
||||||
|
local_8._0_1_ = 6;
|
||||||
|
SysFreeString((BSTR)param_1[0x2b]);
|
||||||
|
local_8._0_1_ = 5;
|
||||||
|
SysFreeString((BSTR)param_1[0x27]);
|
||||||
|
local_8._0_1_ = 4;
|
||||||
|
if (7 < (uint)param_1[0x21]) {
|
||||||
|
operator_delete((void *)param_1[0x1c]);
|
||||||
|
}
|
||||||
|
param_1[0x21] = 7;
|
||||||
|
param_1[0x20] = 0;
|
||||||
|
*(undefined2 *)(param_1 + 0x1c) = 0;
|
||||||
|
local_8._0_1_ = 3;
|
||||||
|
piVar1 = (int *)param_1[0x1a];
|
||||||
|
if (piVar1 != (int *)0x0) {
|
||||||
|
(**(code **)(*piVar1 + 8))(piVar1);
|
||||||
|
}
|
||||||
|
local_8._0_1_ = 0xc;
|
||||||
|
SysFreeString((BSTR)param_1[0x18]);
|
||||||
|
local_8._0_1_ = 0xb;
|
||||||
|
SysFreeString((BSTR)param_1[0x17]);
|
||||||
|
local_8._0_1_ = 10;
|
||||||
|
SysFreeString((BSTR)param_1[0x16]);
|
||||||
|
local_8._0_1_ = 9;
|
||||||
|
SysFreeString((BSTR)param_1[0x15]);
|
||||||
|
local_8._0_1_ = 2;
|
||||||
|
piVar1 = (int *)param_1[0x14];
|
||||||
|
if (piVar1 != (int *)0x0) {
|
||||||
|
(**(code **)(*piVar1 + 8))(piVar1);
|
||||||
|
}
|
||||||
|
local_8._0_1_ = 1;
|
||||||
|
if (7 < (uint)param_1[0x12]) {
|
||||||
|
operator_delete((void *)param_1[0xd]);
|
||||||
|
}
|
||||||
|
param_1[0x12] = 7;
|
||||||
|
param_1[0x11] = 0;
|
||||||
|
*(undefined2 *)(param_1 + 0xd) = 0;
|
||||||
|
local_8 = (uint)local_8._1_3_ << 8;
|
||||||
|
if (7 < (uint)param_1[0xb]) {
|
||||||
|
operator_delete((void *)param_1[6]);
|
||||||
|
}
|
||||||
|
param_1[0xb] = 7;
|
||||||
|
param_1[10] = 0;
|
||||||
|
*(undefined2 *)(param_1 + 6) = 0;
|
||||||
|
*param_1 = MxConnectionCallback::vftable;
|
||||||
|
ExceptionList = local_10;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## FUN_101139c0 at 101139c0
|
||||||
|
|
||||||
|
Signature: `undefined FUN_101139c0(void)`
|
||||||
|
|
||||||
|
```c
|
||||||
|
|
||||||
|
undefined4 * __thiscall FUN_101139c0(undefined4 *param_1,short *param_2,undefined4 param_3)
|
||||||
|
|
||||||
|
{
|
||||||
|
short sVar1;
|
||||||
|
uint uVar2;
|
||||||
|
short *psVar3;
|
||||||
|
void *local_10;
|
||||||
|
undefined1 *puStack_c;
|
||||||
|
undefined4 local_8;
|
||||||
|
|
||||||
|
puStack_c = &LAB_101726b3;
|
||||||
|
local_10 = ExceptionList;
|
||||||
|
uVar2 = DAT_101d60b8 ^ (uint)&stack0xfffffffc;
|
||||||
|
ExceptionList = &local_10;
|
||||||
|
*param_1 = MxConnectionCallback::vftable;
|
||||||
|
local_8 = 0;
|
||||||
|
param_1[1] = CReferenceToResolve::vftable;
|
||||||
|
*(undefined1 *)(param_1 + 2) = 0;
|
||||||
|
param_1[3] = RedundancyResolutionStatusCallback::vftable;
|
||||||
|
*param_1 = PreboundReference::vftable;
|
||||||
|
param_1[1] = PreboundReference::vftable;
|
||||||
|
param_1[3] = PreboundReference::vftable;
|
||||||
|
*(undefined1 *)(param_1 + 4) = 0;
|
||||||
|
param_1[5] = 0;
|
||||||
|
param_1[0xb] = 7;
|
||||||
|
param_1[10] = 0;
|
||||||
|
*(undefined2 *)(param_1 + 6) = 0;
|
||||||
|
psVar3 = param_2;
|
||||||
|
do {
|
||||||
|
sVar1 = *psVar3;
|
||||||
|
psVar3 = psVar3 + 1;
|
||||||
|
} while (sVar1 != 0);
|
||||||
|
FUN_100363d0(param_2,(int)psVar3 - (int)(param_2 + 1) >> 1);
|
||||||
|
local_8._1_3_ = (undefined3)((uint)local_8 >> 8);
|
||||||
|
param_1[0x12] = 7;
|
||||||
|
param_1[0x11] = 0;
|
||||||
|
*(undefined2 *)(param_1 + 0xd) = 0;
|
||||||
|
local_8._0_1_ = 2;
|
||||||
|
FUN_10113900(param_2);
|
||||||
|
param_1[0x19] = param_3;
|
||||||
|
param_1[0x1a] = 0;
|
||||||
|
*(undefined1 *)(param_1 + 0x1b) = 0;
|
||||||
|
param_1[0x21] = 7;
|
||||||
|
param_1[0x20] = 0;
|
||||||
|
*(undefined2 *)(param_1 + 0x1c) = 0;
|
||||||
|
param_1[0x27] = 0;
|
||||||
|
param_1[0x2a] = 0;
|
||||||
|
param_1[0x2b] = 0;
|
||||||
|
local_8 = CONCAT31(local_8._1_3_,8);
|
||||||
|
*(undefined1 *)(param_1 + 0x2c) = 0;
|
||||||
|
param_1[0x29] = 0;
|
||||||
|
if (DAT_101d8c40 == 0) {
|
||||||
|
FUN_10113070(uVar2);
|
||||||
|
}
|
||||||
|
FUN_101133d0();
|
||||||
|
(**(code **)(*(int *)param_1[0x19] + 4))((int *)param_1[0x19]);
|
||||||
|
ExceptionList = local_10;
|
||||||
|
return param_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## FUN_10113b10 at 10113b10
|
||||||
|
|
||||||
|
Signature: `undefined FUN_10113b10(void)`
|
||||||
|
|
||||||
|
```c
|
||||||
|
|
||||||
|
undefined4 * __thiscall FUN_10113b10(undefined4 *param_1,int *param_2,undefined4 param_3)
|
||||||
|
|
||||||
|
{
|
||||||
|
undefined4 *puVar1;
|
||||||
|
short sVar2;
|
||||||
|
char cVar3;
|
||||||
|
uint uVar4;
|
||||||
|
int iVar5;
|
||||||
|
short *psVar6;
|
||||||
|
short *psVar7;
|
||||||
|
undefined2 *puVar8;
|
||||||
|
void *local_10;
|
||||||
|
undefined1 *puStack_c;
|
||||||
|
undefined1 local_8;
|
||||||
|
undefined3 uStack_7;
|
||||||
|
|
||||||
|
puStack_c = &LAB_1017276f;
|
||||||
|
local_10 = ExceptionList;
|
||||||
|
uVar4 = DAT_101d60b8 ^ (uint)&stack0xfffffffc;
|
||||||
|
ExceptionList = &local_10;
|
||||||
|
*param_1 = MxConnectionCallback::vftable;
|
||||||
|
param_1[1] = CReferenceToResolve::vftable;
|
||||||
|
*(undefined1 *)(param_1 + 2) = 0;
|
||||||
|
param_1[3] = RedundancyResolutionStatusCallback::vftable;
|
||||||
|
*param_1 = PreboundReference::vftable;
|
||||||
|
param_1[1] = PreboundReference::vftable;
|
||||||
|
param_1[3] = PreboundReference::vftable;
|
||||||
|
*(undefined1 *)(param_1 + 4) = 0;
|
||||||
|
param_1[5] = 0;
|
||||||
|
param_1[0xb] = 7;
|
||||||
|
param_1[10] = 0;
|
||||||
|
*(undefined2 *)(param_1 + 6) = 0;
|
||||||
|
param_1[0x12] = 7;
|
||||||
|
param_1[0x11] = 0;
|
||||||
|
*(undefined2 *)(param_1 + 0xd) = 0;
|
||||||
|
uStack_7 = 0;
|
||||||
|
local_8 = 2;
|
||||||
|
param_1[0x14] = param_2;
|
||||||
|
if (param_2 != (int *)0x0) {
|
||||||
|
(**(code **)(*param_2 + 4))(param_2,uVar4);
|
||||||
|
}
|
||||||
|
param_1[0x15] = 0;
|
||||||
|
param_1[0x16] = 0;
|
||||||
|
param_1[0x17] = 0;
|
||||||
|
param_1[0x18] = 0;
|
||||||
|
param_1[0x19] = param_3;
|
||||||
|
param_1[0x1a] = 0;
|
||||||
|
*(undefined1 *)(param_1 + 0x1b) = 0;
|
||||||
|
param_1[0x21] = 7;
|
||||||
|
param_1[0x20] = 0;
|
||||||
|
*(undefined2 *)(param_1 + 0x1c) = 0;
|
||||||
|
param_1[0x27] = 0;
|
||||||
|
param_1[0x2a] = 0;
|
||||||
|
param_1[0x2b] = 0;
|
||||||
|
_local_8 = CONCAT31(uStack_7,0xe);
|
||||||
|
*(undefined1 *)(param_1 + 0x2c) = 0;
|
||||||
|
param_1[0x29] = 0;
|
||||||
|
if (DAT_101d8c40 == 0) {
|
||||||
|
FUN_10113070();
|
||||||
|
}
|
||||||
|
if (param_2 != (int *)0x0) {
|
||||||
|
puVar1 = param_1 + 0x15;
|
||||||
|
SysFreeString((BSTR)param_1[0x15]);
|
||||||
|
*puVar1 = 0;
|
||||||
|
iVar5 = (**(code **)(*(int *)param_1[0x14] + 0x20))((int *)param_1[0x14],puVar1);
|
||||||
|
if (iVar5 < 0) {
|
||||||
|
/* WARNING: Subroutine does not return */
|
||||||
|
FUN_1005bf30(iVar5,0,"E:\\BldSrc\\6\\s\\ExtInterfaces\\Lmx\\IMxReferencePtr.h",0x3f);
|
||||||
|
}
|
||||||
|
psVar7 = (short *)*puVar1;
|
||||||
|
if (psVar7 == (short *)0x0) {
|
||||||
|
psVar7 = &DAT_1017a514;
|
||||||
|
}
|
||||||
|
psVar6 = psVar7;
|
||||||
|
do {
|
||||||
|
sVar2 = *psVar6;
|
||||||
|
psVar6 = psVar6 + 1;
|
||||||
|
} while (sVar2 != 0);
|
||||||
|
FUN_100363d0(psVar7,(int)psVar6 - (int)(psVar7 + 1) >> 1);
|
||||||
|
puVar1 = param_1 + 0x15;
|
||||||
|
SysFreeString((BSTR)param_1[0x15]);
|
||||||
|
*puVar1 = 0;
|
||||||
|
iVar5 = (**(code **)(*(int *)param_1[0x14] + 0x20))((int *)param_1[0x14],puVar1);
|
||||||
|
if (iVar5 < 0) {
|
||||||
|
/* WARNING: Subroutine does not return */
|
||||||
|
FUN_1005bf30(iVar5,0,"E:\\BldSrc\\6\\s\\ExtInterfaces\\Lmx\\IMxReferencePtr.h",0x3f);
|
||||||
|
}
|
||||||
|
puVar8 = (undefined2 *)*puVar1;
|
||||||
|
if (puVar8 == (undefined2 *)0x0) {
|
||||||
|
puVar8 = &DAT_1017a514;
|
||||||
|
}
|
||||||
|
cVar3 = FUN_10134a10(puVar8);
|
||||||
|
if (cVar3 != '\0') {
|
||||||
|
psVar6 = (short *)FUN_1005f6b0();
|
||||||
|
psVar7 = psVar6;
|
||||||
|
do {
|
||||||
|
sVar2 = *psVar7;
|
||||||
|
psVar7 = psVar7 + 1;
|
||||||
|
} while (sVar2 != 0);
|
||||||
|
FUN_100363d0(psVar6,(int)psVar7 - (int)(psVar6 + 1) >> 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FUN_101133d0();
|
||||||
|
(**(code **)(*(int *)param_1[0x19] + 4))((int *)param_1[0x19]);
|
||||||
|
ExceptionList = local_10;
|
||||||
|
return param_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## FUN_10114620 at 10114620
|
||||||
|
|
||||||
|
Signature: `undefined FUN_10114620(void)`
|
||||||
|
|
||||||
|
```c
|
||||||
|
|
||||||
|
void __fastcall FUN_10114620(int param_1)
|
||||||
|
|
||||||
|
{
|
||||||
|
char cVar1;
|
||||||
|
undefined4 uVar2;
|
||||||
|
undefined4 uVar3;
|
||||||
|
undefined1 local_18 [4];
|
||||||
|
void *local_14;
|
||||||
|
void *local_10;
|
||||||
|
undefined1 *puStack_c;
|
||||||
|
undefined4 local_8;
|
||||||
|
|
||||||
|
local_8 = 0xffffffff;
|
||||||
|
puStack_c = &LAB_10172866;
|
||||||
|
local_10 = ExceptionList;
|
||||||
|
ExceptionList = &local_10;
|
||||||
|
if (*(char *)(*(int *)(param_1 + 100) + 0x6dc) != '\0') {
|
||||||
|
cVar1 = FUN_1005faf0(DAT_101d60b8 ^ (uint)&stack0xfffffffc);
|
||||||
|
if (cVar1 == '\0') {
|
||||||
|
local_14 = operator_new(0x1c);
|
||||||
|
local_8 = 0;
|
||||||
|
if (local_14 == (void *)0x0) {
|
||||||
|
local_14 = (void *)0x0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
local_14 = (void *)FUN_10060b80();
|
||||||
|
}
|
||||||
|
local_8 = 0xffffffff;
|
||||||
|
FUN_1003ec10(*(undefined4 *)(param_1 + 0x50));
|
||||||
|
uVar3 = 0;
|
||||||
|
uVar2 = FUN_1002c750(&local_14);
|
||||||
|
FUN_10066e70(local_18,uVar2,uVar3);
|
||||||
|
local_14 = operator_new(0x1c);
|
||||||
|
local_8 = 1;
|
||||||
|
if (local_14 == (void *)0x0) {
|
||||||
|
local_14 = (void *)0x0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
local_14 = (void *)FUN_10060b80();
|
||||||
|
}
|
||||||
|
local_8 = 0xffffffff;
|
||||||
|
FUN_1003ec10(*(undefined4 *)(param_1 + 0x50));
|
||||||
|
uVar3 = 0;
|
||||||
|
uVar2 = FUN_1002c750(&local_14);
|
||||||
|
FUN_10066e70(local_18,uVar2,uVar3);
|
||||||
|
*(undefined1 *)(*(int *)(param_1 + 100) + 0x6dd) = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExceptionList = local_10;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## FUN_10112cd0 at 10112cd0
|
||||||
|
|
||||||
|
Signature: `undefined FUN_10112cd0(void)`
|
||||||
|
|
||||||
|
```c
|
||||||
|
|
||||||
|
void __fastcall FUN_10112cd0(int param_1)
|
||||||
|
|
||||||
|
{
|
||||||
|
uint uVar1;
|
||||||
|
void *pvVar2;
|
||||||
|
void *local_10;
|
||||||
|
undefined1 *puStack_c;
|
||||||
|
undefined4 local_8;
|
||||||
|
|
||||||
|
local_8 = 0xffffffff;
|
||||||
|
puStack_c = &LAB_1017247b;
|
||||||
|
local_10 = ExceptionList;
|
||||||
|
uVar1 = DAT_101d60b8 ^ (uint)&stack0xfffffffc;
|
||||||
|
ExceptionList = &local_10;
|
||||||
|
pvVar2 = operator_new(0x38);
|
||||||
|
local_8 = 0;
|
||||||
|
if (pvVar2 != (void *)0x0) {
|
||||||
|
FUN_1009f240(*(undefined4 *)(param_1 + 0x50),*(undefined2 *)(*(int *)(param_1 + 100) + 0x2ac),
|
||||||
|
param_1,*(int *)(param_1 + 100));
|
||||||
|
}
|
||||||
|
local_8 = 0xffffffff;
|
||||||
|
(*(code *)**(undefined4 **)(param_1 + 4))(uVar1);
|
||||||
|
ExceptionList = local_10;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,134 @@
|
|||||||
|
# Lmx.dll xrefs
|
||||||
|
|
||||||
|
## 0x10196410 at 10196410
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
|
## 0x10196400 at 10196400
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
|
## 0x101963f8 at 101963f8
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
|
## 0x101963f0 at 101963f0
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `10112dd3` | `DATA` | `FUN_10112da0` |
|
||||||
|
| `10113a13` | `DATA` | `FUN_101139c0` |
|
||||||
|
| `10113b5f` | `DATA` | `FUN_10113b10` |
|
||||||
|
|
||||||
|
## 0x101963e8 at 101963e8
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `10112dda` | `DATA` | `FUN_10112da0` |
|
||||||
|
| `10113a1a` | `DATA` | `FUN_101139c0` |
|
||||||
|
| `10113b66` | `DATA` | `FUN_10113b10` |
|
||||||
|
|
||||||
|
## 0x101963e0 at 101963e0
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
|
## 0x10196418 at 10196418
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
|
## 0x10196420 at 10196420
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
|
## 0x1018f268 at 1018f268
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
|
## 0x1018f260 at 1018f260
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
|
## 0x1018f258 at 1018f258
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
|
## 0x1018f270 at 1018f270
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
|
## 0x10195488 at 10195488
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
|
## 0x10195480 at 10195480
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
|
## 0x10195478 at 10195478
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
|
## 0x10195490 at 10195490
|
||||||
|
|
||||||
|
Target function: `(none)`
|
||||||
|
|
||||||
|
| From | Ref type | Caller function |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (none) | | |
|
||||||
|
|
||||||
@@ -40,46 +40,74 @@ The `OnBufferedDataChange` **public event shape** the wwtools api-notes describe
|
|||||||
|
|
||||||
**Settles when:** ✅ settled per option (a). Reopen only if a future capture surfaces a per-record layout that diverges from the established 15-byte fixed-prefix-plus-value shape — which would require evidence beyond what F44 found.
|
**Settles when:** ✅ settled per option (a). Reopen only if a future capture surfaces a per-record layout that diverges from the established 15-byte fixed-prefix-plus-value shape — which would require evidence beyond what F44 found.
|
||||||
|
|
||||||
### R3 — `OperationComplete` trigger unproven **(settled 2026-05-06 — no mapping table exists; verbatim-preserve is the canonical answer)**
|
### R3 — `OperationComplete` trigger unproven **(settled 2026-05-06 — Path A landed: synthesizer kernel + typed `OperationStatus` events ported)**
|
||||||
|
|
||||||
**Severity: P1** (was a blocker; now settled per option: verbatim preserve is the canonical native behaviour)
|
**Severity: P1** (was a blocker; settled per Path A — typed promotion landed via `MxStatus::from_packed_u32`)
|
||||||
|
|
||||||
**Status (2026-05-06): SETTLED.** Five-stage Ghidra headless decompile traced the byte-to-`MXSTATUS_PROXY` synthesis path end-to-end across `Lmx.dll` and `LmxProxy.dll`. Logs:
|
**Status (2026-05-06): SETTLED PER PATH A.** The five-stage Ghidra walk that previously settled the verdict at "verbatim preserve" was extended with a sixth stage that found the actual byte→`MXSTATUS_PROXY` synthesizer. It is **`Lmx.dll!FUN_10100ce0`** — a single 4-byte u32 LE → `MxStatus` decoder used by every NMX-frame parser in `Lmx.dll`. Bit layout:
|
||||||
- `analysis/ghidra/exports/Lmx.dll.aadct-decompile.md` — `aaDCT` symbol
|
|
||||||
- `analysis/ghidra/exports/LmxProxy.dll.completion-status-decompile.md` — Fire_* event handlers
|
```
|
||||||
- `analysis/ghidra/exports/LmxProxy.dll.fire-event-xrefs.md` — xrefs to Fire_*
|
bit 31: success (-1 if set, 0 if clear)
|
||||||
- `analysis/ghidra/exports/LmxProxy.dll.status-synthesis-decompile.md` — Fire_* callers (`FUN_1001657f` / `FUN_10016b50` / `FUN_10016d4b`)
|
bits 27..24: category (4 bits, masked by 0xF)
|
||||||
- `analysis/ghidra/exports/LmxProxy.dll.mxstatus-safearray-decompile.md` — `FUN_10003f60` (the SafeArray creator)
|
bits 23..20: detected_by (4 bits, masked by 0xF)
|
||||||
- `analysis/ghidra/exports/Lmx.dll.set-attribute-result-decompile.md` — `PreboundReference::OnSetAttributeResult` (`FUN_10114a90`)
|
bits 15..0: detail (i16 — low 16 bits, signed)
|
||||||
|
bits 30..28, 19..16: reserved/padding
|
||||||
|
```
|
||||||
|
|
||||||
|
The Rust port now ships this kernel as [`MxStatus::from_packed_u32`] (and the inverse `to_packed_u32` for round-trip parity). `Session::operation_status_events()` emits typed [`OperationStatus`] events for every `0x32`/`0x33`-or-similar callback the wire delivers; the synthesizer is byte-deterministic and context-free, so the operation-tracking state machine the original verdict deferred is **not** required for the kernel itself. Per-operation context tracking (correlating completion frames back to outstanding writes/subscribes) is filed as a follow-up: see F54 below.
|
||||||
|
|
||||||
|
A second mapping was also ported: `MxStatus::from_nmx_response_code` covers the constructed-from-response-code path in `Lmx.dll!FUN_1010bd10:741-770` (`ScanOnDemandCallback::GetResponse`), which builds an `MxStatus` from a 1-byte NMX `responseCode` field when no payload status word is present. Six proven mappings: `0x01`/`0x02` → `(CommunicationError, RequestingNmx)`, `0x03` → `(ConfigurationError, RequestingNmx)`, `0x04` → `(ConfigurationError, RespondingNmx)`, `0x05` → `(CommunicationError, RespondingNmx)`, `0x1A` → `(CommunicationError, RequestingNmx)`. Unmapped codes return `None` and the consumer falls back to verbatim preservation per CLAUDE.md "Do not fabricate protocol behavior."
|
||||||
|
|
||||||
|
**What about the 1-byte completion frames `0x00`/`0x41`/`0xEF`?** Those are NOT decoded by `FUN_10100ce0` — they're a different wire field (the NMX operation-status callback payload, not the `INmxService.GetResponse2 responseCode` parameter). `Lmx.dll`'s decoder for those frames does not invoke any status-synthesis logic; they propagate as raw byte → `MxStatus { success: 0, Unknown, Unknown, detail: byte }`. The Rust port preserves this exactly. R4 is settled by the same fact (see below).
|
||||||
|
|
||||||
|
Logs:
|
||||||
|
- `analysis/ghidra/exports/Lmx.dll.aadct-decompile.md` — `aaDCT` symbol (stage 1)
|
||||||
|
- `analysis/ghidra/exports/LmxProxy.dll.completion-status-decompile.md` — Fire_* event handlers (stage 2)
|
||||||
|
- `analysis/ghidra/exports/LmxProxy.dll.fire-event-xrefs.md` — xrefs to Fire_* (stage 3)
|
||||||
|
- `analysis/ghidra/exports/LmxProxy.dll.status-synthesis-decompile.md` — Fire_* callers (stage 4)
|
||||||
|
- `analysis/ghidra/exports/LmxProxy.dll.mxstatus-safearray-decompile.md` — `FUN_10003f60` (stage 5)
|
||||||
|
- `analysis/ghidra/exports/Lmx.dll.set-attribute-result-decompile.md` — `PreboundReference::OnSetAttributeResult` (stage 6, entry to next ring)
|
||||||
|
- `analysis/ghidra/exports/Lmx.dll.set-attribute-result-xrefs.md` — xrefs to `OnSetAttributeResult`/`CancelWithStatus`/`OperationComplete` (next-ring discovery)
|
||||||
|
- `analysis/ghidra/exports/Lmx.dll.synthesizer-decompile.md` — `ScanOnDemandCallback::OperationComplete`/`MultipleOperationComplete` (`FUN_1010b990`), `RemotePlatformResolver::OperationComplete` (`FUN_1010dc80`), and the constructed-from-responseCode synthesizer `FUN_1010bd10` (lines 698-770)
|
||||||
|
- `analysis/ghidra/exports/Lmx.dll.synthesizer-helpers-decompile.md` — `FUN_10003fc0` (the `<success %d category %d ...>` formatter), `FUN_1008f150` (the dispatch helper), `PreboundReference` constructors
|
||||||
|
- `analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md` — **the synthesizer kernel `FUN_10100ce0`** (4-byte u32 → `MxStatus` decoder), `FUN_10100bc0` (3×u16 reader), `FUN_1005e580` (4-byte stream reader), `FUN_1010ee00` (sister NMX-frame parser using the same kernel)
|
||||||
|
- `analysis/ghidra/exports/Lmx.dll.synthesizer-callers-xrefs.md` — caller graph for the synthesizer ring
|
||||||
|
|
||||||
Findings, layer by layer (the wire bytes flow inward; the synthesis flows outward):
|
Findings, layer by layer (the wire bytes flow inward; the synthesis flows outward):
|
||||||
|
|
||||||
1. **`Lmx.aaDCT`** at `0x10178fc0` is a `SysAllocString(L"Lmx.aaDCT")` into a global BSTR — a tracing category name, not a status-mapping table. No array / lookup logic.
|
1. **`Lmx.aaDCT`** at `0x10178fc0` is a `SysAllocString(L"Lmx.aaDCT")` into a global BSTR — a tracing category name, not a status-mapping table. No array / lookup logic.
|
||||||
2. **`MXSTATUS_PROXY`** (16 bytes, Pack=4) is a 4-field marshalled struct: `success: i16` at offset 0, `category: i16` at offset 4, `detectedBy: i16` at offset 8, `detail: i16` at offset 12. It is the *output* of synthesis, not a lookup-table entry.
|
2. **`MXSTATUS_PROXY`** (16 bytes, Pack=4) is a 4-field marshalled struct: `success: i16` at offset 0, `category: i16` at offset 4, `detectedBy: i16` at offset 8, `detail: i16` at offset 12. It is the *output* of synthesis, not a lookup-table entry.
|
||||||
3. **`LmxProxy.dll` Fire_* event handlers** (`FUN_10015f72`, `FUN_1001611f`, `FUN_10016271`, `FUN_100163c0`) take an *already-populated* `MXSTATUS_PROXY[]` and forward it through ATL connection-point dispatch. No synthesis here.
|
3. **`LmxProxy.dll` Fire_* event handlers** (`FUN_10015f72`, `FUN_1001611f`, `FUN_10016271`, `FUN_100163c0`) take an *already-populated* `MXSTATUS_PROXY[]` and forward it through ATL connection-point dispatch. No synthesis here.
|
||||||
4. **`LmxProxy.dll` Fire_* callers** (`FUN_1001657f` for OnDataChange / OnBufferedDataChange, `FUN_10016b50` for OnWriteComplete, `FUN_10016d4b` for OperationComplete) call **`FUN_10003f60(out_safearray, in_status_ptr, count=1)`** which creates the SafeArray. `FUN_10003f60` is **a verbatim memcpy** of an existing 14-byte buffer into the SAFEARRAY data — no transformation. Source confirms: bytes flow `*local_8 = *param_2; *(local_8+2) = *(param_2+2); *(local_8+4) = *(param_2+4); local_8[6] = param_2[6]`.
|
4. **`LmxProxy.dll` Fire_* callers** (`FUN_1001657f` for OnDataChange / OnBufferedDataChange, `FUN_10016b50` for OnWriteComplete, `FUN_10016d4b` for OperationComplete) call **`FUN_10003f60(out_safearray, in_status_ptr, count=1)`** which creates the SafeArray. `FUN_10003f60` is **a verbatim memcpy** of an existing 14-byte buffer into the SAFEARRAY data — no transformation.
|
||||||
5. **`Lmx.dll` `PreboundReference::OnSetAttributeResult`** (`FUN_10114a90`) — the CALLER of step 4's path — receives an already-populated `short *param_7` status buffer. Its log line confirms the layout: `swprintf_s(L"<success %d category %d detectedBy %d detail %d>", (i16)*p, *(p+2), *(p+4), p[6])`. Its dispatch logic checks typed values (`*local_b6c == -1`, `*(int *)(local_b6c + 2) == 3`) — synthesis is upstream of THIS function too.
|
5. **`Lmx.dll` `PreboundReference::OnSetAttributeResult`** (`FUN_10114a90`) — the CALLER of step 4's path — receives an already-populated `short *param_7` status buffer; synthesis is upstream of THIS function too.
|
||||||
|
6. **The synthesizer kernel itself**: **`Lmx.dll!FUN_10100ce0`** (see `analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md`). A 4-byte u32 LE read from a stream → 4-tuple `MxStatus` decoder. Pure transformation, no operation-context dependency. Used by every NMX-frame parser in `Lmx.dll` (`FUN_1010bd10` `ScanOnDemandCallback::GetResponse`, `FUN_1010ee00` `AccessManager::ProcessNmxRequest`, `FUN_10110986`, etc.) — the upstream decoder reads the wire bytes, the kernel translates them.
|
||||||
|
7. **The constructed-when-no-bytes path**: when an NMX `responseCode != 0` arrives without a payload status word, `FUN_1010bd10:741-770` constructs an `MxStatus` from the responseCode itself via a fixed switch. Six proven response codes (1, 2, 3, 4, 5, 0x1A); see the table in the `MxStatus::from_nmx_response_code` doc.
|
||||||
|
|
||||||
**The synthesizer is the NMX-frame decoder in `Lmx.dll`** that calls `OnSetAttributeResult` / `OnGetAttributeResult` / equivalent OperationComplete handler. That decoder takes raw NMX bytes (e.g. 1-byte `0x00`/`0x41`/`0xEF` completion frames or 5-byte `00 00 50 80 00`-style status words) plus operation context (which item, which engine, retry state, last-write-correlation-id) and computes the populated `MXSTATUS_PROXY`. **There is no static lookup table** — the synthesis is per-message contextual.
|
**Path A landed.** The synthesizer kernel and the constructed-from-response-code switch were both portable as pure functions — no operation-tracking state machine required for the kernel itself, because `FUN_10100ce0` is byte-deterministic. Rust port:
|
||||||
|
|
||||||
**Why this means R3/R4 stay at "verbatim preserve" canonically.** Two viable paths exist if a future consumer demands typed promotion (neither is a small Rust patch):
|
- `mxaccess-codec::status::MxStatus::from_packed_u32(packed: u32) -> MxStatus` — the kernel.
|
||||||
|
- `mxaccess-codec::status::MxStatus::to_packed_u32() -> u32` — inverse, for round-trip parity.
|
||||||
|
- `mxaccess-codec::status::MxStatus::from_nmx_response_code(byte: u8) -> Option<MxStatus>` — the response-code switch.
|
||||||
|
- `mxaccess::OperationKind` + `mxaccess::OperationContext` types for future correlation work (per-operation tracking is filed as F54).
|
||||||
|
- `mxaccess::Session::operation_status_events()` returns `broadcast::Receiver<Arc<OperationStatus>>`; `operation_status_stream()` returns the `Stream<Item = Result<...>>` variant.
|
||||||
|
- `mxaccess::OperationStatus { raw, status, context, is_during_recovery }` — matches `MxNativeOperationStatusEvent` (`MxNativeSession.cs:73-78`) plus typed `MxStatus` promotion.
|
||||||
|
- The callback router (`session::callback_router`) now tries operation-status parsing first, mirroring `MxNativeSession.OnCallbackReceived:574`.
|
||||||
|
|
||||||
- **Path A — port the synthesizer.** Find every NMX-decoder callsite of `OnSetAttributeResult`/`OnGetAttributeResult`/`OperationComplete` in `Lmx.dll` (next xref ring beyond `FUN_10114a90`); decompile each; reverse-engineer the per-decoder synthesis logic; port to Rust. The synthesis depends on operation-tracking state (item handles, retry counters, correlation ids) the Rust codec does not currently track — so the port is more than a codec change; it's a session-state-machine extension. Estimate: ~1-2 weeks of focused work.
|
**What about the 1-byte completion frames `0x00`/`0x41`/`0xEF`?** They are NOT decoded by `FUN_10100ce0` (they're a different wire field at a different layer — the NMX operation-status callback payload, not the `INmxService.GetResponse2` responseCode parameter). Per CLAUDE.md "Do not fabricate protocol behavior" they continue to propagate as `MxStatus { success: 0, Unknown, Unknown, detail: byte }`. R4 is settled by the same fact.
|
||||||
- **Path B — empirical capture pairs.** Run the .NET probe in scenarios that produce each completion byte; capture the (input NMX bytes, observed `MXSTATUS_PROXY`) pair via Frida hooks on `LmxProxy.dll!FUN_10003f60`; build an empirical (byte + context → status) mapping. ~30 min × 6-10 scenarios. Output: a mapping table that approximates the synthesizer without re-implementing it. Risk: the mapping is only valid for the captured operation contexts; new contexts may produce statuses outside the table.
|
|
||||||
|
|
||||||
**Current best answer:** unchanged — `Session::operation_status_events()` exposes `Stream<Item = RawOperationStatus>` carrying frame bytes. Promote to a typed `WriteCompleted` only on the proven `00 00 50 80 00` 5-byte pattern. Other bytes stay raw as `MxStatus { Success: 0, Category: Unknown, DetectedBy: Unknown, Detail: byte }`. The Rust codec mirrors `src/MxNativeCodec/NmxOperationStatusMessage.cs:TryParseInner`. The .NET reference does the same, for the same reason: the synthesizer is too context-dependent to mirror without porting the entire operation-tracking state machine, and that exceeds V1 scope.
|
**Current best answer:** Path A landed. `Session::operation_status_events()` emits typed `OperationStatus` events. The synthesizer kernel (`MxStatus::from_packed_u32`) is exposed for any consumer that holds a 4-byte packed status word (e.g. extracted from a subscription record's `status: i32` field). Per-operation context (correlating completion frames back to outstanding writes/subscribes) is the next step — filed as F54.
|
||||||
|
|
||||||
**Reopen when:** either (a) a consumer files a concrete use case for typed promotion of a specific byte+context combination — at which point Path B's empirical capture for that one combination is the cheapest answer; or (b) a major-version bump justifies the operation-tracking state-machine port (Path A). Until then, verbatim preservation is correct by construction.
|
**Reopen when:** F54 lands per-operation correlation, or a future capture surfaces a fresh wire field whose synthesis logic doesn't reduce to `FUN_10100ce0` + `from_nmx_response_code` (no such field has been observed to date).
|
||||||
|
|
||||||
### R4 — Completion-only byte mapping **(settled 2026-05-06 — collapses into R3's resolution)**
|
### R4 — Completion-only byte mapping **(settled 2026-05-06 — verbatim-preserve confirmed; synthesizer doesn't apply at this layer)**
|
||||||
|
|
||||||
**Severity: P1** (was a blocker; now settled per the same R3 finding)
|
**Severity: P1** (was a blocker; now settled per the same R3 Path A finding — by exclusion)
|
||||||
|
|
||||||
**Status (2026-05-06): SETTLED.** Same Ghidra walk as R3. The 1-byte completion frames `0x00`, `0x41`, `0xEF` (`work_remain.md:164–174`) are the same intermediate NMX signals that R3 covers. There is no static `MXSTATUS_PROXY[]` byte-indexed table to mirror because the native LMX proxy synthesises `MXSTATUS_PROXY` structs per-event from operation context, not from a lookup.
|
**Status (2026-05-06): SETTLED.** R3's Path A walk traced the byte→`MxStatus` synthesizer to **`Lmx.dll!FUN_10100ce0`**, a 4-byte u32 LE → `MxStatus` decoder. The 1-byte completion frames `0x00`, `0x41`, `0xEF` (`work_remain.md:164–174`) are NOT input to that decoder — they're a different wire field, observed at a different layer (the NMX operation-status callback payload, not the `INmxService.GetResponse2` responseCode parameter or any 4-byte packed status field). `Lmx.dll`'s decoder for the 1-byte completion-only inner body does not invoke any synthesis logic; the bytes propagate untransformed.
|
||||||
|
|
||||||
**Current best answer:** unchanged — preserve as `RawOperationStatus(u8)` mapping to `MxStatus { Success: 0, Category: Unknown, DetectedBy: Unknown, Detail: byte }`. The .NET reference does the same; the Rust port matches.
|
**Current best answer:** unchanged — preserve as `MxStatus { Success: 0, Category: Unknown, DetectedBy: Unknown, Detail: byte }`. `mxaccess-codec::NmxOperationStatusMessage::promote_to_typed` returns the verbatim placeholder for these frames; `mxaccess::Session::operation_status_events()` surfaces them via the typed `OperationStatus.status` field with the byte preserved in `detail`.
|
||||||
|
|
||||||
**Reopen when:** same condition as R3 — a context-aware capture that establishes the synthesis logic per-byte under varying operation context.
|
**Reopen when:** a fresh capture proves a synthesis rule for a specific 1-byte completion code under a specific operation context (e.g. via Frida pairs `LmxProxy.dll!FUN_10003f60` input vs. observed event payload). At that point file a sub-followup with the captured `(byte, context, observed status)` triple and decide whether to add a typed mapping.
|
||||||
|
|
||||||
### R5 — Activate / Suspend behaviour **(partially observed — F44 documented client-side trigger; wire-side residual gap filed as F46, hook landed pending live re-run)**
|
### R5 — Activate / Suspend behaviour **(partially observed — F44 documented client-side trigger; wire-side residual gap filed as F46, hook landed pending live re-run)**
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,24 @@ Between each publish: wait for the crate to be indexed before the next one's `ca
|
|||||||
|
|
||||||
**Resolves when:** all three optimisations land or are deliberately rejected with a note in the baseline doc.
|
**Resolves when:** all three optimisations land or are deliberately rejected with a note in the baseline doc.
|
||||||
|
|
||||||
|
### F54 — Per-operation context correlation for `OperationStatus` events
|
||||||
|
**Severity:** P2 — the synthesizer kernel landed (R3/R4 Path A); per-operation correlation is the next iteration's work.
|
||||||
|
**Source:** R3/R4 Path A closeout (`design/70-risks-and-open-questions.md`). The Path A walk found `Lmx.dll!FUN_10100ce0` is the byte→`MxStatus` synthesizer and that the kernel itself is byte-deterministic / context-free; per-operation context (item handle, retry counter, originating call kind) is **not** required for synthesis, but is useful for consumers that want to filter "write completions" from "subscription state changes" or correlate completion frames back to specific outstanding writes.
|
||||||
|
|
||||||
|
**Scope.**
|
||||||
|
1. Add an `outstanding_operations: tokio::sync::Mutex<HashMap<[u8; 16], OperationContext>>` registry on `SessionInner`, parallel to the existing `subscriptions` registry.
|
||||||
|
2. Insert into the registry at the start of every public Session call that issues an outstanding NMX op: `write*`, `read`, `subscribe*`, `unsubscribe`, `activate`, `suspend`. Key by the 16-byte correlation id the call generates. Mirror the .NET reference's private `_pendingWrites`/`_pendingReads` dictionaries.
|
||||||
|
3. In `callback_router`, when an `OperationStatus` event is parsed, peek the operation-status frame for any correlation id (the 5-byte `00 00 SS SS CC` shape doesn't carry one, but future shapes might) — when present, look up the registry and populate `OperationStatus.context`. When absent, leave `context = None`.
|
||||||
|
4. Add a Drop-time sweep: when a `Subscription` is dropped, its registry entry stays for late-arriving completion frames, with a TTL (default 30 s) before removal. Mirrors the .NET reference's "completed" dictionary.
|
||||||
|
5. Round-trip tests: synthesize a `0x32` callback with a known correlation id, hand-insert a registry entry, assert the emitted `OperationStatus.context` matches.
|
||||||
|
|
||||||
|
**Definition of done:**
|
||||||
|
1. `Session::operation_status_events()` emits `OperationStatus.context = Some(_)` for at least the subscription-state-change path (`0x32` SubscriptionStatus frames carry the item correlation id, which the registry will already hold).
|
||||||
|
2. Round-trip tests demonstrate the populated-context path.
|
||||||
|
3. R3 in `70-risks-and-open-questions.md` updated from "Path A landed (kernel only)" to "Path A complete (kernel + correlation)".
|
||||||
|
|
||||||
|
**Resolves when:** the registry lives and at least one wire path emits a populated `context`.
|
||||||
|
|
||||||
### F53 — Enable `#![warn(missing_docs)]` workspace-wide
|
### F53 — Enable `#![warn(missing_docs)]` workspace-wide
|
||||||
**Severity:** P3 — doc-coverage tightening; not a correctness or release blocker.
|
**Severity:** P3 — doc-coverage tightening; not a correctness or release blocker.
|
||||||
**Source:** F42 closeout — the missing-docs lint was deferred because enabling it surfaces hundreds of low-priority public-item gaps that are out of scope for that F-number.
|
**Source:** F42 closeout — the missing-docs lint was deferred because enabling it surfaces hundreds of low-priority public-item gaps that are out of scope for that F-number.
|
||||||
|
|||||||
@@ -514,7 +514,9 @@ pub mxaccess_codec::operation_status::NmxOperationStatusMessage::status: mxacces
|
|||||||
pub mxaccess_codec::operation_status::NmxOperationStatusMessage::status_code: u16
|
pub mxaccess_codec::operation_status::NmxOperationStatusMessage::status_code: u16
|
||||||
impl mxaccess_codec::operation_status::NmxOperationStatusMessage
|
impl mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::is_mx_access_write_complete(&self) -> bool
|
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::is_mx_access_write_complete(&self) -> bool
|
||||||
|
pub const fn mxaccess_codec::operation_status::NmxOperationStatusMessage::promote_to_typed(&self) -> mxaccess_codec::status::MxStatus
|
||||||
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::try_parse_inner(inner: &[u8]) -> core::result::Result<Self, mxaccess_codec::error::CodecError>
|
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::try_parse_inner(inner: &[u8]) -> core::result::Result<Self, mxaccess_codec::error::CodecError>
|
||||||
|
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::try_parse_process_data_received_body(body: &[u8]) -> core::result::Result<Self, mxaccess_codec::error::CodecError>
|
||||||
impl core::clone::Clone for mxaccess_codec::operation_status::NmxOperationStatusMessage
|
impl core::clone::Clone for mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::clone(&self) -> mxaccess_codec::operation_status::NmxOperationStatusMessage
|
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::clone(&self) -> mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
impl core::cmp::Eq for mxaccess_codec::operation_status::NmxOperationStatusMessage
|
impl core::cmp::Eq for mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
@@ -618,8 +620,8 @@ pub mxaccess_codec::prelude::MxStatusCategory::SoftwareError = 7
|
|||||||
pub mxaccess_codec::prelude::MxStatusCategory::Unknown = -1
|
pub mxaccess_codec::prelude::MxStatusCategory::Unknown = -1
|
||||||
pub mxaccess_codec::prelude::MxStatusCategory::Warning = 2
|
pub mxaccess_codec::prelude::MxStatusCategory::Warning = 2
|
||||||
impl mxaccess_codec::status::MxStatusCategory
|
impl mxaccess_codec::status::MxStatusCategory
|
||||||
pub fn mxaccess_codec::status::MxStatusCategory::from_i16(value: i16) -> Self
|
pub const fn mxaccess_codec::status::MxStatusCategory::from_i16(value: i16) -> Self
|
||||||
pub fn mxaccess_codec::status::MxStatusCategory::to_i16(self) -> i16
|
pub const fn mxaccess_codec::status::MxStatusCategory::to_i16(self) -> i16
|
||||||
impl core::clone::Clone for mxaccess_codec::status::MxStatusCategory
|
impl core::clone::Clone for mxaccess_codec::status::MxStatusCategory
|
||||||
pub fn mxaccess_codec::status::MxStatusCategory::clone(&self) -> mxaccess_codec::status::MxStatusCategory
|
pub fn mxaccess_codec::status::MxStatusCategory::clone(&self) -> mxaccess_codec::status::MxStatusCategory
|
||||||
impl core::cmp::Eq for mxaccess_codec::status::MxStatusCategory
|
impl core::cmp::Eq for mxaccess_codec::status::MxStatusCategory
|
||||||
@@ -649,8 +651,8 @@ pub mxaccess_codec::prelude::MxStatusSource::RespondingLmx = 1
|
|||||||
pub mxaccess_codec::prelude::MxStatusSource::RespondingNmx = 3
|
pub mxaccess_codec::prelude::MxStatusSource::RespondingNmx = 3
|
||||||
pub mxaccess_codec::prelude::MxStatusSource::Unknown = -1
|
pub mxaccess_codec::prelude::MxStatusSource::Unknown = -1
|
||||||
impl mxaccess_codec::status::MxStatusSource
|
impl mxaccess_codec::status::MxStatusSource
|
||||||
pub fn mxaccess_codec::status::MxStatusSource::from_i16(value: i16) -> Self
|
pub const fn mxaccess_codec::status::MxStatusSource::from_i16(value: i16) -> Self
|
||||||
pub fn mxaccess_codec::status::MxStatusSource::to_i16(self) -> i16
|
pub const fn mxaccess_codec::status::MxStatusSource::to_i16(self) -> i16
|
||||||
impl core::clone::Clone for mxaccess_codec::status::MxStatusSource
|
impl core::clone::Clone for mxaccess_codec::status::MxStatusSource
|
||||||
pub fn mxaccess_codec::status::MxStatusSource::clone(&self) -> mxaccess_codec::status::MxStatusSource
|
pub fn mxaccess_codec::status::MxStatusSource::clone(&self) -> mxaccess_codec::status::MxStatusSource
|
||||||
impl core::cmp::Eq for mxaccess_codec::status::MxStatusSource
|
impl core::cmp::Eq for mxaccess_codec::status::MxStatusSource
|
||||||
@@ -838,7 +840,10 @@ pub const mxaccess_codec::status::MxStatus::INVALID_REFERENCE_CONFIGURATION: Sel
|
|||||||
pub const mxaccess_codec::status::MxStatus::SUSPEND_PENDING: Self
|
pub const mxaccess_codec::status::MxStatus::SUSPEND_PENDING: Self
|
||||||
pub const mxaccess_codec::status::MxStatus::WRITE_COMPLETE_OK: Self
|
pub const mxaccess_codec::status::MxStatus::WRITE_COMPLETE_OK: Self
|
||||||
pub fn mxaccess_codec::status::MxStatus::detail_text(&self) -> core::option::Option<&'static str>
|
pub fn mxaccess_codec::status::MxStatus::detail_text(&self) -> core::option::Option<&'static str>
|
||||||
|
pub const fn mxaccess_codec::status::MxStatus::from_nmx_response_code(response_code: u8) -> core::option::Option<Self>
|
||||||
|
pub const fn mxaccess_codec::status::MxStatus::from_packed_u32(packed: u32) -> Self
|
||||||
pub fn mxaccess_codec::status::MxStatus::is_ok(&self) -> bool
|
pub fn mxaccess_codec::status::MxStatus::is_ok(&self) -> bool
|
||||||
|
pub const fn mxaccess_codec::status::MxStatus::to_packed_u32(self) -> u32
|
||||||
impl core::clone::Clone for mxaccess_codec::status::MxStatus
|
impl core::clone::Clone for mxaccess_codec::status::MxStatus
|
||||||
pub fn mxaccess_codec::status::MxStatus::clone(&self) -> mxaccess_codec::status::MxStatus
|
pub fn mxaccess_codec::status::MxStatus::clone(&self) -> mxaccess_codec::status::MxStatus
|
||||||
impl core::cmp::Eq for mxaccess_codec::status::MxStatus
|
impl core::cmp::Eq for mxaccess_codec::status::MxStatus
|
||||||
@@ -903,7 +908,9 @@ pub mxaccess_codec::prelude::NmxOperationStatusMessage::status: mxaccess_codec::
|
|||||||
pub mxaccess_codec::prelude::NmxOperationStatusMessage::status_code: u16
|
pub mxaccess_codec::prelude::NmxOperationStatusMessage::status_code: u16
|
||||||
impl mxaccess_codec::operation_status::NmxOperationStatusMessage
|
impl mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::is_mx_access_write_complete(&self) -> bool
|
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::is_mx_access_write_complete(&self) -> bool
|
||||||
|
pub const fn mxaccess_codec::operation_status::NmxOperationStatusMessage::promote_to_typed(&self) -> mxaccess_codec::status::MxStatus
|
||||||
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::try_parse_inner(inner: &[u8]) -> core::result::Result<Self, mxaccess_codec::error::CodecError>
|
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::try_parse_inner(inner: &[u8]) -> core::result::Result<Self, mxaccess_codec::error::CodecError>
|
||||||
|
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::try_parse_process_data_received_body(body: &[u8]) -> core::result::Result<Self, mxaccess_codec::error::CodecError>
|
||||||
impl core::clone::Clone for mxaccess_codec::operation_status::NmxOperationStatusMessage
|
impl core::clone::Clone for mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::clone(&self) -> mxaccess_codec::operation_status::NmxOperationStatusMessage
|
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::clone(&self) -> mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
impl core::cmp::Eq for mxaccess_codec::operation_status::NmxOperationStatusMessage
|
impl core::cmp::Eq for mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
@@ -1220,8 +1227,8 @@ pub mxaccess_codec::status::MxStatusCategory::SoftwareError = 7
|
|||||||
pub mxaccess_codec::status::MxStatusCategory::Unknown = -1
|
pub mxaccess_codec::status::MxStatusCategory::Unknown = -1
|
||||||
pub mxaccess_codec::status::MxStatusCategory::Warning = 2
|
pub mxaccess_codec::status::MxStatusCategory::Warning = 2
|
||||||
impl mxaccess_codec::status::MxStatusCategory
|
impl mxaccess_codec::status::MxStatusCategory
|
||||||
pub fn mxaccess_codec::status::MxStatusCategory::from_i16(value: i16) -> Self
|
pub const fn mxaccess_codec::status::MxStatusCategory::from_i16(value: i16) -> Self
|
||||||
pub fn mxaccess_codec::status::MxStatusCategory::to_i16(self) -> i16
|
pub const fn mxaccess_codec::status::MxStatusCategory::to_i16(self) -> i16
|
||||||
impl core::clone::Clone for mxaccess_codec::status::MxStatusCategory
|
impl core::clone::Clone for mxaccess_codec::status::MxStatusCategory
|
||||||
pub fn mxaccess_codec::status::MxStatusCategory::clone(&self) -> mxaccess_codec::status::MxStatusCategory
|
pub fn mxaccess_codec::status::MxStatusCategory::clone(&self) -> mxaccess_codec::status::MxStatusCategory
|
||||||
impl core::cmp::Eq for mxaccess_codec::status::MxStatusCategory
|
impl core::cmp::Eq for mxaccess_codec::status::MxStatusCategory
|
||||||
@@ -1251,8 +1258,8 @@ pub mxaccess_codec::status::MxStatusSource::RespondingLmx = 1
|
|||||||
pub mxaccess_codec::status::MxStatusSource::RespondingNmx = 3
|
pub mxaccess_codec::status::MxStatusSource::RespondingNmx = 3
|
||||||
pub mxaccess_codec::status::MxStatusSource::Unknown = -1
|
pub mxaccess_codec::status::MxStatusSource::Unknown = -1
|
||||||
impl mxaccess_codec::status::MxStatusSource
|
impl mxaccess_codec::status::MxStatusSource
|
||||||
pub fn mxaccess_codec::status::MxStatusSource::from_i16(value: i16) -> Self
|
pub const fn mxaccess_codec::status::MxStatusSource::from_i16(value: i16) -> Self
|
||||||
pub fn mxaccess_codec::status::MxStatusSource::to_i16(self) -> i16
|
pub const fn mxaccess_codec::status::MxStatusSource::to_i16(self) -> i16
|
||||||
impl core::clone::Clone for mxaccess_codec::status::MxStatusSource
|
impl core::clone::Clone for mxaccess_codec::status::MxStatusSource
|
||||||
pub fn mxaccess_codec::status::MxStatusSource::clone(&self) -> mxaccess_codec::status::MxStatusSource
|
pub fn mxaccess_codec::status::MxStatusSource::clone(&self) -> mxaccess_codec::status::MxStatusSource
|
||||||
impl core::cmp::Eq for mxaccess_codec::status::MxStatusSource
|
impl core::cmp::Eq for mxaccess_codec::status::MxStatusSource
|
||||||
@@ -1285,7 +1292,10 @@ pub const mxaccess_codec::status::MxStatus::INVALID_REFERENCE_CONFIGURATION: Sel
|
|||||||
pub const mxaccess_codec::status::MxStatus::SUSPEND_PENDING: Self
|
pub const mxaccess_codec::status::MxStatus::SUSPEND_PENDING: Self
|
||||||
pub const mxaccess_codec::status::MxStatus::WRITE_COMPLETE_OK: Self
|
pub const mxaccess_codec::status::MxStatus::WRITE_COMPLETE_OK: Self
|
||||||
pub fn mxaccess_codec::status::MxStatus::detail_text(&self) -> core::option::Option<&'static str>
|
pub fn mxaccess_codec::status::MxStatus::detail_text(&self) -> core::option::Option<&'static str>
|
||||||
|
pub const fn mxaccess_codec::status::MxStatus::from_nmx_response_code(response_code: u8) -> core::option::Option<Self>
|
||||||
|
pub const fn mxaccess_codec::status::MxStatus::from_packed_u32(packed: u32) -> Self
|
||||||
pub fn mxaccess_codec::status::MxStatus::is_ok(&self) -> bool
|
pub fn mxaccess_codec::status::MxStatus::is_ok(&self) -> bool
|
||||||
|
pub const fn mxaccess_codec::status::MxStatus::to_packed_u32(self) -> u32
|
||||||
impl core::clone::Clone for mxaccess_codec::status::MxStatus
|
impl core::clone::Clone for mxaccess_codec::status::MxStatus
|
||||||
pub fn mxaccess_codec::status::MxStatus::clone(&self) -> mxaccess_codec::status::MxStatus
|
pub fn mxaccess_codec::status::MxStatus::clone(&self) -> mxaccess_codec::status::MxStatus
|
||||||
impl core::cmp::Eq for mxaccess_codec::status::MxStatus
|
impl core::cmp::Eq for mxaccess_codec::status::MxStatus
|
||||||
@@ -1774,8 +1784,8 @@ pub mxaccess_codec::MxStatusCategory::SoftwareError = 7
|
|||||||
pub mxaccess_codec::MxStatusCategory::Unknown = -1
|
pub mxaccess_codec::MxStatusCategory::Unknown = -1
|
||||||
pub mxaccess_codec::MxStatusCategory::Warning = 2
|
pub mxaccess_codec::MxStatusCategory::Warning = 2
|
||||||
impl mxaccess_codec::status::MxStatusCategory
|
impl mxaccess_codec::status::MxStatusCategory
|
||||||
pub fn mxaccess_codec::status::MxStatusCategory::from_i16(value: i16) -> Self
|
pub const fn mxaccess_codec::status::MxStatusCategory::from_i16(value: i16) -> Self
|
||||||
pub fn mxaccess_codec::status::MxStatusCategory::to_i16(self) -> i16
|
pub const fn mxaccess_codec::status::MxStatusCategory::to_i16(self) -> i16
|
||||||
impl core::clone::Clone for mxaccess_codec::status::MxStatusCategory
|
impl core::clone::Clone for mxaccess_codec::status::MxStatusCategory
|
||||||
pub fn mxaccess_codec::status::MxStatusCategory::clone(&self) -> mxaccess_codec::status::MxStatusCategory
|
pub fn mxaccess_codec::status::MxStatusCategory::clone(&self) -> mxaccess_codec::status::MxStatusCategory
|
||||||
impl core::cmp::Eq for mxaccess_codec::status::MxStatusCategory
|
impl core::cmp::Eq for mxaccess_codec::status::MxStatusCategory
|
||||||
@@ -1805,8 +1815,8 @@ pub mxaccess_codec::MxStatusSource::RespondingLmx = 1
|
|||||||
pub mxaccess_codec::MxStatusSource::RespondingNmx = 3
|
pub mxaccess_codec::MxStatusSource::RespondingNmx = 3
|
||||||
pub mxaccess_codec::MxStatusSource::Unknown = -1
|
pub mxaccess_codec::MxStatusSource::Unknown = -1
|
||||||
impl mxaccess_codec::status::MxStatusSource
|
impl mxaccess_codec::status::MxStatusSource
|
||||||
pub fn mxaccess_codec::status::MxStatusSource::from_i16(value: i16) -> Self
|
pub const fn mxaccess_codec::status::MxStatusSource::from_i16(value: i16) -> Self
|
||||||
pub fn mxaccess_codec::status::MxStatusSource::to_i16(self) -> i16
|
pub const fn mxaccess_codec::status::MxStatusSource::to_i16(self) -> i16
|
||||||
impl core::clone::Clone for mxaccess_codec::status::MxStatusSource
|
impl core::clone::Clone for mxaccess_codec::status::MxStatusSource
|
||||||
pub fn mxaccess_codec::status::MxStatusSource::clone(&self) -> mxaccess_codec::status::MxStatusSource
|
pub fn mxaccess_codec::status::MxStatusSource::clone(&self) -> mxaccess_codec::status::MxStatusSource
|
||||||
impl core::cmp::Eq for mxaccess_codec::status::MxStatusSource
|
impl core::cmp::Eq for mxaccess_codec::status::MxStatusSource
|
||||||
@@ -2100,7 +2110,10 @@ pub const mxaccess_codec::status::MxStatus::INVALID_REFERENCE_CONFIGURATION: Sel
|
|||||||
pub const mxaccess_codec::status::MxStatus::SUSPEND_PENDING: Self
|
pub const mxaccess_codec::status::MxStatus::SUSPEND_PENDING: Self
|
||||||
pub const mxaccess_codec::status::MxStatus::WRITE_COMPLETE_OK: Self
|
pub const mxaccess_codec::status::MxStatus::WRITE_COMPLETE_OK: Self
|
||||||
pub fn mxaccess_codec::status::MxStatus::detail_text(&self) -> core::option::Option<&'static str>
|
pub fn mxaccess_codec::status::MxStatus::detail_text(&self) -> core::option::Option<&'static str>
|
||||||
|
pub const fn mxaccess_codec::status::MxStatus::from_nmx_response_code(response_code: u8) -> core::option::Option<Self>
|
||||||
|
pub const fn mxaccess_codec::status::MxStatus::from_packed_u32(packed: u32) -> Self
|
||||||
pub fn mxaccess_codec::status::MxStatus::is_ok(&self) -> bool
|
pub fn mxaccess_codec::status::MxStatus::is_ok(&self) -> bool
|
||||||
|
pub const fn mxaccess_codec::status::MxStatus::to_packed_u32(self) -> u32
|
||||||
impl core::clone::Clone for mxaccess_codec::status::MxStatus
|
impl core::clone::Clone for mxaccess_codec::status::MxStatus
|
||||||
pub fn mxaccess_codec::status::MxStatus::clone(&self) -> mxaccess_codec::status::MxStatus
|
pub fn mxaccess_codec::status::MxStatus::clone(&self) -> mxaccess_codec::status::MxStatus
|
||||||
impl core::cmp::Eq for mxaccess_codec::status::MxStatus
|
impl core::cmp::Eq for mxaccess_codec::status::MxStatus
|
||||||
@@ -2244,7 +2257,9 @@ pub mxaccess_codec::NmxOperationStatusMessage::status: mxaccess_codec::status::M
|
|||||||
pub mxaccess_codec::NmxOperationStatusMessage::status_code: u16
|
pub mxaccess_codec::NmxOperationStatusMessage::status_code: u16
|
||||||
impl mxaccess_codec::operation_status::NmxOperationStatusMessage
|
impl mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::is_mx_access_write_complete(&self) -> bool
|
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::is_mx_access_write_complete(&self) -> bool
|
||||||
|
pub const fn mxaccess_codec::operation_status::NmxOperationStatusMessage::promote_to_typed(&self) -> mxaccess_codec::status::MxStatus
|
||||||
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::try_parse_inner(inner: &[u8]) -> core::result::Result<Self, mxaccess_codec::error::CodecError>
|
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::try_parse_inner(inner: &[u8]) -> core::result::Result<Self, mxaccess_codec::error::CodecError>
|
||||||
|
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::try_parse_process_data_received_body(body: &[u8]) -> core::result::Result<Self, mxaccess_codec::error::CodecError>
|
||||||
impl core::clone::Clone for mxaccess_codec::operation_status::NmxOperationStatusMessage
|
impl core::clone::Clone for mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::clone(&self) -> mxaccess_codec::operation_status::NmxOperationStatusMessage
|
pub fn mxaccess_codec::operation_status::NmxOperationStatusMessage::clone(&self) -> mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
impl core::cmp::Eq for mxaccess_codec::operation_status::NmxOperationStatusMessage
|
impl core::cmp::Eq for mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
|
|||||||
@@ -57,6 +57,65 @@ impl core::marker::UnsafeUnpin for mxaccess::asb_session::AsbSubscription
|
|||||||
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::asb_session::AsbSubscription
|
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::asb_session::AsbSubscription
|
||||||
impl core::panic::unwind_safe::UnwindSafe for mxaccess::asb_session::AsbSubscription
|
impl core::panic::unwind_safe::UnwindSafe for mxaccess::asb_session::AsbSubscription
|
||||||
pub mod mxaccess::session
|
pub mod mxaccess::session
|
||||||
|
#[non_exhaustive] pub enum mxaccess::session::OperationKind
|
||||||
|
pub mxaccess::session::OperationKind::Activate
|
||||||
|
pub mxaccess::session::OperationKind::Other
|
||||||
|
pub mxaccess::session::OperationKind::Read
|
||||||
|
pub mxaccess::session::OperationKind::Subscribe
|
||||||
|
pub mxaccess::session::OperationKind::Suspend
|
||||||
|
pub mxaccess::session::OperationKind::Unsubscribe
|
||||||
|
pub mxaccess::session::OperationKind::Write
|
||||||
|
pub mxaccess::session::OperationKind::WriteSecured
|
||||||
|
impl core::clone::Clone for mxaccess::session::OperationKind
|
||||||
|
pub fn mxaccess::session::OperationKind::clone(&self) -> mxaccess::session::OperationKind
|
||||||
|
impl core::cmp::Eq for mxaccess::session::OperationKind
|
||||||
|
impl core::cmp::PartialEq for mxaccess::session::OperationKind
|
||||||
|
pub fn mxaccess::session::OperationKind::eq(&self, other: &mxaccess::session::OperationKind) -> bool
|
||||||
|
impl core::fmt::Debug for mxaccess::session::OperationKind
|
||||||
|
pub fn mxaccess::session::OperationKind::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
|
||||||
|
impl core::hash::Hash for mxaccess::session::OperationKind
|
||||||
|
pub fn mxaccess::session::OperationKind::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
|
||||||
|
impl core::marker::Copy for mxaccess::session::OperationKind
|
||||||
|
impl core::marker::StructuralPartialEq for mxaccess::session::OperationKind
|
||||||
|
impl core::marker::Freeze for mxaccess::session::OperationKind
|
||||||
|
impl core::marker::Send for mxaccess::session::OperationKind
|
||||||
|
impl core::marker::Sync for mxaccess::session::OperationKind
|
||||||
|
impl core::marker::Unpin for mxaccess::session::OperationKind
|
||||||
|
impl core::marker::UnsafeUnpin for mxaccess::session::OperationKind
|
||||||
|
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::OperationKind
|
||||||
|
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::OperationKind
|
||||||
|
#[non_exhaustive] pub struct mxaccess::session::OperationContext
|
||||||
|
pub mxaccess::session::OperationContext::correlation_id: [u8; 16]
|
||||||
|
pub mxaccess::session::OperationContext::op_kind: mxaccess::session::OperationKind
|
||||||
|
pub mxaccess::session::OperationContext::reference: core::option::Option<alloc::sync::Arc<str>>
|
||||||
|
pub mxaccess::session::OperationContext::retry_count: u32
|
||||||
|
impl core::clone::Clone for mxaccess::session::OperationContext
|
||||||
|
pub fn mxaccess::session::OperationContext::clone(&self) -> mxaccess::session::OperationContext
|
||||||
|
impl core::fmt::Debug for mxaccess::session::OperationContext
|
||||||
|
pub fn mxaccess::session::OperationContext::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
|
||||||
|
impl core::marker::Freeze for mxaccess::session::OperationContext
|
||||||
|
impl core::marker::Send for mxaccess::session::OperationContext
|
||||||
|
impl core::marker::Sync for mxaccess::session::OperationContext
|
||||||
|
impl core::marker::Unpin for mxaccess::session::OperationContext
|
||||||
|
impl core::marker::UnsafeUnpin for mxaccess::session::OperationContext
|
||||||
|
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::OperationContext
|
||||||
|
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::OperationContext
|
||||||
|
#[non_exhaustive] pub struct mxaccess::session::OperationStatus
|
||||||
|
pub mxaccess::session::OperationStatus::context: core::option::Option<mxaccess::session::OperationContext>
|
||||||
|
pub mxaccess::session::OperationStatus::is_during_recovery: bool
|
||||||
|
pub mxaccess::session::OperationStatus::raw: mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
|
pub mxaccess::session::OperationStatus::status: mxaccess_codec::status::MxStatus
|
||||||
|
impl core::clone::Clone for mxaccess::session::OperationStatus
|
||||||
|
pub fn mxaccess::session::OperationStatus::clone(&self) -> mxaccess::session::OperationStatus
|
||||||
|
impl core::fmt::Debug for mxaccess::session::OperationStatus
|
||||||
|
pub fn mxaccess::session::OperationStatus::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
|
||||||
|
impl core::marker::Freeze for mxaccess::session::OperationStatus
|
||||||
|
impl core::marker::Send for mxaccess::session::OperationStatus
|
||||||
|
impl core::marker::Sync for mxaccess::session::OperationStatus
|
||||||
|
impl core::marker::Unpin for mxaccess::session::OperationStatus
|
||||||
|
impl core::marker::UnsafeUnpin for mxaccess::session::OperationStatus
|
||||||
|
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::OperationStatus
|
||||||
|
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::OperationStatus
|
||||||
pub struct mxaccess::session::SessionInner
|
pub struct mxaccess::session::SessionInner
|
||||||
impl core::fmt::Debug for mxaccess::session::SessionInner
|
impl core::fmt::Debug for mxaccess::session::SessionInner
|
||||||
pub fn mxaccess::session::SessionInner::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
|
pub fn mxaccess::session::SessionInner::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
|
||||||
@@ -208,6 +267,33 @@ impl core::marker::Unpin for mxaccess::Error
|
|||||||
impl core::marker::UnsafeUnpin for mxaccess::Error
|
impl core::marker::UnsafeUnpin for mxaccess::Error
|
||||||
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess::Error
|
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess::Error
|
||||||
impl !core::panic::unwind_safe::UnwindSafe for mxaccess::Error
|
impl !core::panic::unwind_safe::UnwindSafe for mxaccess::Error
|
||||||
|
#[non_exhaustive] pub enum mxaccess::OperationKind
|
||||||
|
pub mxaccess::OperationKind::Activate
|
||||||
|
pub mxaccess::OperationKind::Other
|
||||||
|
pub mxaccess::OperationKind::Read
|
||||||
|
pub mxaccess::OperationKind::Subscribe
|
||||||
|
pub mxaccess::OperationKind::Suspend
|
||||||
|
pub mxaccess::OperationKind::Unsubscribe
|
||||||
|
pub mxaccess::OperationKind::Write
|
||||||
|
pub mxaccess::OperationKind::WriteSecured
|
||||||
|
impl core::clone::Clone for mxaccess::session::OperationKind
|
||||||
|
pub fn mxaccess::session::OperationKind::clone(&self) -> mxaccess::session::OperationKind
|
||||||
|
impl core::cmp::Eq for mxaccess::session::OperationKind
|
||||||
|
impl core::cmp::PartialEq for mxaccess::session::OperationKind
|
||||||
|
pub fn mxaccess::session::OperationKind::eq(&self, other: &mxaccess::session::OperationKind) -> bool
|
||||||
|
impl core::fmt::Debug for mxaccess::session::OperationKind
|
||||||
|
pub fn mxaccess::session::OperationKind::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
|
||||||
|
impl core::hash::Hash for mxaccess::session::OperationKind
|
||||||
|
pub fn mxaccess::session::OperationKind::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
|
||||||
|
impl core::marker::Copy for mxaccess::session::OperationKind
|
||||||
|
impl core::marker::StructuralPartialEq for mxaccess::session::OperationKind
|
||||||
|
impl core::marker::Freeze for mxaccess::session::OperationKind
|
||||||
|
impl core::marker::Send for mxaccess::session::OperationKind
|
||||||
|
impl core::marker::Sync for mxaccess::session::OperationKind
|
||||||
|
impl core::marker::Unpin for mxaccess::session::OperationKind
|
||||||
|
impl core::marker::UnsafeUnpin for mxaccess::session::OperationKind
|
||||||
|
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::OperationKind
|
||||||
|
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::OperationKind
|
||||||
#[non_exhaustive] pub enum mxaccess::ProtocolError
|
#[non_exhaustive] pub enum mxaccess::ProtocolError
|
||||||
pub mxaccess::ProtocolError::Decode
|
pub mxaccess::ProtocolError::Decode
|
||||||
pub mxaccess::ProtocolError::Decode::buffer_len: usize
|
pub mxaccess::ProtocolError::Decode::buffer_len: usize
|
||||||
@@ -391,6 +477,38 @@ impl core::marker::Unpin for mxaccess::DataChange
|
|||||||
impl core::marker::UnsafeUnpin for mxaccess::DataChange
|
impl core::marker::UnsafeUnpin for mxaccess::DataChange
|
||||||
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::DataChange
|
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::DataChange
|
||||||
impl core::panic::unwind_safe::UnwindSafe for mxaccess::DataChange
|
impl core::panic::unwind_safe::UnwindSafe for mxaccess::DataChange
|
||||||
|
#[non_exhaustive] pub struct mxaccess::OperationContext
|
||||||
|
pub mxaccess::OperationContext::correlation_id: [u8; 16]
|
||||||
|
pub mxaccess::OperationContext::op_kind: mxaccess::session::OperationKind
|
||||||
|
pub mxaccess::OperationContext::reference: core::option::Option<alloc::sync::Arc<str>>
|
||||||
|
pub mxaccess::OperationContext::retry_count: u32
|
||||||
|
impl core::clone::Clone for mxaccess::session::OperationContext
|
||||||
|
pub fn mxaccess::session::OperationContext::clone(&self) -> mxaccess::session::OperationContext
|
||||||
|
impl core::fmt::Debug for mxaccess::session::OperationContext
|
||||||
|
pub fn mxaccess::session::OperationContext::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
|
||||||
|
impl core::marker::Freeze for mxaccess::session::OperationContext
|
||||||
|
impl core::marker::Send for mxaccess::session::OperationContext
|
||||||
|
impl core::marker::Sync for mxaccess::session::OperationContext
|
||||||
|
impl core::marker::Unpin for mxaccess::session::OperationContext
|
||||||
|
impl core::marker::UnsafeUnpin for mxaccess::session::OperationContext
|
||||||
|
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::OperationContext
|
||||||
|
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::OperationContext
|
||||||
|
#[non_exhaustive] pub struct mxaccess::OperationStatus
|
||||||
|
pub mxaccess::OperationStatus::context: core::option::Option<mxaccess::session::OperationContext>
|
||||||
|
pub mxaccess::OperationStatus::is_during_recovery: bool
|
||||||
|
pub mxaccess::OperationStatus::raw: mxaccess_codec::operation_status::NmxOperationStatusMessage
|
||||||
|
pub mxaccess::OperationStatus::status: mxaccess_codec::status::MxStatus
|
||||||
|
impl core::clone::Clone for mxaccess::session::OperationStatus
|
||||||
|
pub fn mxaccess::session::OperationStatus::clone(&self) -> mxaccess::session::OperationStatus
|
||||||
|
impl core::fmt::Debug for mxaccess::session::OperationStatus
|
||||||
|
pub fn mxaccess::session::OperationStatus::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
|
||||||
|
impl core::marker::Freeze for mxaccess::session::OperationStatus
|
||||||
|
impl core::marker::Send for mxaccess::session::OperationStatus
|
||||||
|
impl core::marker::Sync for mxaccess::session::OperationStatus
|
||||||
|
impl core::marker::Unpin for mxaccess::session::OperationStatus
|
||||||
|
impl core::marker::UnsafeUnpin for mxaccess::session::OperationStatus
|
||||||
|
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::OperationStatus
|
||||||
|
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::OperationStatus
|
||||||
pub struct mxaccess::RecoveryPolicy
|
pub struct mxaccess::RecoveryPolicy
|
||||||
pub mxaccess::RecoveryPolicy::delay: core::time::Duration
|
pub mxaccess::RecoveryPolicy::delay: core::time::Duration
|
||||||
pub mxaccess::RecoveryPolicy::max_attempts: u32
|
pub mxaccess::RecoveryPolicy::max_attempts: u32
|
||||||
@@ -437,6 +555,8 @@ pub async fn mxaccess::Session::callback_exporter_addr(&self) -> core::option::O
|
|||||||
pub fn mxaccess::Session::callbacks(&self) -> tokio::sync::broadcast::Receiver<alloc::sync::Arc<mxaccess_codec::subscription_message::NmxSubscriptionMessage>>
|
pub fn mxaccess::Session::callbacks(&self) -> tokio::sync::broadcast::Receiver<alloc::sync::Arc<mxaccess_codec::subscription_message::NmxSubscriptionMessage>>
|
||||||
pub async fn mxaccess::Session::connect_nmx(addr: core::net::socket_addr::SocketAddr, options: mxaccess::SessionOptions, ntlm: mxaccess_rpc::ntlm::NtlmClientContext, service_ipid: mxaccess_rpc::guid::Guid, resolver: alloc::sync::Arc<dyn mxaccess_galaxy::resolver::Resolver>, recovery: mxaccess::RecoveryPolicy) -> core::result::Result<Self, mxaccess::Error>
|
pub async fn mxaccess::Session::connect_nmx(addr: core::net::socket_addr::SocketAddr, options: mxaccess::SessionOptions, ntlm: mxaccess_rpc::ntlm::NtlmClientContext, service_ipid: mxaccess_rpc::guid::Guid, resolver: alloc::sync::Arc<dyn mxaccess_galaxy::resolver::Resolver>, recovery: mxaccess::RecoveryPolicy) -> core::result::Result<Self, mxaccess::Error>
|
||||||
pub async fn mxaccess::Session::has_recovery_factory(&self) -> bool
|
pub async fn mxaccess::Session::has_recovery_factory(&self) -> bool
|
||||||
|
pub fn mxaccess::Session::operation_status_events(&self) -> tokio::sync::broadcast::Receiver<alloc::sync::Arc<mxaccess::session::OperationStatus>>
|
||||||
|
pub fn mxaccess::Session::operation_status_stream(&self) -> impl futures_core::stream::Stream<Item = core::result::Result<alloc::sync::Arc<mxaccess::session::OperationStatus>, mxaccess::Error>> + core::marker::Send
|
||||||
pub async fn mxaccess::Session::read(&self, reference: &str, timeout: core::time::Duration) -> core::result::Result<mxaccess::DataChange, mxaccess::Error>
|
pub async fn mxaccess::Session::read(&self, reference: &str, timeout: core::time::Duration) -> core::result::Result<mxaccess::DataChange, mxaccess::Error>
|
||||||
pub async fn mxaccess::Session::recover_connection(&self, policy: mxaccess::RecoveryPolicy) -> core::result::Result<(), mxaccess::Error>
|
pub async fn mxaccess::Session::recover_connection(&self, policy: mxaccess::RecoveryPolicy) -> core::result::Result<(), mxaccess::Error>
|
||||||
pub fn mxaccess::Session::recovery_events(&self) -> tokio::sync::broadcast::Receiver<alloc::sync::Arc<mxaccess::RecoveryEvent>>
|
pub fn mxaccess::Session::recovery_events(&self) -> tokio::sync::broadcast::Receiver<alloc::sync::Arc<mxaccess::RecoveryEvent>>
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ use std::alloc::{GlobalAlloc, Layout, System};
|
|||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
use mxaccess_codec::{
|
use mxaccess_codec::{
|
||||||
MxReferenceHandle, NmxSubscriptionMessage, write_message,
|
MxReferenceHandle, NmxSubscriptionMessage, write_message, write_message::WriteValue,
|
||||||
write_message::WriteValue,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- counting allocator -------------------------------------------------
|
// ---- counting allocator -------------------------------------------------
|
||||||
@@ -218,19 +217,9 @@ fn bench_subscription_decode() -> Row {
|
|||||||
|
|
||||||
fn bench_handle_from_names() -> Row {
|
fn bench_handle_from_names() -> Row {
|
||||||
measure("MxReferenceHandle::from_names", 10_000, || {
|
measure("MxReferenceHandle::from_names", 10_000, || {
|
||||||
let h = MxReferenceHandle::from_names(
|
let h =
|
||||||
0,
|
MxReferenceHandle::from_names(0, 1, 2, 3, "TestChildObject", 0, 1, 0, "TestInt", false)
|
||||||
1,
|
.unwrap();
|
||||||
2,
|
|
||||||
3,
|
|
||||||
"TestChildObject",
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
"TestInt",
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
std::hint::black_box(h);
|
std::hint::black_box(h);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,33 @@
|
|||||||
//! [`NmxOperationStatusMessage::try_parse_inner`] is provided here. When
|
//! [`NmxOperationStatusMessage::try_parse_inner`] is provided here. When
|
||||||
//! `NmxObservedEnvelope` lands, add `try_parse_process_data_received_body` as
|
//! `NmxObservedEnvelope` lands, add `try_parse_process_data_received_body` as
|
||||||
//! a thin wrapper.
|
//! a thin wrapper.
|
||||||
|
//!
|
||||||
|
//! ## Typed promotion and the synthesizer kernel
|
||||||
|
//!
|
||||||
|
//! [`NmxOperationStatusMessage::promote_to_typed`] returns the same
|
||||||
|
//! [`MxStatus`] the parser already attached to the message — the
|
||||||
|
//! verbatim-preserve placeholder for unknown shapes, the
|
||||||
|
//! [`MxStatus::WRITE_COMPLETE_OK`] sentinel for the proven
|
||||||
|
//! `(status_code=0x8050, completion_code=0x00)` shape. The 5-byte
|
||||||
|
//! `00 00 SS SS CC` inner body is **not** the same wire field as the
|
||||||
|
//! 4-byte packed status word `Lmx.dll!FUN_10100ce0` decodes
|
||||||
|
//! ([`MxStatus::from_packed_u32`]) — that kernel applies one layer up,
|
||||||
|
//! to the `INmxService.GetResponse2` payload's `status: i32` field
|
||||||
|
//! (carried e.g. in subscription records). See
|
||||||
|
//! `analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md`
|
||||||
|
//! and `design/70-risks-and-open-questions.md` R3/R4 Path A for the
|
||||||
|
//! evidence chain.
|
||||||
|
//!
|
||||||
|
//! `promote_to_typed` is therefore a thin convenience over the existing
|
||||||
|
//! `status` field: callers that want the canonical bit-layout decoder
|
||||||
|
//! should reach for [`MxStatus::from_packed_u32`] directly when they
|
||||||
|
//! have a 4-byte packed value in hand.
|
||||||
|
|
||||||
// Direct byte indexing — see reference_handle.rs for rationale.
|
// Direct byte indexing — see reference_handle.rs for rationale.
|
||||||
#![allow(clippy::indexing_slicing)]
|
#![allow(clippy::indexing_slicing)]
|
||||||
|
|
||||||
use crate::error::CodecError;
|
use crate::error::CodecError;
|
||||||
|
use crate::observed_frame::NmxObservedEnvelope;
|
||||||
use crate::status::{MxStatus, MxStatusCategory, MxStatusSource};
|
use crate::status::{MxStatus, MxStatusCategory, MxStatusSource};
|
||||||
|
|
||||||
/// Which of the two recognised inner-frame shapes was decoded
|
/// Which of the two recognised inner-frame shapes was decoded
|
||||||
@@ -78,6 +100,47 @@ impl NmxOperationStatusMessage {
|
|||||||
&& self.completion_code == 0x00
|
&& self.completion_code == 0x00
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the typed [`MxStatus`] for this frame.
|
||||||
|
///
|
||||||
|
/// This is a thin convenience over [`Self::status`] — same value,
|
||||||
|
/// no transformation. Provided for API symmetry with
|
||||||
|
/// [`MxStatus::from_packed_u32`] (the canonical 4-byte synthesizer
|
||||||
|
/// kernel) and to give consumers a single entry point that can
|
||||||
|
/// be extended in future revisions if new evidence pins additional
|
||||||
|
/// `(status_code, completion_code)` shapes.
|
||||||
|
///
|
||||||
|
/// **What this method does NOT do:** apply the
|
||||||
|
/// `Lmx.dll!FUN_10100ce0` synthesizer to the 5-byte inner body.
|
||||||
|
/// The 5-byte `00 00 SS SS CC` shape and the 4-byte packed-u32
|
||||||
|
/// shape are different wire fields at different layers — see the
|
||||||
|
/// module docs and
|
||||||
|
/// `design/70-risks-and-open-questions.md` R3/R4 Path A. Callers
|
||||||
|
/// holding a 4-byte packed `MxStatus` (e.g. extracted from a
|
||||||
|
/// subscription record's `status: i32`) should call
|
||||||
|
/// [`MxStatus::from_packed_u32`] directly.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn promote_to_typed(&self) -> MxStatus {
|
||||||
|
self.status
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Peel the outer [`NmxObservedEnvelope`] off a `ProcessDataReceived`
|
||||||
|
/// payload and parse the inner body. Mirrors
|
||||||
|
/// `NmxOperationStatusMessage.TryParseProcessDataReceivedBody`
|
||||||
|
/// (`NmxOperationStatusMessage.cs:20-32`).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns `Err` when the outer envelope cannot be parsed or the
|
||||||
|
/// inner body matches no recognised shape (1- or 5-byte completion
|
||||||
|
/// frame). The .NET reference returns `false` and a `null!`
|
||||||
|
/// out-param in both cases; the Rust port surfaces a typed
|
||||||
|
/// [`CodecError`] so callers can distinguish "not a process-data
|
||||||
|
/// frame" from "successfully parsed".
|
||||||
|
pub fn try_parse_process_data_received_body(body: &[u8]) -> Result<Self, CodecError> {
|
||||||
|
let envelope = NmxObservedEnvelope::parse_process_data_received_body_flexible(body)?;
|
||||||
|
Self::try_parse_inner(&envelope.inner_body)
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse an inner body — either 1 byte (`CompletionOnly`) or 5 bytes
|
/// Parse an inner body — either 1 byte (`CompletionOnly`) or 5 bytes
|
||||||
/// (`StatusWord` with leading `00 00`).
|
/// (`StatusWord` with leading `00 00`).
|
||||||
///
|
///
|
||||||
@@ -281,4 +344,38 @@ mod tests {
|
|||||||
let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap();
|
let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap();
|
||||||
assert_eq!(msg.status_code, 0xBBAA);
|
assert_eq!(msg.status_code, 0xBBAA);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn promote_to_typed_returns_existing_status_for_status_word() {
|
||||||
|
// The proven shape — must keep returning the canonical sentinel.
|
||||||
|
let frame = [0x00, 0x00, 0x50, 0x80, 0x00];
|
||||||
|
let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap();
|
||||||
|
assert_eq!(msg.promote_to_typed(), MxStatus::WRITE_COMPLETE_OK);
|
||||||
|
assert_eq!(msg.promote_to_typed(), msg.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn promote_to_typed_returns_verbatim_status_for_completion_only() {
|
||||||
|
// 1-byte frames: no synthesizer evidence — must stay verbatim.
|
||||||
|
for byte in [0x00_u8, 0x41, 0xEF] {
|
||||||
|
let msg = NmxOperationStatusMessage::try_parse_inner(&[byte]).unwrap();
|
||||||
|
let promoted = msg.promote_to_typed();
|
||||||
|
assert_eq!(promoted, msg.status);
|
||||||
|
assert_eq!(promoted.category, MxStatusCategory::Unknown);
|
||||||
|
assert_eq!(promoted.detected_by, MxStatusSource::Unknown);
|
||||||
|
assert_eq!(promoted.detail, i16::from(byte));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn promote_to_typed_does_not_change_existing_status_field() {
|
||||||
|
// promote_to_typed must not mutate the verbatim-preserve `status`
|
||||||
|
// field. This guards the byte-for-byte parity contract with the
|
||||||
|
// .NET reference.
|
||||||
|
let frame = [0x00, 0x00, 0x55, 0xAA, 0x33];
|
||||||
|
let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap();
|
||||||
|
let original_status = msg.status;
|
||||||
|
let _typed = msg.promote_to_typed();
|
||||||
|
assert_eq!(msg.status, original_status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pub enum MxStatusCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MxStatusCategory {
|
impl MxStatusCategory {
|
||||||
pub fn from_i16(value: i16) -> Self {
|
pub const fn from_i16(value: i16) -> Self {
|
||||||
match value {
|
match value {
|
||||||
0 => Self::Ok,
|
0 => Self::Ok,
|
||||||
1 => Self::Pending,
|
1 => Self::Pending,
|
||||||
@@ -37,7 +37,7 @@ impl MxStatusCategory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_i16(self) -> i16 {
|
pub const fn to_i16(self) -> i16 {
|
||||||
self as i16
|
self as i16
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ pub enum MxStatusSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MxStatusSource {
|
impl MxStatusSource {
|
||||||
pub fn from_i16(value: i16) -> Self {
|
pub const fn from_i16(value: i16) -> Self {
|
||||||
match value {
|
match value {
|
||||||
0 => Self::RequestingLmx,
|
0 => Self::RequestingLmx,
|
||||||
1 => Self::RespondingLmx,
|
1 => Self::RespondingLmx,
|
||||||
@@ -71,7 +71,7 @@ impl MxStatusSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_i16(self) -> i16 {
|
pub const fn to_i16(self) -> i16 {
|
||||||
self as i16
|
self as i16
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,6 +85,135 @@ pub struct MxStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MxStatus {
|
impl MxStatus {
|
||||||
|
/// Decode a 4-byte packed `MxStatus` word.
|
||||||
|
///
|
||||||
|
/// Mirrors the canonical NMX wire-frame status decoder
|
||||||
|
/// `Lmx.dll!FUN_10100ce0` (see
|
||||||
|
/// `analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md`).
|
||||||
|
/// That function reads 4 bytes from a stream into a u32 and unpacks
|
||||||
|
/// them via the bit layout:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// bit 31: success (-1 if set, 0 if clear)
|
||||||
|
/// bits 27..24: category (4 bits, masked by 0xF)
|
||||||
|
/// bits 23..20: detected_by (4 bits, masked by 0xF)
|
||||||
|
/// bits 15..0: detail (i16 — low 16 bits, signed)
|
||||||
|
/// bits 30..28, 19..16: reserved/padding (ignored)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This is the **synthesizer kernel** documented in
|
||||||
|
/// `design/70-risks-and-open-questions.md` R3/R4 Path A. Every NMX
|
||||||
|
/// wire frame that carries a status word emits one of these 4-byte
|
||||||
|
/// packings; the consumer-side dispatch (retry counters, callback
|
||||||
|
/// fan-out) is layered on top of the decoded `MxStatus`, but the
|
||||||
|
/// decoder itself is byte-deterministic and context-free.
|
||||||
|
///
|
||||||
|
/// The `success` field is normalized to either `0` or `-1` per the
|
||||||
|
/// native `Lmx.dll` semantics: any value with bit 31 set decodes to
|
||||||
|
/// `-1`, any value with bit 31 clear decodes to `0`. (Native code:
|
||||||
|
/// `*param_1 = -(ushort)(((uint)param_2 & 0x80000000) != 0)`.)
|
||||||
|
///
|
||||||
|
/// Unknown category / detected_by codes (i.e. a 4-bit value that
|
||||||
|
/// does not match a documented [`MxStatusCategory`] /
|
||||||
|
/// [`MxStatusSource`] variant) decode to the corresponding
|
||||||
|
/// `Unknown` variant. The padding bits are silently discarded.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn from_packed_u32(packed: u32) -> Self {
|
||||||
|
// Bit layout — see fn doc.
|
||||||
|
let success: i16 = if packed & 0x8000_0000 != 0 { -1 } else { 0 };
|
||||||
|
let category_bits = ((packed >> 24) & 0xF) as i16;
|
||||||
|
let detected_by_bits = ((packed >> 20) & 0xF) as i16;
|
||||||
|
let detail = packed as i16;
|
||||||
|
Self {
|
||||||
|
success,
|
||||||
|
category: MxStatusCategory::from_i16(category_bits),
|
||||||
|
detected_by: MxStatusSource::from_i16(detected_by_bits),
|
||||||
|
detail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct an `MxStatus` from a single-byte NMX response code.
|
||||||
|
///
|
||||||
|
/// Mirrors the synthesis switch in
|
||||||
|
/// `Lmx.dll!FUN_1010bd10` (`ScanOnDemandCallback::GetResponse`)
|
||||||
|
/// at lines 741-770 of
|
||||||
|
/// `analysis/ghidra/exports/Lmx.dll.synthesizer-decompile.md`.
|
||||||
|
/// When the NMX `responseCode` is non-zero (no payload status word
|
||||||
|
/// to parse), `Lmx.dll` constructs an `MxStatus` from the response
|
||||||
|
/// code itself using this fixed mapping:
|
||||||
|
///
|
||||||
|
/// | responseCode | category | detected_by |
|
||||||
|
/// |---|---|---|
|
||||||
|
/// | `0x01`, `0x02` | `CommunicationError` | `RequestingNmx` |
|
||||||
|
/// | `0x03` | `ConfigurationError` | `RequestingNmx` |
|
||||||
|
/// | `0x04` | `ConfigurationError` | `RespondingNmx` |
|
||||||
|
/// | `0x05` | `CommunicationError` | `RespondingNmx` |
|
||||||
|
/// | `0x1A` | `CommunicationError` | `RequestingNmx` |
|
||||||
|
///
|
||||||
|
/// `success` is `0` (not `-1`) and `detail` carries the response
|
||||||
|
/// code unchanged. Unmapped codes return `None` — the native code's
|
||||||
|
/// `default` branch leaves the synthesized status untouched, so the
|
||||||
|
/// caller falls back to a verbatim raw-byte placeholder per
|
||||||
|
/// `design/70-risks-and-open-questions.md` R3/R4.
|
||||||
|
///
|
||||||
|
/// This is **not** the same wire field as the 1-byte completion
|
||||||
|
/// frames `0x00`/`0x41`/`0xEF` parsed by
|
||||||
|
/// [`crate::NmxOperationStatusMessage::try_parse_inner`]: those
|
||||||
|
/// live inside a `0x32`/`0x33` callback body, while this
|
||||||
|
/// `responseCode` is the second `out` parameter of
|
||||||
|
/// `INmxService.GetResponse2(...)` (one layer up the stack).
|
||||||
|
/// `Lmx.dll`'s decoder for the 1-byte completion frames does not
|
||||||
|
/// apply this synthesis.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn from_nmx_response_code(response_code: u8) -> Option<Self> {
|
||||||
|
// Per `FUN_1010bd10:741-770` switch.
|
||||||
|
let (category, detected_by) = match response_code {
|
||||||
|
0x01 | 0x02 => (
|
||||||
|
MxStatusCategory::CommunicationError,
|
||||||
|
MxStatusSource::RequestingNmx,
|
||||||
|
),
|
||||||
|
0x03 => (
|
||||||
|
MxStatusCategory::ConfigurationError,
|
||||||
|
MxStatusSource::RequestingNmx,
|
||||||
|
),
|
||||||
|
0x04 => (
|
||||||
|
MxStatusCategory::ConfigurationError,
|
||||||
|
MxStatusSource::RespondingNmx,
|
||||||
|
),
|
||||||
|
0x05 => (
|
||||||
|
MxStatusCategory::CommunicationError,
|
||||||
|
MxStatusSource::RespondingNmx,
|
||||||
|
),
|
||||||
|
0x1A => (
|
||||||
|
MxStatusCategory::CommunicationError,
|
||||||
|
MxStatusSource::RequestingNmx,
|
||||||
|
),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(Self {
|
||||||
|
success: 0,
|
||||||
|
category,
|
||||||
|
detected_by,
|
||||||
|
detail: response_code as i16,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pack `self` back into the 4-byte NMX wire layout. Inverse of
|
||||||
|
/// [`Self::from_packed_u32`]. Useful for round-trip tests and
|
||||||
|
/// future encoder paths.
|
||||||
|
///
|
||||||
|
/// Padding bits (30..28, 19..16) are emitted as zero. Bit 31 mirrors
|
||||||
|
/// `success != 0` — any non-zero `success` round-trips to `-1`
|
||||||
|
/// because the decoder normalizes to `0`/`-1` only.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn to_packed_u32(self) -> u32 {
|
||||||
|
let success_bit: u32 = if self.success != 0 { 0x8000_0000 } else { 0 };
|
||||||
|
let category_bits = ((self.category as i16) as u32 & 0xF) << 24;
|
||||||
|
let detected_by_bits = ((self.detected_by as i16) as u32 & 0xF) << 20;
|
||||||
|
let detail_bits = (self.detail as u16) as u32;
|
||||||
|
success_bit | category_bits | detected_by_bits | detail_bits
|
||||||
|
}
|
||||||
|
|
||||||
/// `(success=-1, Ok, RequestingLmx, detail=0)` — `MxStatus.DataChangeOk`
|
/// `(success=-1, Ok, RequestingLmx, detail=0)` — `MxStatus.DataChangeOk`
|
||||||
/// from `MxStatus.cs:36-40`.
|
/// from `MxStatus.cs:36-40`.
|
||||||
pub const DATA_CHANGE_OK: Self = Self {
|
pub const DATA_CHANGE_OK: Self = Self {
|
||||||
@@ -311,4 +440,199 @@ mod tests {
|
|||||||
assert!(!MxStatus::SUSPEND_PENDING.is_ok());
|
assert!(!MxStatus::SUSPEND_PENDING.is_ok());
|
||||||
assert!(!MxStatus::INVALID_REFERENCE_CONFIGURATION.is_ok());
|
assert!(!MxStatus::INVALID_REFERENCE_CONFIGURATION.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_packed_u32_zero_decodes_to_all_zeros() {
|
||||||
|
// packed=0 → success=0, category=Ok(0), detected_by=RequestingLmx(0), detail=0.
|
||||||
|
// The "all zeros" status is the simplest data-change-pending shape
|
||||||
|
// the wire can carry.
|
||||||
|
let s = MxStatus::from_packed_u32(0);
|
||||||
|
assert_eq!(s.success, 0);
|
||||||
|
assert_eq!(s.category, MxStatusCategory::Ok);
|
||||||
|
assert_eq!(s.detected_by, MxStatusSource::RequestingLmx);
|
||||||
|
assert_eq!(s.detail, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_packed_u32_high_bit_sets_success_to_negative_one() {
|
||||||
|
// Native: `*param_1 = -(ushort)(((uint)param_2 & 0x80000000) != 0)`
|
||||||
|
// For packed=0x80000000, success=-1, all other fields 0.
|
||||||
|
let s = MxStatus::from_packed_u32(0x8000_0000);
|
||||||
|
assert_eq!(s.success, -1);
|
||||||
|
assert_eq!(s.category, MxStatusCategory::Ok);
|
||||||
|
assert_eq!(s.detected_by, MxStatusSource::RequestingLmx);
|
||||||
|
assert_eq!(s.detail, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_packed_u32_decodes_data_change_ok_layout() {
|
||||||
|
// `MxStatus::DATA_CHANGE_OK` = (success=-1, Ok=0, RequestingLmx=0,
|
||||||
|
// detail=0). Pack: bit31=1, bits27..24=0, bits23..20=0, bits15..0=0.
|
||||||
|
// → 0x80000000.
|
||||||
|
let packed = MxStatus::DATA_CHANGE_OK.to_packed_u32();
|
||||||
|
assert_eq!(packed, 0x8000_0000);
|
||||||
|
let round_trip = MxStatus::from_packed_u32(packed);
|
||||||
|
assert_eq!(round_trip, MxStatus::DATA_CHANGE_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_packed_u32_decodes_write_complete_ok_layout() {
|
||||||
|
// `MxStatus::WRITE_COMPLETE_OK` = (success=-1, Ok=0,
|
||||||
|
// RespondingAutomationObject=5, detail=0). Pack: bit31=1,
|
||||||
|
// bits27..24=0 (Ok), bits23..20=5, bits15..0=0.
|
||||||
|
// → 0x80500000.
|
||||||
|
let expected_packed: u32 = 0x80_50_00_00;
|
||||||
|
let s = MxStatus::from_packed_u32(expected_packed);
|
||||||
|
assert_eq!(s, MxStatus::WRITE_COMPLETE_OK);
|
||||||
|
assert_eq!(MxStatus::WRITE_COMPLETE_OK.to_packed_u32(), expected_packed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_packed_u32_extracts_category_from_bits_24_to_27() {
|
||||||
|
// category=4 (ConfigurationError) at bits 24..27.
|
||||||
|
// → 0x04000000.
|
||||||
|
let s = MxStatus::from_packed_u32(0x0400_0000);
|
||||||
|
assert_eq!(s.category, MxStatusCategory::ConfigurationError);
|
||||||
|
assert_eq!(s.detected_by, MxStatusSource::RequestingLmx);
|
||||||
|
assert_eq!(s.detail, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_packed_u32_extracts_detected_by_from_bits_20_to_23() {
|
||||||
|
// detected_by=2 (RequestingNmx) at bits 20..23.
|
||||||
|
// → 0x00200000.
|
||||||
|
let s = MxStatus::from_packed_u32(0x0020_0000);
|
||||||
|
assert_eq!(s.category, MxStatusCategory::Ok);
|
||||||
|
assert_eq!(s.detected_by, MxStatusSource::RequestingNmx);
|
||||||
|
assert_eq!(s.detail, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_packed_u32_extracts_detail_as_signed_low_16_bits() {
|
||||||
|
// detail=21 ("Invalid reference") at bits 0..15.
|
||||||
|
// → 0x00000015.
|
||||||
|
let s = MxStatus::from_packed_u32(0x0000_0015);
|
||||||
|
assert_eq!(s.detail, 21);
|
||||||
|
assert_eq!(s.detail_text(), Some("Invalid reference"));
|
||||||
|
|
||||||
|
// Negative detail — high bit of low-16 set: 0xFFFF → -1.
|
||||||
|
let s = MxStatus::from_packed_u32(0x0000_FFFF);
|
||||||
|
assert_eq!(s.detail, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_packed_u32_padding_bits_are_ignored() {
|
||||||
|
// Bits 30..28 and 19..16 are padding/reserved per `FUN_10100ce0`.
|
||||||
|
// Setting them should not affect any decoded field.
|
||||||
|
// bit 31: success
|
||||||
|
// bits 30..28: padding (0x70_00_00_00)
|
||||||
|
// bits 27..24: category
|
||||||
|
// bits 23..20: detected_by
|
||||||
|
// bits 19..16: padding (0x00_0F_00_00)
|
||||||
|
// bits 15..0: detail
|
||||||
|
// Padding-only mask: 0x70_00_00_00 | 0x00_0F_00_00 = 0x700F_0000.
|
||||||
|
let with_padding = MxStatus::from_packed_u32(0x700F_0000);
|
||||||
|
let without_padding = MxStatus::from_packed_u32(0x0000_0000);
|
||||||
|
assert_eq!(with_padding, without_padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_packed_u32_unknown_category_decodes_to_unknown_variant() {
|
||||||
|
// Category bits = 0xF (not a defined variant).
|
||||||
|
// → 0x0F000000.
|
||||||
|
let s = MxStatus::from_packed_u32(0x0F00_0000);
|
||||||
|
assert_eq!(s.category, MxStatusCategory::Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_packed_u32_unknown_detected_by_decodes_to_unknown_variant() {
|
||||||
|
// detected_by bits = 0xF (not a defined variant).
|
||||||
|
// → 0x00F00000.
|
||||||
|
let s = MxStatus::from_packed_u32(0x00F0_0000);
|
||||||
|
assert_eq!(s.detected_by, MxStatusSource::Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trip_canonical_sentinels() {
|
||||||
|
// Every canonical sentinel must round-trip through pack→decode.
|
||||||
|
for &expected in &[
|
||||||
|
MxStatus::DATA_CHANGE_OK,
|
||||||
|
MxStatus::WRITE_COMPLETE_OK,
|
||||||
|
MxStatus::ACTIVATE_OK,
|
||||||
|
// SuspendPending: detail=0, success=-1, Pending=1, RequestingLmx=0.
|
||||||
|
// → 0x81000000.
|
||||||
|
MxStatus::SUSPEND_PENDING,
|
||||||
|
// InvalidReferenceConfiguration: success=0, ConfigError=4,
|
||||||
|
// RequestingLmx=0, detail=6. → 0x04000006.
|
||||||
|
MxStatus::INVALID_REFERENCE_CONFIGURATION,
|
||||||
|
] {
|
||||||
|
let packed = expected.to_packed_u32();
|
||||||
|
let round_trip = MxStatus::from_packed_u32(packed);
|
||||||
|
assert_eq!(round_trip, expected, "round-trip failed for {expected:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_nmx_response_code_proven_mappings() {
|
||||||
|
// Per `FUN_1010bd10:741-770` switch.
|
||||||
|
// 0x01, 0x02 → CommunicationError + RequestingNmx
|
||||||
|
for code in [0x01_u8, 0x02] {
|
||||||
|
let s = MxStatus::from_nmx_response_code(code).unwrap();
|
||||||
|
assert_eq!(s.success, 0);
|
||||||
|
assert_eq!(s.category, MxStatusCategory::CommunicationError);
|
||||||
|
assert_eq!(s.detected_by, MxStatusSource::RequestingNmx);
|
||||||
|
assert_eq!(s.detail, i16::from(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0x03 → ConfigurationError + RequestingNmx
|
||||||
|
let s = MxStatus::from_nmx_response_code(0x03).unwrap();
|
||||||
|
assert_eq!(s.category, MxStatusCategory::ConfigurationError);
|
||||||
|
assert_eq!(s.detected_by, MxStatusSource::RequestingNmx);
|
||||||
|
assert_eq!(s.detail, 3);
|
||||||
|
|
||||||
|
// 0x04 → ConfigurationError + RespondingNmx
|
||||||
|
let s = MxStatus::from_nmx_response_code(0x04).unwrap();
|
||||||
|
assert_eq!(s.category, MxStatusCategory::ConfigurationError);
|
||||||
|
assert_eq!(s.detected_by, MxStatusSource::RespondingNmx);
|
||||||
|
assert_eq!(s.detail, 4);
|
||||||
|
|
||||||
|
// 0x05 → CommunicationError + RespondingNmx
|
||||||
|
let s = MxStatus::from_nmx_response_code(0x05).unwrap();
|
||||||
|
assert_eq!(s.category, MxStatusCategory::CommunicationError);
|
||||||
|
assert_eq!(s.detected_by, MxStatusSource::RespondingNmx);
|
||||||
|
assert_eq!(s.detail, 5);
|
||||||
|
|
||||||
|
// 0x1A → CommunicationError + RequestingNmx
|
||||||
|
let s = MxStatus::from_nmx_response_code(0x1A).unwrap();
|
||||||
|
assert_eq!(s.category, MxStatusCategory::CommunicationError);
|
||||||
|
assert_eq!(s.detected_by, MxStatusSource::RequestingNmx);
|
||||||
|
assert_eq!(s.detail, 0x1A);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_nmx_response_code_unmapped_returns_none() {
|
||||||
|
// Codes outside the proven {1,2,3,4,5,0x1a} set return None — the
|
||||||
|
// native code falls through `default` and leaves the synthesized
|
||||||
|
// status untouched. Per `design/70-risks-and-open-questions.md`
|
||||||
|
// R3/R4 the consumer must preserve the raw byte verbatim.
|
||||||
|
for code in [0x00_u8, 0x06, 0x10, 0x19, 0x1B, 0x41, 0xEF, 0xFF] {
|
||||||
|
assert!(
|
||||||
|
MxStatus::from_nmx_response_code(code).is_none(),
|
||||||
|
"response code 0x{code:02X} should be unmapped"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_packed_u32_normalizes_arbitrary_success_to_high_bit_only() {
|
||||||
|
// The decoder produces `success ∈ {0, -1}`, so `to_packed_u32`
|
||||||
|
// only checks `success != 0` — the actual integer doesn't
|
||||||
|
// matter beyond zero/non-zero.
|
||||||
|
let mut s = MxStatus::DATA_CHANGE_OK;
|
||||||
|
s.success = 42; // Non-canonical value.
|
||||||
|
let packed = s.to_packed_u32();
|
||||||
|
assert_eq!(packed & 0x8000_0000, 0x8000_0000);
|
||||||
|
// Round-trip normalizes to -1.
|
||||||
|
assert_eq!(MxStatus::from_packed_u32(packed).success, -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// -- Subscribe-flow ----------------------------------------------------
|
// -- Subscribe-flow ----------------------------------------------------
|
||||||
if env.run_subscribe {
|
if env.run_subscribe {
|
||||||
eprintln!("creating subscription [canonical XML CreateSubscription] (max_queue=100, sample=1s)");
|
eprintln!(
|
||||||
|
"creating subscription [canonical XML CreateSubscription] (max_queue=100, sample=1s)"
|
||||||
|
);
|
||||||
// SampleInterval is in **milliseconds** on the wire — the .NET
|
// SampleInterval is in **milliseconds** on the wire — the .NET
|
||||||
// reference's `MxAsbDataClient.CreateSubscription` /
|
// reference's `MxAsbDataClient.CreateSubscription` /
|
||||||
// `AddMonitoredItems` default is `ulong sampleInterval = 1000`
|
// `AddMonitoredItems` default is `ulong sampleInterval = 1000`
|
||||||
@@ -140,7 +142,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// poll would always come back empty.
|
// poll would always come back empty.
|
||||||
let sample_interval_ms: u64 = 1000;
|
let sample_interval_ms: u64 = 1000;
|
||||||
let max_queue_size: i64 = 100;
|
let max_queue_size: i64 = 100;
|
||||||
let sub_response = match client.create_subscription(max_queue_size, sample_interval_ms).await {
|
let sub_response = match client
|
||||||
|
.create_subscription(max_queue_size, sample_interval_ms)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(" create_subscription failed: {e}");
|
eprintln!(" create_subscription failed: {e}");
|
||||||
@@ -165,11 +170,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
)];
|
)];
|
||||||
|
|
||||||
eprintln!("adding monitored items [canonical XML AddMonitoredItems]");
|
eprintln!("adding monitored items [canonical XML AddMonitoredItems]");
|
||||||
let add = match client.add_monitored_items(sub_response.subscription_id, &monitored, true).await {
|
let add = match client
|
||||||
|
.add_monitored_items(sub_response.subscription_id, &monitored, true)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(" add_monitored_items failed: {e}");
|
eprintln!(" add_monitored_items failed: {e}");
|
||||||
let _ = client.delete_subscription(sub_response.subscription_id).await;
|
let _ = client
|
||||||
|
.delete_subscription(sub_response.subscription_id)
|
||||||
|
.await;
|
||||||
eprintln!("disconnecting");
|
eprintln!("disconnecting");
|
||||||
client.disconnect().await?;
|
client.disconnect().await?;
|
||||||
client.send_end().await?;
|
client.send_end().await?;
|
||||||
@@ -184,17 +194,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
add.status.first().map(|s| s.error_code).unwrap_or(0),
|
add.status.first().map(|s| s.error_code).unwrap_or(0),
|
||||||
);
|
);
|
||||||
|
|
||||||
eprintln!("publishing [canonical XML Publish] (target {} polls × 5s)", env.subscribe_count);
|
eprintln!(
|
||||||
|
"publishing [canonical XML Publish] (target {} polls × 5s)",
|
||||||
|
env.subscribe_count
|
||||||
|
);
|
||||||
let mut total_values = 0usize;
|
let mut total_values = 0usize;
|
||||||
for poll in 0..env.subscribe_count {
|
for poll in 0..env.subscribe_count {
|
||||||
match tokio::time::timeout(
|
match tokio::time::timeout(
|
||||||
Duration::from_secs(5),
|
Duration::from_secs(5),
|
||||||
client.publish(sub_response.subscription_id),
|
client.publish(sub_response.subscription_id),
|
||||||
).await {
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(Ok(resp)) => {
|
Ok(Ok(resp)) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
" poll {poll}: {} value(s); result_code={:?} success={:?}",
|
" poll {poll}: {} value(s); result_code={:?} success={:?}",
|
||||||
resp.values.len(), resp.result_code, resp.success
|
resp.values.len(),
|
||||||
|
resp.result_code,
|
||||||
|
resp.success
|
||||||
);
|
);
|
||||||
for v in &resp.values {
|
for v in &resp.values {
|
||||||
total_values += 1;
|
total_values += 1;
|
||||||
@@ -204,9 +221,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
v.value.value
|
v.value.value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if resp.result_code
|
if resp.result_code == Some(mxaccess_asb::RESULT_CODE_INVALID_CONNECTION_ID) {
|
||||||
== Some(mxaccess_asb::RESULT_CODE_INVALID_CONNECTION_ID)
|
|
||||||
{
|
|
||||||
eprintln!(" publish surfaced InvalidConnectionId; bailing the loop");
|
eprintln!(" publish surfaced InvalidConnectionId; bailing the loop");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -235,12 +250,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -- DeleteMonitoredItems / DeleteSubscription
|
// -- DeleteMonitoredItems / DeleteSubscription
|
||||||
if let Err(e) = client.delete_monitored_items(sub_response.subscription_id, &monitored).await {
|
if let Err(e) = client
|
||||||
|
.delete_monitored_items(sub_response.subscription_id, &monitored)
|
||||||
|
.await
|
||||||
|
{
|
||||||
eprintln!("delete_monitored_items failed: {e}");
|
eprintln!("delete_monitored_items failed: {e}");
|
||||||
} else {
|
} else {
|
||||||
eprintln!("delete_monitored_items ok [canonical XML DeleteMonitoredItems]");
|
eprintln!("delete_monitored_items ok [canonical XML DeleteMonitoredItems]");
|
||||||
}
|
}
|
||||||
if let Err(e) = client.delete_subscription(sub_response.subscription_id).await {
|
if let Err(e) = client
|
||||||
|
.delete_subscription(sub_response.subscription_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
eprintln!("delete_subscription failed: {e}");
|
eprintln!("delete_subscription failed: {e}");
|
||||||
} else {
|
} else {
|
||||||
eprintln!("delete_subscription ok [canonical XML DeleteSubscription]");
|
eprintln!("delete_subscription ok [canonical XML DeleteSubscription]");
|
||||||
@@ -290,8 +311,12 @@ impl LiveEnv {
|
|||||||
let via_uri =
|
let via_uri =
|
||||||
std::env::var("MX_ASB_VIA").unwrap_or_else(|_| format!("net.tcp://{host}/ASBService"));
|
std::env::var("MX_ASB_VIA").unwrap_or_else(|_| format!("net.tcp://{host}/ASBService"));
|
||||||
let tag = std::env::var("MX_TEST_TAG").unwrap_or_else(|_| "TestChildObject.TestInt".into());
|
let tag = std::env::var("MX_TEST_TAG").unwrap_or_else(|_| "TestChildObject.TestInt".into());
|
||||||
let run_write = std::env::var("MX_RUN_WRITE").map(|v| v != "0").unwrap_or(true);
|
let run_write = std::env::var("MX_RUN_WRITE")
|
||||||
let run_subscribe = std::env::var("MX_RUN_SUBSCRIBE").map(|v| v != "0").unwrap_or(true);
|
.map(|v| v != "0")
|
||||||
|
.unwrap_or(true);
|
||||||
|
let run_subscribe = std::env::var("MX_RUN_SUBSCRIBE")
|
||||||
|
.map(|v| v != "0")
|
||||||
|
.unwrap_or(true);
|
||||||
let subscribe_count = std::env::var("MX_SUBSCRIBE_COUNT")
|
let subscribe_count = std::env::var("MX_SUBSCRIBE_COUNT")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| s.parse().ok())
|
.and_then(|s| s.parse().ok())
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ use mxaccess_asb::{
|
|||||||
};
|
};
|
||||||
use mxaccess_asb_nettcp::auth::CryptoParameters;
|
use mxaccess_asb_nettcp::auth::CryptoParameters;
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{Mutex, mpsc};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
|
|
||||||
@@ -410,17 +410,13 @@ async fn publish_loop<F, Fut>(
|
|||||||
// on every Publish poll while values are still
|
// on every Publish poll while values are still
|
||||||
// delivered, so blanket "bail on any non-zero"
|
// delivered, so blanket "bail on any non-zero"
|
||||||
// (the original F33 fix) was too aggressive.
|
// (the original F33 fix) was too aggressive.
|
||||||
if response.result_code
|
if response.result_code == Some(mxaccess_asb::RESULT_CODE_INVALID_CONNECTION_ID) {
|
||||||
== Some(mxaccess_asb::RESULT_CODE_INVALID_CONNECTION_ID)
|
|
||||||
{
|
|
||||||
let _ = tx
|
let _ = tx
|
||||||
.send(Err(Error::Connection(
|
.send(Err(Error::Connection(ConnectionError::TransportFailure {
|
||||||
ConnectionError::TransportFailure {
|
detail: "publish returned InvalidConnectionId — \
|
||||||
detail: "publish returned InvalidConnectionId — \
|
|
||||||
session desynced, terminating stream"
|
session desynced, terminating stream"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
},
|
})))
|
||||||
)))
|
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -609,7 +605,13 @@ mod tests {
|
|||||||
let calls_clone = calls.clone();
|
let calls_clone = calls.clone();
|
||||||
let publish_fn = move || {
|
let publish_fn = move || {
|
||||||
calls_clone.fetch_add(1, Ordering::Relaxed);
|
calls_clone.fetch_add(1, Ordering::Relaxed);
|
||||||
async move { Ok(fake_response(vec![fake_value(7), fake_value(8), fake_value(9)])) }
|
async move {
|
||||||
|
Ok(fake_response(vec![
|
||||||
|
fake_value(7),
|
||||||
|
fake_value(8),
|
||||||
|
fake_value(9),
|
||||||
|
]))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Drop the receiver immediately — first send triggers exit.
|
// Drop the receiver immediately — first send triggers exit.
|
||||||
drop(rx);
|
drop(rx);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ pub use transport_asb::AsbTransport;
|
|||||||
|
|
||||||
pub use mxaccess_galaxy::{GalaxyTagMetadata, Resolver, ResolverError};
|
pub use mxaccess_galaxy::{GalaxyTagMetadata, Resolver, ResolverError};
|
||||||
pub use mxaccess_nmx::WriteValue;
|
pub use mxaccess_nmx::WriteValue;
|
||||||
pub use session::{RebuildFactory, Subscription};
|
pub use session::{OperationContext, OperationKind, OperationStatus, RebuildFactory, Subscription};
|
||||||
|
|
||||||
/// Async session façade. Cheap clones share the inner state; drop of the last
|
/// Async session façade. Cheap clones share the inner state; drop of the last
|
||||||
/// clone fires `UnregisterEngine` best-effort. For deterministic shutdown,
|
/// clone fires `UnregisterEngine` best-effort. For deterministic shutdown,
|
||||||
@@ -391,7 +391,9 @@ pub enum ConfigError {
|
|||||||
/// `Session::recover_connection` was called without a
|
/// `Session::recover_connection` was called without a
|
||||||
/// [`crate::RebuildFactory`] installed via
|
/// [`crate::RebuildFactory`] installed via
|
||||||
/// [`crate::Session::set_recovery_factory`]. F16.
|
/// [`crate::Session::set_recovery_factory`]. F16.
|
||||||
#[error("recover_connection: no rebuild factory installed (call Session::set_recovery_factory)")]
|
#[error(
|
||||||
|
"recover_connection: no rebuild factory installed (call Session::set_recovery_factory)"
|
||||||
|
)]
|
||||||
RecoveryNotConfigured,
|
RecoveryNotConfigured,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ use std::time::SystemTime;
|
|||||||
|
|
||||||
use mxaccess_callback::{CallbackEvent, CallbackExporter, ExporterIdentities};
|
use mxaccess_callback::{CallbackEvent, CallbackExporter, ExporterIdentities};
|
||||||
use mxaccess_codec::{
|
use mxaccess_codec::{
|
||||||
MxStatus, NmxReferenceRegistrationMessage, NmxSubscriptionMessage, NmxSubscriptionRecord,
|
MxStatus, NmxOperationStatusMessage, NmxReferenceRegistrationMessage, NmxSubscriptionMessage,
|
||||||
|
NmxSubscriptionRecord,
|
||||||
};
|
};
|
||||||
use mxaccess_galaxy::{GalaxyTagMetadata, Resolver, ResolverError};
|
use mxaccess_galaxy::{GalaxyTagMetadata, Resolver, ResolverError};
|
||||||
use mxaccess_nmx::{NmxClient, NmxClientError, WriteValue};
|
use mxaccess_nmx::{NmxClient, NmxClientError, WriteValue};
|
||||||
@@ -51,7 +52,7 @@ use tokio_stream::wrappers::BroadcastStream;
|
|||||||
|
|
||||||
use crate::metrics as session_metrics;
|
use crate::metrics as session_metrics;
|
||||||
use crate::{DataChange, RecoveryEvent};
|
use crate::{DataChange, RecoveryEvent};
|
||||||
use futures_util::Stream;
|
use futures_util::{Stream, StreamExt};
|
||||||
|
|
||||||
/// Capacity of the broadcast channel that fans out
|
/// Capacity of the broadcast channel that fans out
|
||||||
/// [`RecoveryEvent`]s to consumers via [`Session::recovery_events`].
|
/// [`RecoveryEvent`]s to consumers via [`Session::recovery_events`].
|
||||||
@@ -77,6 +78,124 @@ use crate::{
|
|||||||
/// either keep up or accept lag-loss.
|
/// either keep up or accept lag-loss.
|
||||||
const CALLBACK_BROADCAST_CAPACITY: usize = 256;
|
const CALLBACK_BROADCAST_CAPACITY: usize = 256;
|
||||||
|
|
||||||
|
/// Capacity of the broadcast channel that fans out parsed
|
||||||
|
/// [`OperationStatus`] events to consumers via
|
||||||
|
/// [`Session::operation_status_events`].
|
||||||
|
///
|
||||||
|
/// Operation-status frames are bursty (one per write completion / one
|
||||||
|
/// per subscription state change) but lower-volume than data updates.
|
||||||
|
/// Picked to absorb a short burst without dropping for a briefly slow
|
||||||
|
/// consumer.
|
||||||
|
const OPERATION_STATUS_BROADCAST_CAPACITY: usize = 64;
|
||||||
|
|
||||||
|
/// Operation kind associated with an outstanding RPC. Mirrors the
|
||||||
|
/// distinct request paths the .NET reference tracks across
|
||||||
|
/// `MxNativeSession.{WriteAsync, WriteSecuredAsync, ReadAsync,
|
||||||
|
/// SubscribeAsync, UnsubscribeAsync, ActivateAsync, SuspendAsync}`.
|
||||||
|
///
|
||||||
|
/// The Rust port uses this to enrich [`OperationStatus`] events with
|
||||||
|
/// the originating call's intent — the synthesizer kernel
|
||||||
|
/// ([`MxStatus::from_packed_u32`]) is byte-deterministic and does NOT
|
||||||
|
/// depend on `OperationKind`, but consumers often want to filter
|
||||||
|
/// "write completions" from "subscription state changes" without
|
||||||
|
/// peeking at the raw frame bytes.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum OperationKind {
|
||||||
|
/// Plain `Write` (`MxNativeSession.WriteAsync`).
|
||||||
|
Write,
|
||||||
|
/// `WriteSecured` / `WriteSecured2` (two-token writes; see R6).
|
||||||
|
WriteSecured,
|
||||||
|
/// `Read` (read-as-subscribe pattern at `cs:312-359`).
|
||||||
|
Read,
|
||||||
|
/// `Subscribe` / `AdviseSupervisory` / `RegisterReference`.
|
||||||
|
Subscribe,
|
||||||
|
/// `Unsubscribe` / `UnAdvise`.
|
||||||
|
Unsubscribe,
|
||||||
|
/// `Activate` (re-enable a suspended subscription).
|
||||||
|
Activate,
|
||||||
|
/// `Suspend` (pause an active subscription).
|
||||||
|
Suspend,
|
||||||
|
/// Operation kind unknown to the Rust port — surfaced as a
|
||||||
|
/// fallback when the originating call doesn't fit a typed variant
|
||||||
|
/// (e.g. raw transport-level operations).
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-operation context tracked for outstanding RPCs.
|
||||||
|
///
|
||||||
|
/// The Rust port currently uses this struct only to enrich
|
||||||
|
/// [`OperationStatus`] events surfaced via
|
||||||
|
/// [`Session::operation_status_events`]. Future work
|
||||||
|
/// (`design/70-risks-and-open-questions.md` R3/R4 Path A follow-on)
|
||||||
|
/// will let the consumer correlate completion frames back to specific
|
||||||
|
/// outstanding write/subscribe calls; the current bring-up always
|
||||||
|
/// emits `OperationStatus.context = None` because the operation→
|
||||||
|
/// completion correlation channel is not yet wired.
|
||||||
|
///
|
||||||
|
/// Mirrors the bookkeeping `MxNativeSession` does in its private
|
||||||
|
/// `_pendingWrites` / `_pendingReads` dictionaries (referenced
|
||||||
|
/// in the source but not exposed publicly).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct OperationContext {
|
||||||
|
/// 16-byte correlation id the originating call generated. For
|
||||||
|
/// subscribe/unsubscribe this matches `Subscription::correlation_id`;
|
||||||
|
/// for write/read this is the request's `correlationId` field.
|
||||||
|
pub correlation_id: [u8; 16],
|
||||||
|
/// Intent of the originating operation — see [`OperationKind`].
|
||||||
|
pub op_kind: OperationKind,
|
||||||
|
/// Reference string (`Object.Attribute`) the operation targets,
|
||||||
|
/// when known. `None` for operations that don't carry one (e.g.
|
||||||
|
/// session-level ops).
|
||||||
|
pub reference: Option<Arc<str>>,
|
||||||
|
/// Retry counter — incremented each time the consumer re-issues
|
||||||
|
/// the operation (e.g. via `Session::recover_connection`'s
|
||||||
|
/// re-advise loop). Always `0` on the first attempt.
|
||||||
|
pub retry_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One operation-status event surfaced to consumers via
|
||||||
|
/// [`Session::operation_status_events`].
|
||||||
|
///
|
||||||
|
/// Mirrors `MxNativeOperationStatusEvent` (`MxNativeSession.cs:73-78`)
|
||||||
|
/// with the addition of typed [`MxStatus`] promotion (the
|
||||||
|
/// `Lmx.dll!FUN_10100ce0` synthesizer kernel — see
|
||||||
|
/// `design/70-risks-and-open-questions.md` R3/R4 Path A).
|
||||||
|
///
|
||||||
|
/// - [`Self::raw`] preserves the parsed frame byte-for-byte (matching
|
||||||
|
/// the .NET `Message` field).
|
||||||
|
/// - [`Self::status`] is the typed `MxStatus`. For 5-byte status-word
|
||||||
|
/// frames this is the canonical sentinel
|
||||||
|
/// ([`MxStatus::WRITE_COMPLETE_OK`] for the proven `(0x8050, 0x00)`
|
||||||
|
/// shape) or the verbatim-preserve placeholder for unknown shapes.
|
||||||
|
/// For 1-byte completion frames this is the verbatim-preserve
|
||||||
|
/// placeholder per R3/R4. **Callers holding a 4-byte packed status
|
||||||
|
/// word from a different layer should call
|
||||||
|
/// [`MxStatus::from_packed_u32`] directly.**
|
||||||
|
/// - [`Self::context`] carries the originating
|
||||||
|
/// [`OperationContext`] when the event can be correlated back to a
|
||||||
|
/// tracked outstanding operation. The current implementation
|
||||||
|
/// always emits `None` — operation-tracking plumbing lands as a
|
||||||
|
/// follow-up (see the module-level docs).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct OperationStatus {
|
||||||
|
/// Raw parsed frame, byte-for-byte preserved.
|
||||||
|
pub raw: NmxOperationStatusMessage,
|
||||||
|
/// Typed status (synthesizer-promoted for known shapes; verbatim
|
||||||
|
/// for unknown).
|
||||||
|
pub status: MxStatus,
|
||||||
|
/// Optional originating-call context. Always `None` until the
|
||||||
|
/// operation-tracking plumbing is wired (see module-level docs).
|
||||||
|
pub context: Option<OperationContext>,
|
||||||
|
/// `true` when the frame arrived during an active
|
||||||
|
/// `Session::recover_connection` window. Mirrors
|
||||||
|
/// `MxNativeOperationStatusEvent.IsDuringRecovery`
|
||||||
|
/// (`MxNativeSession.cs:78`).
|
||||||
|
pub is_during_recovery: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Subscription handle returned by [`Session::subscribe`]. Implements
|
/// Subscription handle returned by [`Session::subscribe`]. Implements
|
||||||
/// `Stream<Item = Result<DataChange, Error>>` — driving it forward
|
/// `Stream<Item = Result<DataChange, Error>>` — driving it forward
|
||||||
/// yields one [`DataChange`] per matching record observed on the
|
/// yields one [`DataChange`] per matching record observed on the
|
||||||
@@ -336,6 +455,19 @@ pub struct SessionInner {
|
|||||||
/// Broadcast channel that fans out parsed callback messages. Tap
|
/// Broadcast channel that fans out parsed callback messages. Tap
|
||||||
/// via [`Session::callbacks`].
|
/// via [`Session::callbacks`].
|
||||||
pub(crate) callback_tx: broadcast::Sender<Arc<NmxSubscriptionMessage>>,
|
pub(crate) callback_tx: broadcast::Sender<Arc<NmxSubscriptionMessage>>,
|
||||||
|
/// Broadcast channel that fans out parsed operation-status events.
|
||||||
|
/// Tap via [`Session::operation_status_events`].
|
||||||
|
pub(crate) operation_status_tx: broadcast::Sender<Arc<OperationStatus>>,
|
||||||
|
/// Atomic counter incremented by `recover_connection` while a
|
||||||
|
/// recovery attempt is in flight. The router reads this when
|
||||||
|
/// constructing `OperationStatus` events to populate
|
||||||
|
/// `is_during_recovery`. Mirrors `MxNativeSession._recoveryActive`
|
||||||
|
/// (`MxNativeSession.cs:573` — `Volatile.Read(ref _recoveryActive)`).
|
||||||
|
///
|
||||||
|
/// Wrapped in `Arc` so the router task (spawned at session
|
||||||
|
/// bring-up) can observe flips from `recover_connection` without
|
||||||
|
/// holding a strong reference to the entire `SessionInner`.
|
||||||
|
pub(crate) recovery_active: Arc<std::sync::atomic::AtomicU32>,
|
||||||
/// Handle to the router task that drains the
|
/// Handle to the router task that drains the
|
||||||
/// [`CallbackExporter`]'s `CallbackEvent` channel and pushes parsed
|
/// [`CallbackExporter`]'s `CallbackEvent` channel and pushes parsed
|
||||||
/// `NmxSubscriptionMessage`s onto `callback_tx`. `None` after
|
/// `NmxSubscriptionMessage`s onto `callback_tx`. `None` after
|
||||||
@@ -448,9 +580,8 @@ pub(crate) enum SubscriptionMode {
|
|||||||
pub type RebuildFactory = Arc<
|
pub type RebuildFactory = Arc<
|
||||||
dyn Fn() -> std::pin::Pin<
|
dyn Fn() -> std::pin::Pin<
|
||||||
Box<
|
Box<
|
||||||
dyn std::future::Future<
|
dyn std::future::Future<Output = Result<NmxClient, mxaccess_nmx::NmxClientError>>
|
||||||
Output = Result<NmxClient, mxaccess_nmx::NmxClientError>,
|
+ Send,
|
||||||
> + Send,
|
|
||||||
>,
|
>,
|
||||||
> + Send
|
> + Send
|
||||||
+ Sync,
|
+ Sync,
|
||||||
@@ -472,8 +603,9 @@ impl std::fmt::Debug for SessionInner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drain `CallbackExporter` events, decode `CallbackInvoked` bodies as
|
/// Drain `CallbackExporter` events, decode `CallbackInvoked` bodies,
|
||||||
/// `NmxSubscriptionMessage`, and broadcast each parsed message.
|
/// and broadcast typed messages onto `callback_tx` (subscription
|
||||||
|
/// callbacks) or `operation_status_tx` (operation-status frames).
|
||||||
///
|
///
|
||||||
/// Exits when the upstream `CallbackEvent` channel closes (which
|
/// Exits when the upstream `CallbackEvent` channel closes (which
|
||||||
/// happens when the `CallbackExporter` is dropped or
|
/// happens when the `CallbackExporter` is dropped or
|
||||||
@@ -482,17 +614,46 @@ impl std::fmt::Debug for SessionInner {
|
|||||||
/// need them can subscribe to the raw `CallbackExporter` events
|
/// need them can subscribe to the raw `CallbackExporter` events
|
||||||
/// directly via a future "diagnostic-channel" hook (no followup yet
|
/// directly via a future "diagnostic-channel" hook (no followup yet
|
||||||
/// — surface only when a real consumer asks).
|
/// — surface only when a real consumer asks).
|
||||||
|
///
|
||||||
|
/// Dispatch order mirrors
|
||||||
|
/// `MxNativeSession.OnCallbackReceived` (`cs:571-607`):
|
||||||
|
/// operation-status first (the simplest 1- or 5-byte payload), then
|
||||||
|
/// fall through to subscription messages. The `is_during_recovery`
|
||||||
|
/// flag on each emitted [`OperationStatus`] is taken from the live
|
||||||
|
/// `recovery_active` counter so the receiver matches the .NET
|
||||||
|
/// reference's volatile-read semantics at `cs:573`.
|
||||||
pub(crate) async fn callback_router(
|
pub(crate) async fn callback_router(
|
||||||
mut events: tokio::sync::mpsc::UnboundedReceiver<CallbackEvent>,
|
mut events: tokio::sync::mpsc::UnboundedReceiver<CallbackEvent>,
|
||||||
callback_tx: broadcast::Sender<Arc<NmxSubscriptionMessage>>,
|
callback_tx: broadcast::Sender<Arc<NmxSubscriptionMessage>>,
|
||||||
|
operation_status_tx: broadcast::Sender<Arc<OperationStatus>>,
|
||||||
|
recovery_active: Arc<std::sync::atomic::AtomicU32>,
|
||||||
) {
|
) {
|
||||||
while let Some(event) = events.recv().await {
|
while let Some(event) = events.recv().await {
|
||||||
if let CallbackEvent::CallbackInvoked { body, .. } = event {
|
if let CallbackEvent::CallbackInvoked { body, .. } = event {
|
||||||
// The body is the inner NMX subscription message — same
|
// 1. Try operation-status first — peels the outer envelope
|
||||||
// 23-byte preamble + records as `NmxSubscriptionMessage::parse_inner`
|
// and parses a 1- or 5-byte completion frame. Mirrors
|
||||||
// expects. Parse failures are silent (no consumer) since the
|
// `MxNativeSession.OnCallbackReceived:574`.
|
||||||
// .NET reference also fires `UnparsedCallbackReceived` events
|
if let Ok(op) = NmxOperationStatusMessage::try_parse_process_data_received_body(&body) {
|
||||||
// separately and we don't model that yet.
|
let is_during_recovery =
|
||||||
|
recovery_active.load(std::sync::atomic::Ordering::Acquire) > 0;
|
||||||
|
let typed = op.promote_to_typed();
|
||||||
|
let _ = operation_status_tx.send(Arc::new(OperationStatus {
|
||||||
|
raw: op,
|
||||||
|
status: typed,
|
||||||
|
// Operation-tracking plumbing not yet wired —
|
||||||
|
// always emit context=None for now (R3/R4
|
||||||
|
// follow-on tracks adding the correlation channel).
|
||||||
|
context: None,
|
||||||
|
is_during_recovery,
|
||||||
|
}));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fall through to subscription messages — same 23-byte
|
||||||
|
// preamble + records as `NmxSubscriptionMessage::parse_inner`
|
||||||
|
// expects. Parse failures are silent (no consumer) since
|
||||||
|
// the .NET reference also fires `UnparsedCallbackReceived`
|
||||||
|
// events separately and we don't model that yet.
|
||||||
if let Ok(msg) = NmxSubscriptionMessage::parse_inner(&body) {
|
if let Ok(msg) = NmxSubscriptionMessage::parse_inner(&body) {
|
||||||
// `send` returns `Err(SendError)` only when there are zero
|
// `send` returns `Err(SendError)` only when there are zero
|
||||||
// receivers — that's fine for this wire path; nothing to do.
|
// receivers — that's fine for this wire path; nothing to do.
|
||||||
@@ -618,7 +779,15 @@ impl Session {
|
|||||||
// 2. Spawn the router task that broadcasts parsed callback
|
// 2. Spawn the router task that broadcasts parsed callback
|
||||||
// messages.
|
// messages.
|
||||||
let (callback_tx, _) = broadcast::channel(CALLBACK_BROADCAST_CAPACITY);
|
let (callback_tx, _) = broadcast::channel(CALLBACK_BROADCAST_CAPACITY);
|
||||||
let router_handle = tokio::spawn(callback_router(callback_events, callback_tx.clone()));
|
let (operation_status_tx, _) =
|
||||||
|
broadcast::channel::<Arc<OperationStatus>>(OPERATION_STATUS_BROADCAST_CAPACITY);
|
||||||
|
let recovery_active = Arc::new(std::sync::atomic::AtomicU32::new(0));
|
||||||
|
let router_handle = tokio::spawn(callback_router(
|
||||||
|
callback_events,
|
||||||
|
callback_tx.clone(),
|
||||||
|
operation_status_tx.clone(),
|
||||||
|
recovery_active.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
// 3. RegisterEngine2 with the callback OBJREF. Mirrors cs:163-175.
|
// 3. RegisterEngine2 with the callback OBJREF. Mirrors cs:163-175.
|
||||||
let hr = nmx
|
let hr = nmx
|
||||||
@@ -662,6 +831,8 @@ impl Session {
|
|||||||
nmx: Mutex::new(nmx),
|
nmx: Mutex::new(nmx),
|
||||||
callback_exporter: Mutex::new(Some(exporter)),
|
callback_exporter: Mutex::new(Some(exporter)),
|
||||||
callback_tx,
|
callback_tx,
|
||||||
|
operation_status_tx,
|
||||||
|
recovery_active,
|
||||||
router_handle: std::sync::Mutex::new(Some(router_handle)),
|
router_handle: std::sync::Mutex::new(Some(router_handle)),
|
||||||
recovery_tx,
|
recovery_tx,
|
||||||
connected: std::sync::atomic::AtomicBool::new(true),
|
connected: std::sync::atomic::AtomicBool::new(true),
|
||||||
@@ -689,6 +860,59 @@ impl Session {
|
|||||||
self.inner.recovery_tx.subscribe()
|
self.inner.recovery_tx.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Subscribe to operation-status events.
|
||||||
|
///
|
||||||
|
/// Returns a [`broadcast::Receiver`] that yields one
|
||||||
|
/// [`OperationStatus`] per parsed completion frame. Mirrors
|
||||||
|
/// `MxNativeSession.OperationStatusReceived`
|
||||||
|
/// (`MxNativeSession.cs:118`) but exposes the typed
|
||||||
|
/// [`MxStatus`] (the synthesizer kernel
|
||||||
|
/// [`MxStatus::from_packed_u32`] is applied where the bit layout
|
||||||
|
/// matches; verbatim-preserve placeholders are returned for the
|
||||||
|
/// 1-byte completion frames per
|
||||||
|
/// `design/70-risks-and-open-questions.md` R3/R4).
|
||||||
|
///
|
||||||
|
/// Slow consumers see `RecvError::Lagged(n)` from the underlying
|
||||||
|
/// broadcast — the wire protocol does not replay missed
|
||||||
|
/// operation-status frames so consumers must keep up or accept
|
||||||
|
/// lag-loss.
|
||||||
|
///
|
||||||
|
/// The first emitted event will have
|
||||||
|
/// [`OperationStatus::context`] == `None` for now —
|
||||||
|
/// operation-tracking plumbing (correlating completion frames
|
||||||
|
/// back to outstanding writes/subscribes) is the next step in the
|
||||||
|
/// R3/R4 follow-on work. The synthesizer kernel itself is in place
|
||||||
|
/// today.
|
||||||
|
#[must_use]
|
||||||
|
pub fn operation_status_events(&self) -> broadcast::Receiver<Arc<OperationStatus>> {
|
||||||
|
self.inner.operation_status_tx.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stream variant of [`Self::operation_status_events`]: yields
|
||||||
|
/// `Result<Arc<OperationStatus>, Error>` per item, mapping
|
||||||
|
/// broadcast lag to a typed error.
|
||||||
|
///
|
||||||
|
/// Mirrors the `Stream`-based access pattern already provided by
|
||||||
|
/// the `Subscription::Stream` impl. Use the raw
|
||||||
|
/// [`broadcast::Receiver`] returned by
|
||||||
|
/// [`Self::operation_status_events`] when control over backpressure
|
||||||
|
/// or lag-handling matters.
|
||||||
|
pub fn operation_status_stream(
|
||||||
|
&self,
|
||||||
|
) -> impl Stream<Item = Result<Arc<OperationStatus>, Error>> + Send {
|
||||||
|
let rx = self.inner.operation_status_tx.subscribe();
|
||||||
|
BroadcastStream::new(rx).map(|item| match item {
|
||||||
|
Ok(ev) => Ok(ev),
|
||||||
|
Err(tokio_stream::wrappers::errors::BroadcastStreamRecvError::Lagged(n)) => {
|
||||||
|
Err(Error::Configuration(ConfigError::InvalidArgument {
|
||||||
|
detail: format!(
|
||||||
|
"operation-status stream lagged behind broadcast by {n} events"
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Install the [`RebuildFactory`] used by [`Self::recover_connection`]
|
/// Install the [`RebuildFactory`] used by [`Self::recover_connection`]
|
||||||
/// to build a fresh [`NmxClient`] on each retry attempt. Without
|
/// to build a fresh [`NmxClient`] on each retry attempt. Without
|
||||||
/// a factory, `recover_connection` returns
|
/// a factory, `recover_connection` returns
|
||||||
@@ -744,9 +968,26 @@ impl Session {
|
|||||||
// recovery body can take the nmx mutex without deadlocking.
|
// recovery body can take the nmx mutex without deadlocking.
|
||||||
let factory = {
|
let factory = {
|
||||||
let lock = self.inner.rebuild_factory.lock().await;
|
let lock = self.inner.rebuild_factory.lock().await;
|
||||||
lock.clone().ok_or(Error::Configuration(
|
lock.clone()
|
||||||
ConfigError::RecoveryNotConfigured,
|
.ok_or(Error::Configuration(ConfigError::RecoveryNotConfigured))?
|
||||||
))?
|
};
|
||||||
|
|
||||||
|
// Mark the session as in-recovery so the callback router
|
||||||
|
// stamps `OperationStatus.is_during_recovery = true` for any
|
||||||
|
// events that arrive during the attempt. Mirrors
|
||||||
|
// `MxNativeSession._recoveryActive` (`cs:573` — volatile
|
||||||
|
// increment around `RecoverConnectionCore`).
|
||||||
|
struct RecoveryGuard(Arc<std::sync::atomic::AtomicU32>);
|
||||||
|
impl Drop for RecoveryGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.fetch_sub(1, std::sync::atomic::Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _recovery_guard = {
|
||||||
|
self.inner
|
||||||
|
.recovery_active
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
|
||||||
|
RecoveryGuard(self.inner.recovery_active.clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut last_error: Option<Error> = None;
|
let mut last_error: Option<Error> = None;
|
||||||
@@ -772,10 +1013,9 @@ impl Session {
|
|||||||
// `Error` doesn't impl `Clone` (the io::Error source isn't
|
// `Error` doesn't impl `Clone` (the io::Error source isn't
|
||||||
// cloneable), so capture a string copy for the bubbled-up
|
// cloneable), so capture a string copy for the bubbled-up
|
||||||
// last_error and hand the original to the broadcast event.
|
// last_error and hand the original to the broadcast event.
|
||||||
let bubbled =
|
let bubbled = Error::Connection(ConnectionError::TransportFailure {
|
||||||
Error::Connection(ConnectionError::TransportFailure {
|
detail: e.to_string(),
|
||||||
detail: e.to_string(),
|
});
|
||||||
});
|
|
||||||
let _ = self.inner.recovery_tx.send(Arc::new(RecoveryEvent::Failed {
|
let _ = self.inner.recovery_tx.send(Arc::new(RecoveryEvent::Failed {
|
||||||
attempt,
|
attempt,
|
||||||
error: e,
|
error: e,
|
||||||
@@ -789,9 +1029,7 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(last_error.unwrap_or(Error::Connection(
|
Err(last_error.unwrap_or(Error::Connection(ConnectionError::EngineNotRegistered)))
|
||||||
ConnectionError::EngineNotRegistered,
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Single-attempt body of [`Self::recover_connection`], split out so
|
/// Single-attempt body of [`Self::recover_connection`], split out so
|
||||||
@@ -907,9 +1145,7 @@ impl Session {
|
|||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
Error::Configuration(ConfigError::InvalidArgument {
|
Error::Configuration(ConfigError::InvalidArgument {
|
||||||
detail: format!(
|
detail: format!("recovery: buffered item definition: {e}"),
|
||||||
"recovery: buffered item definition: {e}"
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
let registration = NmxReferenceRegistrationMessage {
|
let registration = NmxReferenceRegistrationMessage {
|
||||||
@@ -1308,13 +1544,14 @@ impl Session {
|
|||||||
// reference's split-context form is reachable via the
|
// reference's split-context form is reachable via the
|
||||||
// compat-server layer F35 once it lands). The codec helper
|
// compat-server layer F35 once it lands). The codec helper
|
||||||
// rejects empty/whitespace inputs with `CodecError::InvalidName`.
|
// rejects empty/whitespace inputs with `CodecError::InvalidName`.
|
||||||
let item_definition =
|
let item_definition = NmxReferenceRegistrationMessage::to_buffered_item_definition(
|
||||||
NmxReferenceRegistrationMessage::to_buffered_item_definition(reference)
|
reference,
|
||||||
.map_err(|e| {
|
)
|
||||||
Error::Configuration(ConfigError::InvalidArgument {
|
.map_err(|e| {
|
||||||
detail: format!("buffered item definition: {e}"),
|
Error::Configuration(ConfigError::InvalidArgument {
|
||||||
})
|
detail: format!("buffered item definition: {e}"),
|
||||||
})?;
|
})
|
||||||
|
})?;
|
||||||
let registration = NmxReferenceRegistrationMessage {
|
let registration = NmxReferenceRegistrationMessage {
|
||||||
item_handle: 0,
|
item_handle: 0,
|
||||||
item_correlation_id: correlation_id,
|
item_correlation_id: correlation_id,
|
||||||
@@ -1876,7 +2113,15 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let (callback_tx, _) = broadcast::channel(CALLBACK_BROADCAST_CAPACITY);
|
let (callback_tx, _) = broadcast::channel(CALLBACK_BROADCAST_CAPACITY);
|
||||||
let router_handle = tokio::spawn(callback_router(callback_events, callback_tx.clone()));
|
let (operation_status_tx, _) =
|
||||||
|
broadcast::channel::<Arc<OperationStatus>>(OPERATION_STATUS_BROADCAST_CAPACITY);
|
||||||
|
let recovery_active = Arc::new(std::sync::atomic::AtomicU32::new(0));
|
||||||
|
let router_handle = tokio::spawn(callback_router(
|
||||||
|
callback_events,
|
||||||
|
callback_tx.clone(),
|
||||||
|
operation_status_tx.clone(),
|
||||||
|
recovery_active.clone(),
|
||||||
|
));
|
||||||
let (recovery_tx, _) = broadcast::channel(RECOVERY_BROADCAST_CAPACITY);
|
let (recovery_tx, _) = broadcast::channel(RECOVERY_BROADCAST_CAPACITY);
|
||||||
|
|
||||||
Ok(Session {
|
Ok(Session {
|
||||||
@@ -1886,6 +2131,8 @@ mod tests {
|
|||||||
nmx: Mutex::new(nmx),
|
nmx: Mutex::new(nmx),
|
||||||
callback_exporter: Mutex::new(Some(exporter)),
|
callback_exporter: Mutex::new(Some(exporter)),
|
||||||
callback_tx,
|
callback_tx,
|
||||||
|
operation_status_tx,
|
||||||
|
recovery_active,
|
||||||
router_handle: std::sync::Mutex::new(Some(router_handle)),
|
router_handle: std::sync::Mutex::new(Some(router_handle)),
|
||||||
recovery_tx,
|
recovery_tx,
|
||||||
connected: std::sync::atomic::AtomicBool::new(true),
|
connected: std::sync::atomic::AtomicBool::new(true),
|
||||||
@@ -2323,8 +2570,15 @@ mod tests {
|
|||||||
// broadcast pair to test the routing logic in isolation.
|
// broadcast pair to test the routing logic in isolation.
|
||||||
let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
let (callback_tx, mut callback_rx) = broadcast::channel(8);
|
let (callback_tx, mut callback_rx) = broadcast::channel(8);
|
||||||
|
let (operation_status_tx, _) = broadcast::channel::<Arc<OperationStatus>>(8);
|
||||||
|
let recovery_active = Arc::new(std::sync::atomic::AtomicU32::new(0));
|
||||||
|
|
||||||
let router_h = tokio::spawn(callback_router(event_rx, callback_tx));
|
let router_h = tokio::spawn(callback_router(
|
||||||
|
event_rx,
|
||||||
|
callback_tx,
|
||||||
|
operation_status_tx,
|
||||||
|
recovery_active,
|
||||||
|
));
|
||||||
|
|
||||||
// Build a minimal valid 0x32 SubscriptionStatus body: 23-byte
|
// Build a minimal valid 0x32 SubscriptionStatus body: 23-byte
|
||||||
// preamble + 16-byte item_correlation_id, record_count=0 so no
|
// preamble + 16-byte item_correlation_id, record_count=0 so no
|
||||||
@@ -2578,9 +2832,9 @@ mod tests {
|
|||||||
let stub: crate::RebuildFactory = Arc::new(|| {
|
let stub: crate::RebuildFactory = Arc::new(|| {
|
||||||
Box::pin(async {
|
Box::pin(async {
|
||||||
Err(mxaccess_nmx::NmxClientError::Transport(
|
Err(mxaccess_nmx::NmxClientError::Transport(
|
||||||
mxaccess_rpc::transport::TransportError::Io(
|
mxaccess_rpc::transport::TransportError::Io(std::io::Error::other(
|
||||||
std::io::Error::other("synthetic rebuild failure"),
|
"synthetic rebuild failure",
|
||||||
),
|
)),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -2603,9 +2857,7 @@ mod tests {
|
|||||||
for _ in 0..expected_events {
|
for _ in 0..expected_events {
|
||||||
match &*rx.recv().await.unwrap() {
|
match &*rx.recv().await.unwrap() {
|
||||||
RecoveryEvent::Started { .. } => started += 1,
|
RecoveryEvent::Started { .. } => started += 1,
|
||||||
RecoveryEvent::Failed {
|
RecoveryEvent::Failed { will_retry, .. } => {
|
||||||
will_retry, ..
|
|
||||||
} => {
|
|
||||||
failed += 1;
|
failed += 1;
|
||||||
last_will_retry = Some(*will_retry);
|
last_will_retry = Some(*will_retry);
|
||||||
}
|
}
|
||||||
@@ -2631,8 +2883,7 @@ mod tests {
|
|||||||
// F16: every successful subscribe() inserts into the
|
// F16: every successful subscribe() inserts into the
|
||||||
// SubscriptionEntry registry; unsubscribe() removes it.
|
// SubscriptionEntry registry; unsubscribe() removes it.
|
||||||
// Recovery walks this registry to replay AdviseSupervisory.
|
// Recovery walks this registry to replay AdviseSupervisory.
|
||||||
let (addr, handle) =
|
let (addr, handle) = unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new())]).await;
|
||||||
unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new())]).await;
|
|
||||||
let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[(
|
let resolver: Arc<dyn Resolver> = Arc::new(StaticResolver::new(&[(
|
||||||
"TestObj.TestInt",
|
"TestObj.TestInt",
|
||||||
sample_metadata(),
|
sample_metadata(),
|
||||||
@@ -2706,10 +2957,7 @@ mod tests {
|
|||||||
let pfc_object_uuid = (req_h.packet_flags & 0x80) != 0;
|
let pfc_object_uuid = (req_h.packet_flags & 0x80) != 0;
|
||||||
let stub_offset = if pfc_object_uuid { 8 + 16 } else { 8 };
|
let stub_offset = if pfc_object_uuid { 8 + 16 } else { 8 };
|
||||||
let stub = body[stub_offset..].to_vec();
|
let stub = body[stub_offset..].to_vec();
|
||||||
recorded_for_task
|
recorded_for_task.lock().unwrap().push((opnum, stub));
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.push((opnum, stub));
|
|
||||||
|
|
||||||
let mut stub_resp = Vec::new();
|
let mut stub_resp = Vec::new();
|
||||||
stub_resp.extend_from_slice(&OrpcThat::default().encode());
|
stub_resp.extend_from_slice(&OrpcThat::default().encode());
|
||||||
@@ -2940,9 +3188,9 @@ mod tests {
|
|||||||
let stub: crate::RebuildFactory = Arc::new(|| {
|
let stub: crate::RebuildFactory = Arc::new(|| {
|
||||||
Box::pin(async {
|
Box::pin(async {
|
||||||
Err(mxaccess_nmx::NmxClientError::Transport(
|
Err(mxaccess_nmx::NmxClientError::Transport(
|
||||||
mxaccess_rpc::transport::TransportError::Io(
|
mxaccess_rpc::transport::TransportError::Io(std::io::Error::other(
|
||||||
std::io::Error::other("stub factory: rebuild always fails"),
|
"stub factory: rebuild always fails",
|
||||||
),
|
)),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -2951,9 +3199,7 @@ mod tests {
|
|||||||
let mut rx_a = session.recovery_events();
|
let mut rx_a = session.recovery_events();
|
||||||
let mut rx_b = session.recovery_events();
|
let mut rx_b = session.recovery_events();
|
||||||
|
|
||||||
let _ = session
|
let _ = session.recover_connection(RecoveryPolicy::default()).await;
|
||||||
.recover_connection(RecoveryPolicy::default())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// First event from each receiver is the same Started Arc.
|
// First event from each receiver is the same Started Arc.
|
||||||
let a = rx_a.recv().await.unwrap();
|
let a = rx_a.recv().await.unwrap();
|
||||||
@@ -3071,6 +3317,181 @@ mod tests {
|
|||||||
handle.abort();
|
handle.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a `ProcessDataReceived`-style envelope wrapping a 5-byte
|
||||||
|
/// operation-status inner body. Mirrors what `NmxObservedEnvelope`
|
||||||
|
/// serialises (`mxaccess-codec/src/observed_frame.rs:115-141`):
|
||||||
|
///
|
||||||
|
/// - 4-byte total-length prefix
|
||||||
|
/// - 46-byte header with `inner_length` at offset 6 (header
|
||||||
|
/// offset 4 + INNER_LENGTH_OFFSET 2)
|
||||||
|
/// - inner body
|
||||||
|
fn wrap_op_status_envelope(inner: &[u8]) -> Vec<u8> {
|
||||||
|
const HEADER_LENGTH: usize = 46;
|
||||||
|
let total_len = 4 + HEADER_LENGTH + inner.len();
|
||||||
|
let mut body = vec![0u8; total_len];
|
||||||
|
// Total-length prefix at offset 0.
|
||||||
|
body[0..4].copy_from_slice(&(total_len as i32).to_le_bytes());
|
||||||
|
// `actualInnerLength = declaredInnerLength - sizeof(int)` per
|
||||||
|
// the parser at `observed_frame.rs:134`. So
|
||||||
|
// `declaredInnerLength = inner.len() + 4`.
|
||||||
|
let declared_inner: i32 = inner.len() as i32 + 4;
|
||||||
|
// Inner-length field sits at headerOffset + INNER_LENGTH_OFFSET
|
||||||
|
// = 4 + 2 = 6.
|
||||||
|
body[6..10].copy_from_slice(&declared_inner.to_le_bytes());
|
||||||
|
// Inner body follows the header.
|
||||||
|
body[4 + HEADER_LENGTH..].copy_from_slice(inner);
|
||||||
|
body
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn router_dispatches_status_word_frame_to_operation_status_channel() {
|
||||||
|
// End-to-end: hand-build an operation-status `ProcessDataReceived`
|
||||||
|
// body and confirm the router parses it + broadcasts an
|
||||||
|
// `OperationStatus` (NOT a subscription message).
|
||||||
|
let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
let (callback_tx, mut callback_rx) = broadcast::channel(8);
|
||||||
|
let (operation_status_tx, mut operation_status_rx) =
|
||||||
|
broadcast::channel::<Arc<OperationStatus>>(8);
|
||||||
|
let recovery_active = Arc::new(std::sync::atomic::AtomicU32::new(0));
|
||||||
|
|
||||||
|
let router_h = tokio::spawn(callback_router(
|
||||||
|
event_rx,
|
||||||
|
callback_tx,
|
||||||
|
operation_status_tx,
|
||||||
|
recovery_active,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Inner body is the proven `00 00 50 80 00` 5-byte status-word frame.
|
||||||
|
let inner = [0x00, 0x00, 0x50, 0x80, 0x00];
|
||||||
|
let body = wrap_op_status_envelope(&inner);
|
||||||
|
event_tx
|
||||||
|
.send(CallbackEvent::CallbackInvoked { opnum: 4, body })
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let received = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
operation_status_rx.recv(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("router timed out");
|
||||||
|
let event = received.expect("broadcast recv error");
|
||||||
|
|
||||||
|
// Raw frame round-trips byte-exact.
|
||||||
|
assert_eq!(event.raw.command, 0x00);
|
||||||
|
assert_eq!(event.raw.status_code, 0x8050);
|
||||||
|
assert_eq!(event.raw.completion_code, 0x00);
|
||||||
|
|
||||||
|
// Synthesizer-promoted status equals the canonical sentinel.
|
||||||
|
assert_eq!(event.status, MxStatus::WRITE_COMPLETE_OK);
|
||||||
|
|
||||||
|
// Context not yet wired — always None for this iteration.
|
||||||
|
assert!(event.context.is_none());
|
||||||
|
// No recovery in flight when the event was dispatched.
|
||||||
|
assert!(!event.is_during_recovery);
|
||||||
|
|
||||||
|
// Subscription channel must NOT have received anything — the
|
||||||
|
// dispatcher's `continue` after operation-status hit means
|
||||||
|
// subscription parsing never runs for this body.
|
||||||
|
let cb_res =
|
||||||
|
tokio::time::timeout(std::time::Duration::from_millis(100), callback_rx.recv()).await;
|
||||||
|
assert!(
|
||||||
|
cb_res.is_err(),
|
||||||
|
"subscription channel got an unexpected event"
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(event_tx);
|
||||||
|
let _ = router_h.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn router_dispatches_completion_only_frames_under_each_proven_byte() {
|
||||||
|
// Per `design/70-risks-and-open-questions.md` R3/R4 the three
|
||||||
|
// observed completion bytes are 0x00, 0x41, 0xEF. The synthesizer
|
||||||
|
// does NOT promote them (no upstream evidence per Path A's
|
||||||
|
// Ghidra walk); they should arrive on the operation-status
|
||||||
|
// channel as verbatim-preserve placeholders.
|
||||||
|
for byte in [0x00_u8, 0x41, 0xEF] {
|
||||||
|
let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
let (callback_tx, _callback_rx) = broadcast::channel(8);
|
||||||
|
let (operation_status_tx, mut operation_status_rx) =
|
||||||
|
broadcast::channel::<Arc<OperationStatus>>(8);
|
||||||
|
let recovery_active = Arc::new(std::sync::atomic::AtomicU32::new(0));
|
||||||
|
let router_h = tokio::spawn(callback_router(
|
||||||
|
event_rx,
|
||||||
|
callback_tx,
|
||||||
|
operation_status_tx,
|
||||||
|
recovery_active,
|
||||||
|
));
|
||||||
|
|
||||||
|
let inner = [byte];
|
||||||
|
let body = wrap_op_status_envelope(&inner);
|
||||||
|
event_tx
|
||||||
|
.send(CallbackEvent::CallbackInvoked { opnum: 4, body })
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let received = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
operation_status_rx.recv(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("router timed out");
|
||||||
|
let event = received.expect("broadcast recv error");
|
||||||
|
|
||||||
|
assert_eq!(event.raw.completion_code, byte);
|
||||||
|
assert_eq!(event.status.detail, i16::from(byte));
|
||||||
|
// R3/R4: completion-only bytes stay verbatim (Unknown/Unknown).
|
||||||
|
assert_eq!(
|
||||||
|
event.status.category,
|
||||||
|
mxaccess_codec::MxStatusCategory::Unknown
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
event.status.detected_by,
|
||||||
|
mxaccess_codec::MxStatusSource::Unknown
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(event_tx);
|
||||||
|
let _ = router_h.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn router_marks_is_during_recovery_when_counter_nonzero() {
|
||||||
|
// Stamp `recovery_active = 1` BEFORE feeding an event — the
|
||||||
|
// router should observe the volatile load and emit
|
||||||
|
// `OperationStatus.is_during_recovery = true`. Mirrors
|
||||||
|
// `MxNativeSession.OnCallbackReceived:573` which reads the same
|
||||||
|
// flag via `Volatile.Read(ref _recoveryActive)`.
|
||||||
|
let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
let (callback_tx, _callback_rx) = broadcast::channel(8);
|
||||||
|
let (operation_status_tx, mut operation_status_rx) =
|
||||||
|
broadcast::channel::<Arc<OperationStatus>>(8);
|
||||||
|
let recovery_active = Arc::new(std::sync::atomic::AtomicU32::new(1));
|
||||||
|
let router_h = tokio::spawn(callback_router(
|
||||||
|
event_rx,
|
||||||
|
callback_tx,
|
||||||
|
operation_status_tx,
|
||||||
|
recovery_active,
|
||||||
|
));
|
||||||
|
|
||||||
|
let inner = [0x00, 0x00, 0x50, 0x80, 0x00];
|
||||||
|
let body = wrap_op_status_envelope(&inner);
|
||||||
|
event_tx
|
||||||
|
.send(CallbackEvent::CallbackInvoked { opnum: 4, body })
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let event = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
operation_status_rx.recv(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("router timed out")
|
||||||
|
.expect("broadcast recv error");
|
||||||
|
assert!(event.is_during_recovery);
|
||||||
|
|
||||||
|
drop(event_tx);
|
||||||
|
let _ = router_h.await;
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn filetime_to_system_time_round_trip() {
|
fn filetime_to_system_time_round_trip() {
|
||||||
// Build a SystemTime, convert to FILETIME, convert back.
|
// Build a SystemTime, convert to FILETIME, convert back.
|
||||||
@@ -3098,7 +3519,14 @@ mod tests {
|
|||||||
// window.
|
// window.
|
||||||
let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
let (callback_tx, mut callback_rx) = broadcast::channel(8);
|
let (callback_tx, mut callback_rx) = broadcast::channel(8);
|
||||||
let router_h = tokio::spawn(callback_router(event_rx, callback_tx));
|
let (operation_status_tx, _) = broadcast::channel::<Arc<OperationStatus>>(8);
|
||||||
|
let recovery_active = Arc::new(std::sync::atomic::AtomicU32::new(0));
|
||||||
|
let router_h = tokio::spawn(callback_router(
|
||||||
|
event_rx,
|
||||||
|
callback_tx,
|
||||||
|
operation_status_tx,
|
||||||
|
recovery_active,
|
||||||
|
));
|
||||||
|
|
||||||
event_tx
|
event_tx
|
||||||
.send(CallbackEvent::Bind {
|
.send(CallbackEvent::Bind {
|
||||||
@@ -3142,7 +3570,11 @@ mod tests {
|
|||||||
// Issue a plain subscribe — server records AdviseSupervisory.
|
// Issue a plain subscribe — server records AdviseSupervisory.
|
||||||
let sub = session.subscribe("TestObj.TestInt").await.unwrap();
|
let sub = session.subscribe("TestObj.TestInt").await.unwrap();
|
||||||
let cid = sub.correlation_id;
|
let cid = sub.correlation_id;
|
||||||
assert_eq!(recorded.lock().unwrap().len(), 1, "subscribe should issue 1 RPC");
|
assert_eq!(
|
||||||
|
recorded.lock().unwrap().len(),
|
||||||
|
1,
|
||||||
|
"subscribe should issue 1 RPC"
|
||||||
|
);
|
||||||
|
|
||||||
// Mutate the registry entry's mode to Buffered (synthesise the
|
// Mutate the registry entry's mode to Buffered (synthesise the
|
||||||
// state subscribe_buffered_nmx would have produced).
|
// state subscribe_buffered_nmx would have produced).
|
||||||
|
|||||||
Reference in New Issue
Block a user