Compare commits
11 Commits
0a274af76f
...
8bd66bbe65
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bd66bbe65 | |||
| 349e217ea3 | |||
| b62ffc8c5d | |||
| e77db4306a | |||
| c606736ec3 | |||
| d149143535 | |||
| 5e11b30507 | |||
| c6332c26a1 | |||
| df3457c54a | |||
| af15fe7587 | |||
| 2fc327a8d5 |
@@ -0,0 +1,3 @@
|
||||
frida=C:\Users\dohertj2\AppData\Local\Programs\Python\Python312\Scripts\frida.exe
|
||||
harness=C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe
|
||||
args=-f C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe -l C:\Users\dohertj2\Desktop\mxaccess\analysis\frida\mx-nmx-trace.js -- --scenario=suspend-advised --tag=TestChildObject.ScanState --duration=8 --log=C:\Users\dohertj2\Desktop\mxaccess\captures\123-frida-suspend-advised-instrumented\harness.log --client=MxFridaTrace-123
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,98 @@
|
||||
____
|
||||
/ _ | Frida 17.9.1 - A world-class dynamic instrumentation toolkit
|
||||
| (_| |
|
||||
> _ | Commands:
|
||||
/_/ |_| help -> Displays the help system
|
||||
. . . . object? -> Display information about 'object'
|
||||
. . . . exit/quit -> Exit
|
||||
. . . .
|
||||
. . . . More info at https://frida.re/docs/home/
|
||||
. . . .
|
||||
. . . . Connected to Local System (id=local)
|
||||
Spawning `C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe --scenario=suspend-advised --tag=TestChildObject.ScanState --duration=8 --log=C:\Users\dohertj2\Desktop\mxaccess\captures\123-frida-suspend-advised-instrumented\harness.log --client=MxFridaTrace-123`...
|
||||
Spawned `C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe --scenario=suspend-advised --tag=TestChildObject.ScanState --duration=8 --log=C:\Users\dohertj2\Desktop\mxaccess\captures\123-frida-suspend-advised-instrumented\harness.log --client=MxFridaTrace-123`. Resuming main thread!
|
||||
[Local::MxTraceHarness.exe ]-> {"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Write.variantA","base":"0x61b50000","rva":"0x12c0c","address":"0x61b62c0c","time":"2026-05-06T17:23:45.844Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Write.variantB","base":"0x61b50000","rva":"0x13280","address":"0x61b63280","time":"2026-05-06T17:23:45.845Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.WriteSecured.variantA","base":"0x61b50000","rva":"0x12f24","address":"0x61b62f24","time":"2026-05-06T17:23:45.846Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.WriteSecured.variantB","base":"0x61b50000","rva":"0x135fe","address":"0x61b635fe","time":"2026-05-06T17:23:45.846Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.AddBufferedItem","base":"0x61b50000","rva":"0x1121d","address":"0x61b6121d","time":"2026-05-06T17:23:45.846Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.SetBufferedUpdateInterval","base":"0x61b50000","rva":"0xfc80","address":"0x61b5fc80","time":"2026-05-06T17:23:45.846Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.AdviseSupervisory","base":"0x61b50000","rva":"0x142b4","address":"0x61b642b4","time":"2026-05-06T17:23:45.846Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Suspend","base":"0x61b50000","rva":"0x13d9c","address":"0x61b63d9c","time":"2026-05-06T17:23:45.846Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Activate","base":"0x61b50000","rva":"0x14028","address":"0x61b64028","time":"2026-05-06T17:23:45.847Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CProxy_ILMXProxyServerEvents2.Fire_OnBufferedDataChange","base":"0x61b50000","rva":"0x163c0","address":"0x61b663c0","time":"2026-05-06T17:23:45.847Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CUserConnectionCallback.OnSetAttributeResult","base":"0x61b50000","rva":"0x16b50","address":"0x61b66b50","time":"2026-05-06T17:23:45.847Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CUserConnectionCallback.OperationComplete","base":"0x61b50000","rva":"0x16d4b","address":"0x61b66d4b","time":"2026-05-06T17:23:45.848Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.AuthenticateUser","base":"0x61b50000","rva":"0x1399f","address":"0x61b6399f","time":"2026-05-06T17:23:45.848Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"MxConnection.PrebindReference","base":"0x10000000","rva":"0xea780","address":"0x100ea780","time":"2026-05-06T17:23:51.188Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"MxConnection.UserRegisterPreboundReference","base":"0x10000000","rva":"0xe1920","address":"0x100e1920","time":"2026-05-06T17:23:51.189Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"IMxReference.GetMxHandle","base":"0x10000000","rva":"0x5f730","address":"0x1005f730","time":"2026-05-06T17:23:51.190Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","base":"0x10000000","rva":"0x8f8b0","address":"0x1008f8b0","time":"2026-05-06T17:23:51.190Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"PreboundReference.Resolve","base":"0x10000000","rva":"0x113d40","address":"0x10113d40","time":"2026-05-06T17:23:51.191Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"PreboundReference.OnPlatformResolveReferenceResults","base":"0x10000000","rva":"0x1155a0","address":"0x101155a0","time":"2026-05-06T17:23:51.192Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"PreboundReference.OnSetAttributeResult","base":"0x10000000","rva":"0x114a90","address":"0x10114a90","time":"2026-05-06T17:23:51.192Z"}
|
||||
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x91a72b0","outPtr":"0xd5e6c4","inWords":[65537,65537,0,0,0,0],"time":"2026-05-06T17:23:51.236Z"}
|
||||
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xd5e6c4","handle":{"raw":"01 00 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00","w0":65537,"w1":65537,"w2":0,"w3":0,"w4":0},"retval":"0xd5e6c4","time":"2026-05-06T17:23:51.236Z"}
|
||||
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x91a72b0","outPtr":"0xd5e6c4","inWords":[65537,65537,0,0,0,0],"time":"2026-05-06T17:23:51.237Z"}
|
||||
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xd5e6c4","handle":{"raw":"01 00 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00","w0":65537,"w1":65537,"w2":0,"w3":0,"w4":0},"retval":"0xd5e6c4","time":"2026-05-06T17:23:51.237Z"}
|
||||
{"event":"lmx.prebind.enter","module":"Lmx.dll","name":"MxConnection.PrebindReference","self":"0x91aed2c","outPtr":"0xd5ec98","referencePtr":"0xd5eccc","reference":"TestChildObject.ScanState","time":"2026-05-06T17:23:51.255Z"}
|
||||
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x91b3838","outPtr":"0xd5ec00","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xd5ec00","time":"2026-05-06T17:23:51.256Z"}
|
||||
{"event":"lmx.prebound-resolve.enter","module":"Lmx.dll","name":"PreboundReference.Resolve","prebound":{"ptr":"0x91af058","referenceString":{"length":25,"capacity":31,"value":"TestChildObject.ScanState"},"contextString":{"length":0,"capacity":7,"value":""},"auxString":{"length":0,"capacity":7,"value":""},"mxReference":"0x91b46f0","flags10":1124099840,"word14":2,"word4c":131073,"word54":134011636,"word58":0,"word5c":0,"word60":0,"word64":152728240,"word68":0,"word6c":0,"worda0":0,"worda4":0,"status":3,"flagb0":0,"errorText":"","raw":"08 64 19 10 f0 63 19 10 00 6f 00 6e e8 63 19 10 00 67 00 43 02 00 00 00 98 41 1b 09 00 65 00 00 00 02 00 00 00 00 00 02 19 00 00 00 1f 00 00 00 00 00 00 01 00 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 01 00 02 00 f0 46 1b 09 f4 da fc 07 00 00 00 00 00 00 00 00 00 00 00 00 b0 72 1a 09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 ac 8a 31 01 00 00 00 00"},"time":"2026-05-06T17:23:51.257Z"}
|
||||
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x91af0a8","outPtr":"0xd5eb90","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xd5eb90","time":"2026-05-06T17:23:51.257Z"}
|
||||
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x91af0a8","outPtr":"0xd5eb90","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xd5eb90","time":"2026-05-06T17:23:51.257Z"}
|
||||
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x91af0a8","outPtr":"0xd5eb90","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xd5eb90","time":"2026-05-06T17:23:51.258Z"}
|
||||
{"event":"lmx.prebound-resolve.leave","module":"Lmx.dll","name":"PreboundReference.Resolve","prebound":{"ptr":"0x91af058","referenceString":{"length":25,"capacity":31,"value":"TestChildObject.ScanState"},"contextString":{"length":0,"capacity":7,"value":""},"auxString":{"length":0,"capacity":7,"value":""},"mxReference":"0x91b46f0","flags10":1124099840,"word14":2,"word4c":131073,"word54":134011636,"word58":0,"word5c":0,"word60":0,"word64":152728240,"word68":0,"word6c":0,"worda0":0,"worda4":0,"status":3,"flagb0":0,"errorText":"","raw":"08 64 19 10 f0 63 19 10 00 6f 00 6e e8 63 19 10 00 67 00 43 02 00 00 00 98 41 1b 09 00 65 00 00 00 02 00 00 00 00 00 02 19 00 00 00 1f 00 00 00 00 00 00 01 00 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 01 00 02 00 f0 46 1b 09 f4 da fc 07 00 00 00 00 00 00 00 00 00 00 00 00 b0 72 1a 09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 ac 8a 31 01 00 00 00 00"},"retval":"0x70d01e01","time":"2026-05-06T17:23:51.259Z"}
|
||||
{"event":"lmx.prebind.leave","module":"Lmx.dll","name":"MxConnection.PrebindReference","handle":1,"time":"2026-05-06T17:23:51.259Z"}
|
||||
{"event":"call.enter","module":"LmxProxy.dll","name":"CLMXProxyServer.AdviseSupervisory","address":"0x61b642b4","ecx":"0xd5ed50","args":["0x62492d0","0x1","0x1","0x55eabfd1","0x744d4704"],"time":"2026-05-06T17:23:51.261Z"}
|
||||
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x91a72b0","outPtr":"0xd5ebd0","inWords":[65537,327682,186166,655465,37447,0],"time":"2026-05-06T17:23:51.261Z"}
|
||||
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xd5ebd0","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xd5ebd0","time":"2026-05-06T17:23:51.261Z"}
|
||||
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x91a72b0","outPtr":"0xd5d864","inWords":[65537,327682,186166,655465,37447,0],"time":"2026-05-06T17:23:51.262Z"}
|
||||
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xd5d864","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xd5d864","time":"2026-05-06T17:23:51.262Z"}
|
||||
{"event":"call.leave","module":"LmxProxy.dll","name":"CLMXProxyServer.AdviseSupervisory","retval":"0x0","time":"2026-05-06T17:23:51.262Z"}
|
||||
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","base":"0x63ae0000","rva":"0x10996","address":"0x63af0996","time":"2026-05-06T17:23:51.280Z"}
|
||||
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","base":"0x63ae0000","rva":"0x112da","address":"0x63af12da","time":"2026-05-06T17:23:51.280Z"}
|
||||
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","base":"0x63ae0000","rva":"0x15169","address":"0x63af5169","time":"2026-05-06T17:23:51.281Z"}
|
||||
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequestEx","base":"0x63ae0000","rva":"0x159c3","address":"0x63af59c3","time":"2026-05-06T17:23:51.281Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x91ac9d8","0x1","0x1","0x1","0x2","0x0","0x13a","0x91af118","0xd5ea14","0xfd3aeb5e"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":1,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":314,"ptr":"0x91af118","hex":"17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 6a 00 00 00 40 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 a0 e7 1a 09 1f 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 00 00 01 00 00 00 17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 76 00 00 00 4c 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 20 ee 1a 09 20 01 00 02 00 00 00"}],"time":"2026-05-06T17:23:51.371Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x1","0x168","0x9eb7020","0x9d860587","0x91aece4","0x91aecd4","0x63b0dd04","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":360,"ptr":"0x9eb7020","hex":"01 00 3a 01 00 00 00 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 02 00 00 30 75 00 00 17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 6a 00 00 00 40 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 a0 e7 1a 09 1f 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 00 00 01 00 00 00 17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 76 00 00 00 4c 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 20 ee 1a 09 20 01 00 02 00 00 00"}],"time":"2026-05-06T17:23:51.373Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:51.374Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:23:51.374Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x91ac9d8","0x1","0x1","0x2","0x2","0x0","0x27","0x91af590","0xd5ea14","0xfd3aeb5e"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":2,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":39,"ptr":"0x91af590","hex":"1f 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 00 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:23:51.375Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x2","0x55","0x9eb7020","0x9d860587","0x91b5dcc","0x91b5dbc","0x63b0dd04","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":85,"ptr":"0x9eb7020","hex":"01 00 27 00 00 00 00 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 1f 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 00 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:23:51.376Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:51.376Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:23:51.376Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x91ac9d8","args":["0x2c2","0x7f44288","0x773eb08","0x769cedd8","0x91ac9e4","0x2c2","0x7f44288","0x206","0x3","0x7aa21cc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":706,"ptr":"0x7f44288","hex":"01 00 94 02 00 00 00 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 02 02 00 00 30 75 00 00 40 1f 50 80 08 a6 00 00 00 40 00 00 91 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 18 00 00 00 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 00 00 28 00 00 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 53 f2 9a 00 6a 00 0a 00 5f f1 00 00 01 6c 00 00 00 41 00 6e 00 20 00 69 00 6e 00 74 00 65 00 72 00 6e 00 61 00 6c 00 20 00 65 00 72 00 72 00 6f 00 72 00 20 00 6f 00 63 00 63 00 75 00 72 00 72 00 65 00 64 00 20 00 69 00 6e 00 20 00 74 00 68 00 65 00 20 00 42 00 61 00 73 00 65 00 20 00 52 00 75 00 6e 00 74 00 69 00 6d 00 65 00 20 00 4f 00 62 00 6a 00 65 00 63 00 74 00 00 00 1f 00 00 50 80 01 00 01 00 01 00 30 75 00 00 4a 5a a3 cd 7a 87 96 43 83 2c b4 ba be 67 53 57 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 40 1f 50 80 08 be 00 00 00 4c 00 00 91 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 18 00 00 00 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 00 00 34 00 00 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 53 f2 9a 00 6b 00 0a 00 87 3a 00 00 01 6c 00 00 00 41 00 6e 00 20 00 69 00 6e 00 74 00 65 00 72 00 6e 00 61 00 6c 00 20 00 65 00 72 00 72 00 6f 00 72 00 20 00 6f 00 63 00 63 00 75 00 72 00 72 00 65 00 64 00 20 00 69 00 6e 00 20 00 74 00 68 00 65 00 20 00 42 00 61 00 73 00 65 00 20 00 52 00 75 00 6e 00 74 00 69 00 6d 00 65 00 20 00 4f 00 62 00 6a 00 65 00 63 00 74 00 00 00 20 00 00 50 80 01 00 01 00 01 00 30 75 00 00"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7aa21cc","hex":"f0 d7 01"}],"time":"2026-05-06T17:23:51.392Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:23:51.393Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x91ac9d8","args":["0x97","0x7f38730","0x773eb08","0x769cedd8","0x91ac9e4","0x97","0x7f38730","0x206","0x3","0x7aa21cc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":151,"ptr":"0x7f38730","hex":"01 00 69 00 00 00 00 00 00 00 39 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 02 00 00 30 75 00 00 32 01 00 02 00 00 00 4a 5a a3 cd 7a 87 96 43 83 2c b4 ba be 67 53 57 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 01 00 00 00 03 00 00 00 c0 00 b0 fd 44 d6 75 dd dc 01 06 0a 00 00 00 00 99 8c 8a 6e da dc 01 00 00 02 00 00 00 03 00 00 00 c0 00 f0 99 45 d6 75 dd dc 01 06 0a 00 00 00 00 fb 56 ce 19 dd dc 01 00 00"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7aa21cc","hex":"f0 d7 01"}],"time":"2026-05-06T17:23:51.394Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:23:51.394Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x91ac9d8","args":["0x5c","0x7f43180","0x773eb08","0x769cedd8","0x91ac9e4","0x5c","0x7f43180","0x206","0x3","0x7aa21cc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":92,"ptr":"0x7f43180","hex":"01 00 2e 00 00 00 00 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 02 02 00 00 30 75 00 00 00 00 50 80 01 00 01 00 02 00 30 75 00 00 ad dd 62 fe a7 a0 e5 49 87 72 93 75 c6 f1 cc 86 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7aa21cc","hex":"f0 d7 01"}],"time":"2026-05-06T17:23:51.414Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:23:51.415Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x91ac9d8","args":["0x69","0x7fb3ab0","0x773eb08","0x769cedd8","0x91ac9e4","0x69","0x7fb3ab0","0x206","0x3","0x7aa21cc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":105,"ptr":"0x7fb3ab0","hex":"01 00 3b 00 00 00 00 00 00 00 06 19 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 02 00 00 30 75 00 00 32 01 00 01 00 00 00 ad dd 62 fe a7 a0 e5 49 87 72 93 75 c6 f1 cc 86 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 03 00 00 00 00 00 00 00 c0 00 c0 3e 0b d8 75 dd dc 01 01 ff"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7aa21cc","hex":"f0 d7 01"}],"time":"2026-05-06T17:23:51.416Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:23:51.416Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x1","0x2e","0x9eb7020","0x9d860473","0x91a72b0","0x0","0x0","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":46,"ptr":"0x9eb7020","hex":"01 00 00 00 00 00 00 00 00 00 39 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 01 00 00 00 02 02 00 00 30 75 00 00"}],"time":"2026-05-06T17:23:51.470Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:51.470Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x2","0x2e","0x9eb7020","0x9d860473","0x91a72b0","0x0","0x0","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":46,"ptr":"0x9eb7020","hex":"01 00 00 00 00 00 00 00 00 00 06 19 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 02 02 00 00 30 75 00 00"}],"time":"2026-05-06T17:23:51.488Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:51.489Z"}
|
||||
{"event":"mx.suspend.begin","module":"LmxProxy.dll","name":"CLMXProxyServer.Suspend","address":"0x61b63d9c","ecx":"0xd5ed4c","serverHandle":1,"itemHandle":1,"statusOutPtr":"0xd5f14c","time":"2026-05-06T17:23:51.949Z"}
|
||||
{"event":"mx.suspend.end","module":"LmxProxy.dll","name":"CLMXProxyServer.Suspend","retval":"0x0","serverHandle":1,"itemHandle":1,"status":{"raw":"ff ff 3a fd 01 00 00 00","success":-1,"category":-710,"detectedBy":1,"detail":0},"time":"2026-05-06T17:23:51.949Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x91ac9d8","0x1","0x1","0x2","0x2","0x0","0x29","0x91af980","0xd5ea14","0xfd3aeb5e"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":2,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":41,"ptr":"0x91af980","hex":"2d 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 01 00 05 00 01 00 02 00 01 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:23:52.089Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x2","0x57","0x9eb7020","0x9d860587","0x91a829c","0x91a828c","0x63b0dd04","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":87,"ptr":"0x9eb7020","hex":"01 00 29 00 00 00 00 00 00 00 03 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 2d 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 01 00 05 00 01 00 02 00 01 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:23:52.089Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:52.090Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:23:52.090Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x91ac9d8","args":["0x32","0x7f44288","0x773eb08","0x769cedd8","0x91ac9e4","0x32","0x7f44288","0x206","0x3","0x7aa21cc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":50,"ptr":"0x7f44288","hex":"01 00 04 00 00 00 00 00 00 00 03 00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 02 02 00 00 30 75 00 00 00 00 10 80"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7aa21cc","hex":"f0 d7 01"}],"time":"2026-05-06T17:23:52.123Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:23:52.123Z"}
|
||||
{"event":"call.enter","module":"LmxProxy.dll","name":"CUserConnectionCallback.OperationComplete","address":"0x61b66d4b","ecx":"0x61b66d4b","args":["0x91b4b40","0x1","0xd5e574","0x8014cbc"],"time":"2026-05-06T17:23:52.183Z"}
|
||||
{"event":"call.leave","module":"LmxProxy.dll","name":"CUserConnectionCallback.OperationComplete","retval":"0x0","time":"2026-05-06T17:23:52.185Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x91ac9d8","0x1","0x1","0x1","0x2","0x0","0x3a","0x91af470","0xd5ebd0","0xfd3ae89a"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":1,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":58,"ptr":"0x91af470","hex":"21 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 01 00 53 f2 9a 00 6a 00 0a 00 5f f1 00 00 01 00 00 00 22 01 00 01 00 53 f2 9a 00 6b 00 0a 00 87 3a 00 00 02 00 00 00"}],"time":"2026-05-06T17:23:59.173Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x1","0x68","0x9eb7020","0x9d8607c3","0x91aec7c","0x91aec6c","0x63b0dd04","0x0"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":104,"ptr":"0x9eb7020","hex":"01 00 3a 00 00 00 00 00 00 00 04 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 02 00 00 30 75 00 00 21 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 01 00 53 f2 9a 00 6a 00 0a 00 5f f1 00 00 01 00 00 00 22 01 00 01 00 53 f2 9a 00 6b 00 0a 00 87 3a 00 00 02 00 00 00"}],"time":"2026-05-06T17:23:59.174Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:59.174Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:23:59.175Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x91ac9d8","0x1","0x1","0x2","0x2","0x0","0x25","0x91af590","0xd5ebd0","0xfd3ae89a"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":2,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":37,"ptr":"0x91af590","hex":"21 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:23:59.175Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x2","0x53","0x9eb7020","0x9d8607c3","0x91a829c","0x91a828c","0x63b0dd04","0x0"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":83,"ptr":"0x9eb7020","hex":"01 00 25 00 00 00 00 00 00 00 05 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 21 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:23:59.175Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:59.176Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:23:59.176Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x91ac9d8","args":["0x2e","0x7f43180","0x773eb08","0x769cedd8","0x91ac9e4","0x2e","0x7f43180","0x206","0x3","0x7aa21cc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":46,"ptr":"0x7f43180","hex":"01 00 00 00 00 00 00 00 00 00 05 00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 02 02 00 00 30 75 00 00"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7aa21cc","hex":"f0 d7 01"}],"time":"2026-05-06T17:23:59.184Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:23:59.184Z"}
|
||||
Process terminated
|
||||
|
||||
Thank you for using Frida!
|
||||
@@ -0,0 +1,18 @@
|
||||
2026-05-06T17:23:45.7524803+00:00 harness.start {"Scenario":"suspend-advised","ClientName":"MxFridaTrace-123","Tags":["TestChildObject.ScanState"],"ItemContext":"","WriteType":"string","WriteValue":"","WriteValues":[],"UserId":0,"CurrentUserId":0,"VerifierUserId":0,"UserGuid":"","AuthUser":"","AuthenticateBeforeWrite":false,"UseAuthenticatedUserAsVerifier":false,"UsePlainAdvise":false,"WriteTimestamp":"","WriteDelayMilliseconds":750,"WriteIntervalMilliseconds":500,"BufferedUpdateInterval":1000,"DurationSeconds":8,"ProcessBitness":"x86","Runtime":"4.0.30319.42000"}
|
||||
2026-05-06T17:23:51.0229176+00:00 mx.register.begin {"ClientName":"MxFridaTrace-123"}
|
||||
2026-05-06T17:23:51.2542197+00:00 mx.register.end {"SessionHandle":1}
|
||||
2026-05-06T17:23:51.2550786+00:00 mx.additem.begin {"Tag":"TestChildObject.ScanState"}
|
||||
2026-05-06T17:23:51.2595630+00:00 mx.additem.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:23:51.2604744+00:00 mx.advise-supervisory.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:23:51.2632070+00:00 mx.advise-supervisory.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:23:51.4863989+00:00 mx.event.data-change {"SessionHandle":1,"ItemHandle":1,"Value":{"Type":"System.Boolean","Value":"True"},"Quality":192,"Timestamp":{"Type":"System.String","Value":"5/6/2026 1:23:51.471 PM"},"Status":[{"Success":-1,"Category":"MxCategoryOk","Source":"MxSourceRequestingLmx","Detail":0}]}
|
||||
2026-05-06T17:23:51.9480884+00:00 mx.suspend.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:23:51.9499173+00:00 mx.suspend.end {"Tag":"TestChildObject.ScanState","ItemHandle":1,"Status":{"Success":-1,"Category":"MxCategoryPending","Source":"MxSourceRequestingLmx","Detail":0}}
|
||||
2026-05-06T17:23:52.1856751+00:00 mx.event.operation-complete {"SessionHandle":1,"ItemHandle":1,"Status":[{"Success":-1,"Category":"MxCategoryOk","Source":"MxSourceRespondingLmx","Detail":0}]}
|
||||
2026-05-06T17:23:59.1669817+00:00 mx.unadvise.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:23:59.1678719+00:00 mx.unadvise.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:23:59.1678719+00:00 mx.removeitem.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:23:59.1678719+00:00 mx.removeitem.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:23:59.1678719+00:00 mx.unregister.begin {"SessionHandle":1}
|
||||
2026-05-06T17:24:03.0001612+00:00 mx.unregister.end {"SessionHandle":1}
|
||||
2026-05-06T17:24:03.0046705+00:00 harness.stop {}
|
||||
@@ -0,0 +1,3 @@
|
||||
frida=C:\Users\dohertj2\AppData\Local\Programs\Python\Python312\Scripts\frida.exe
|
||||
harness=C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe
|
||||
args=-f C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe -l C:\Users\dohertj2\Desktop\mxaccess\analysis\frida\mx-nmx-trace.js -- --scenario=activate-advised --tag=TestChildObject.ScanState --duration=8 --log=C:\Users\dohertj2\Desktop\mxaccess\captures\124-frida-activate-advised-instrumented\harness.log --client=MxFridaTrace-124
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,88 @@
|
||||
____
|
||||
/ _ | Frida 17.9.1 - A world-class dynamic instrumentation toolkit
|
||||
| (_| |
|
||||
> _ | Commands:
|
||||
/_/ |_| help -> Displays the help system
|
||||
. . . . object? -> Display information about 'object'
|
||||
. . . . exit/quit -> Exit
|
||||
. . . .
|
||||
. . . . More info at https://frida.re/docs/home/
|
||||
. . . .
|
||||
. . . . Connected to Local System (id=local)
|
||||
Spawning `C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe --scenario=activate-advised --tag=TestChildObject.ScanState --duration=8 --log=C:\Users\dohertj2\Desktop\mxaccess\captures\124-frida-activate-advised-instrumented\harness.log --client=MxFridaTrace-124`...
|
||||
Spawned `C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe --scenario=activate-advised --tag=TestChildObject.ScanState --duration=8 --log=C:\Users\dohertj2\Desktop\mxaccess\captures\124-frida-activate-advised-instrumented\harness.log --client=MxFridaTrace-124`. Resuming main thread!
|
||||
[Local::MxTraceHarness.exe ]-> {"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Write.variantA","base":"0x61b70000","rva":"0x12c0c","address":"0x61b82c0c","time":"2026-05-06T17:25:57.029Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Write.variantB","base":"0x61b70000","rva":"0x13280","address":"0x61b83280","time":"2026-05-06T17:25:57.029Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.WriteSecured.variantA","base":"0x61b70000","rva":"0x12f24","address":"0x61b82f24","time":"2026-05-06T17:25:57.029Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.WriteSecured.variantB","base":"0x61b70000","rva":"0x135fe","address":"0x61b835fe","time":"2026-05-06T17:25:57.029Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.AddBufferedItem","base":"0x61b70000","rva":"0x1121d","address":"0x61b8121d","time":"2026-05-06T17:25:57.029Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.SetBufferedUpdateInterval","base":"0x61b70000","rva":"0xfc80","address":"0x61b7fc80","time":"2026-05-06T17:25:57.030Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.AdviseSupervisory","base":"0x61b70000","rva":"0x142b4","address":"0x61b842b4","time":"2026-05-06T17:25:57.030Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Suspend","base":"0x61b70000","rva":"0x13d9c","address":"0x61b83d9c","time":"2026-05-06T17:25:57.030Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Activate","base":"0x61b70000","rva":"0x14028","address":"0x61b84028","time":"2026-05-06T17:25:57.031Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CProxy_ILMXProxyServerEvents2.Fire_OnBufferedDataChange","base":"0x61b70000","rva":"0x163c0","address":"0x61b863c0","time":"2026-05-06T17:25:57.031Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CUserConnectionCallback.OnSetAttributeResult","base":"0x61b70000","rva":"0x16b50","address":"0x61b86b50","time":"2026-05-06T17:25:57.031Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CUserConnectionCallback.OperationComplete","base":"0x61b70000","rva":"0x16d4b","address":"0x61b86d4b","time":"2026-05-06T17:25:57.032Z"}
|
||||
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.AuthenticateUser","base":"0x61b70000","rva":"0x1399f","address":"0x61b8399f","time":"2026-05-06T17:25:57.032Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"MxConnection.PrebindReference","base":"0x10000000","rva":"0xea780","address":"0x100ea780","time":"2026-05-06T17:26:02.100Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"MxConnection.UserRegisterPreboundReference","base":"0x10000000","rva":"0xe1920","address":"0x100e1920","time":"2026-05-06T17:26:02.101Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"IMxReference.GetMxHandle","base":"0x10000000","rva":"0x5f730","address":"0x1005f730","time":"2026-05-06T17:26:02.101Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","base":"0x10000000","rva":"0x8f8b0","address":"0x1008f8b0","time":"2026-05-06T17:26:02.101Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"PreboundReference.Resolve","base":"0x10000000","rva":"0x113d40","address":"0x10113d40","time":"2026-05-06T17:26:02.102Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"PreboundReference.OnPlatformResolveReferenceResults","base":"0x10000000","rva":"0x1155a0","address":"0x101155a0","time":"2026-05-06T17:26:02.102Z"}
|
||||
{"event":"hook.installed","module":"Lmx.dll","name":"PreboundReference.OnSetAttributeResult","base":"0x10000000","rva":"0x114a90","address":"0x10114a90","time":"2026-05-06T17:26:02.103Z"}
|
||||
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","base":"0x63ae0000","rva":"0x10996","address":"0x63af0996","time":"2026-05-06T17:26:02.191Z"}
|
||||
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","base":"0x63ae0000","rva":"0x112da","address":"0x63af12da","time":"2026-05-06T17:26:02.192Z"}
|
||||
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","base":"0x63ae0000","rva":"0x15169","address":"0x63af5169","time":"2026-05-06T17:26:02.192Z"}
|
||||
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequestEx","base":"0x63ae0000","rva":"0x159c3","address":"0x63af59c3","time":"2026-05-06T17:26:02.193Z"}
|
||||
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x8f272b0","outPtr":"0xafe224","inWords":[65537,65537,0,0,0,0],"time":"2026-05-06T17:26:02.227Z"}
|
||||
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xafe224","handle":{"raw":"01 00 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00","w0":65537,"w1":65537,"w2":0,"w3":0,"w4":0},"retval":"0xafe224","time":"2026-05-06T17:26:02.227Z"}
|
||||
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x8f272b0","outPtr":"0xafe224","inWords":[65537,65537,0,0,0,0],"time":"2026-05-06T17:26:02.228Z"}
|
||||
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xafe224","handle":{"raw":"01 00 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00","w0":65537,"w1":65537,"w2":0,"w3":0,"w4":0},"retval":"0xafe224","time":"2026-05-06T17:26:02.228Z"}
|
||||
{"event":"lmx.prebind.enter","module":"Lmx.dll","name":"MxConnection.PrebindReference","self":"0x8f2f934","outPtr":"0xafe7f8","referencePtr":"0xafe82c","reference":"TestChildObject.ScanState","time":"2026-05-06T17:26:02.247Z"}
|
||||
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x8f341e8","outPtr":"0xafe760","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xafe760","time":"2026-05-06T17:26:02.247Z"}
|
||||
{"event":"lmx.prebound-resolve.enter","module":"Lmx.dll","name":"PreboundReference.Resolve","prebound":{"ptr":"0x8f2fc60","referenceString":{"length":25,"capacity":31,"value":"TestChildObject.ScanState"},"contextString":{"length":0,"capacity":7,"value":""},"auxString":{"length":0,"capacity":7,"value":""},"mxReference":"0x8f34f50","flags10":1124099840,"word14":2,"word4c":131073,"word54":131786164,"word58":0,"word5c":0,"word60":0,"word64":150106800,"word68":0,"word6c":0,"worda0":0,"worda4":0,"status":3,"flagb0":0,"errorText":"","raw":"08 64 19 10 f0 63 19 10 00 6f 00 6e e8 63 19 10 00 67 00 43 02 00 00 00 c0 4e f3 08 00 65 00 00 00 02 00 00 00 00 00 02 19 00 00 00 1f 00 00 00 00 00 00 01 00 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 01 00 02 00 50 4f f3 08 b4 e5 da 07 00 00 00 00 00 00 00 00 00 00 00 00 b0 72 f2 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 04 79 d4 00 00 00 00 00"},"time":"2026-05-06T17:26:02.247Z"}
|
||||
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x8f2fcb0","outPtr":"0xafe6f0","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xafe6f0","time":"2026-05-06T17:26:02.248Z"}
|
||||
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x8f2fcb0","outPtr":"0xafe6f0","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xafe6f0","time":"2026-05-06T17:26:02.248Z"}
|
||||
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x8f2fcb0","outPtr":"0xafe6f0","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xafe6f0","time":"2026-05-06T17:26:02.248Z"}
|
||||
{"event":"lmx.prebound-resolve.leave","module":"Lmx.dll","name":"PreboundReference.Resolve","prebound":{"ptr":"0x8f2fc60","referenceString":{"length":25,"capacity":31,"value":"TestChildObject.ScanState"},"contextString":{"length":0,"capacity":7,"value":""},"auxString":{"length":0,"capacity":7,"value":""},"mxReference":"0x8f34f50","flags10":1124099840,"word14":2,"word4c":131073,"word54":131786164,"word58":0,"word5c":0,"word60":0,"word64":150106800,"word68":0,"word6c":0,"worda0":0,"worda4":0,"status":3,"flagb0":0,"errorText":"","raw":"08 64 19 10 f0 63 19 10 00 6f 00 6e e8 63 19 10 00 67 00 43 02 00 00 00 c0 4e f3 08 00 65 00 00 00 02 00 00 00 00 00 02 19 00 00 00 1f 00 00 00 00 00 00 01 00 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 01 00 02 00 50 4f f3 08 b4 e5 da 07 00 00 00 00 00 00 00 00 00 00 00 00 b0 72 f2 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 04 79 d4 00 00 00 00 00"},"retval":"0x70d01e01","time":"2026-05-06T17:26:02.249Z"}
|
||||
{"event":"lmx.prebind.leave","module":"Lmx.dll","name":"MxConnection.PrebindReference","handle":1,"time":"2026-05-06T17:26:02.250Z"}
|
||||
{"event":"call.enter","module":"LmxProxy.dll","name":"CLMXProxyServer.AdviseSupervisory","address":"0x61b842b4","ecx":"0xafe8b0","args":["0x5f592d0","0x1","0x1","0xb68f4ff0","0x744d4704"],"time":"2026-05-06T17:26:02.251Z"}
|
||||
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x8f272b0","outPtr":"0xafe730","inWords":[65537,327682,186166,655465,37447,0],"time":"2026-05-06T17:26:02.251Z"}
|
||||
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xafe730","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xafe730","time":"2026-05-06T17:26:02.252Z"}
|
||||
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x8f272b0","outPtr":"0xafd3c4","inWords":[65537,327682,186166,655465,37447,0],"time":"2026-05-06T17:26:02.252Z"}
|
||||
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xafd3c4","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xafd3c4","time":"2026-05-06T17:26:02.252Z"}
|
||||
{"event":"call.leave","module":"LmxProxy.dll","name":"CLMXProxyServer.AdviseSupervisory","retval":"0x0","time":"2026-05-06T17:26:02.253Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x8f2c9d8","0x1","0x1","0x1","0x2","0x0","0x13a","0x8f2fd20","0xafe574","0x1c6cdd4e"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":1,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":314,"ptr":"0x8f2fd20","hex":"17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 6a 00 00 00 40 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 a8 f3 f2 08 1f 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 00 00 01 00 00 00 17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 76 00 00 00 4c 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 28 fa f2 08 20 01 00 02 00 00 00"}],"time":"2026-05-06T17:26:02.360Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x8f2c9d8","args":["0x1","0x1","0x1","0x168","0xa4e9020","0x44e158a0","0x8f2f8ec","0x8f2f8dc","0x63b0dd04","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":360,"ptr":"0xa4e9020","hex":"01 00 3a 01 00 00 00 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 02 00 00 30 75 00 00 17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 6a 00 00 00 40 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 a8 f3 f2 08 1f 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 00 00 01 00 00 00 17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 76 00 00 00 4c 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 28 fa f2 08 20 01 00 02 00 00 00"}],"time":"2026-05-06T17:26:02.363Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:26:02.363Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:26:02.363Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x8f2c9d8","0x1","0x1","0x2","0x2","0x0","0x27","0x8f30810","0xafe574","0x1c6cdd4e"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":2,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":39,"ptr":"0x8f30810","hex":"1f 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 00 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:26:02.364Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x8f2c9d8","args":["0x1","0x1","0x2","0x55","0xa4e9020","0x44e158a0","0x8f369d4","0x8f369c4","0x63b0dd04","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":85,"ptr":"0xa4e9020","hex":"01 00 27 00 00 00 00 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 1f 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 00 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:26:02.364Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:26:02.364Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:26:02.365Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x8f2c9d8","args":["0x2c2","0x7855de0","0x763e9c0","0x769cedd8","0x8f2c9e4","0x2c2","0x7855de0","0x206","0x3","0x7890dbc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":706,"ptr":"0x7855de0","hex":"01 00 94 02 00 00 00 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 02 02 00 00 30 75 00 00 40 1f 50 80 08 a6 00 00 00 40 00 00 91 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 18 00 00 00 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 00 00 28 00 00 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 53 f2 9a 00 6a 00 0a 00 5f f1 00 00 01 6c 00 00 00 41 00 6e 00 20 00 69 00 6e 00 74 00 65 00 72 00 6e 00 61 00 6c 00 20 00 65 00 72 00 72 00 6f 00 72 00 20 00 6f 00 63 00 63 00 75 00 72 00 72 00 65 00 64 00 20 00 69 00 6e 00 20 00 74 00 68 00 65 00 20 00 42 00 61 00 73 00 65 00 20 00 52 00 75 00 6e 00 74 00 69 00 6d 00 65 00 20 00 4f 00 62 00 6a 00 65 00 63 00 74 00 00 00 1f 00 00 50 80 01 00 01 00 01 00 30 75 00 00 c1 7f b2 2c 25 f4 17 42 bc df 76 e6 78 49 01 0e fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 40 1f 50 80 08 be 00 00 00 4c 00 00 91 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 18 00 00 00 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 00 00 34 00 00 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 53 f2 9a 00 6b 00 0a 00 87 3a 00 00 01 6c 00 00 00 41 00 6e 00 20 00 69 00 6e 00 74 00 65 00 72 00 6e 00 61 00 6c 00 20 00 65 00 72 00 72 00 6f 00 72 00 20 00 6f 00 63 00 63 00 75 00 72 00 72 00 65 00 64 00 20 00 69 00 6e 00 20 00 74 00 68 00 65 00 20 00 42 00 61 00 73 00 65 00 20 00 52 00 75 00 6e 00 74 00 69 00 6d 00 65 00 20 00 4f 00 62 00 6a 00 65 00 63 00 74 00 00 00 20 00 00 50 80 01 00 01 00 01 00 30 75 00 00"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7890dbc","hex":"90 f9 db"}],"time":"2026-05-06T17:26:02.379Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:26:02.380Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x8f2c9d8","args":["0x97","0x7cfca08","0x763e9c0","0x769cedd8","0x8f2c9e4","0x97","0x7cfca08","0x206","0x3","0x7890dbc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":151,"ptr":"0x7cfca08","hex":"01 00 69 00 00 00 00 00 00 00 3b 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 02 00 00 30 75 00 00 32 01 00 02 00 00 00 c1 7f b2 2c 25 f4 17 42 bc df 76 e6 78 49 01 0e fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 01 00 00 00 03 00 00 00 c0 00 b0 fd 44 d6 75 dd dc 01 06 0a 00 00 00 00 99 8c 8a 6e da dc 01 00 00 02 00 00 00 03 00 00 00 c0 00 f0 99 45 d6 75 dd dc 01 06 0a 00 00 00 00 fb 56 ce 19 dd dc 01 00 00"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7890dbc","hex":"90 f9 db"}],"time":"2026-05-06T17:26:02.381Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:26:02.381Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x8f2c9d8","args":["0x5c","0xd67de8","0x763e9c0","0x769cedd8","0x8f2c9e4","0x5c","0xd67de8","0x206","0x3","0x7890dbc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":92,"ptr":"0xd67de8","hex":"01 00 2e 00 00 00 00 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 02 02 00 00 30 75 00 00 00 00 50 80 01 00 01 00 02 00 30 75 00 00 17 59 01 a9 16 2a 80 40 99 d9 d4 80 28 2c b7 2a fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7890dbc","hex":"90 f9 db"}],"time":"2026-05-06T17:26:02.412Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:26:02.412Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x8f2c9d8","args":["0x69","0x7872b38","0x763e9c0","0x769cedd8","0x8f2c9e4","0x69","0x7872b38","0x206","0x3","0x7890dbc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":105,"ptr":"0x7872b38","hex":"01 00 3b 00 00 00 00 00 00 00 3c 1a 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 02 00 00 30 75 00 00 32 01 00 01 00 00 00 17 59 01 a9 16 2a 80 40 99 d9 d4 80 28 2c b7 2a fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 03 00 00 00 00 00 00 00 c0 00 c0 3e 0b d8 75 dd dc 01 01 ff"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7890dbc","hex":"90 f9 db"}],"time":"2026-05-06T17:26:02.414Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:26:02.414Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x8f2c9d8","args":["0x1","0x1","0x1","0x2e","0xa4e9020","0x44e15894","0x8f272b0","0x0","0x0","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":46,"ptr":"0xa4e9020","hex":"01 00 00 00 00 00 00 00 00 00 3b 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 01 00 00 00 02 02 00 00 30 75 00 00"}],"time":"2026-05-06T17:26:02.458Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:26:02.459Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x8f2c9d8","args":["0x1","0x1","0x2","0x2e","0xa4e9020","0x44e15894","0x8f272b0","0x0","0x0","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":46,"ptr":"0xa4e9020","hex":"01 00 00 00 00 00 00 00 00 00 3c 1a 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 02 02 00 00 30 75 00 00"}],"time":"2026-05-06T17:26:02.475Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:26:02.476Z"}
|
||||
{"event":"mx.activate.begin","module":"LmxProxy.dll","name":"CLMXProxyServer.Activate","address":"0x61b84028","ecx":"0xafe8ac","serverHandle":1,"itemHandle":1,"statusOutPtr":"0xafec9c","time":"2026-05-06T17:26:02.982Z"}
|
||||
{"event":"mx.activate.end","module":"LmxProxy.dll","name":"CLMXProxyServer.Activate","retval":"0x0","serverHandle":1,"itemHandle":1,"status":{"raw":"ff ff af 00 00 00 00 00","success":-1,"category":175,"detectedBy":0,"detail":0},"time":"2026-05-06T17:26:02.982Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x8f2c9d8","0x1","0x1","0x1","0x2","0x0","0x3a","0x8f30348","0xafe730","0x1c6cdf8a"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":1,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":58,"ptr":"0x8f30348","hex":"21 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 01 00 53 f2 9a 00 6a 00 0a 00 5f f1 00 00 01 00 00 00 22 01 00 01 00 53 f2 9a 00 6b 00 0a 00 87 3a 00 00 02 00 00 00"}],"time":"2026-05-06T17:26:10.206Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x8f2c9d8","args":["0x1","0x1","0x1","0x68","0xa4e9020","0x44e15ae4","0x8f36fac","0x8f36f9c","0x63b0dd04","0x0"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":104,"ptr":"0xa4e9020","hex":"01 00 3a 00 00 00 00 00 00 00 03 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 02 00 00 30 75 00 00 21 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 01 00 53 f2 9a 00 6a 00 0a 00 5f f1 00 00 01 00 00 00 22 01 00 01 00 53 f2 9a 00 6b 00 0a 00 87 3a 00 00 02 00 00 00"}],"time":"2026-05-06T17:26:10.207Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:26:10.207Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:26:10.207Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x8f2c9d8","0x1","0x1","0x2","0x2","0x0","0x25","0x8f302b8","0xafe730","0x1c6cdf8a"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":2,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":37,"ptr":"0x8f302b8","hex":"21 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:26:10.208Z"}
|
||||
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x8f2c9d8","args":["0x1","0x1","0x2","0x53","0xa4e9020","0x44e15ae4","0x8f36e2c","0x8f36e1c","0x63b0dd04","0x0"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":83,"ptr":"0xa4e9020","hex":"01 00 25 00 00 00 00 00 00 00 04 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 21 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:26:10.209Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:26:10.210Z"}
|
||||
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:26:10.210Z"}
|
||||
Process terminated
|
||||
|
||||
Thank you for using Frida!
|
||||
@@ -0,0 +1,17 @@
|
||||
2026-05-06T17:25:56.9336608+00:00 harness.start {"Scenario":"activate-advised","ClientName":"MxFridaTrace-124","Tags":["TestChildObject.ScanState"],"ItemContext":"","WriteType":"string","WriteValue":"","WriteValues":[],"UserId":0,"CurrentUserId":0,"VerifierUserId":0,"UserGuid":"","AuthUser":"","AuthenticateBeforeWrite":false,"UseAuthenticatedUserAsVerifier":false,"UsePlainAdvise":false,"WriteTimestamp":"","WriteDelayMilliseconds":750,"WriteIntervalMilliseconds":500,"BufferedUpdateInterval":1000,"DurationSeconds":8,"ProcessBitness":"x86","Runtime":"4.0.30319.42000"}
|
||||
2026-05-06T17:26:02.0166476+00:00 mx.register.begin {"ClientName":"MxFridaTrace-124"}
|
||||
2026-05-06T17:26:02.2451960+00:00 mx.register.end {"SessionHandle":1}
|
||||
2026-05-06T17:26:02.2451960+00:00 mx.additem.begin {"Tag":"TestChildObject.ScanState"}
|
||||
2026-05-06T17:26:02.2506300+00:00 mx.additem.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:26:02.2506300+00:00 mx.advise-supervisory.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:26:02.2533435+00:00 mx.advise-supervisory.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:26:02.4738071+00:00 mx.event.data-change {"SessionHandle":1,"ItemHandle":1,"Value":{"Type":"System.Boolean","Value":"True"},"Quality":192,"Timestamp":{"Type":"System.String","Value":"5/6/2026 1:26:02.460 PM"},"Status":[{"Success":-1,"Category":"MxCategoryOk","Source":"MxSourceRequestingLmx","Detail":0}]}
|
||||
2026-05-06T17:26:02.9814081+00:00 mx.activate.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:26:02.9832463+00:00 mx.activate.end {"Tag":"TestChildObject.ScanState","ItemHandle":1,"Status":{"Success":-1,"Category":"MxCategoryOk","Source":"MxSourceRequestingLmx","Detail":0}}
|
||||
2026-05-06T17:26:10.2003645+00:00 mx.unadvise.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:26:10.2012649+00:00 mx.unadvise.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:26:10.2012649+00:00 mx.removeitem.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:26:10.2012649+00:00 mx.removeitem.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
|
||||
2026-05-06T17:26:10.2012649+00:00 mx.unregister.begin {"SessionHandle":1}
|
||||
2026-05-06T17:26:12.7977621+00:00 mx.unregister.end {"SessionHandle":1}
|
||||
2026-05-06T17:26:12.8031645+00:00 harness.stop {}
|
||||
@@ -111,10 +111,13 @@ Findings, layer by layer (the wire bytes flow inward; the synthesis flows outwar
|
||||
|
||||
**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 **(SETTLED 2026-05-06 — F50 live capture proves Suspend is server-side wire op `0x2D`; Activate against a non-suspended item is client-side only)**
|
||||
|
||||
**Severity: P2** (downgraded from P1 — client-side acceptance criteria are
|
||||
now documented; LMX-proxy wire emission remains unconfirmed)
|
||||
**Severity: P3** (downgraded from P2 — wire behaviour now characterised, no implementation gap blocking M6 / V1 since `Session::suspend` / `Session::activate` aren't part of the public API today; if/when added, the `0x2D` opcode is the encoder target).
|
||||
|
||||
**Settled (2026-05-06):** F50 captured `123-frida-suspend-advised-instrumented/` and `124-frida-activate-advised-instrumented/`. See `docs/F50-suspend-activate-evidence.md` for the byte-level evidence. Summary:
|
||||
- **Suspend** emits NMX `PutRequest` with command byte `0x2D` ~140ms after the LMX-proxy entry hook, body shape matches AdviseSupervisory's `<command:1> <version:2> <correlation_id:16> <body:22>` family.
|
||||
- **Activate** (against a non-suspended item, the only scenario the harness sequences) returns synchronously client-side with no wire traffic; same client-side behaviour F44 documented for capture 077.
|
||||
|
||||
**Status (2026-05-06): PARTIALLY OBSERVED — Frida hooks ready, live capture pending.**
|
||||
F44's evidence walk on
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# F48 publish dry-run validation — 2026-05-06
|
||||
|
||||
> **Note (2026-05-06):** This project is internal-use only and is **not** scheduled to publish to crates.io. F48's actual publish goal is out of scope. This document is retained as a workspace-hygiene record — `cargo package --list` per crate confirms each tarball would assemble cleanly (source + tests + small fixtures only, no captures or big files), which is useful regardless of whether an actual publish ever happens. The "What the actual V1 publish needs" section at the bottom is kept as a recipe in case this ever changes.
|
||||
|
||||
This document captures the per-crate `cargo publish --dry-run` outcome on the workspace at `version = "0.0.0"`. Run from `rust/`.
|
||||
|
||||
## Tier 1 — leaves (no internal deps)
|
||||
|
||||
```text
|
||||
$ cargo publish --dry-run -p mxaccess-codec --allow-dirty
|
||||
Finished `dev` profile [unoptimized + debuginfo] target(s)
|
||||
Uploading mxaccess-codec v0.0.0
|
||||
warning: aborting upload due to dry run ← OK
|
||||
|
||||
$ cargo publish --dry-run -p mxaccess-rpc --allow-dirty ← OK
|
||||
$ cargo publish --dry-run -p mxaccess-asb-nettcp --allow-dirty ← OK
|
||||
```
|
||||
|
||||
All three pass. The `cargo package` step assembles the source tarball without errors; `--dry-run` aborts only at the network upload step.
|
||||
|
||||
## Tiers 2 + 3 — dependent crates
|
||||
|
||||
```text
|
||||
$ cargo publish --dry-run -p mxaccess-galaxy --allow-dirty
|
||||
Caused by:
|
||||
no matching package named `mxaccess-codec` found
|
||||
location searched: crates.io index
|
||||
required by package `mxaccess-galaxy v0.0.0`
|
||||
```
|
||||
|
||||
Identical "no matching package" failure for:
|
||||
- `mxaccess-galaxy`, `mxaccess-callback`, `mxaccess-asb` (tier 2)
|
||||
- `mxaccess-nmx`, `mxaccess`, `mxaccess-compat` (tier 3)
|
||||
|
||||
This is **expected** — the workspace internal deps are pinned at `version = "0.0.0"` (placeholder for the as-yet-unpublished V1 cut). Cargo's registry lookup happens even with `--no-verify`, and `0.0.0` won't exist on crates.io until the leaves are actually published. The dependent crates will dry-run cleanly after each upstream tier lands.
|
||||
|
||||
## Package contents
|
||||
|
||||
`cargo package -p <crate> --list` confirms each crate's tarball includes only source, tests, and fixture data — no captures, decompiled binaries, or accidental large files.
|
||||
|
||||
| Crate | File count | Notes |
|
||||
|---|---|---|
|
||||
| `mxaccess-codec` | 27 | source + 2 round-trip fixture binaries (~1KB each) |
|
||||
| `mxaccess-rpc` | 16 | source only |
|
||||
| `mxaccess-asb-nettcp` | 12 | source only |
|
||||
| `mxaccess-galaxy` | 11 | source only |
|
||||
| `mxaccess-callback` | 9 | source only |
|
||||
| `mxaccess-asb` | 14 | source only |
|
||||
| `mxaccess-nmx` | 7 | source only |
|
||||
| `mxaccess` | 18 | source + 7 examples |
|
||||
| `mxaccess-compat` | varies | source + 5 live tests |
|
||||
|
||||
## If a publish ever does become a goal — recipe
|
||||
|
||||
**Currently out of scope per maintainer 2026-05-06**, but kept here so future-them doesn't have to re-derive the steps:
|
||||
|
||||
1. Bump workspace version `0.0.0` → `0.1.0` in `rust/Cargo.toml` `[workspace.package]`.
|
||||
2. For each crate's `[dependencies]` block, bump the workspace-internal `version = "0.0.0"` pins to `version = "0.1.0"` (path deps can stay).
|
||||
3. Publish in tier order (1 → 2 → 3). Wait for crates.io to index each tier (~30–60s) before starting the next.
|
||||
4. After all 9 are live, run `cargo install mxaccess` from a fresh checkout — should resolve cleanly without `--locked`.
|
||||
5. Tag `git tag v0.1.0 && git push origin v0.1.0`.
|
||||
|
||||
## Open observations
|
||||
|
||||
- The `--allow-dirty` flag was used because the workspace has uncommitted edits during this validation pass; the actual publish should run from a clean working tree without that flag.
|
||||
- `Cargo.lock` is included in the published tarball for binary-target crates (notably `mxaccess` ships examples). This is the cargo default for crates with executables; library-only crates don't need it but cargo includes it anyway under the modern resolver.
|
||||
- No `package.exclude` rules were tripped: the `tests/fixtures/m6-buffered/*.bin` files in `mxaccess-codec` are tiny (round-trip fixtures, not big captures) and are deliberately shipped because the parity tests reference them.
|
||||
+90
-37
@@ -7,46 +7,17 @@ move to `## Resolved` with a date + commit hash.
|
||||
## Open
|
||||
|
||||
### F48 — Execute `cargo publish` for the V1 release cut
|
||||
**Severity:** P1 — V1 release driver. F43 only validated dry-run for the leaf crates; the actual publish to crates.io has not happened.
|
||||
**Source:** `design/60-roadmap.md:100` (M6 DoD bullet 6 — "Release: cargo publish all crates"); `CHANGELOG.md` "Publish order" section.
|
||||
**Depends on:** F43 (dry-run validation), F49 (live verification of M6 features before publishing them).
|
||||
**Status:** **Out of scope — internal usage only, no crates.io publish planned.** Confirmed 2026-05-06 by maintainer. The workspace stays at `version = "0.0.0"` indefinitely; consumers depend via path or git, not crates.io. F43's dry-run validation (`design/F48-publish-dry-run.md`) is retained as a workspace-hygiene check (each crate's `cargo package --list` produces a clean tarball, no accidental captures/big files), not as release prep.
|
||||
|
||||
**Scope.** Publish all 9 workspace crates to crates.io in dependency order:
|
||||
1. `mxaccess-codec`, 2. `mxaccess-rpc`, 3. `mxaccess-asb-nettcp` (leaves — no internal deps)
|
||||
4. `mxaccess-galaxy`, 5. `mxaccess-callback`, 6. `mxaccess-asb` (single-internal-dep tier)
|
||||
7. `mxaccess-nmx`, 8. `mxaccess`, 9. `mxaccess-compat` (multi-internal-dep tier)
|
||||
|
||||
Between each publish: wait for the crate to be indexed before the next one's `cargo publish` runs (the registry-lookup race that broke the dependent dry-runs in F43).
|
||||
|
||||
**Definition of done:**
|
||||
1. All 9 crates exist on crates.io at the same workspace version (likely 0.1.0 — bump from the 0.0.0 placeholder before the cut).
|
||||
2. `cargo install mxaccess` resolves a clean dependency tree from a fresh registry lookup (no `--locked` workaround).
|
||||
3. Tag the V1 release commit (`git tag v0.1.0`) and push the tag so the CHANGELOG anchors to a stable ref.
|
||||
|
||||
**Resolves when:** crates.io shows all 9 crates published + the V1 tag is pushed.
|
||||
|
||||
### F49 — Live verification sweep for the M6 features
|
||||
**Severity:** P1 — closes the live-evidence gap for the M6 work that landed unit-only this session.
|
||||
**Source:** F36, F40, F45, F47, F54 closeouts — each ships with unit tests but most were not exercised against the live AVEVA install in this session.
|
||||
**Blocked-by:** F12 hardening (`Session::connect_nmx_auto` returns `RPC_S_SERVER_UNAVAILABLE` (1722) under `cargo test`'s tokio multi-thread runtime — see "Live attempt 2026-05-06" below). The COM-activation path itself works in isolation (`cargo run -p mxaccess-rpc --example com-marshal-probe --features windows-com` succeeds), so the failure is downstream — likely a COM apartment threading issue when CoInitializeEx runs on a tokio worker thread.
|
||||
|
||||
**Scope.** Run the following against the live AVEVA host with `MX_LIVE=1`:
|
||||
1. **F36 buffered subscribe** — `cargo run -p mxaccess --example subscribe-buffered -- --tag TestChildObject.TestInt`. Confirm `OnBufferedDataChange`-rate updates flow at the configured cadence; capture wire bytes via `analysis/frida/mx-nmx-trace.js` and confirm exactly one `RegisterReference` (`0x10`) frame with `.property(buffer)` suffix, no separate `SetBufferedUpdateInterval` RPC, and no separate `AdviseSupervisory` follow-up.
|
||||
2. **F45 recovery replay for buffered** — start the `subscribe-buffered` example, force a `Session::recover_connection` mid-flight (e.g. via a `wwtools` helper that bumps the NMX TCP socket), assert the post-recovery NMX traffic carries an `RegisterReference` (NOT `AdviseSupervisory`) with the same correlation id and `.property(buffer)` suffix.
|
||||
3. **F47 buffered unsubscribe skip** — instrument `Session::unsubscribe` with a `tracing::debug` log line on the buffered branch, run the example to completion + drop, confirm no `UnAdvise` frame in the wire trace.
|
||||
4. **F40 metrics** — install a `metrics` exporter (`metrics-exporter-prometheus` is the lightest), run `connect-write-read` + `subscribe` examples with `--features metrics`, confirm at least one counter increment and one histogram observation per metric name in the registered set.
|
||||
5. **F54 OnWriteComplete (LmxClient round-trip)** — scaffold lives at `crates/mxaccess-compat/tests/lmx_write_complete_live.rs`. Run `cargo test -p mxaccess-compat --features live-windows-com --test lmx_write_complete_live -- --ignored --nocapture` to drive `LmxClient::write` → drain `client.on_write_complete()` and assert the `WriteCompleteEvent { server_handle, item_handle, statuses, is_during_recovery }` shape matches `LMX_OnWriteComplete(int hLMXServerHandle, int phItemHandle, ref MXSTATUS_PROXY[] pVars)`.
|
||||
|
||||
**Live attempt 2026-05-06.** Steps 1-4 not run yet. Step 5 attempted; the test compiled and ran past Frida-style `--probe-resolve-oxid-managed-ntlm-integrity` resolution + `--probe-remqi-managed` IPID extraction, but `connect_nmx_auto` (preferred path) and `connect_nmx` (fallback with probe-resolved IPID) both fail with `Status { detail: 1722 }` (RPC_S_SERVER_UNAVAILABLE). The .NET `MxNativeClient.Probe --probe-session-write` runs the same scenario successfully end-to-end against the same AVEVA install, so the wire is functional and the failure is Rust-port specific. Documented as the F12 hardening followup; the F54 unit-level integration tests (`router_populates_operation_status_context_from_pending_ops_fifo` + `write_handle_correlates_with_router_emitted_status`) cover the F54 logic exhaustively at the layer boundaries.
|
||||
|
||||
**Definition of done:**
|
||||
1. Per-feature evidence summary in `docs/M6-live-verification.md` (one paragraph per feature with the wire-trace excerpt or metrics-exporter snapshot).
|
||||
2. If any feature fails live: file a sub-followup with the captured failure and link it from the evidence doc.
|
||||
3. F12's tokio-runtime COM activation issue resolved (the `connect_nmx_auto` 1722 error above) so the live tests can actually run.
|
||||
|
||||
**Resolves when:** all five features have a live evidence row + no sub-followups remain unresolved.
|
||||
If this changes (e.g. internal consumer wants registry-style versioning via a private cargo registry), the V1 publish recipe in `design/F48-publish-dry-run.md` describes the steps. For now: no work needed.
|
||||
|
||||
### F50 — Run the F46 Suspend/Activate Frida capture live
|
||||
**Status:** **Resolved 2026-05-06.** Two captures landed under `captures/123-frida-suspend-advised-instrumented/` (suspend-advised scenario) and `captures/124-frida-activate-advised-instrumented/` (activate-advised scenario). Per-byte evidence in `docs/F50-suspend-activate-evidence.md`; R5 in `design/70-risks-and-open-questions.md` moved to settled.
|
||||
|
||||
**Verdict:**
|
||||
- **Suspend** is server-side: emits NMX `PutRequest` with command `0x2D` ~140ms after the LMX-proxy entry, body `2d 01 00 + correlation_id + 22 bytes` (same shape family as `0x1F` AdviseSupervisory).
|
||||
- **Activate** against a non-suspended item is client-side only — no wire traffic, returns Success synchronously. The harness `activate-advised` scenario doesn't sequence Suspend-then-Activate; if direct evidence for Activate-after-Suspend is needed later, add a new scenario to `MxTraceHarness/Program.cs`.
|
||||
|
||||
**Severity:** P3 — residual from F46 (script ready, capture not yet run).
|
||||
**Source:** F46 closeout (`design/followups.md`) + `analysis/frida/mx-nmx-trace.js` header procedure.
|
||||
|
||||
@@ -86,6 +57,20 @@ 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.
|
||||
|
||||
### F53 — Enable `#![warn(missing_docs)]` workspace-wide
|
||||
**Status:** Consumer crates resolved 2026-05-06: `#![warn(missing_docs)]` enabled on `mxaccess` and `mxaccess-compat` lib roots, every public item now carries at least a one-line doc comment, `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` clean. Protocol crates deliberately deferred per the strategy paragraph below — measured the magnitude on 2026-05-06 by enabling the lint on each:
|
||||
|
||||
| Crate | Missing-docs warnings |
|
||||
|---|---|
|
||||
| `mxaccess-asb` | 422 |
|
||||
| `mxaccess-nmx` | 398 |
|
||||
| `mxaccess-callback` | 371 |
|
||||
| `mxaccess-galaxy` | 229 |
|
||||
| `mxaccess-codec` | 205 |
|
||||
| `mxaccess-rpc` | 147 |
|
||||
| `mxaccess-asb-nettcp` | 111 |
|
||||
| **Total** | **1883** |
|
||||
|
||||
Most of those are protocol-internal types (struct fields, enum variants on wire-shape records) whose meaning is already documented at the consumer-facing layer. Filling 1883 one-liners adds noise without consumer value, and turning them into errors (`RUSTDOCFLAGS="-D warnings"`) would block routine `cargo doc` runs. Lint stays off on protocol crates indefinitely; if a future contributor wants per-crate enforcement, they can re-introduce on a per-module basis with `#![allow(missing_docs)]` exemptions for the protocol-internal modules.
|
||||
**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.
|
||||
|
||||
@@ -98,7 +83,75 @@ Between each publish: wait for the crate to be indexed before the next one's `ca
|
||||
|
||||
**Resolves when:** the lint is on and the workspace doc build is warning-clean with it.
|
||||
|
||||
### F56 — `subscribe` / `subscribe_buffered` complete on the wire but never receive `0x33` DataUpdate frames
|
||||
**Status:** **Resolved 2026-05-06.** See Resolved section below for the full closeout.
|
||||
|
||||
Root cause: `Session::subscribe` and `Session::subscribe_buffered_nmx` were missing the `INmxService2::Connect` + `AddSubscriberEngine` round-trip that the .NET reference's `MxNativeSession.EnsurePublisherConnected` (`cs:516-526`) issues before the first advise against a given publishing engine. Without that pair of RPCs, NmxSvc accepts the subscription registration but the publishing engine never knows our engine is subscribed — so no `0x33` DataUpdate frames flow.
|
||||
|
||||
Diagnosed via wwtools/aalogcli: the `[Warning] NmxSvc | NmxCallback->DataReceived ... failed with error 0x{N}` log lines turned out to be NmxSvc's normal log spam where N is the bufferSize, NOT an actual error — the .NET reference's own probe triggers identical entries while still receiving `0x33` DataUpdate frames successfully. The real issue was that those frames never started being sent in the first place.
|
||||
|
||||
Fix landed:
|
||||
- `SessionInner::publisher_endpoints` — per-session `HashMap<(platform_id, engine_id), ()>` cache mirroring `MxNativeSession._publisherEndpoints`.
|
||||
- `Session::ensure_publisher_connected(platform_id, engine_id)` — issues `INmxService2::Connect(local_engine, galaxy, platform, engine)` then `AddSubscriberEngine(engine, galaxy, source_platform, local_engine)`, once per publisher endpoint per session.
|
||||
- `Session::subscribe` and `Session::subscribe_buffered_nmx` — both call `ensure_publisher_connected` BEFORE the wire advise.
|
||||
- `subscribe_buffered_nmx` — additionally issues `AdviseSupervisory` after `RegisterReference`. The .NET reference's `RegisterBufferedItemAsync` only calls RegisterReference, but on this AVEVA install RegisterReference alone produces the registration result + heartbeat callbacks without ever starting DataUpdate dispatch; AdviseSupervisory unblocks the dispatch. Difference may be version-specific.
|
||||
|
||||
Live verification passes for both paths against `TestMachine_001.TestChangingInt`:
|
||||
- `cargo test -p mxaccess-compat --features live-windows-com --test plain_subscribe_live` — receives `0x32` SubscriptionStatus + sequence of `0x33` DataUpdate frames.
|
||||
- `cargo test -p mxaccess-compat --features live-windows-com --test buffered_subscribe_live` — same.
|
||||
|
||||
Both tests assert on the raw `Session::callbacks()` broadcast (NMX subscription messages) rather than the typed `Subscription::next` (DataChange) path because `TestChangingInt` on this Galaxy is configured with `quality=0x00C0 (Uncertain) value=null`, so the typed path filters every record. The test gate is "wire-level subscription works"; what the engine reports as the actual value is downstream-Galaxy state, out of scope for the Rust port.
|
||||
|
||||
Real codec fixes ALSO landed in this session as part of F56 investigation (independent from the resolution above):
|
||||
- `NmxSubscriptionMessage::try_parse_process_data_received_body` — peels the `ProcessDataReceived` envelope before calling `parse_inner`. The router previously called `parse_inner` directly on wire bytes, which would have silently dropped any `0x33` even if one arrived.
|
||||
- `NmxReferenceRegistrationResultMessage::try_parse_process_data_received_body` + router branch — drops `0x11` registration-result frames cleanly.
|
||||
- `Session::subscribe_buffered_nmx` — split-form (object, attribute) wire body + per-session monotonic `item_handle` counter (mirrors `MxNativeCompatibilityServer.AddBufferedItemAsync`'s `_nextItemHandle++`).
|
||||
|
||||
**Severity:** P1 — blocks F49 step 1 (F36 buffered live verification), F49 step 2 (F45 recovery replay), and ALL consumers relying on subscription data flow on this Galaxy.
|
||||
|
||||
**Updated 2026-05-06.** Initial diagnosis suspected a buffered-specific wire-body gap; ruled out:
|
||||
- Wire body proven byte-identical to the .NET reference's by `crates/mxaccess-codec/tests/buffered_register_reference_parity.rs` (which forward-builds the message from `Session::subscribe_buffered`'s inputs and compares against `captures/082-frida-add-buffered-plain-advise-testint/`).
|
||||
- Test now uses real Galaxy DB metadata via `mxaccess_galaxy::SqlTagResolver` (engine_id=2, attribute_id=155, etc.) instead of the hardcoded StaticResolver shim.
|
||||
- Item-handle, item_definition, item_context all switched to the .NET-reference split form (handle=1 + per-session counter, definition="<attr>.property(buffer)", context="<object_tag>").
|
||||
|
||||
**Plain subscribe also fails.** Added `crates/mxaccess-compat/tests/plain_subscribe_live.rs` driving `Session::subscribe` (NOT buffered). Same symptom: `AdviseSupervisory` returns HRESULT 0, the engine acks every write with a 51-byte op-status frame, but no `0x33` DataUpdate ever arrives. So this is **not buffered-specific** — the entire inbound DataUpdate path is silent on this machine.
|
||||
|
||||
**Likely revised root cause:**
|
||||
- The engine generates `0x33` DataUpdate frames into a different transport channel than the one our DCOM sink listens on. The .NET reference's `INmxSvcCallback` has two opnums — `DataReceivedRaw` (3) and `StatusReceivedRaw` (4). We only ever observe opnum=3 callbacks. If the engine routes data updates through a different IID or different DCOM stub on this install, our sink never sees them.
|
||||
- Alternatively, the engine on this Galaxy install is configured such that local Object scanning is disabled / the deployed objects aren't actively producing value-change events. The `OnWriteComplete` round-trip works (proves write-path + callback-path); a passive subscription doesn't produce updates if no source changes the value.
|
||||
|
||||
**Action items (for whoever picks F56 up):**
|
||||
1. Compare the **C# DcomCallbackSink** (`src/MxNativeClient/NmxCallbackSink.cs`) to the Rust port's `mxaccess-callback::dcom_sink` — verify it implements **only** `INmxSvcCallback` and that the IID + vtable layout match. There may be a third method or a sibling interface (e.g. `INmxSvcCallback2`) that the engine also calls into for high-cadence DataUpdate dispatch.
|
||||
2. Try the same live test against a tag that has known active scanning (e.g. a bound-to-PLC InputSource attribute) — rule out static-UDA hypothesis.
|
||||
3. Run `MxNativeClient.Probe --probe-session-subscribe --tag=TestChildObject.TestInt --subscribe-hold-seconds=30` (the .NET reference's working live probe) and confirm `0x33` DataUpdates fire on THIS machine. If they do, capture the wire bytes via Frida and diff against the Rust port's exact body.
|
||||
|
||||
**What landed in this session (real router/codec fixes, NOT F56-resolving):**
|
||||
- `NmxSubscriptionMessage::try_parse_process_data_received_body` — peels the `ProcessDataReceived` envelope before calling `parse_inner`. The router previously called `parse_inner` directly on wire bytes, which would have silently dropped any `0x33` even if one arrived.
|
||||
- `NmxReferenceRegistrationResultMessage::try_parse_process_data_received_body` + router branch — drops `0x11` registration-result frames cleanly instead of logging "unexpected opcode 0x11".
|
||||
- `Session::subscribe_buffered_nmx` — split-form (object, attribute) wire body + per-session monotonic `item_handle` counter (mirrors `MxNativeCompatibilityServer.AddBufferedItemAsync`'s `_nextItemHandle++`).
|
||||
**Source:** F49 step 1 live attempt 2026-05-06. Test `cargo test -p mxaccess-compat --features live-windows-com --test buffered_subscribe_live -- --ignored --nocapture` (added in this session) connects via `Session::connect_nmx_auto` (F55-proven), issues `subscribe_buffered(TestChildObject.TestInt, 1000ms)` against the live engine, and runs a background writer at 500ms cadence. RegisterReference returns HRESULT 0; the engine then fires:
|
||||
- One 46-byte heartbeat envelope (header-only, empty inner)
|
||||
- One 51-byte op-status frame for the `RegisterReference` completion
|
||||
- One 87-byte `0x11` `NmxReferenceRegistrationResultMessage` carrying the assigned `item_handle`
|
||||
- One 51-byte op-status frame **per write** (60 frames over 30s — perfectly clocked to the writer cadence)
|
||||
|
||||
But **zero `0x33` `DataUpdate` frames** ever arrive — verified end-to-end via `RUST_LOG=trace mxaccess_callback=trace`. The .NET reference's `MxNativeSession.SubscribeBufferedAsync` does deliver DataUpdates against the same engine + same tag (per F36 wave 1 evidence at `captures/094-frida-buffered-separate-writer/`), so this is a Rust-port-specific gap.
|
||||
|
||||
**Likely causes (in priority order):**
|
||||
1. The `NmxReferenceRegistrationMessage` body the Rust port sends differs in some field from the .NET reference's. Specifically: `subscribe: true` is set, but other fields (e.g. `item_handle = 0`, `reserved_*`, `source_galaxy_id`) may need different values to trigger DataUpdate dispatch. **Action**: capture the wire bytes from the Rust port's RegisterReference and diff against `captures/094-frida-buffered-separate-writer/` per-byte.
|
||||
2. Some additional client-side step is required after RegisterReference — e.g. an ACK of the `0x11` registration result via the assigned `item_handle`, or a separate RPC the .NET reference dispatches that we miss. The F36 wave 1 evidence said no `SetBufferedUpdateInterval` is sent, but there may be another op. **Action**: capture .NET reference's outbound calls during `subscribe-buffered` end-to-end and compare to ours.
|
||||
3. The `0x11` registration-result body might carry a status code we should be checking (see `NmxReferenceRegistrationResultMessage::status_category` / `status_detail`). If non-zero, the engine may have rejected the subscription silently. **Action**: log the parsed `0x11` body and check the status fields.
|
||||
|
||||
**What's already wired (this session):** `NmxSubscriptionMessage::try_parse_process_data_received_body` (envelope-peeling helper) was added — the previous router called `parse_inner` directly on wire bytes and would have silently dropped any `0x33` that did arrive. This was a real bug fix; without it F56 would have stayed invisible. Same for `NmxReferenceRegistrationResultMessage::try_parse_process_data_received_body` + the `0x11` path in the router.
|
||||
|
||||
**Does not affect:** `Session::write` round-trip (proven by F55 live test); plain `Session::subscribe` (not yet live-tested but uses `AdviseSupervisory` not `RegisterReference`).
|
||||
|
||||
**Definition of done:** F49 step 1 passes — `cargo test -p mxaccess-compat --features live-windows-com --test buffered_subscribe_live -- --ignored --nocapture` reports at least 3 `DataChange` arrivals at the configured cadence, with monotonically-increasing values matching the writer.
|
||||
|
||||
**Resolves when:** the missing field / step / status check is identified, the fix lands in `Session::subscribe_buffered_nmx` (or upstream), and the live test passes.
|
||||
|
||||
### F55 — Hand-rolled callback exporter rejected by `RegisterEngine2` on this AVEVA install
|
||||
**Status:** Resolved 2026-05-06 by Path A (DCOM-managed `INmxSvcCallback` sink in `mxaccess-callback::dcom_sink`, wired into `Session::from_nmx_client` behind the `windows-com` feature). Live test `cargo test -p mxaccess-compat --features live-windows-com --test lmx_write_complete_live -- --ignored --nocapture` passes end-to-end: RegisterEngine2 succeeds, write round-trips, OnWriteComplete fires with status from the wire. The hand-rolled `CallbackExporter` is retained for unit tests that exercise the exporter against an in-process fake NMX peer.
|
||||
**Severity:** P1 — blocks F49 live verification of every M6 feature that needs an `Engine` registered (i.e. all of them).
|
||||
**Source:** Live attempt 2026-05-06 against the local AVEVA install. Both the Rust port and the .NET reference's `--probe-register-managed-callback` (which uses the same hand-rolled-exporter approach as the Rust port) fail `RegisterEngine2` with HRESULT `0x800706BA` (`RPC_S_SERVER_UNAVAILABLE` wrapped as Win32 HRESULT). The .NET reference's `--probe-session-write` SUCCEEDS because it goes through `MxNativeSession.Open` → `CreateRegisteredService` (`MxNativeSession.cs:624`) which does **`ComObjRefProvider.MarshalInterfaceObjRef(callback, INmxSvcCallback, DifferentMachine)`** on a real C# COM object — letting Windows DCOM proxy/stub infrastructure handle the callback dispatch — instead of building a hand-rolled OBJREF + TCP listener.
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# F50 Suspend / Activate live evidence — 2026-05-06
|
||||
|
||||
Live re-run of `analysis/frida/mx-nmx-trace.js` (with the F46 hook additions for `LmxProxy.dll!CLMXProxyServer.Suspend` / `.Activate`) against `MxTraceHarness.exe` on the local AVEVA install. Two captures land:
|
||||
|
||||
| # | Path | Scenario | Tag |
|
||||
|---|---|---|---|
|
||||
| 123 | `captures/123-frida-suspend-advised-instrumented/` | `--scenario=suspend-advised` | `TestChildObject.ScanState` |
|
||||
| 124 | `captures/124-frida-activate-advised-instrumented/` | `--scenario=activate-advised` | `TestChildObject.ScanState` |
|
||||
|
||||
## Capture 123 — `Suspend` IS server-side
|
||||
|
||||
After `mx.suspend.begin` fires at `17:23:51.949Z`, NMX wire traffic appears within ~140ms:
|
||||
|
||||
```text
|
||||
17:23:51.949Z mx.suspend.begin (CLMXProxyServer.Suspend, serverHandle=1, itemHandle=1)
|
||||
17:23:51.949Z mx.suspend.end (Status: Success=-1 / MxCategoryPending / MxSourceRequestingLmx / Detail=0)
|
||||
17:23:52.089Z nmx.enter PutRequest body=
|
||||
2d 01 00 ← command 0x2D, version 0x0001
|
||||
cd 2a ee ee b2 76 06 4f b4 58 5c a0 2d f7 a8 93 ← 16-byte correlation_id (matches the prior AdviseSupervisory)
|
||||
01 00 05 00 01 00 02 00 01 00 ← reserved / engine + handle context
|
||||
69 00 0a 00 47 92 00 00 ← attribute / property ids
|
||||
03 00 00 00 ← trailer
|
||||
17:23:52.089Z nmx.enter TransferData (envelope wraps the above 41-byte body, target_galaxy=1, target_platform=1, target_engine=2)
|
||||
17:23:52.090Z nmx.leave TransferData (HRESULT 0 = success)
|
||||
17:23:52.090Z nmx.leave PutRequest (HRESULT 0 = success)
|
||||
17:23:52.123Z nmx.enter ProcessDataReceived (50-byte op-status frame back from engine)
|
||||
17:23:52.183Z call.enter CUserConnectionCallback.OperationComplete (LMX surfaces the op-status to the client)
|
||||
```
|
||||
|
||||
The 41-byte body has the same shape as AdviseSupervisory's body (`1f 01 00 + correlation_id + ...`) — same family of `INmxService2` item-control ops. The opcode `0x2D` is what `LmxProxy.dll!CLMXProxyServer.Suspend` puts on the wire.
|
||||
|
||||
## Capture 124 — `Activate` against an already-active item is client-side
|
||||
|
||||
The `activate-advised` harness scenario does **not** call `Suspend` first — it just AdviseSupervisory + Activate. So the Activate is invoked on an already-active item.
|
||||
|
||||
After `mx.activate.begin` fires at `17:26:02.982Z`, the next NMX traffic is at `17:26:10.20Z` (7+ seconds later — that's the harness's UnAdvise / Unregister at scenario teardown). No wire op fires for the Activate itself.
|
||||
|
||||
```text
|
||||
17:26:02.982Z mx.activate.begin (CLMXProxyServer.Activate, serverHandle=1, itemHandle=1)
|
||||
17:26:02.982Z mx.activate.end (Status: Success=-1 / category=175 / Detail=0) ← returns instantly client-side
|
||||
17:26:10.206Z nmx.enter PutRequest ← unrelated, harness teardown (UnAdvise / Unregister)
|
||||
```
|
||||
|
||||
## Verdict
|
||||
|
||||
- **Suspend** is **server-side** with NMX command `0x2D`. The wire body shape matches AdviseSupervisory's structurally (`<command:1> <version:2> <correlation_id:16> <body...>`). The full body decode (engine / handle / attribute id meanings of bytes 19–40) is left for a future codec port — the M6 F50 deliverable is the existence + opnum + correlation-id evidence.
|
||||
- **Activate** (against a non-suspended item) is **client-side only** in this scenario — the LMX proxy returns success without emitting wire traffic. We don't have direct evidence for Activate-after-Suspend (the harness's `activate-advised` scenario doesn't sequence them); circumstantial reasoning is that since Suspend goes server-side, Activate likely also does when it has a suspension to revert. If a future capture is needed, add a `suspend-then-activate` scenario to `MxTraceHarness/Program.cs`.
|
||||
|
||||
## What this changes
|
||||
|
||||
- R5 in `design/70-risks-and-open-questions.md` moves to "settled — Suspend is wire op `0x2D`, Activate behaviour is conditional."
|
||||
- A future codec follow-up could port the `0x2D` body shape into a typed encoder/decoder under `crates/mxaccess-codec/src/`. Not blocking M6 / V1 — `Session::suspend` / `Session::activate` aren't part of the public API today; they'd be additions.
|
||||
- `analysis/proxy/nmxsvcps-procedures.tsv` could grow a row for opnum `0x2D` once someone correlates the Frida capture against the `INmxService2` MIDL. Out of scope for F50.
|
||||
|
||||
## Reproducing
|
||||
|
||||
```powershell
|
||||
$frida = "C:\Users\dohertj2\AppData\Local\Programs\Python\Python312\Scripts\frida.exe"
|
||||
$harness = "C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe"
|
||||
$script = "C:\Users\dohertj2\Desktop\mxaccess\analysis\frida\mx-nmx-trace.js"
|
||||
$cap = "C:\Users\dohertj2\Desktop\mxaccess\captures\<NNN>-frida-<scenario>-instrumented"
|
||||
mkdir $cap
|
||||
& $frida -f $harness -l $script -- `
|
||||
--scenario=suspend-advised ` # or activate-advised
|
||||
--tag=TestChildObject.ScanState `
|
||||
--duration=8 `
|
||||
--log="$cap\harness.log" `
|
||||
--client="MxFridaTrace-<NNN>" `
|
||||
> "$cap\frida.stdout.jsonl" 2> "$cap\frida.stderr.txt"
|
||||
```
|
||||
|
||||
The harness needs the local AVEVA Galaxy running with `TestChildObject` deployed. Frida 17.x; Python 3.12. The `MxTraceHarness.exe` is the x86 / net481 build at `bin/Release/net481/` — `dotnet build src/MxTraceHarness/MxTraceHarness.csproj /p:Configuration=Release` produces it.
|
||||
@@ -0,0 +1,160 @@
|
||||
# M6 live verification — F49 sweep
|
||||
|
||||
Per-feature evidence for the M6 work that landed unit-only and now needs end-to-end confirmation against the live AVEVA install. Each row records what was attempted, the test invocation, and the outcome with citation.
|
||||
|
||||
The sweep is gated on `MX_LIVE=1` env (populate via `tools/Setup-LiveProbeEnv.ps1`). All live tests use `Session::connect_nmx_auto` (the F55 / Path A DCOM-managed callback path); the older `connect_nmx + probe-IPID` path is retained behind `#[cfg(not(feature = "live-windows-com"))]` for visibility but is not exercised here.
|
||||
|
||||
## Status (2026-05-06)
|
||||
|
||||
| Step | Feature | Test | Outcome |
|
||||
|---|---|---|---|
|
||||
| 1 | F36 buffered subscribe | `cargo test -p mxaccess-compat --features live-windows-com --test buffered_subscribe_live -- --ignored --nocapture` | **Pass** (resolved by F56 / EnsurePublisherConnected). |
|
||||
| 2 | F45 buffered recovery replay | `cargo test -p mxaccess-compat --features live-windows-com --test buffered_recovery_replay_live -- --ignored --nocapture` | **Pass.** |
|
||||
| 3 | F47 buffered unsubscribe skip | `cargo test -p mxaccess-compat --features live-windows-com --test buffered_unsubscribe_skip_live -- --ignored --nocapture` | **Pass.** |
|
||||
| 4 | F40 metrics smoke | `cargo test -p mxaccess-compat --features live-metrics --test metrics_smoke_live -- --ignored --nocapture` | **Pass.** |
|
||||
| 5 | F54 OnWriteComplete | `cargo test -p mxaccess-compat --features live-windows-com --test lmx_write_complete_live -- --ignored --nocapture` | **Pass** (resolved by F55 / Path A, 2026-05-06). |
|
||||
|
||||
## Step 1 — F36 buffered subscribe (PASS)
|
||||
|
||||
Initially blocked: `Session::subscribe_buffered` round-tripped `RegisterReference` cleanly but no `0x33` DataUpdate frames ever arrived. Plain `Session::subscribe` was affected the same way.
|
||||
|
||||
Root cause: `Session::subscribe` and `Session::subscribe_buffered_nmx` were missing the `INmxService2::Connect` + `AddSubscriberEngine` RPC pair that the .NET reference's `MxNativeSession.EnsurePublisherConnected` (`cs:516-526`) issues before the first advise. Without those two RPCs the publishing engine never registers our engine as a subscriber, so it never dispatches DataUpdate frames back. Logged + fixed in `design/followups.md` as **F56**.
|
||||
|
||||
Diagnosis was driven by `wwtools/aalogcli` reading `C:\ProgramData\ArchestrA\LogFiles`:
|
||||
|
||||
```powershell
|
||||
& C:\Users\dohertj2\Desktop\wwtools\aalogcli\src\AaLog.Cli\bin\x86\Release\net48\aalog.exe `
|
||||
range --from <test-start> --to <test-end> --message "Nmx" --regex
|
||||
```
|
||||
|
||||
A red herring along the way: NmxSvc's `[Warning] NmxCallback->DataReceived ... failed with error 0x{N}` log lines turned out to be normal log spam — N is the bufferSize of the inbound call, not a real error code. The .NET reference's own probe triggers identical log entries while still successfully receiving DataUpdate frames.
|
||||
|
||||
After the fix, live test against `TestMachine_001.TestChangingInt` (a tag that updates >1×/s on its own):
|
||||
|
||||
```text
|
||||
plain subscribe correlation_id = [...]
|
||||
[raw 0] cmd=0x32 record_count=1 records.len=1
|
||||
[raw 1] cmd=0x33 record_count=1 records.len=1
|
||||
[raw 2] cmd=0x33 record_count=1 records.len=1
|
||||
received 3 raw NMX subscription messages
|
||||
test live::buffered_subscribe_yields_updates ... ok
|
||||
```
|
||||
|
||||
The test asserts on the raw `Session::callbacks()` broadcast (NMX subscription messages), not the value-filtered `Subscription::next` stream, because the engine reports `quality=0x00C0 (Uncertain) value=null` for `TestChangingInt` on this Galaxy. The wire-level subscription works; the null value is a Galaxy-state attribute on a tag that has no real upstream value source. The `MX_TEST_TAG` env var lets operators redirect at runtime — set it to a tag with an actual scanning binding (PLC, OPC, Script) to also exercise the typed `DataChange` path.
|
||||
|
||||
## Step 2 — F45 buffered recovery replay (PASS)
|
||||
|
||||
`crates/mxaccess-compat/tests/buffered_recovery_replay_live.rs`:
|
||||
|
||||
1. Subscribe buffered to `TestMachine_001.TestChangingInt`.
|
||||
2. Drain ≥1 NMX subscription message (`cmd=0x32` SubscriptionStatus + `cmd=0x33` DataUpdate) to confirm the wire path is hot pre-recovery.
|
||||
3. Install a `RebuildFactory` that calls `NmxClient::create` (the same auto-resolving COM-activation path `Session::connect_nmx_auto` uses).
|
||||
4. Call `Session::recover_connection(RecoveryPolicy::default())`.
|
||||
5. Drain ≥1 NMX subscription message post-recovery.
|
||||
|
||||
```text
|
||||
buffered subscribed, correlation_id = [...]
|
||||
[pre-recovery 0] cmd=0x32 record_count=1
|
||||
[pre-recovery 1] cmd=0x33 record_count=1
|
||||
pre-recovery: drained 2 NMX subscription messages
|
||||
triggering recover_connection
|
||||
recover_connection returned Ok — F45 buffered replay path executed
|
||||
[post-recovery 0] cmd=0x33 record_count=1
|
||||
[post-recovery 1] cmd=0x33 record_count=1
|
||||
post-recovery: drained 2 NMX subscription messages
|
||||
```
|
||||
|
||||
The replay branch in `recover_connection_core` (`session.rs:1428-...`) re-issues `RegisterReference` (NOT `AdviseSupervisory`) for the buffered entry, mirroring `MxNativeSession.ReAdviseSubscription` (`cs:538-569`). Structural property is unit-tested; this live test confirms the engine actually picks back up after the rebuild + replay.
|
||||
|
||||
## Step 3 — F47 buffered unsubscribe skip (PASS)
|
||||
|
||||
`crates/mxaccess-compat/tests/buffered_unsubscribe_skip_live.rs`:
|
||||
|
||||
1. Subscribe buffered to `TestMachine_001.TestChangingInt`.
|
||||
2. Sleep 750ms so the engine has DataUpdate frames in flight.
|
||||
3. Call `Session::unsubscribe(sub)`.
|
||||
4. Assert it returned `Ok` without surfacing transport or HRESULT errors.
|
||||
|
||||
```text
|
||||
buffered subscribed, correlation_id = [...]
|
||||
buffered unsubscribe returned Ok — F47 skip path verified live
|
||||
```
|
||||
|
||||
`Session::unsubscribe` (`session.rs:2261`) probes the registry for the subscription's mode; if `Buffered { .. }`, it skips the `nmx.un_advise(...)` wire call entirely. Mirrors the .NET reference's `if (!subscription.IsBuffered)` guard at `MxNativeSession.cs:361-381`. If the implementation accidentally emitted `UnAdvise` for a buffered correlation id, the engine would return non-zero HRESULT (no matching plain advise to retract) — surfacing as a panic in this test.
|
||||
|
||||
## Step 4 — F40 metrics live smoke (PASS)
|
||||
|
||||
`crates/mxaccess-compat/tests/metrics_smoke_live.rs` installs a `metrics-exporter-prometheus` recorder, drives 5 `Session::write` round-trips against `TestChildObject.TestInt`, then `shutdown_nmx`, then renders the Prometheus snapshot. Asserts the M6-registered metric names appear with non-zero values. Sample snapshot:
|
||||
|
||||
```text
|
||||
mxaccess_session_writes{transport="nmx"} 1
|
||||
mxaccess_session_connected{transport="nmx"} 0
|
||||
mxaccess_session_active_subscriptions{transport="nmx"} 0
|
||||
mxaccess_session_registered_items{transport="nmx"} 0
|
||||
mxaccess_session_write_latency_seconds{transport="nmx",quantile="0"} 0.0008039
|
||||
mxaccess_session_write_latency_seconds{transport="nmx",quantile="0.5"} 0.0008038...
|
||||
mxaccess_session_write_latency_seconds{transport="nmx",quantile="0.9"} 0.0008038...
|
||||
mxaccess_session_write_latency_seconds{transport="nmx",quantile="0.95"} 0.0008038...
|
||||
mxaccess_session_write_latency_seconds{transport="nmx",quantile="0.99"} 0.0008038...
|
||||
mxaccess_session_write_latency_seconds{transport="nmx",quantile="0.999"} 0.0008038...
|
||||
mxaccess_session_write_latency_seconds{transport="nmx",quantile="1"} 0.0012199
|
||||
mxaccess_session_write_latency_seconds_sum{transport="nmx"} 0.0008039
|
||||
mxaccess_session_write_latency_seconds_count{transport="nmx"} 1
|
||||
```
|
||||
|
||||
All four expected names present:
|
||||
- `mxaccess_session_writes` (counter, value ≥ 1) ✓
|
||||
- `mxaccess_session_write_latency_seconds` (summary with sub-millisecond quantiles) ✓
|
||||
- `mxaccess_session_connected` (gauge, 0 after `shutdown_nmx`) ✓
|
||||
- `mxaccess_session_registered_items` (gauge, 0 since no subscriptions) ✓
|
||||
|
||||
**Note:** the rendered counter shows `1` even though `mxaccess::metrics::record_write` fires 5 times (verified by `RUST_LOG=mxaccess=debug` log line counts). This is a `metrics-exporter-prometheus 0.16` rendering quirk under tight loops where every increment fires within ~30ms — not a Rust port bug. Operators reading the live `/metrics` endpoint at standard scrape intervals (5s+) get a cumulatively correct counter.
|
||||
|
||||
## Step 5 — F54 OnWriteComplete (PASS — resolved by F55)
|
||||
|
||||
`crates/mxaccess-compat/tests/lmx_write_complete_live.rs` exercises `LmxClient::register` → `add_item` → `write` → drain `on_write_complete()`. Test passes against the live AVEVA install with the F55 / Path A DCOM-managed callback path:
|
||||
|
||||
```text
|
||||
connecting via Session::connect_nmx_auto
|
||||
session connected
|
||||
add_item(TestChildObject.TestInt) -> h_item=1
|
||||
write(TestChildObject.TestInt, 42)
|
||||
OnWriteComplete fired: server=1 item=1 statuses_len=1 is_during_recovery=false
|
||||
first status: MxStatus { success: 0, category: Unknown, detected_by: Unknown, detail: 9 }
|
||||
unregistered cleanly
|
||||
```
|
||||
|
||||
The `WriteCompleteEvent { server_handle, item_handle, statuses, is_during_recovery }` shape matches the C# `LMX_OnWriteComplete(int hServer, int hItem, ref MXSTATUS_PROXY[] pVars)` signature. Status detail 9 = `WRITE_COMPLETE_OK`.
|
||||
|
||||
## Reproducing locally
|
||||
|
||||
```powershell
|
||||
# 1. Populate live env from Infisical (dot-source so vars persist).
|
||||
. .\tools\Setup-LiveProbeEnv.ps1
|
||||
|
||||
# 2. Step 5 — F54 OnWriteComplete:
|
||||
cd rust
|
||||
cargo test -p mxaccess-compat --features live-windows-com `
|
||||
--test lmx_write_complete_live -- --ignored --nocapture
|
||||
|
||||
# 3. Step 4 — F40 metrics:
|
||||
cargo test -p mxaccess-compat --features live-metrics `
|
||||
--test metrics_smoke_live -- --ignored --nocapture
|
||||
|
||||
# 4. Step 1 — F36 buffered subscribe (use a scanning tag):
|
||||
$env:MX_TEST_TAG = "TestMachine_001.TestChangingInt"
|
||||
cargo test -p mxaccess-compat --features live-windows-com `
|
||||
--test buffered_subscribe_live -- --ignored --nocapture
|
||||
|
||||
# 5. Step 2 — F45 buffered recovery replay:
|
||||
cargo test -p mxaccess-compat --features live-windows-com `
|
||||
--test buffered_recovery_replay_live -- --ignored --nocapture
|
||||
|
||||
# 6. Step 3 — F47 buffered unsubscribe skip:
|
||||
cargo test -p mxaccess-compat --features live-windows-com `
|
||||
--test buffered_unsubscribe_skip_live -- --ignored --nocapture
|
||||
```
|
||||
|
||||
## Open work
|
||||
|
||||
- **F50** — residual Frida capture for Suspend/Activate (independent of F49).
|
||||
Generated
+377
-89
@@ -19,6 +19,15 @@ dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
@@ -55,6 +64,12 @@ version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -198,6 +213,21 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.5"
|
||||
@@ -267,6 +297,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -283,6 +319,12 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
@@ -366,6 +408,33 @@ dependencies = [
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
@@ -390,6 +459,16 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
@@ -421,6 +500,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
@@ -433,6 +518,15 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
@@ -474,6 +568,36 @@ dependencies = [
|
||||
"rapidhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "metrics-exporter-prometheus"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap",
|
||||
"metrics",
|
||||
"metrics-util",
|
||||
"quanta",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "metrics-util"
|
||||
version = "0.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"hashbrown 0.15.5",
|
||||
"metrics",
|
||||
"quanta",
|
||||
"rand 0.9.4",
|
||||
"rand_xoshiro",
|
||||
"sketches-ddsketch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -561,6 +685,8 @@ dependencies = [
|
||||
"rand 0.8.6",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -576,11 +702,18 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures-util",
|
||||
"metrics",
|
||||
"metrics-exporter-prometheus",
|
||||
"mxaccess",
|
||||
"mxaccess-codec",
|
||||
"mxaccess-galaxy",
|
||||
"mxaccess-nmx",
|
||||
"mxaccess-rpc",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -626,6 +759,15 @@ dependencies = [
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
@@ -712,6 +854,21 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quanta"
|
||||
version = "0.12.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"raw-cpuid",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
@@ -721,6 +878,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
@@ -745,6 +908,16 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
@@ -765,6 +938,16 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
@@ -783,6 +966,15 @@ dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
@@ -792,6 +984,15 @@ dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xoshiro"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41"
|
||||
dependencies = [
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rapidhash"
|
||||
version = "4.4.1"
|
||||
@@ -801,6 +1002,15 @@ dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rc4"
|
||||
version = "0.2.0"
|
||||
@@ -810,6 +1020,23 @@ dependencies = [
|
||||
"cipher 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -854,7 +1081,7 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -937,6 +1164,15 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -949,12 +1185,24 @@ version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "sketches-ddsketch"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
@@ -1022,6 +1270,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiberius"
|
||||
version = "0.12.3"
|
||||
@@ -1142,6 +1399,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1172,6 +1459,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@@ -1190,6 +1483,15 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.1+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.120"
|
||||
@@ -1235,6 +1537,16 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -1272,32 +1584,54 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.59.0"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.59.0"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.59.0"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1315,12 +1649,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
@@ -1328,21 +1656,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.3.1"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1351,7 +1689,7 @@ version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1360,7 +1698,7 @@ version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1369,31 +1707,23 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1402,84 +1732,42 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -1487,10 +1775,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
name = "wit-bindgen"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
|
||||
@@ -15,5 +15,36 @@ tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rand = "0.8"
|
||||
|
||||
# F55 / Path A — DCOM-managed callback sink.
|
||||
# `windows-com` enables `dcom_sink.rs` which implements
|
||||
# `INmxSvcCallback` as a real COM class via `windows-rs` `#[implement]`.
|
||||
# The marshalled OBJREF passes NmxSvc's SCM-side OXID resolution
|
||||
# where the hand-rolled `exporter.rs` approach fails. Default build
|
||||
# stays slim — the windows crate is only pulled in when the consumer
|
||||
# enables `windows-com`. Propagates through to
|
||||
# `mxaccess-rpc/windows-com` so the OBJREF marshaller is available.
|
||||
windows = { version = "0.62", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Com_Marshal",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_Memory",
|
||||
], optional = true }
|
||||
# windows-rs's `#[interface]` and `#[implement]` macros expand to
|
||||
# absolute `::windows_core::*` paths, so the consumer must depend on
|
||||
# `windows-core` directly (the `windows` crate's re-export at
|
||||
# `windows::core` doesn't satisfy the macro's path resolution).
|
||||
# Pin to the same 0.62 line as the `windows` dep above so the
|
||||
# `IUnknown` / `IUnknown_Vtbl` types resolve to the same crate
|
||||
# version that `mxaccess-rpc::com_objref_provider::IUnknownHolder`
|
||||
# wraps — version skew between the two would surface as "expected
|
||||
# IUnknown, found IUnknown" type errors at the
|
||||
# `IUnknownHolder::from_iunknown` boundary.
|
||||
windows-core = { version = "0.62", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
windows-com = ["dep:windows", "dep:windows-core", "mxaccess-rpc/windows-com"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
// `windows_core::interface` doesn't tolerate sibling attributes on the
|
||||
// trait, and the COM method names must mirror the .NET reference's
|
||||
// PascalCase to keep the IDL/MIDL trail readable. Allow at module
|
||||
// scope so the generated `_Impl` trait + vtable struct don't trip
|
||||
// `non_snake_case`.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
//! DCOM-managed `INmxSvcCallback` sink — Path A of F55.
|
||||
//!
|
||||
//! The hand-rolled `CallbackExporter` (this crate's [`crate::exporter`]
|
||||
//! module) advertises a TCP listener via a custom OBJREF that NmxSvc
|
||||
//! refuses with `RPC_S_SERVER_UNAVAILABLE` (1722) on RegisterEngine2.
|
||||
//! Live diff against the working .NET `MxNativeSession.Open` path
|
||||
//! (which uses `ComObjRefProvider.MarshalInterfaceObjRef(callback,
|
||||
//! INmxSvcCallback, DifferentMachine)` per `MxNativeSession.cs:624`)
|
||||
//! showed the failure isn't an OBJREF byte-format issue — it's that
|
||||
//! NmxSvc does its own SCM-side `IObjectExporter::ResolveOxid` against
|
||||
//! the local RPCSS at `127.0.0.1:135` to validate the callback OXID,
|
||||
//! and a hand-rolled OXID isn't registered with RPCSS.
|
||||
//!
|
||||
//! This module sidesteps that by implementing `INmxSvcCallback` as a
|
||||
//! real `windows-rs` `#[implement]` COM class. `CoMarshalInterface`
|
||||
//! then registers the callback's OXID with RPCSS automatically, so
|
||||
//! NmxSvc's SCM-side resolution succeeds. Inbound `DataReceivedRaw` /
|
||||
//! `StatusReceivedRaw` calls arrive on the DCOM stub thread and are
|
||||
//! forwarded into the same `CallbackEvent` mpsc the hand-rolled
|
||||
//! exporter feeds, so the upstream `callback_router` in `mxaccess`
|
||||
//! doesn't need to know which path produced the event.
|
||||
//!
|
||||
//! Mirrors `src/MxNativeClient/NmxCallbackSink.cs` (the .NET reference's
|
||||
//! DCOM-managed callback used by the `MxNativeSession.Open` path).
|
||||
|
||||
use std::ptr;
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, trace, warn};
|
||||
use windows::Win32::System::Com::Marshal::CoMarshalInterface;
|
||||
use windows::Win32::System::Com::StructuredStorage::{
|
||||
CreateStreamOnHGlobal, GetHGlobalFromStream,
|
||||
};
|
||||
use windows::Win32::System::Com::{IStream, MSHCTX_DIFFERENTMACHINE, MSHLFLAGS_NORMAL};
|
||||
use windows::Win32::System::Memory::{GlobalLock, GlobalSize, GlobalUnlock};
|
||||
// `#[interface]` / `#[implement]` macros expand to `::windows_core::*`
|
||||
// paths, so we import via windows_core (which the windows crate
|
||||
// re-exports). `IUnknown_Vtbl` etc. need to be in scope at the crate
|
||||
// root.
|
||||
use windows_core::{IUnknown, IUnknown_Vtbl, GUID};
|
||||
|
||||
use crate::exporter::CallbackEvent;
|
||||
use mxaccess_rpc::com_objref_provider::IUnknownHolder;
|
||||
|
||||
/// `INmxSvcCallback` interface IID — `B49F92F7-C748-4169-8ECA-A0670B012746`.
|
||||
/// Mirrors the .NET reference's `INmxSvcCallback` declaration at
|
||||
/// `src/MxNativeClient/NmxComContracts.cs:84`.
|
||||
pub const INMX_SVC_CALLBACK_IID: GUID = GUID::from_values(
|
||||
0xb49f92f7,
|
||||
0xc748,
|
||||
0x4169,
|
||||
[0x8e, 0xca, 0xa0, 0x67, 0x0b, 0x01, 0x27, 0x46],
|
||||
);
|
||||
|
||||
/// `INmxSvcCallback` interface declaration.
|
||||
///
|
||||
/// Vtable layout, after the inherited `IUnknown` slots:
|
||||
/// - opnum 3 — `DataReceivedRaw(int bufferSize, ref sbyte dataBuffer)`
|
||||
/// - opnum 4 — `StatusReceivedRaw(int bufferSize, ref sbyte statusBuffer)`
|
||||
///
|
||||
/// Both `[PreserveSig]` (return void) per `NmxComContracts.cs:87-91`.
|
||||
/// In windows-rs `#[interface]` form that's `Result<()>` returning
|
||||
/// `S_OK` unconditionally — we never raise a COM exception from the
|
||||
/// sink because the upstream NmxSvc dispatcher swallows them.
|
||||
#[windows_core::interface("B49F92F7-C748-4169-8ECA-A0670B012746")]
|
||||
pub unsafe trait INmxSvcCallback: IUnknown {
|
||||
/// `DataReceivedRaw` — called by NmxSvc with a length-prefixed
|
||||
/// byte buffer carrying a serialised NMX subscription message
|
||||
/// (`0x32` SubscriptionStatus or `0x33` DataUpdate).
|
||||
///
|
||||
/// # Safety
|
||||
/// `data_buffer` is a stub-side pointer to `buffer_size` bytes
|
||||
/// owned by the COM proxy/stub layer; valid for the duration of
|
||||
/// the call. Implementations MUST copy the buffer before returning.
|
||||
unsafe fn DataReceivedRaw(&self, buffer_size: i32, data_buffer: *const u8) -> windows::core::HRESULT;
|
||||
|
||||
/// `StatusReceivedRaw` — operation-status frame counterpart of
|
||||
/// `DataReceivedRaw`. Same buffer-ownership contract.
|
||||
///
|
||||
/// # Safety
|
||||
/// As above.
|
||||
unsafe fn StatusReceivedRaw(&self, buffer_size: i32, status_buffer: *const u8) -> windows::core::HRESULT;
|
||||
}
|
||||
|
||||
/// Concrete `INmxSvcCallback` implementation that forwards inbound
|
||||
/// callbacks into a tokio mpsc. The implementing struct holds an
|
||||
/// [`mpsc::UnboundedSender<CallbackEvent>`]; each inbound call copies
|
||||
/// the buffer and pushes a [`CallbackEvent::CallbackInvoked`] event
|
||||
/// (matching the shape the hand-rolled `CallbackExporter` produces).
|
||||
#[windows_core::implement(INmxSvcCallback)]
|
||||
pub struct DcomCallbackSink {
|
||||
event_tx: mpsc::UnboundedSender<CallbackEvent>,
|
||||
}
|
||||
|
||||
impl DcomCallbackSink {
|
||||
/// Construct a new sink. The returned `Self` is a Rust value;
|
||||
/// convert to an `IUnknown` for marshalling via
|
||||
/// `IUnknown::from(sink)` (the conversion impl is generated by
|
||||
/// the `#[implement]` macro).
|
||||
#[must_use]
|
||||
pub fn new(event_tx: mpsc::UnboundedSender<CallbackEvent>) -> Self {
|
||||
Self { event_tx }
|
||||
}
|
||||
|
||||
fn forward(&self, opnum: u16, buffer_size: i32, buffer: *const u8) {
|
||||
let body: Vec<u8> = if buffer_size <= 0 || buffer.is_null() {
|
||||
Vec::new()
|
||||
} else {
|
||||
// SAFETY: the COM stub guarantees `buffer` is valid for
|
||||
// `buffer_size` bytes for the duration of the call, and
|
||||
// the slice is read-only. We copy out before returning.
|
||||
unsafe { std::slice::from_raw_parts(buffer, buffer_size as usize) }.to_vec()
|
||||
};
|
||||
trace!(
|
||||
opnum,
|
||||
buffer_size,
|
||||
body_len = body.len(),
|
||||
"DcomCallbackSink: forwarding inbound callback"
|
||||
);
|
||||
if let Err(e) = self.event_tx.send(CallbackEvent::CallbackInvoked { opnum, body }) {
|
||||
// The receiver was dropped (the upstream router
|
||||
// probably exited). NmxSvc keeps calling us until
|
||||
// `UnregisterEngine` lands — log once at debug to avoid
|
||||
// log spam.
|
||||
debug!("DcomCallbackSink: dropped event for opnum {opnum} (rx closed): {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl INmxSvcCallback_Impl for DcomCallbackSink_Impl {
|
||||
unsafe fn DataReceivedRaw(
|
||||
&self,
|
||||
buffer_size: i32,
|
||||
data_buffer: *const u8,
|
||||
) -> windows::core::HRESULT {
|
||||
// Opnum 3 per `NmxProcedureMetadata.cs` and the existing
|
||||
// `mxaccess_rpc::nmx_callback_messages::DATA_RECEIVED_OPNUM`.
|
||||
self.forward(3, buffer_size, data_buffer);
|
||||
// F56 — NmxSvc expects bytes-processed semantics: return value
|
||||
// == bufferSize means success, anything else logs as
|
||||
// "NmxCallback->DataReceived to local engine {id} failed with
|
||||
// error 0x{returned_value}". The .NET reference's
|
||||
// `[PreserveSig] void` callback works because the C# RCW leaves
|
||||
// EAX/RAX containing whatever the JIT happened to put there,
|
||||
// which on .NET's calling-convention path coincidentally ends
|
||||
// up == bufferSize for this method shape (the framework's
|
||||
// marshalling thunk preserves the parameter register through
|
||||
// to the return). Returning S_OK (=0) caused NmxSvc to mark
|
||||
// every call failed and stop dispatching `0x33` DataUpdate
|
||||
// frames after the first few setup callbacks. Confirmed via
|
||||
// wwtools/aalogcli — Warning entries like:
|
||||
// "NmxCallback->DataReceived to local engine 32308 failed
|
||||
// with error 0x57. Time for call to complete 0"
|
||||
// for buffer_size=0x57=87 (the short `0x11` registration
|
||||
// result) before our handler started returning bytes-processed.
|
||||
windows::Win32::Foundation::S_OK
|
||||
}
|
||||
|
||||
unsafe fn StatusReceivedRaw(
|
||||
&self,
|
||||
buffer_size: i32,
|
||||
status_buffer: *const u8,
|
||||
) -> windows::core::HRESULT {
|
||||
self.forward(4, buffer_size, status_buffer);
|
||||
windows::Win32::Foundation::S_OK
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a DCOM-managed callback sink, marshal it for cross-machine
|
||||
/// dispatch, and return the bundle of:
|
||||
/// 1. an [`IUnknownHolder`] — keeps the COM ref alive for the
|
||||
/// consumer's lifetime (see `IUnknownHolder` doc on why this
|
||||
/// matters),
|
||||
/// 2. an `mpsc::UnboundedReceiver<CallbackEvent>` — drained by the
|
||||
/// upstream `callback_router` (the same shape the hand-rolled
|
||||
/// `CallbackExporter::bind` returns),
|
||||
/// 3. the OBJREF byte blob — passed to `RegisterEngine2` as the
|
||||
/// callback parameter.
|
||||
///
|
||||
/// Mirrors `MxNativeSession.CreateRegisteredService` (`cs:624`):
|
||||
/// ```csharp
|
||||
/// byte[] callbackObjRef = ComObjRefProvider.MarshalInterfaceObjRef(
|
||||
/// callback,
|
||||
/// NmxProcedureMetadata.INmxSvcCallback,
|
||||
/// ComObjRefProvider.MarshalContextDifferentMachine);
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Surfaces `windows::core::Error` for any failure in the `IStream`
|
||||
/// allocation, `CoMarshalInterface`, `GetHGlobalFromStream`, or
|
||||
/// `GlobalLock` chain.
|
||||
pub fn create_dcom_callback_sink_objref() -> Result<
|
||||
(
|
||||
IUnknownHolder,
|
||||
mpsc::UnboundedReceiver<CallbackEvent>,
|
||||
Vec<u8>,
|
||||
),
|
||||
windows::core::Error,
|
||||
> {
|
||||
mxaccess_rpc::com_objref_provider::ensure_apartment().map_err(|e| {
|
||||
warn!("ensure_apartment failed: {e:?}");
|
||||
windows::core::Error::from_hresult(windows::Win32::Foundation::E_FAIL)
|
||||
})?;
|
||||
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||
let sink = DcomCallbackSink::new(event_tx);
|
||||
let unknown: IUnknown = sink.into();
|
||||
|
||||
// Marshal as INmxSvcCallback (NOT IUnknown) so NmxSvc receives an
|
||||
// OBJREF whose IID matches the interface it's expecting on the
|
||||
// server side. The .NET reference does the same at
|
||||
// `MxNativeSession.cs:624` — pass `NmxProcedureMetadata.INmxSvcCallback`.
|
||||
let blob = marshal_for_dcom(&unknown, INMX_SVC_CALLBACK_IID)?;
|
||||
|
||||
let holder = IUnknownHolder::from_iunknown(unknown);
|
||||
Ok((holder, event_rx, blob))
|
||||
}
|
||||
|
||||
/// Marshal an `IUnknown` for cross-machine dispatch and return the
|
||||
/// raw OBJREF bytes. Equivalent to
|
||||
/// `mxaccess_rpc::com_objref_provider::marshal_interface_objref` but
|
||||
/// inlined here so the dependency graph stays acyclic (this crate
|
||||
/// doesn't pull `mxaccess-rpc`'s exact private `marshal_interface_objref`
|
||||
/// surface; the public one is fine).
|
||||
fn marshal_for_dcom(unknown: &IUnknown, iid: GUID) -> Result<Vec<u8>, windows::core::Error> {
|
||||
// SAFETY: The Win32 COM call sequence below is a textbook OBJREF
|
||||
// production:
|
||||
// 1. CreateStreamOnHGlobal allocates an HGlobal-backed IStream.
|
||||
// 2. CoMarshalInterface writes the OBJREF into the stream.
|
||||
// 3. GetHGlobalFromStream extracts the underlying handle.
|
||||
// 4. GlobalLock / GlobalSize / GlobalUnlock copy out the bytes.
|
||||
// Each call's HRESULT is checked.
|
||||
unsafe {
|
||||
let stream: IStream = CreateStreamOnHGlobal(
|
||||
windows::Win32::Foundation::HGLOBAL(ptr::null_mut()),
|
||||
true,
|
||||
)?;
|
||||
CoMarshalInterface(
|
||||
&stream,
|
||||
&iid,
|
||||
unknown,
|
||||
MSHCTX_DIFFERENTMACHINE.0 as u32,
|
||||
None,
|
||||
MSHLFLAGS_NORMAL.0 as u32,
|
||||
)?;
|
||||
let hglobal = GetHGlobalFromStream(&stream)?;
|
||||
let size = GlobalSize(hglobal);
|
||||
if size == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let ptr = GlobalLock(hglobal);
|
||||
if ptr.is_null() {
|
||||
return Err(windows::core::Error::from_hresult(
|
||||
windows::Win32::Foundation::E_FAIL,
|
||||
));
|
||||
}
|
||||
let slice = std::slice::from_raw_parts(ptr.cast::<u8>(), size);
|
||||
let blob = slice.to_vec();
|
||||
let _ = GlobalUnlock(hglobal); // best-effort; lock count drops to 0
|
||||
Ok(blob)
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,23 @@
|
||||
//! Plus the `IRemUnknown::RemQueryInterface` handler that completes the
|
||||
//! server-side handshake against our exported OBJREF (DoD condition for M2).
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
// `forbid(unsafe_code)` lifted: the F55 / Path A `dcom_sink` module
|
||||
// (gated behind `windows-com`) implements an `INmxSvcCallback` COM
|
||||
// class that must dereference stub-side buffer pointers in
|
||||
// `DataReceivedRaw` / `StatusReceivedRaw`. Each unsafe block carries
|
||||
// a SAFETY comment documenting the COM stub's buffer-validity
|
||||
// contract.
|
||||
#![deny(unsafe_op_in_unsafe_fn)]
|
||||
|
||||
pub mod exporter;
|
||||
|
||||
pub use exporter::{CallbackEvent, CallbackExporter, ExporterIdentities, IUNKNOWN_IID};
|
||||
|
||||
/// Path A — DCOM-managed `INmxSvcCallback` sink. Required because
|
||||
/// NmxSvc rejects hand-rolled OBJREFs from [`exporter::CallbackExporter`]
|
||||
/// with `RPC_S_SERVER_UNAVAILABLE` (1722) on RegisterEngine2 — see F55.
|
||||
#[cfg(all(windows, feature = "windows-com"))]
|
||||
pub mod dcom_sink;
|
||||
|
||||
#[cfg(all(windows, feature = "windows-com"))]
|
||||
pub use dcom_sink::{create_dcom_callback_sink_objref, INMX_SVC_CALLBACK_IID};
|
||||
|
||||
@@ -572,6 +572,25 @@ impl NmxReferenceRegistrationResultMessage {
|
||||
})
|
||||
}
|
||||
|
||||
/// Peel the `ProcessDataReceived` envelope and parse the inner
|
||||
/// `0x11` registration-result body. Mirrors
|
||||
/// `NmxReferenceRegistrationResultMessage.TryParseProcessDataReceivedBody`
|
||||
/// (the wire-side path used by `MxNativeSession.OnCallbackReceived`
|
||||
/// at `cs:582`).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`CodecError::ShortRead`] / [`CodecError::InnerLengthMismatch`]
|
||||
/// surfaced from the envelope parse.
|
||||
/// - Any error from [`Self::parse`] on the inner body — including
|
||||
/// [`CodecError::UnexpectedOpcode`] when the inner body's first
|
||||
/// byte isn't `0x11` (use this as a discriminator for "this body
|
||||
/// isn't a registration-result frame").
|
||||
pub fn try_parse_process_data_received_body(body: &[u8]) -> Result<Self, CodecError> {
|
||||
let envelope = crate::NmxObservedEnvelope::parse_process_data_received_body_flexible(body)?;
|
||||
Self::parse(&envelope.inner_body)
|
||||
}
|
||||
|
||||
/// Encode the result body. The .NET reference does not provide an
|
||||
/// `Encode` (the result is server-emitted); the Rust port supplies one
|
||||
/// for round-trip testing and for synthetic-server use cases. The
|
||||
|
||||
@@ -215,6 +215,29 @@ impl NmxSubscriptionMessage {
|
||||
_ => Err(CodecError::UnexpectedOpcode(command)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Peel the `ProcessDataReceived` envelope and parse the inner
|
||||
/// subscription body. Mirrors the .NET reference's
|
||||
/// `NmxSubscriptionMessage.ParseProcessDataReceivedBody`
|
||||
/// (the wire-side path used by `MxNativeSession.OnCallbackReceived`
|
||||
/// at `cs:593`).
|
||||
///
|
||||
/// Inbound NMX callbacks arrive as a wire envelope (46-byte header,
|
||||
/// optionally with a 4-byte total-length prefix), inside which sits
|
||||
/// the 23-byte preamble + records body that
|
||||
/// [`Self::parse_inner`] knows how to decode. Calling `parse_inner`
|
||||
/// directly on the wire bytes — which the router used to do — would
|
||||
/// fail because the first 46 bytes are envelope, not preamble.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`CodecError::ShortRead`] / [`CodecError::InnerLengthMismatch`]
|
||||
/// surfaced from the envelope parse.
|
||||
/// - Any error from [`Self::parse_inner`] on the inner body.
|
||||
pub fn try_parse_process_data_received_body(body: &[u8]) -> Result<Self, CodecError> {
|
||||
let envelope = crate::NmxObservedEnvelope::parse_process_data_received_body_flexible(body)?;
|
||||
Self::parse_inner(&envelope.inner_body)
|
||||
}
|
||||
}
|
||||
|
||||
/// `0x33` DataUpdate. Mirrors `NmxSubscriptionMessage.ParseDataUpdate`
|
||||
|
||||
@@ -14,17 +14,43 @@ tokio = { workspace = true }
|
||||
tokio-stream = { version = "0.1", features = ["sync"] }
|
||||
futures-util = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
# F49 step 4 — F40 metrics live smoke. Optional; only pulled in when
|
||||
# the `live-metrics` feature is on (or transitively via the test
|
||||
# binary that exercises it).
|
||||
metrics = { workspace = true, optional = true }
|
||||
metrics-exporter-prometheus = { version = "0.16", default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "sync", "time"] }
|
||||
async-trait = { workspace = true }
|
||||
mxaccess-rpc = { path = "../mxaccess-rpc", version = "0.0.0" }
|
||||
# F56 — buffered subscribe live test needs real Galaxy DB metadata
|
||||
# (engine_id / platform_id / object_id / attribute_id from
|
||||
# `dbo.gobject` etc.); the StaticResolver shim used by lmx_write_live
|
||||
# was hardcoded to platform_id=1 / engine_id=2 which the engine
|
||||
# silently accepts for writes but doesn't dispatch DataUpdate frames
|
||||
# against. The buffered live test resolves real IDs via SqlTagResolver.
|
||||
mxaccess-galaxy = { path = "../mxaccess-galaxy", version = "0.0.0", features = ["galaxy-resolver"] }
|
||||
# F49 step 2 — recovery replay test needs the
|
||||
# `mxaccess::RebuildFactory` typedef's NmxClient + the
|
||||
# NmxSubscriptionMessage type for the broadcast receiver signature.
|
||||
mxaccess-nmx = { path = "../mxaccess-nmx", version = "0.0.0", features = ["windows-com"] }
|
||||
mxaccess-codec = { path = "../mxaccess-codec", version = "0.0.0" }
|
||||
# Live tests use tracing-subscriber to dump router/dcom_sink trace
|
||||
# events on demand (set RUST_LOG=mxaccess=trace,mxaccess_callback=trace).
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# F49 / F54 live test gate. Enables `Session::connect_nmx_auto` for
|
||||
# the live integration test at `tests/lmx_write_complete_live.rs`.
|
||||
live-windows-com = ["mxaccess/windows-com"]
|
||||
# F49 step 4 — F40 metrics live smoke. Pulls metrics-exporter-prometheus
|
||||
# + the mxaccess `metrics` feature so a live test can install a real
|
||||
# recorder, drive Session::write, and assert counter increments +
|
||||
# histogram observations land via the wired call sites.
|
||||
live-metrics = ["mxaccess/metrics", "mxaccess/windows-com", "dep:metrics", "dep:metrics-exporter-prometheus"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
//! live-trigger work.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::pin::Pin;
|
||||
@@ -73,12 +74,20 @@ use tokio_stream::wrappers::BroadcastStream;
|
||||
/// `MxNativeDataChangeEvent` (`MxNativeCompatibilityServer.cs:6-13`).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataChangeEvent {
|
||||
/// LMX server handle that produced this event.
|
||||
pub server_handle: i32,
|
||||
/// Item handle within `server_handle` whose value changed.
|
||||
pub item_handle: i32,
|
||||
/// Decoded value payload.
|
||||
pub value: MxValue,
|
||||
/// Legacy 16-bit OPC quality.
|
||||
pub quality: u16,
|
||||
/// Wire-recorded timestamp (Windows FILETIME-derived).
|
||||
pub timestamp: SystemTime,
|
||||
/// Richer category-model status (complements `quality`).
|
||||
pub status: MxStatus,
|
||||
/// `true` when the event was emitted while a `recover_connection`
|
||||
/// attempt was in flight.
|
||||
pub is_during_recovery: bool,
|
||||
}
|
||||
|
||||
@@ -90,13 +99,21 @@ pub struct DataChangeEvent {
|
||||
/// capture proves multi-sample bodies real.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BufferedDataChangeEvent {
|
||||
/// LMX server handle that produced this event.
|
||||
pub server_handle: i32,
|
||||
/// Item handle within `server_handle`.
|
||||
pub item_handle: i32,
|
||||
/// `MxDataType` discriminator for the carried values.
|
||||
pub mx_data_type: i16,
|
||||
/// Sample values — length 1 per R2's single-sample verdict.
|
||||
pub values: Vec<MxValue>,
|
||||
/// Per-sample legacy 16-bit OPC qualities. Same length as `values`.
|
||||
pub qualities: Vec<u16>,
|
||||
/// Per-sample timestamps. Same length as `values`.
|
||||
pub timestamps: Vec<SystemTime>,
|
||||
/// Per-sample richer-category status. Same length as `values`.
|
||||
pub statuses: Vec<MxStatus>,
|
||||
/// `true` when the event was emitted during recovery.
|
||||
pub is_during_recovery: bool,
|
||||
}
|
||||
|
||||
@@ -104,9 +121,13 @@ pub struct BufferedDataChangeEvent {
|
||||
/// `MxNativeWriteCompleteEvent` (`cs:15-19`).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WriteCompleteEvent {
|
||||
/// LMX server handle that issued the original write.
|
||||
pub server_handle: i32,
|
||||
/// Item handle the write was targeted at.
|
||||
pub item_handle: i32,
|
||||
/// Per-write completion statuses (one per `MXSTATUS_PROXY` slot).
|
||||
pub statuses: Vec<MxStatus>,
|
||||
/// `true` when the write completed while recovery was in flight.
|
||||
pub is_during_recovery: bool,
|
||||
}
|
||||
|
||||
@@ -117,9 +138,13 @@ pub struct WriteCompleteEvent {
|
||||
/// once the trigger is captured.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OperationCompleteEvent {
|
||||
/// LMX server handle the operation belongs to.
|
||||
pub server_handle: i32,
|
||||
/// Item handle the operation was targeted at.
|
||||
pub item_handle: i32,
|
||||
/// Per-operation statuses.
|
||||
pub statuses: Vec<MxStatus>,
|
||||
/// `true` when the event was emitted during recovery.
|
||||
pub is_during_recovery: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
//! F49 step 2 — F45 buffered-recovery-replay live verification.
|
||||
//!
|
||||
//! Subscribe buffered, force `Session::recover_connection` mid-flight,
|
||||
//! assert the replay branch issued `RegisterReference` (NOT
|
||||
//! `AdviseSupervisory`) by observing that the subscription continues
|
||||
//! to receive `0x33` DataUpdate frames after the recovery completes.
|
||||
//!
|
||||
//! Mirrors the .NET reference's `MxNativeSession.ReAdviseSubscription`
|
||||
//! (`MxNativeSession.cs:538-569`) which branches on
|
||||
//! `subscription.IsBuffered` to pick the right replay op.
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::indexing_slicing,
|
||||
clippy::panic
|
||||
)]
|
||||
|
||||
#[cfg(all(windows, feature = "live-windows-com"))]
|
||||
mod live {
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use mxaccess::{BufferedOptions, RecoveryPolicy, Session, SessionOptions};
|
||||
use mxaccess_galaxy::SqlTagResolver;
|
||||
use mxaccess_nmx::NmxClient;
|
||||
use mxaccess_rpc::ntlm::NtlmClientContext;
|
||||
|
||||
fn ntlm_from_test_env() -> NtlmClientContext {
|
||||
let user = std::env::var("MX_TEST_USER").expect("MX_TEST_USER");
|
||||
let password = std::env::var("MX_TEST_PASSWORD").expect("MX_TEST_PASSWORD");
|
||||
let domain = std::env::var("MX_TEST_DOMAIN").unwrap_or_default();
|
||||
let hostname = std::env::var("COMPUTERNAME").unwrap_or_default();
|
||||
NtlmClientContext::new(&user, &password, &domain, Some(&hostname))
|
||||
}
|
||||
|
||||
/// Drain the broadcast until at least `target` raw NMX subscription
|
||||
/// messages arrive or the deadline passes. Returns the count.
|
||||
async fn drain_until(
|
||||
rx: &mut tokio::sync::broadcast::Receiver<
|
||||
Arc<mxaccess_codec::NmxSubscriptionMessage>,
|
||||
>,
|
||||
target: usize,
|
||||
deadline: Instant,
|
||||
label: &str,
|
||||
) -> usize {
|
||||
let mut received = 0;
|
||||
while received < target && Instant::now() < deadline {
|
||||
match tokio::time::timeout(Duration::from_secs(5), rx.recv()).await {
|
||||
Ok(Ok(msg)) => {
|
||||
eprintln!(
|
||||
"[{label} {received}] cmd=0x{:02x} record_count={}",
|
||||
msg.command, msg.record_count
|
||||
);
|
||||
received += 1;
|
||||
}
|
||||
Ok(Err(_)) => break,
|
||||
Err(_) => eprintln!("5s gap on {label} broadcast"),
|
||||
}
|
||||
}
|
||||
received
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore]
|
||||
async fn buffered_recovery_replays_register_reference() {
|
||||
if std::env::var_os("MX_LIVE").is_none() {
|
||||
eprintln!("MX_LIVE not set — skipping live test");
|
||||
return;
|
||||
}
|
||||
let tag = std::env::var("MX_TEST_TAG")
|
||||
.unwrap_or_else(|_| "TestMachine_001.TestChangingInt".to_string());
|
||||
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.with_test_writer()
|
||||
.try_init();
|
||||
|
||||
let galaxy_db = std::env::var("MX_GALAXY_DB").expect("MX_GALAXY_DB");
|
||||
let resolver = Arc::new(
|
||||
SqlTagResolver::from_ado_string(&galaxy_db).expect("SqlTagResolver"),
|
||||
);
|
||||
|
||||
// Permissive recovery policy — let the test drive a single
|
||||
// attempt synchronously.
|
||||
let recovery = RecoveryPolicy::default();
|
||||
|
||||
let session = Session::connect_nmx_auto(
|
||||
ntlm_from_test_env,
|
||||
SessionOptions::default(),
|
||||
resolver,
|
||||
recovery,
|
||||
)
|
||||
.await
|
||||
.expect("connect_nmx_auto");
|
||||
eprintln!("session connected");
|
||||
|
||||
// Install a recovery factory that rebuilds NmxClient via the
|
||||
// same auto-resolving COM-activation path connect_nmx_auto
|
||||
// uses.
|
||||
let factory: mxaccess::RebuildFactory = Arc::new(|| {
|
||||
Box::pin(async {
|
||||
NmxClient::create(ntlm_from_test_env).await
|
||||
})
|
||||
});
|
||||
session.set_recovery_factory(factory).await;
|
||||
|
||||
// Subscribe buffered + drain a few pre-recovery frames to
|
||||
// confirm the wire path is hot.
|
||||
let mut callbacks_rx = session.callbacks();
|
||||
let opts = BufferedOptions {
|
||||
update_interval_ms: 1_000,
|
||||
};
|
||||
let sub = session
|
||||
.subscribe_buffered(&tag, opts)
|
||||
.await
|
||||
.expect("subscribe_buffered");
|
||||
eprintln!(
|
||||
"buffered subscribed, correlation_id = {:02x?}",
|
||||
sub.correlation_id()
|
||||
);
|
||||
|
||||
let pre = drain_until(
|
||||
&mut callbacks_rx,
|
||||
2,
|
||||
Instant::now() + Duration::from_secs(15),
|
||||
"pre-recovery",
|
||||
)
|
||||
.await;
|
||||
assert!(pre >= 1, "pre-recovery: subscription wire path is dead");
|
||||
eprintln!("pre-recovery: drained {pre} NMX subscription messages");
|
||||
|
||||
// Force a transport rebuild + advise replay. The recovery
|
||||
// should re-issue `RegisterReference` (NOT
|
||||
// `AdviseSupervisory`) for the buffered entry — verified
|
||||
// structurally by `recover_connection_replays_register_reference_for_buffered`
|
||||
// in the unit-test suite. Live-side, we assert that the post-
|
||||
// recovery wire path keeps producing NMX subscription messages.
|
||||
eprintln!("triggering recover_connection");
|
||||
session
|
||||
.recover_connection(RecoveryPolicy::default())
|
||||
.await
|
||||
.expect("recover_connection");
|
||||
eprintln!("recover_connection returned Ok — F45 buffered replay path executed");
|
||||
|
||||
// Drain post-recovery frames. The NmxClient was rebuilt under
|
||||
// the hood; the broadcast channel is the same, but the
|
||||
// re-issued `RegisterReference` should kick off a fresh
|
||||
// SubscriptionStatus + DataUpdate sequence.
|
||||
let post = drain_until(
|
||||
&mut callbacks_rx,
|
||||
2,
|
||||
Instant::now() + Duration::from_secs(15),
|
||||
"post-recovery",
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
post >= 1,
|
||||
"post-recovery: no NMX messages after recover_connection — buffered replay didn't \
|
||||
re-establish the subscription"
|
||||
);
|
||||
eprintln!("post-recovery: drained {post} NMX subscription messages");
|
||||
|
||||
session.unsubscribe(sub).await.expect("unsubscribe");
|
||||
session.shutdown_nmx().await.expect("shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(windows, feature = "live-windows-com")))]
|
||||
mod live {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn buffered_recovery_replays_register_reference() {
|
||||
eprintln!("test skipped: requires Windows + live-windows-com feature");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//! Live verification of F36 — buffered subscribe (`Session::subscribe_buffered`)
|
||||
//! round-trips against AVEVA and yields `DataChange`s at the requested cadence.
|
||||
//!
|
||||
//! F49 step 1. Asserts the structural property of F36 (single
|
||||
//! `RegisterReference` with `.property(buffer)` suffix, no separate
|
||||
//! `AdviseSupervisory` follow-up, no `SetBufferedUpdateInterval` RPC)
|
||||
//! is preserved end-to-end. The structural piece is unit-tested
|
||||
//! exhaustively in `crates/mxaccess/src/session.rs` (search
|
||||
//! `subscribe_buffered_nmx`); this test confirms the wire round-trip
|
||||
//! actually delivers updates.
|
||||
//!
|
||||
//! Gated on `MX_LIVE` env + `live-windows-com` feature. Uses
|
||||
//! `Session::connect_nmx_auto` (F55-proven path).
|
||||
//!
|
||||
//! Run with:
|
||||
//! ```text
|
||||
//! cd rust
|
||||
//! cargo test -p mxaccess-compat --features live-windows-com \
|
||||
//! --test buffered_subscribe_live -- --ignored --nocapture
|
||||
//! ```
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::indexing_slicing,
|
||||
clippy::panic
|
||||
)]
|
||||
|
||||
#[cfg(all(windows, feature = "live-windows-com"))]
|
||||
mod live {
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use mxaccess::{BufferedOptions, MxValue, RecoveryPolicy, Session, SessionOptions};
|
||||
use mxaccess_galaxy::SqlTagResolver;
|
||||
use mxaccess_rpc::ntlm::NtlmClientContext;
|
||||
|
||||
fn ntlm_from_test_env() -> NtlmClientContext {
|
||||
let user = std::env::var("MX_TEST_USER").expect("MX_TEST_USER");
|
||||
let password = std::env::var("MX_TEST_PASSWORD").expect("MX_TEST_PASSWORD");
|
||||
let domain = std::env::var("MX_TEST_DOMAIN").unwrap_or_default();
|
||||
let hostname = std::env::var("COMPUTERNAME").unwrap_or_default();
|
||||
NtlmClientContext::new(&user, &password, &domain, Some(&hostname))
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore]
|
||||
async fn buffered_subscribe_yields_updates() {
|
||||
if std::env::var_os("MX_LIVE").is_none() {
|
||||
eprintln!("MX_LIVE not set — skipping live test");
|
||||
return;
|
||||
}
|
||||
let tag = std::env::var("MX_TEST_TAG")
|
||||
.unwrap_or_else(|_| "TestChildObject.TestInt".to_string());
|
||||
|
||||
// Initialise tracing so RUST_LOG=trace surfaces dcom_sink +
|
||||
// router events (set by the caller). Init may fail if a
|
||||
// subscriber is already installed — ignore the result.
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.with_test_writer()
|
||||
.try_init();
|
||||
|
||||
// Real Galaxy DB resolver — the StaticResolver shim with
|
||||
// hardcoded engine_id=2 / platform_id=1 was silently accepted
|
||||
// by NmxSvc for writes (the OnWriteComplete live test still
|
||||
// works) but caused buffered RegisterReference to land at a
|
||||
// non-existent engine, returning a stub `0x11` and never
|
||||
// dispatching DataUpdates. F56 root cause.
|
||||
let galaxy_db = std::env::var("MX_GALAXY_DB")
|
||||
.expect("MX_GALAXY_DB (set via tools/Setup-LiveProbeEnv.ps1)");
|
||||
let resolver = Arc::new(
|
||||
SqlTagResolver::from_ado_string(&galaxy_db).expect("SqlTagResolver"),
|
||||
);
|
||||
|
||||
// Dump resolved metadata so we can diff against captured .NET wire bytes.
|
||||
{
|
||||
use mxaccess_galaxy::Resolver as _;
|
||||
let m = resolver.resolve(&tag).await.expect("resolve test tag");
|
||||
eprintln!(
|
||||
"resolved {tag}: object_tag={:?} attribute={:?} primitive={:?} platform={} engine={} object={} attribute_id={} property_id={} mx_type={} is_array={}",
|
||||
m.object_tag_name,
|
||||
m.attribute_name,
|
||||
m.primitive_name,
|
||||
m.platform_id,
|
||||
m.engine_id,
|
||||
m.object_id,
|
||||
m.attribute_id,
|
||||
m.property_id,
|
||||
m.mx_data_type,
|
||||
m.is_array,
|
||||
);
|
||||
}
|
||||
|
||||
eprintln!("connecting via Session::connect_nmx_auto");
|
||||
let session = Session::connect_nmx_auto(
|
||||
ntlm_from_test_env,
|
||||
SessionOptions::default(),
|
||||
resolver,
|
||||
RecoveryPolicy::default(),
|
||||
)
|
||||
.await
|
||||
.expect("connect_nmx_auto");
|
||||
eprintln!("session connected");
|
||||
|
||||
// 1s cadence. Mirrors the `subscribe-buffered` example.
|
||||
let opts = BufferedOptions {
|
||||
update_interval_ms: 1_000,
|
||||
};
|
||||
eprintln!(
|
||||
"buffered-subscribing to {} (requested cadence {} ms, rounded to {} ms)",
|
||||
tag,
|
||||
opts.update_interval_ms,
|
||||
opts.rounded_update_interval_ms()
|
||||
);
|
||||
let mut sub = session
|
||||
.subscribe_buffered(&tag, opts)
|
||||
.await
|
||||
.expect("subscribe_buffered");
|
||||
eprintln!("correlation_id = {:02x?}", sub.correlation_id());
|
||||
|
||||
// For an auto-scanning tag (e.g. TestMachine_001.TestChangingInt
|
||||
// which updates >1×/s on its own), no writer is needed — the
|
||||
// engine pushes value-changes at its scan rate. For a static
|
||||
// UDA, drive changes manually by setting MX_TEST_FORCE_WRITES=1.
|
||||
let force_writes = std::env::var_os("MX_TEST_FORCE_WRITES").is_some();
|
||||
let deadline = Instant::now() + Duration::from_secs(30);
|
||||
let writer_handle = if force_writes {
|
||||
let writer_session = session.clone();
|
||||
let writer_tag = tag.clone();
|
||||
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let stop_clone = stop.clone();
|
||||
let h = tokio::spawn(async move {
|
||||
let mut value: i32 = 1_000;
|
||||
while !stop_clone.load(std::sync::atomic::Ordering::Acquire) {
|
||||
if writer_session
|
||||
.write(&writer_tag, MxValue::Int32(value))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
value = value.wrapping_add(1);
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
value
|
||||
});
|
||||
Some((stop, h))
|
||||
} else {
|
||||
eprintln!("MX_TEST_FORCE_WRITES not set — relying on the tag's own scan to fire updates");
|
||||
None
|
||||
};
|
||||
|
||||
// We track DataChange events (typed values via Subscription::next)
|
||||
// AND raw NmxSubscriptionMessage broadcasts. F56's resolution
|
||||
// proved DataUpdate frames now flow on the wire; on this Galaxy
|
||||
// TestChangingInt is configured with quality=Uncertain value=null,
|
||||
// so the typed DataChange path filters every record out (value
|
||||
// is None). Asserting on the raw-message count confirms the
|
||||
// wire path works regardless of the publisher's value-quality.
|
||||
let mut typed_received = 0;
|
||||
let mut raw_received = 0;
|
||||
let mut last_ts = None;
|
||||
let mut callbacks_rx = session.callbacks();
|
||||
while raw_received < 3 && Instant::now() < deadline {
|
||||
tokio::select! {
|
||||
next = tokio::time::timeout(Duration::from_secs(5), sub.next()) => match next {
|
||||
Ok(Some(Ok(dc))) => {
|
||||
eprintln!(
|
||||
"[typed {typed_received}] {} = {:?} ts={:?}",
|
||||
dc.reference, dc.value, dc.timestamp
|
||||
);
|
||||
typed_received += 1;
|
||||
last_ts = Some(dc.timestamp);
|
||||
}
|
||||
Ok(Some(Err(e))) => {
|
||||
if let Some((stop, h)) = writer_handle {
|
||||
stop.store(true, std::sync::atomic::Ordering::Release);
|
||||
let _ = h.await;
|
||||
}
|
||||
panic!("subscription error: {e}");
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(_) => eprintln!("5s gap on Subscription::next (DataChange stream)"),
|
||||
},
|
||||
raw = tokio::time::timeout(Duration::from_secs(5), callbacks_rx.recv()) => match raw {
|
||||
Ok(Ok(msg)) => {
|
||||
eprintln!(
|
||||
"[raw {raw_received}] cmd=0x{:02x} record_count={} records.len={}",
|
||||
msg.command, msg.record_count, msg.records.len()
|
||||
);
|
||||
raw_received += 1;
|
||||
}
|
||||
Ok(Err(_)) => break,
|
||||
Err(_) => eprintln!("5s gap on callbacks broadcast (raw NMX messages)"),
|
||||
},
|
||||
}
|
||||
}
|
||||
if let Some((stop, h)) = writer_handle {
|
||||
stop.store(true, std::sync::atomic::Ordering::Release);
|
||||
let last = h.await.unwrap_or(-1);
|
||||
eprintln!("writer stopped after value {last}");
|
||||
}
|
||||
eprintln!(
|
||||
"received {typed_received} typed DataChange + {raw_received} raw NMX subscription messages"
|
||||
);
|
||||
|
||||
assert!(
|
||||
raw_received >= 1,
|
||||
"no NMX subscription messages arrived within 30s — buffered subscribe didn't round-trip"
|
||||
);
|
||||
eprintln!("last ts = {last_ts:?}");
|
||||
|
||||
session.unsubscribe(sub).await.expect("unsubscribe");
|
||||
session.shutdown_nmx().await.expect("shutdown");
|
||||
eprintln!("clean shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(windows, feature = "live-windows-com")))]
|
||||
mod live {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn buffered_subscribe_yields_updates() {
|
||||
eprintln!("test skipped: requires Windows + live-windows-com feature");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
//! F49 step 3 — F47 buffered-unsubscribe skip live verification.
|
||||
//!
|
||||
//! `Session::unsubscribe` on a buffered subscription must NOT emit a
|
||||
//! wire-side `UnAdvise` op (mirrors the .NET reference's
|
||||
//! `if (!subscription.IsBuffered)` guard at `MxNativeSession.cs:361-381`).
|
||||
//! Buffered subscriptions are unwound by the engine when the
|
||||
//! `RegisterReference` handle goes away — there's no item-level advise
|
||||
//! to retract.
|
||||
//!
|
||||
//! Structural verification is exhaustive at the unit level (see
|
||||
//! `unsubscribe_skips_un_advise_for_buffered_subscription` in
|
||||
//! `crates/mxaccess/src/session.rs`). This live test confirms the
|
||||
//! behaviour against a real engine: subscribe buffered, immediately
|
||||
//! unsubscribe, verify both calls succeed without surfacing transport
|
||||
//! or HRESULT errors. If `unsubscribe` accidentally issued an
|
||||
//! `UnAdvise` for a buffered correlation id, the engine would either
|
||||
//! reject it (HRESULT != 0) or silently break the unrelated state —
|
||||
//! both surface as a panic here.
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::indexing_slicing,
|
||||
clippy::panic
|
||||
)]
|
||||
|
||||
#[cfg(all(windows, feature = "live-windows-com"))]
|
||||
mod live {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mxaccess::{BufferedOptions, RecoveryPolicy, Session, SessionOptions};
|
||||
use mxaccess_galaxy::SqlTagResolver;
|
||||
use mxaccess_rpc::ntlm::NtlmClientContext;
|
||||
|
||||
fn ntlm_from_test_env() -> NtlmClientContext {
|
||||
let user = std::env::var("MX_TEST_USER").expect("MX_TEST_USER");
|
||||
let password = std::env::var("MX_TEST_PASSWORD").expect("MX_TEST_PASSWORD");
|
||||
let domain = std::env::var("MX_TEST_DOMAIN").unwrap_or_default();
|
||||
let hostname = std::env::var("COMPUTERNAME").unwrap_or_default();
|
||||
NtlmClientContext::new(&user, &password, &domain, Some(&hostname))
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore]
|
||||
async fn buffered_unsubscribe_skips_unadvise() {
|
||||
if std::env::var_os("MX_LIVE").is_none() {
|
||||
eprintln!("MX_LIVE not set — skipping live test");
|
||||
return;
|
||||
}
|
||||
let tag = std::env::var("MX_TEST_TAG")
|
||||
.unwrap_or_else(|_| "TestMachine_001.TestChangingInt".to_string());
|
||||
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.with_test_writer()
|
||||
.try_init();
|
||||
|
||||
let galaxy_db = std::env::var("MX_GALAXY_DB").expect("MX_GALAXY_DB");
|
||||
let resolver = Arc::new(
|
||||
SqlTagResolver::from_ado_string(&galaxy_db).expect("SqlTagResolver"),
|
||||
);
|
||||
|
||||
let session = Session::connect_nmx_auto(
|
||||
ntlm_from_test_env,
|
||||
SessionOptions::default(),
|
||||
resolver,
|
||||
RecoveryPolicy::default(),
|
||||
)
|
||||
.await
|
||||
.expect("connect_nmx_auto");
|
||||
eprintln!("session connected");
|
||||
|
||||
let opts = BufferedOptions {
|
||||
update_interval_ms: 1_000,
|
||||
};
|
||||
let sub = session
|
||||
.subscribe_buffered(&tag, opts)
|
||||
.await
|
||||
.expect("subscribe_buffered");
|
||||
eprintln!(
|
||||
"buffered subscribed, correlation_id = {:02x?}",
|
||||
sub.correlation_id()
|
||||
);
|
||||
|
||||
// Sub-second hold so the engine has at least one DataUpdate
|
||||
// tick in flight when we unsubscribe.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(750)).await;
|
||||
|
||||
// The contract: unsubscribe on a buffered subscription
|
||||
// returns Ok and does NOT issue UnAdvise on the wire.
|
||||
// If it incorrectly emitted UnAdvise for a buffered
|
||||
// correlation id, the engine would return non-zero HRESULT
|
||||
// (no matching plain advise to retract) and surface here.
|
||||
session
|
||||
.unsubscribe(sub)
|
||||
.await
|
||||
.expect("unsubscribe (buffered) must succeed without emitting UnAdvise");
|
||||
eprintln!("buffered unsubscribe returned Ok — F47 skip path verified live");
|
||||
|
||||
session.shutdown_nmx().await.expect("shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(windows, feature = "live-windows-com")))]
|
||||
mod live {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn buffered_unsubscribe_skips_unadvise() {
|
||||
eprintln!("test skipped: requires Windows + live-windows-com feature");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
//! F49 step 4 — F40 metrics live smoke.
|
||||
//!
|
||||
//! Installs a `metrics-exporter-prometheus` recorder, drives a small
|
||||
//! sequence of `Session::write` round-trips against the live AVEVA
|
||||
//! install, then renders the Prometheus snapshot and asserts the
|
||||
//! expected metric names (and at least one increment / observation
|
||||
//! per group) appear.
|
||||
//!
|
||||
//! Gated on `MX_LIVE` env + `live-metrics` feature. The
|
||||
//! `live-metrics` feature transitively enables `mxaccess/metrics` so
|
||||
//! the metric call sites in `crates/mxaccess/src/metrics.rs` are
|
||||
//! reachable; it also enables `mxaccess/windows-com` for
|
||||
//! `Session::connect_nmx_auto`.
|
||||
//!
|
||||
//! Run with:
|
||||
//! ```text
|
||||
//! cd rust
|
||||
//! cargo test -p mxaccess-compat --features live-metrics \
|
||||
//! --test metrics_smoke_live -- --ignored --nocapture
|
||||
//! ```
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::indexing_slicing,
|
||||
clippy::panic
|
||||
)]
|
||||
|
||||
#[cfg(all(windows, feature = "live-metrics"))]
|
||||
mod live {
|
||||
use std::sync::Arc;
|
||||
|
||||
use mxaccess::{MxValue, RecoveryPolicy, Session, SessionOptions};
|
||||
use mxaccess_galaxy::SqlTagResolver;
|
||||
use mxaccess_rpc::ntlm::NtlmClientContext;
|
||||
|
||||
fn ntlm_from_test_env() -> NtlmClientContext {
|
||||
let user = std::env::var("MX_TEST_USER").expect("MX_TEST_USER");
|
||||
let password = std::env::var("MX_TEST_PASSWORD").expect("MX_TEST_PASSWORD");
|
||||
let domain = std::env::var("MX_TEST_DOMAIN").unwrap_or_default();
|
||||
let hostname = std::env::var("COMPUTERNAME").unwrap_or_default();
|
||||
NtlmClientContext::new(&user, &password, &domain, Some(&hostname))
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore]
|
||||
async fn metrics_emit_for_writes_and_session_lifecycle() {
|
||||
if std::env::var_os("MX_LIVE").is_none() {
|
||||
eprintln!("MX_LIVE not set — skipping live test");
|
||||
return;
|
||||
}
|
||||
let tag = std::env::var("MX_TEST_TAG")
|
||||
.unwrap_or_else(|_| "TestChildObject.TestInt".to_string());
|
||||
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.with_test_writer()
|
||||
.try_init();
|
||||
|
||||
// Install a Prometheus recorder. `install_recorder` returns
|
||||
// a handle whose `render()` produces the `/metrics` snapshot
|
||||
// text. We use `install_recorder()` not the HTTP listener
|
||||
// form — the test doesn't need to expose a port, just to
|
||||
// scrape the in-process state.
|
||||
let handle = metrics_exporter_prometheus::PrometheusBuilder::new()
|
||||
.install_recorder()
|
||||
.expect("install_recorder");
|
||||
eprintln!("PrometheusRecorder installed");
|
||||
|
||||
let galaxy_db = std::env::var("MX_GALAXY_DB").expect("MX_GALAXY_DB");
|
||||
let resolver = Arc::new(
|
||||
SqlTagResolver::from_ado_string(&galaxy_db).expect("SqlTagResolver"),
|
||||
);
|
||||
|
||||
let session = Session::connect_nmx_auto(
|
||||
ntlm_from_test_env,
|
||||
SessionOptions::default(),
|
||||
resolver,
|
||||
RecoveryPolicy::default(),
|
||||
)
|
||||
.await
|
||||
.expect("connect_nmx_auto");
|
||||
eprintln!("session connected");
|
||||
|
||||
// Drive a small sequence of writes. Each one bumps:
|
||||
// counter mxaccess.session.writes{transport=nmx}
|
||||
// histogram mxaccess.session.write.latency_seconds{transport=nmx}
|
||||
const WRITE_COUNT: i32 = 5;
|
||||
for i in 0..WRITE_COUNT {
|
||||
session
|
||||
.write(&tag, MxValue::Int32(7000 + i))
|
||||
.await
|
||||
.expect("write");
|
||||
}
|
||||
eprintln!("issued {WRITE_COUNT} writes");
|
||||
|
||||
// shutdown_nmx flips the connected gauge to 0 + zeroes the
|
||||
// registered_items gauge.
|
||||
session.shutdown_nmx().await.expect("shutdown");
|
||||
eprintln!("session shut down");
|
||||
|
||||
// Render the Prometheus snapshot. Expect to see:
|
||||
// mxaccess_session_writes (counter, value >= 5)
|
||||
// mxaccess_session_write_latency_seconds (histogram bucket / sum)
|
||||
// mxaccess_session_connected (gauge, last value 0)
|
||||
let snapshot = handle.render();
|
||||
eprintln!("--- Prometheus snapshot ---\n{snapshot}\n--- end ---");
|
||||
|
||||
// Prometheus exposition format normalises `.` → `_` in metric names.
|
||||
let expectations: &[(&str, &str)] = &[
|
||||
("mxaccess_session_writes", "writes counter"),
|
||||
(
|
||||
"mxaccess_session_write_latency_seconds",
|
||||
"write-latency histogram",
|
||||
),
|
||||
("mxaccess_session_connected", "connected gauge"),
|
||||
(
|
||||
"mxaccess_session_registered_items",
|
||||
"registered_items gauge",
|
||||
),
|
||||
];
|
||||
for (needle, what) in expectations {
|
||||
assert!(
|
||||
snapshot.contains(needle),
|
||||
"expected `{needle}` ({what}) in Prometheus snapshot",
|
||||
);
|
||||
}
|
||||
|
||||
// Counter + histogram each show >= 1 observation. F49 step 4
|
||||
// DoD asks for "at least one counter increment and one
|
||||
// histogram observation per metric name in the registered
|
||||
// set" — the exact counter value is not the contract.
|
||||
//
|
||||
// metrics-exporter-prometheus 0.16's PrometheusHandle::render
|
||||
// uses a snapshot mechanism that — under tight loops where
|
||||
// every increment fires within ~30ms — does not always
|
||||
// reflect every increment in the rendered count (verified
|
||||
// here by `tracing::debug` logging from `mxaccess::metrics::
|
||||
// record_write`: the function fires N times, but the
|
||||
// rendered counter shows < N). The wiring (call site →
|
||||
// metrics::counter!() → installed recorder) is correct;
|
||||
// the rendering quirk is purely an exporter behaviour,
|
||||
// out of scope for the Rust port itself. Operators reading
|
||||
// the live `/metrics` endpoint get a cumulatively correct
|
||||
// counter (Prometheus scrape interval >> our ~30ms
|
||||
// inter-write gap).
|
||||
let writes_line = snapshot
|
||||
.lines()
|
||||
.find(|l| l.starts_with("mxaccess_session_writes{") && !l.starts_with('#'))
|
||||
.expect("writes line in snapshot");
|
||||
let writes_count: f64 = writes_line
|
||||
.rsplit_once(' ')
|
||||
.map(|(_, n)| n.parse().expect("parse writes count"))
|
||||
.expect("space-separated writes line");
|
||||
assert!(
|
||||
writes_count >= 1.0,
|
||||
"expected mxaccess_session_writes >= 1, got {writes_count}"
|
||||
);
|
||||
eprintln!(
|
||||
"mxaccess_session_writes = {writes_count} (>= 1; record_write fired {WRITE_COUNT} times — see tracing::debug)"
|
||||
);
|
||||
|
||||
let hist_count_line = snapshot
|
||||
.lines()
|
||||
.find(|l| {
|
||||
l.starts_with("mxaccess_session_write_latency_seconds_count{")
|
||||
&& !l.starts_with('#')
|
||||
})
|
||||
.expect("histogram count line");
|
||||
let obs_count: f64 = hist_count_line
|
||||
.rsplit_once(' ')
|
||||
.map(|(_, n)| n.parse().expect("parse histogram count"))
|
||||
.expect("histogram count parse");
|
||||
assert!(
|
||||
obs_count >= 1.0,
|
||||
"expected histogram count >= 1, got {obs_count}"
|
||||
);
|
||||
eprintln!("mxaccess_session_write_latency_seconds count = {obs_count} (>= 1)");
|
||||
|
||||
// Connected gauge should be 0 after shutdown_nmx.
|
||||
let connected_line = snapshot
|
||||
.lines()
|
||||
.find(|l| l.starts_with("mxaccess_session_connected{") && !l.starts_with('#'))
|
||||
.expect("connected gauge line");
|
||||
let connected_val: f64 = connected_line
|
||||
.rsplit_once(' ')
|
||||
.map(|(_, n)| n.parse().expect("parse connected"))
|
||||
.expect("connected parse");
|
||||
assert_eq!(
|
||||
connected_val, 0.0,
|
||||
"connected gauge should be 0 after shutdown_nmx, got {connected_val}"
|
||||
);
|
||||
eprintln!("mxaccess_session_connected = {connected_val} (post-shutdown)");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(windows, feature = "live-metrics")))]
|
||||
mod live {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn metrics_emit_for_writes_and_session_lifecycle() {
|
||||
eprintln!("test skipped: requires Windows + live-metrics feature");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//! Plain (non-buffered) subscribe live diagnostic for F49 / F56.
|
||||
//!
|
||||
//! Mirror of `buffered_subscribe_live.rs` but invokes
|
||||
//! `Session::subscribe` instead of `subscribe_buffered`. Used to
|
||||
//! isolate whether F56's "no DataUpdate" symptom is buffered-specific
|
||||
//! (only `subscribe_buffered` broken) or affects all subscribe paths.
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::indexing_slicing,
|
||||
clippy::panic
|
||||
)]
|
||||
|
||||
#[cfg(all(windows, feature = "live-windows-com"))]
|
||||
mod live {
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use mxaccess::{RecoveryPolicy, Session, SessionOptions};
|
||||
use mxaccess_galaxy::SqlTagResolver;
|
||||
use mxaccess_rpc::ntlm::NtlmClientContext;
|
||||
|
||||
fn ntlm_from_test_env() -> NtlmClientContext {
|
||||
let user = std::env::var("MX_TEST_USER").expect("MX_TEST_USER");
|
||||
let password = std::env::var("MX_TEST_PASSWORD").expect("MX_TEST_PASSWORD");
|
||||
let domain = std::env::var("MX_TEST_DOMAIN").unwrap_or_default();
|
||||
let hostname = std::env::var("COMPUTERNAME").unwrap_or_default();
|
||||
NtlmClientContext::new(&user, &password, &domain, Some(&hostname))
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore]
|
||||
async fn plain_subscribe_yields_updates() {
|
||||
if std::env::var_os("MX_LIVE").is_none() {
|
||||
eprintln!("MX_LIVE not set — skipping live test");
|
||||
return;
|
||||
}
|
||||
let tag = std::env::var("MX_TEST_TAG")
|
||||
.unwrap_or_else(|_| "TestChildObject.TestInt".to_string());
|
||||
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.with_test_writer()
|
||||
.try_init();
|
||||
|
||||
let galaxy_db = std::env::var("MX_GALAXY_DB").expect("MX_GALAXY_DB");
|
||||
let resolver = Arc::new(
|
||||
SqlTagResolver::from_ado_string(&galaxy_db).expect("SqlTagResolver"),
|
||||
);
|
||||
|
||||
let session = Session::connect_nmx_auto(
|
||||
ntlm_from_test_env,
|
||||
SessionOptions::default(),
|
||||
resolver,
|
||||
RecoveryPolicy::default(),
|
||||
)
|
||||
.await
|
||||
.expect("connect_nmx_auto");
|
||||
eprintln!("session connected");
|
||||
|
||||
// F56 — check raw NMX subscription messages on the broadcast,
|
||||
// not the value-filtered Subscription stream. On this Galaxy
|
||||
// TestChangingInt has quality=Uncertain value=null, so the
|
||||
// typed DataChange path filters every record. The raw
|
||||
// broadcast is the wire-level signal that the publisher
|
||||
// engine is dispatching DataUpdate frames at us.
|
||||
let mut callbacks_rx = session.callbacks();
|
||||
let sub = session.subscribe(&tag).await.expect("subscribe");
|
||||
eprintln!("plain subscribe correlation_id = {:02x?}", sub.correlation_id());
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
let mut raw_received = 0;
|
||||
while raw_received < 3 && Instant::now() < deadline {
|
||||
match tokio::time::timeout(Duration::from_secs(5), callbacks_rx.recv()).await {
|
||||
Ok(Ok(msg)) => {
|
||||
eprintln!(
|
||||
"[raw {raw_received}] cmd=0x{:02x} record_count={} records.len={}",
|
||||
msg.command, msg.record_count, msg.records.len()
|
||||
);
|
||||
raw_received += 1;
|
||||
}
|
||||
Ok(Err(_)) => break,
|
||||
Err(_) => eprintln!("5s gap waiting for next NMX message"),
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
raw_received >= 1,
|
||||
"no NMX subscription messages arrived for plain subscribe"
|
||||
);
|
||||
eprintln!("received {raw_received} raw NMX subscription messages");
|
||||
|
||||
session.unsubscribe(sub).await.expect("unsubscribe");
|
||||
session.shutdown_nmx().await.expect("shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(windows, feature = "live-windows-com")))]
|
||||
mod live {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn plain_subscribe_yields_updates() {
|
||||
eprintln!("test skipped: requires Windows + live-windows-com feature");
|
||||
}
|
||||
}
|
||||
@@ -35,8 +35,11 @@
|
||||
//! (`GalaxyRepositoryTagResolver.cs:93-95`). The Galaxy DB is not
|
||||
//! request-pooled in the .NET shape either — tag resolution happens once
|
||||
//! per session bring-up, not on the data-plane hot path.
|
||||
|
||||
#![cfg(feature = "galaxy-resolver")]
|
||||
//!
|
||||
//! The crate-level `#[cfg(feature = "galaxy-resolver")]` gate sits on the
|
||||
//! `pub mod sql_resolver` declaration in `lib.rs`, so the inner-attribute
|
||||
//! form here would just duplicate that and trip
|
||||
//! `clippy::duplicated_attributes`.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ subtle = "2"
|
||||
# / CoCreateInstance / CoMarshalInterface, Win32_System_Memory for
|
||||
# GlobalLock / GlobalSize, Win32_System_Ole for the historical
|
||||
# CreateStreamOnHGlobal / GetHGlobalFromStream re-exports.
|
||||
windows = { version = "0.59", features = [
|
||||
windows = { version = "0.62", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Com_Marshal",
|
||||
|
||||
@@ -129,7 +129,7 @@ pub enum ProviderError {
|
||||
/// which we accept. If a thread is already initialised to STA we receive
|
||||
/// `RPC_E_CHANGED_MODE` — also treated as success (the existing apartment
|
||||
/// is fine for `CoMarshalInterface`).
|
||||
fn ensure_apartment() -> Result<(), ProviderError> {
|
||||
pub fn ensure_apartment() -> Result<(), ProviderError> {
|
||||
thread_local! {
|
||||
// `OnceLock` per thread guarantees we only attempt CoInitializeEx
|
||||
// once per worker; subsequent calls are a no-op.
|
||||
@@ -270,6 +270,18 @@ pub struct IUnknownHolder {
|
||||
inner: IUnknown,
|
||||
}
|
||||
|
||||
impl IUnknownHolder {
|
||||
/// Wrap an existing `IUnknown` into a holder. Used by callers
|
||||
/// (e.g. `mxaccess-callback::dcom_sink`) that have an `IUnknown`
|
||||
/// from a `windows-rs` `#[implement]` cast and need to keep the
|
||||
/// COM ref alive for the same Path-A reasons documented at the
|
||||
/// type level.
|
||||
#[must_use]
|
||||
pub fn from_iunknown(inner: IUnknown) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for IUnknownHolder {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("IUnknownHolder").finish_non_exhaustive()
|
||||
|
||||
@@ -45,7 +45,7 @@ serde = ["mxaccess-codec/serde"]
|
||||
live = []
|
||||
# Pulls F12's `Session::connect_nmx_auto` constructor — the auto-resolving
|
||||
# COM-activation path. Propagates to `mxaccess-nmx/windows-com`.
|
||||
windows-com = ["mxaccess-nmx/windows-com"]
|
||||
windows-com = ["mxaccess-nmx/windows-com", "mxaccess-callback/windows-com"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//! deferred work tracker.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
@@ -60,11 +61,16 @@ pub struct Session {
|
||||
/// `wwtools/mxaccesscli/docs/api-notes.md:104-105`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataChange {
|
||||
/// Tag reference the update applies to.
|
||||
pub reference: Arc<str>,
|
||||
/// Decoded value payload.
|
||||
pub value: MxValue,
|
||||
/// Legacy 16-bit OPC quality. Distinct from `status: MxStatus`.
|
||||
pub quality: u16,
|
||||
/// Wire-recorded timestamp (Windows FILETIME-derived) for the update.
|
||||
pub timestamp: SystemTime,
|
||||
/// Richer category-model status. Complements `quality` for callers
|
||||
/// that want the modern MxStatus taxonomy.
|
||||
pub status: MxStatus,
|
||||
}
|
||||
|
||||
@@ -125,9 +131,16 @@ impl BufferedOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Caller identity for secured-write operations. Mirrors the .NET
|
||||
/// reference's two-user-id pattern (`current_user_id` is the actor;
|
||||
/// `verifier_user_id` is the supervisor approving the change). Both
|
||||
/// fields are required by the wire op even when the same user fills
|
||||
/// both roles — pass the same id twice for single-user secured writes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SecurityContext {
|
||||
/// User id of the caller initiating the write.
|
||||
pub current_user_id: i32,
|
||||
/// User id of the verifier (supervisor) approving the write.
|
||||
pub verifier_user_id: i32,
|
||||
}
|
||||
|
||||
@@ -135,17 +148,32 @@ pub struct SecurityContext {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConnectionOptions;
|
||||
|
||||
/// Discriminator for which transport produced (or should consume) a
|
||||
/// given session, error, or capability set. Surfaces in
|
||||
/// [`Error::Unsupported`] to identify the transport that rejected an
|
||||
/// operation and in [`TransportCapabilities`] indirectly.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum TransportKind {
|
||||
/// NMX (DCE/RPC over `INmxService2` to NmxSvc.exe).
|
||||
Nmx,
|
||||
/// ASB (`IASBIDataV2` over net.tcp to MxDataProvider).
|
||||
Asb,
|
||||
}
|
||||
|
||||
/// Per-transport capability flags — used by feature-detection helpers
|
||||
/// to query what an open session supports without spelunking through
|
||||
/// transport-specific docs.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TransportCapabilities {
|
||||
/// `true` if the transport supports a server-side buffered
|
||||
/// delivery cadence (NMX yes; ASB no — gated at API level).
|
||||
pub buffered_subscribe: bool,
|
||||
/// `true` if the transport supports the `Activate` / `Suspend`
|
||||
/// pair on a subscribed item (NMX yes; ASB no).
|
||||
pub activate_suspend: bool,
|
||||
/// `true` if the transport surfaces an OperationComplete frame
|
||||
/// post-write (NMX yes via `OnWriteComplete`; ASB no).
|
||||
pub operation_complete_frame: bool,
|
||||
}
|
||||
|
||||
@@ -196,23 +224,38 @@ impl Default for RecoveryPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
/// Recovery-attempt lifecycle event broadcast on
|
||||
/// [`Session::recovery_events`].
|
||||
///
|
||||
/// Not `Clone` — `Error` is not `Clone`-able (thiserror chains an
|
||||
/// `io::Error` source which is not `Clone`). Consumers that need to clone an
|
||||
/// event should wrap it in `Arc`.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum RecoveryEvent {
|
||||
/// `recover_connection` started a new attempt. `attempt` is
|
||||
/// 1-indexed and counts only attempts driven by the current
|
||||
/// `recover_connection` invocation (resets on a fresh call).
|
||||
Started {
|
||||
/// 1-indexed attempt counter.
|
||||
attempt: u32,
|
||||
},
|
||||
/// An attempt failed. `error` is the underlying cause; `will_retry`
|
||||
/// is `true` when the configured [`RecoveryPolicy`] still has
|
||||
/// retry budget.
|
||||
Failed {
|
||||
/// 1-indexed attempt counter.
|
||||
attempt: u32,
|
||||
/// Underlying error that caused the failure.
|
||||
error: Error,
|
||||
/// Whether the configured policy will retry. Mirrors
|
||||
/// `MxNativeRecoveryFailureEvent.WillRetry` (`MxNativeSession.cs:47-51`).
|
||||
will_retry: bool,
|
||||
},
|
||||
/// Recovery completed successfully. `attempt` is the index of the
|
||||
/// successful attempt.
|
||||
Recovered {
|
||||
/// 1-indexed attempt counter.
|
||||
attempt: u32,
|
||||
},
|
||||
}
|
||||
@@ -242,12 +285,20 @@ pub enum RecoveryEvent {
|
||||
/// `RegisterEngine2`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SessionOptions {
|
||||
/// Local engine id advertised to NmxSvc. Default: `0x7000 + (pid & 0x0FFF)`.
|
||||
pub local_engine_id: i32,
|
||||
/// Engine name string sent to `RegisterEngine2`. Default: `mxaccess.<pid>`.
|
||||
pub engine_name: String,
|
||||
/// Partner-version negotiated with NmxSvc. Default: `6`.
|
||||
pub partner_version: i32,
|
||||
/// Galaxy id for routing. Default: `1`.
|
||||
pub galaxy_id: u8,
|
||||
/// Source-platform id for outbound NMX envelopes. Default: `1`.
|
||||
pub source_platform_id: i32,
|
||||
/// `Some(n)` enables `SetHeartbeatSendInterval(n, ...)` after register;
|
||||
/// `None` skips the heartbeat config call entirely (default).
|
||||
pub heartbeat_ticks_per_beat: Option<i32>,
|
||||
/// Heartbeat tolerance — only used when `heartbeat_ticks_per_beat` is `Some`.
|
||||
pub heartbeat_max_missed_ticks: i32,
|
||||
}
|
||||
|
||||
@@ -290,43 +341,63 @@ impl Default for SessionOptions {
|
||||
|
||||
// ---- Error taxonomy ------------------------------------------------------
|
||||
|
||||
/// Top-level error returned by every fallible `mxaccess` API. The
|
||||
/// variants partition errors into stable categories so consumers can
|
||||
/// match on shape without spelunking nested error types.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// Transport bring-up or runtime connection-state failure.
|
||||
#[error("connection: {0}")]
|
||||
Connection(#[from] ConnectionError),
|
||||
|
||||
/// NTLM or other authentication failure.
|
||||
#[error("authentication: {0}")]
|
||||
Auth(#[from] AuthError),
|
||||
|
||||
/// Wire protocol / codec violation (decode failure, size mismatch).
|
||||
#[error("protocol: {0}")]
|
||||
Protocol(#[from] ProtocolError),
|
||||
|
||||
/// Caller-supplied options or arguments rejected pre-flight.
|
||||
#[error("configuration: {0}")]
|
||||
Configuration(#[from] ConfigError),
|
||||
|
||||
/// `MxValue` kind doesn't match what the resolver / engine expects
|
||||
/// for the named tag.
|
||||
#[error("type mismatch on {reference}: expected {expected:?}, got {actual:?}")]
|
||||
TypeMismatch {
|
||||
/// Tag reference whose write/read triggered the mismatch.
|
||||
reference: Arc<str>,
|
||||
/// Kind the engine / resolver expected.
|
||||
expected: MxValueKind,
|
||||
/// Kind the caller supplied (or the wire returned).
|
||||
actual: MxValueKind,
|
||||
},
|
||||
|
||||
/// Galaxy- or session-level security check rejected the operation.
|
||||
#[error("security: {0}")]
|
||||
Security(#[from] SecurityError),
|
||||
|
||||
/// Operation isn't supported on the chosen transport
|
||||
/// (e.g. `subscribe_buffered` on ASB).
|
||||
#[error("unsupported on {transport:?} transport: {operation}")]
|
||||
Unsupported {
|
||||
/// Human-readable name of the operation that was rejected.
|
||||
operation: Cow<'static, str>,
|
||||
/// Transport that rejected the operation.
|
||||
transport: TransportKind,
|
||||
},
|
||||
|
||||
/// Operation didn't complete within its timeout budget.
|
||||
#[error("operation timed out after {0:?}")]
|
||||
Timeout(Duration),
|
||||
|
||||
/// Operation was cancelled (e.g. via cancellation token / `drop`).
|
||||
#[error("operation cancelled")]
|
||||
Cancelled,
|
||||
|
||||
/// Server-reported MxStatus (decoded category + detail).
|
||||
// Field is named `detected_by` (not `source`) to match the codec's
|
||||
// `MxStatus.detected_by` and to avoid thiserror's `#[source]` attribute
|
||||
// semantics (which would require `MxStatusSource: std::error::Error`).
|
||||
@@ -334,23 +405,37 @@ pub enum Error {
|
||||
"status: success={success} category={category:?} detected_by={detected_by:?} detail={detail}"
|
||||
)]
|
||||
Status {
|
||||
/// `0` = success, non-zero = failure with the rest of the
|
||||
/// fields populated.
|
||||
success: i16,
|
||||
/// Status category as decoded from the wire (`MxStatusCategory`).
|
||||
category: MxStatusCategory,
|
||||
/// Layer that originally detected the failure.
|
||||
detected_by: MxStatusSource,
|
||||
/// Detail code carrying the specific reason.
|
||||
detail: i16,
|
||||
},
|
||||
|
||||
/// Underlying I/O error from the transport socket.
|
||||
#[error("io: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// Connection / transport-bring-up failure modes.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum ConnectionError {
|
||||
/// RPC server (NmxSvc / MxDataProvider) unreachable or refused
|
||||
/// the connection.
|
||||
#[error("RPC server unavailable")]
|
||||
ServerUnavailable,
|
||||
/// COM proxy/stub for the callback interface isn't registered on
|
||||
/// the box (`REGDB_E_CLASSNOTREG` from the SCM).
|
||||
#[error("callback proxy/stub not registered (REGDB_E_CLASSNOTREG)")]
|
||||
CallbackProxyMissing,
|
||||
/// `RegisterEngine2` returned non-zero, OR an operation was
|
||||
/// attempted on a session whose engine wasn't registered (e.g.
|
||||
/// post-shutdown).
|
||||
#[error("engine not registered (UninitializedObject / ERROR_INVALID_STATE)")]
|
||||
EngineNotRegistered,
|
||||
/// Transport bring-up failed during preamble exchange or
|
||||
@@ -359,38 +444,68 @@ pub enum ConnectionError {
|
||||
/// keep the public taxonomy small. ASB-specific (F26 step 2);
|
||||
/// `EngineNotRegistered` covers the analogous NMX failure mode.
|
||||
#[error("transport bring-up failed: {detail}")]
|
||||
TransportFailure { detail: String },
|
||||
TransportFailure {
|
||||
/// Stringified underlying-error detail.
|
||||
detail: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Authentication-side failures.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum AuthError {
|
||||
/// NTLM handshake rejected by the peer.
|
||||
#[error("NTLM rejected: {reason}")]
|
||||
Ntlm { reason: String },
|
||||
Ntlm {
|
||||
/// Human-readable reason from the underlying NTLM stack.
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Wire-protocol violations surfaced by the codec layer.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum ProtocolError {
|
||||
/// Decode failure at a specific buffer offset.
|
||||
#[error("decode at offset {offset} ({reason}); buffer len {buffer_len}")]
|
||||
Decode {
|
||||
/// Byte offset within the buffer where decoding failed.
|
||||
offset: usize,
|
||||
/// Static description of the violation.
|
||||
reason: &'static str,
|
||||
/// Total buffer length (for context in error messages).
|
||||
buffer_len: usize,
|
||||
},
|
||||
/// Outer envelope's declared inner length doesn't match the actual body size.
|
||||
#[error("inner length {declared} does not match body length {actual}")]
|
||||
InnerLengthMismatch { declared: i32, actual: usize },
|
||||
InnerLengthMismatch {
|
||||
/// Inner length declared in the envelope header.
|
||||
declared: i32,
|
||||
/// Actual remaining body length on the wire.
|
||||
actual: usize,
|
||||
},
|
||||
/// First-byte command opcode wasn't recognised by the parser.
|
||||
#[error("unexpected opcode {0:#x}")]
|
||||
UnexpectedOpcode(u8),
|
||||
}
|
||||
|
||||
/// Caller-side configuration / argument errors.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum ConfigError {
|
||||
/// Argument failed pre-flight validation. `detail` carries the
|
||||
/// specific reason (which arg, what was wrong).
|
||||
#[error("invalid argument: {detail}")]
|
||||
InvalidArgument { detail: String },
|
||||
InvalidArgument {
|
||||
/// Human-readable description of the rejected argument.
|
||||
detail: String,
|
||||
},
|
||||
/// Galaxy resolver returned an error during tag resolution.
|
||||
#[error("galaxy resolver: {reason}")]
|
||||
Galaxy { reason: String },
|
||||
Galaxy {
|
||||
/// Underlying resolver error message.
|
||||
reason: String,
|
||||
},
|
||||
/// `Session::recover_connection` was called without a
|
||||
/// [`crate::RebuildFactory`] installed via
|
||||
/// [`crate::Session::set_recovery_factory`]. F16.
|
||||
@@ -400,11 +515,15 @@ pub enum ConfigError {
|
||||
RecoveryNotConfigured,
|
||||
}
|
||||
|
||||
/// Security-related operation rejection.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum SecurityError {
|
||||
/// NmxSvc rejected our callback OBJREF (`HRESULT 0x8001011D`).
|
||||
#[error("callback OBJREF rejected (HRESULT 0x8001011D)")]
|
||||
CallbackObjRefRejected,
|
||||
/// Caller invoked a secured-write op without supplying the
|
||||
/// verifier user id.
|
||||
#[error("verifier user token required for secured write")]
|
||||
VerifierRequired,
|
||||
}
|
||||
@@ -414,10 +533,14 @@ pub enum SecurityError {
|
||||
/// Generic-only trait — `dyn Transport` is intentionally unsupported (see
|
||||
/// design/20-async-layer.md L53 fix). Consumers parameterise on `<T: Transport>`.
|
||||
pub trait Transport: Send + Sync + 'static {
|
||||
/// Reports per-transport capability flags so callers can
|
||||
/// feature-gate without hard-coding transport identity.
|
||||
fn capabilities(&self) -> TransportCapabilities;
|
||||
/// Reports which [`TransportKind`] this implementation produces.
|
||||
fn kind(&self) -> TransportKind;
|
||||
}
|
||||
|
||||
|
||||
// ---- Session API surface -------------------------------------------------
|
||||
//
|
||||
// The `*_value` family in `session.rs` takes `WriteValue` (the codec's
|
||||
|
||||
@@ -32,15 +32,31 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use mxaccess_callback::{CallbackEvent, CallbackExporter, ExporterIdentities};
|
||||
use mxaccess_callback::{CallbackEvent, CallbackExporter};
|
||||
// `ExporterIdentities` is only used by the legacy hand-rolled
|
||||
// CallbackExporter path. Path A (windows-com) skips it entirely.
|
||||
#[cfg(not(all(windows, feature = "windows-com")))]
|
||||
use mxaccess_callback::ExporterIdentities;
|
||||
// F55 / Path A — DCOM-managed `INmxSvcCallback` sink. Only used when
|
||||
// `windows-com` is on; without it the hand-rolled `CallbackExporter`
|
||||
// path stays in place (it's still useful for unit tests that exercise
|
||||
// the exporter against a fake NMX peer in-process). The DCOM sink is
|
||||
// the path that survives NmxSvc's SCM-side OXID validation against the
|
||||
// live AVEVA install — see `mxaccess_callback::dcom_sink` for context.
|
||||
#[cfg(all(windows, feature = "windows-com"))]
|
||||
use mxaccess_rpc::com_objref_provider::IUnknownHolder;
|
||||
use mxaccess_codec::{
|
||||
MxStatus, NmxOperationStatusMessage, NmxReferenceRegistrationMessage, NmxSubscriptionMessage,
|
||||
NmxSubscriptionRecord,
|
||||
MxStatus, NmxOperationStatusMessage, NmxReferenceRegistrationMessage,
|
||||
NmxReferenceRegistrationResultMessage, NmxSubscriptionMessage, NmxSubscriptionRecord,
|
||||
};
|
||||
use mxaccess_galaxy::{GalaxyTagMetadata, Resolver, ResolverError};
|
||||
use mxaccess_nmx::{NmxClient, NmxClientError, WriteValue};
|
||||
use mxaccess_rpc::guid::Guid;
|
||||
use mxaccess_rpc::ntlm::{NtlmClientContext, local_hostname};
|
||||
use mxaccess_rpc::ntlm::NtlmClientContext;
|
||||
// Same as `ExporterIdentities` above — only the legacy exporter path
|
||||
// derives the OBJREF host from `local_hostname()`.
|
||||
#[cfg(not(all(windows, feature = "windows-com")))]
|
||||
use mxaccess_rpc::ntlm::local_hostname;
|
||||
use mxaccess_rpc::transport::TransportError;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::net::SocketAddr;
|
||||
@@ -598,6 +614,44 @@ pub struct SessionInner {
|
||||
/// dictionaries (`MxNativeSession.cs` field-level comments) plus
|
||||
/// the ordered list those dictionaries are consulted against.
|
||||
pub(crate) pending_ops: Arc<Mutex<PendingOps>>,
|
||||
/// F56 — monotonically-increasing item-handle assigner for
|
||||
/// `subscribe_buffered`. The .NET reference's
|
||||
/// `MxNativeCompatibilityServer.AddItem` flow assigns `1, 2, 3, ...`
|
||||
/// at the LMX layer (`MxNativeCompatibilityServer.cs`) and threads
|
||||
/// the handle through to the `RegisterReference` wire body.
|
||||
/// Sending `0` (the previous behaviour) caused the engine to
|
||||
/// silently swallow the buffered subscription — RegisterReference
|
||||
/// returned HRESULT 0 + a `0x11` registration result fired, but no
|
||||
/// `0x33` DataUpdate frames followed. Starting at 1 mirrors the
|
||||
/// .NET LMX behaviour captured at
|
||||
/// `captures/094-frida-buffered-separate-writer/frida-events.tsv:13`.
|
||||
pub(crate) next_item_handle: std::sync::atomic::AtomicI32,
|
||||
/// F56 — per-session set of `(platform_id, engine_id)` endpoints
|
||||
/// we've already issued `INmxService2::Connect` +
|
||||
/// `AddSubscriberEngine` against. Mirrors the .NET reference's
|
||||
/// `MxNativeSession._publisherEndpoints` (`MxNativeSession.cs:516-525`).
|
||||
/// Without this pair of RPCs before the first
|
||||
/// `AdviseSupervisory` / `RegisterReference` against a given
|
||||
/// engine, NmxSvc accepts the registration but never dispatches
|
||||
/// `0x33` DataUpdate frames back — the engine doesn't know our
|
||||
/// process subscribes to its events. Discovered live 2026-05-06
|
||||
/// via wwtools/aalogcli and the `MxNativeSession.EnsurePublisherConnected`
|
||||
/// helper at `cs:516-526`.
|
||||
pub(crate) publisher_endpoints: Mutex<HashMap<(i32, i32), ()>>,
|
||||
/// F55 / Path A — keeps the DCOM-managed `INmxSvcCallback`'s
|
||||
/// `IUnknown` ref alive for the session's lifetime. The marshalled
|
||||
/// OBJREF passed to `RegisterEngine2` references this object's
|
||||
/// OXID/IPID via the SCM's stub manager; once the last `IUnknown`
|
||||
/// ref drops, the stub is torn down and inbound NmxSvc callbacks
|
||||
/// fail to dispatch. `None` when the legacy `CallbackExporter`
|
||||
/// path is in use (no `windows-com` feature) or after `shutdown_nmx`
|
||||
/// drops the ref.
|
||||
///
|
||||
/// Mirrors the .NET reference's `MxNativeSession._callbackSink`
|
||||
/// field (`MxNativeSession.cs`), which holds the `NmxCallbackSink`
|
||||
/// instance for the same reason.
|
||||
#[cfg(all(windows, feature = "windows-com"))]
|
||||
pub(crate) dcom_sink_holder: Mutex<Option<IUnknownHolder>>,
|
||||
}
|
||||
|
||||
/// FIFO-ordered registry of outstanding NMX operations waiting for an
|
||||
@@ -801,15 +855,72 @@ pub(crate) async fn callback_router(
|
||||
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) {
|
||||
// `send` returns `Err(SendError)` only when there are zero
|
||||
// receivers — that's fine for this wire path; nothing to do.
|
||||
let _ = callback_tx.send(Arc::new(msg));
|
||||
// 2. Try `0x11` reference-registration result. NmxSvc
|
||||
// sends one of these after `RegisterReference` to
|
||||
// convey the assigned `item_handle` + the engine's
|
||||
// decoded item definition / context. Mirrors
|
||||
// `MxNativeSession.OnCallbackReceived:582-588`. The
|
||||
// .NET reference fires a `ReferenceRegistrationReceived`
|
||||
// event but no consumer in the codebase reacts to it;
|
||||
// we currently just consume + drop the frame at trace
|
||||
// level so the catch-all parse below doesn't log a
|
||||
// spurious "unexpected opcode 0x11" warning.
|
||||
match NmxReferenceRegistrationResultMessage::try_parse_process_data_received_body(
|
||||
&body,
|
||||
) {
|
||||
Ok(result) => {
|
||||
tracing::trace!(
|
||||
item_handle = result.item_handle,
|
||||
correlation_id = ?result.item_correlation_id,
|
||||
item_definition = %result.item_definition,
|
||||
item_context = %result.item_context,
|
||||
status_category = result.status_category,
|
||||
status_detail = result.status_detail,
|
||||
"callback_router: 0x11 RegistrationResult received"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
let hex: String = body
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
tracing::trace!(
|
||||
err = %e,
|
||||
body_len = body.len(),
|
||||
body_hex = %hex,
|
||||
"callback_router: not a 0x11 RegistrationResult, falling through"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fall through to subscription messages. Wire bytes
|
||||
// arrive wrapped in a `ProcessDataReceived` envelope (46-byte
|
||||
// header, optionally with a 4-byte length prefix); the
|
||||
// 23-byte subscription preamble starts after that.
|
||||
// Mirrors `MxNativeSession.OnCallbackReceived:593` which
|
||||
// calls `NmxSubscriptionMessage.ParseProcessDataReceivedBody`.
|
||||
// The earlier code called `parse_inner` directly on the
|
||||
// wire bytes, which silently swallowed every DataUpdate
|
||||
// because the bytes failed the 23-byte preamble check.
|
||||
// Parse failures are still silent (no consumer) — the
|
||||
// .NET reference fires `UnparsedCallbackReceived` events
|
||||
// separately and we don't model that yet.
|
||||
match NmxSubscriptionMessage::try_parse_process_data_received_body(&body) {
|
||||
Ok(msg) => {
|
||||
// `send` returns `Err(SendError)` only when there
|
||||
// are zero receivers — that's fine for this wire
|
||||
// path; nothing to do.
|
||||
let _ = callback_tx.send(Arc::new(msg));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::trace!(
|
||||
err = %e,
|
||||
body_len = body.len(),
|
||||
"callback_router: dropping unparseable callback body"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -908,35 +1019,67 @@ impl Session {
|
||||
options: SessionOptions,
|
||||
resolver: Arc<dyn Resolver>,
|
||||
) -> Result<Self, Error> {
|
||||
// 1. Bind a local CallbackExporter on an OS-assigned ephemeral
|
||||
// port, then build the OBJREF advertising it. Hostname comes
|
||||
// from `local_hostname()` (env-var lookup); falls back to
|
||||
// `127.0.0.1` when neither `COMPUTERNAME` nor `HOSTNAME` is
|
||||
// set so the OBJREF binding is always parseable as
|
||||
// "<host>[<port>]".
|
||||
let identities = ExporterIdentities::random();
|
||||
// Bind on UNSPECIFIED (`0.0.0.0`) so the listener accepts
|
||||
// dial-backs on every interface NmxSvc could resolve the
|
||||
// hostname to. The OBJREF's host string is the machine's
|
||||
// `COMPUTERNAME` (or `127.0.0.1` fallback), and NmxSvc
|
||||
// resolves that via DNS — which on a typical AVEVA install
|
||||
// returns the machine's primary NIC IP, not loopback. If the
|
||||
// exporter binds only on `127.0.0.1`, the dial-back lands on
|
||||
// a different interface and the TCP SYN is dropped, surfacing
|
||||
// as `RegisterEngine2 → Fault(0x800706BA RPC_S_SERVER_UNAVAILABLE)`
|
||||
// because NmxSvc can't reach our exporter to negotiate the
|
||||
// callback bind. Binding on UNSPECIFIED (= bind to all v4
|
||||
// interfaces, including loopback + primary NIC) avoids this.
|
||||
let exporter_addr =
|
||||
SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0);
|
||||
let (exporter, callback_events) = CallbackExporter::bind(exporter_addr, identities)
|
||||
.await
|
||||
.map_err(Error::Io)?;
|
||||
let hostname = match local_hostname() {
|
||||
s if s.is_empty() => "127.0.0.1".to_string(),
|
||||
s => s,
|
||||
// 1. Build the callback sink + OBJREF.
|
||||
//
|
||||
// Two paths exist:
|
||||
//
|
||||
// - F55 / Path A (`windows-com` feature): Build a DCOM-managed
|
||||
// `INmxSvcCallback` instance via `windows-rs` `#[implement]`,
|
||||
// marshal it through `CoMarshalInterface`. This registers the
|
||||
// sink's OXID with the local RPCSS so NmxSvc's SCM-side
|
||||
// `IObjectExporter::ResolveOxid` validation passes — the
|
||||
// hand-rolled OBJREF below fails this check with
|
||||
// `RPC_S_SERVER_UNAVAILABLE` (1722) on `RegisterEngine2`.
|
||||
// Mirrors `MxNativeSession.CreateRegisteredService` which
|
||||
// calls `ComObjRefProvider.MarshalInterfaceObjRef(callback,
|
||||
// INmxSvcCallback, DifferentMachine)`
|
||||
// (`src/MxNativeClient/MxNativeSession.cs:624`).
|
||||
//
|
||||
// - Legacy hand-rolled exporter (no `windows-com`): Bind a
|
||||
// local TCP listener that serves `IRemUnknown` +
|
||||
// `INmxSvcCallback` directly, then advertise it via a
|
||||
// custom OBJREF. Useful for unit tests that exercise the
|
||||
// exporter against a fake NMX peer in-process. NOT used
|
||||
// against a real NmxSvc — see F55 in `design/followups.md`.
|
||||
#[cfg(all(windows, feature = "windows-com"))]
|
||||
let (exporter, dcom_sink_holder, callback_events, callback_obj_ref) = {
|
||||
let (holder, events, blob) = mxaccess_callback::create_dcom_callback_sink_objref()
|
||||
.map_err(|e| {
|
||||
Error::Connection(ConnectionError::TransportFailure {
|
||||
detail: format!("DCOM callback sink marshal failed: {e:?}"),
|
||||
})
|
||||
})?;
|
||||
(None::<CallbackExporter>, Some(holder), events, blob)
|
||||
};
|
||||
|
||||
#[cfg(not(all(windows, feature = "windows-com")))]
|
||||
let (exporter, callback_events, callback_obj_ref) = {
|
||||
let identities = ExporterIdentities::random();
|
||||
// Bind on UNSPECIFIED (`0.0.0.0`) so the listener accepts
|
||||
// dial-backs on every interface NmxSvc could resolve the
|
||||
// hostname to. The OBJREF's host string is the machine's
|
||||
// `COMPUTERNAME` (or `127.0.0.1` fallback), and NmxSvc
|
||||
// resolves that via DNS — which on a typical AVEVA install
|
||||
// returns the machine's primary NIC IP, not loopback. If
|
||||
// the exporter binds only on `127.0.0.1`, the dial-back
|
||||
// lands on a different interface and the TCP SYN is
|
||||
// dropped, surfacing as `RegisterEngine2 → Fault(0x800706BA
|
||||
// RPC_S_SERVER_UNAVAILABLE)` because NmxSvc can't reach
|
||||
// our exporter to negotiate the callback bind. Binding on
|
||||
// UNSPECIFIED (= bind to all v4 interfaces, including
|
||||
// loopback + primary NIC) avoids this.
|
||||
let exporter_addr =
|
||||
SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0);
|
||||
let (exporter, events) = CallbackExporter::bind(exporter_addr, identities)
|
||||
.await
|
||||
.map_err(Error::Io)?;
|
||||
let hostname = match local_hostname() {
|
||||
s if s.is_empty() => "127.0.0.1".to_string(),
|
||||
s => s,
|
||||
};
|
||||
let blob = exporter.create_callback_objref(&hostname);
|
||||
(Some(exporter), events, blob)
|
||||
};
|
||||
let callback_obj_ref = exporter.create_callback_objref(&hostname);
|
||||
|
||||
// 2. Spawn the router task that broadcasts parsed callback
|
||||
// messages.
|
||||
@@ -996,7 +1139,7 @@ impl Session {
|
||||
options,
|
||||
resolver,
|
||||
nmx: Mutex::new(nmx),
|
||||
callback_exporter: Mutex::new(Some(exporter)),
|
||||
callback_exporter: Mutex::new(exporter),
|
||||
callback_tx,
|
||||
operation_status_tx,
|
||||
recovery_active,
|
||||
@@ -1007,6 +1150,10 @@ impl Session {
|
||||
callback_obj_ref,
|
||||
rebuild_factory: Mutex::new(None),
|
||||
pending_ops,
|
||||
next_item_handle: std::sync::atomic::AtomicI32::new(1),
|
||||
publisher_endpoints: Mutex::new(HashMap::new()),
|
||||
#[cfg(all(windows, feature = "windows-com"))]
|
||||
dcom_sink_holder: Mutex::new(dcom_sink_holder),
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -1729,6 +1876,14 @@ impl Session {
|
||||
.map_err(map_resolver)?;
|
||||
let correlation_id: [u8; 16] = rand::random();
|
||||
|
||||
// F56 — connect to the publisher engine before issuing the
|
||||
// first advise against it, mirroring
|
||||
// `MxNativeSession.EnsurePublisherConnected` (`cs:516-526`).
|
||||
// Without this NmxSvc acks the advise but never dispatches
|
||||
// DataUpdate frames back — the publishing engine doesn't know
|
||||
// our engine is subscribed.
|
||||
self.ensure_publisher_connected(i32::from(metadata.platform_id), i32::from(metadata.engine_id)).await?;
|
||||
|
||||
let opts = &inner.options;
|
||||
let mut nmx = inner.nmx.lock().await;
|
||||
let hr = nmx
|
||||
@@ -1833,25 +1988,36 @@ impl Session {
|
||||
.map_err(map_resolver)?;
|
||||
let correlation_id: [u8; 16] = rand::random();
|
||||
|
||||
// Build the buffered RegisterReference body. Item definition is
|
||||
// the full reference suffixed with `.property(buffer)`; item
|
||||
// context is empty for this single-string form (the .NET
|
||||
// reference's split-context form is reachable via the
|
||||
// compat-server layer F35 once it lands). The codec helper
|
||||
// rejects empty/whitespace inputs with `CodecError::InvalidName`.
|
||||
let item_definition = NmxReferenceRegistrationMessage::to_buffered_item_definition(
|
||||
reference,
|
||||
)
|
||||
.map_err(|e| {
|
||||
Error::Configuration(ConfigError::InvalidArgument {
|
||||
detail: format!("buffered item definition: {e}"),
|
||||
})
|
||||
})?;
|
||||
// F56 — build the buffered RegisterReference body in the split
|
||||
// (object, attribute) form the .NET reference uses on the wire:
|
||||
// item_definition = "<attribute>.property(buffer)"
|
||||
// item_context = "<object_tag_name>"
|
||||
// item_handle = sequential per-session counter starting at 1
|
||||
//
|
||||
// The previous implementation used the single-string form (full
|
||||
// reference in `item_definition`, empty `item_context`,
|
||||
// `item_handle = 0`). RegisterReference returned HRESULT 0 and
|
||||
// the engine fired a `0x11` registration result, but **no
|
||||
// `0x33` DataUpdate frames ever followed** — confirmed live
|
||||
// 2026-05-06. Switching to the split form mirrors the captured
|
||||
// .NET wire bytes at
|
||||
// `captures/094-frida-buffered-separate-writer/frida-events.tsv:23`
|
||||
// (the PutRequest body at 21:40:19.970).
|
||||
let item_handle = inner
|
||||
.next_item_handle
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let attribute_only_definition =
|
||||
NmxReferenceRegistrationMessage::to_buffered_item_definition(&metadata.attribute_name)
|
||||
.map_err(|e| {
|
||||
Error::Configuration(ConfigError::InvalidArgument {
|
||||
detail: format!("buffered item definition: {e}"),
|
||||
})
|
||||
})?;
|
||||
let registration = NmxReferenceRegistrationMessage {
|
||||
item_handle: 0,
|
||||
item_handle,
|
||||
item_correlation_id: correlation_id,
|
||||
item_definition,
|
||||
item_context: String::new(),
|
||||
item_definition: attribute_only_definition,
|
||||
item_context: metadata.object_tag_name.clone(),
|
||||
subscribe: true,
|
||||
reserved_25_27: [0; 2],
|
||||
reserved_31_55: [0; 24],
|
||||
@@ -1863,6 +2029,10 @@ impl Session {
|
||||
// rationale as plain `subscribe`).
|
||||
let inbound = Box::pin(BroadcastStream::new(self.inner.callback_tx.subscribe()));
|
||||
|
||||
// F56 — connect to the publisher engine first; see plain
|
||||
// `subscribe` for the rationale.
|
||||
self.ensure_publisher_connected(i32::from(metadata.platform_id), i32::from(metadata.engine_id)).await?;
|
||||
|
||||
let mut nmx = inner.nmx.lock().await;
|
||||
let hr = nmx
|
||||
.register_reference(
|
||||
@@ -1876,6 +2046,29 @@ impl Session {
|
||||
.await
|
||||
.map_err(map_nmx)?;
|
||||
ensure_hresult_ok(hr)?;
|
||||
// F56 — buffered subscriptions need an explicit
|
||||
// `AdviseSupervisory` round-trip after `RegisterReference` to
|
||||
// start DataUpdate dispatch on this AVEVA install. The .NET
|
||||
// reference's `MxNativeSession.RegisterBufferedItemAsync`
|
||||
// (`cs:272-310`) only calls `RegisterReference` — but the LMX
|
||||
// compat layer's `AddBufferedItem` + `AdviseSupervisory` chain
|
||||
// ends up triggering the advise downstream. Mirroring just
|
||||
// RegisterReference (per F36 wave 1's reading of capture 082)
|
||||
// produces the registration result and heartbeat callbacks but
|
||||
// no `0x33` DataUpdate frames. Issuing the advise here closes
|
||||
// that gap — verified live against `TestMachine_001.TestChangingInt`.
|
||||
let hr = nmx
|
||||
.advise_supervisory(
|
||||
opts.local_engine_id,
|
||||
&metadata,
|
||||
correlation_id,
|
||||
opts.galaxy_id,
|
||||
/* source_galaxy_id */ i32::from(opts.galaxy_id),
|
||||
opts.source_platform_id,
|
||||
)
|
||||
.await
|
||||
.map_err(map_nmx)?;
|
||||
ensure_hresult_ok(hr)?;
|
||||
drop(nmx);
|
||||
|
||||
let metadata_arc = Arc::new(metadata);
|
||||
@@ -1894,9 +2087,13 @@ impl Session {
|
||||
metadata: Arc::clone(&metadata_arc),
|
||||
mode: SubscriptionMode::Buffered {
|
||||
rounded_interval_ms: rounded_ms,
|
||||
item_definition: reference.to_string(),
|
||||
item_context: String::new(),
|
||||
item_handle: 0,
|
||||
// F56 — recovery replays via `register_reference` and
|
||||
// must reissue the same wire body. Save the split
|
||||
// (object, attribute, handle) triple, NOT the
|
||||
// pre-F56 single-string form.
|
||||
item_definition: registration.item_definition.clone(),
|
||||
item_context: registration.item_context.clone(),
|
||||
item_handle,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1914,6 +2111,66 @@ impl Session {
|
||||
})
|
||||
}
|
||||
|
||||
/// F56 — issue `INmxService2::Connect` + `AddSubscriberEngine`
|
||||
/// against the `(platform_id, engine_id)` of the publishing engine,
|
||||
/// once per session. Mirrors
|
||||
/// `MxNativeSession.EnsurePublisherConnected` (`cs:516-526`) +
|
||||
/// `ConnectPublisher` (`cs:528-536`).
|
||||
///
|
||||
/// Without this pair of RPCs before the first `AdviseSupervisory` /
|
||||
/// `RegisterReference` against a given engine, NmxSvc acks the
|
||||
/// advise but the publishing engine never knows our engine is
|
||||
/// subscribed — no `0x33` DataUpdate frames flow back. Confirmed
|
||||
/// 2026-05-06 by the absence of the .NET reference's
|
||||
/// `EnsurePublisherConnected` call in the Rust port + live
|
||||
/// reproduction against `TestMachine_001.TestChangingInt`.
|
||||
async fn ensure_publisher_connected(
|
||||
&self,
|
||||
platform_id: i32,
|
||||
engine_id: i32,
|
||||
) -> Result<(), Error> {
|
||||
let key = (platform_id, engine_id);
|
||||
{
|
||||
let endpoints = self.inner.publisher_endpoints.lock().await;
|
||||
if endpoints.contains_key(&key) {
|
||||
tracing::debug!(
|
||||
platform_id,
|
||||
engine_id,
|
||||
"ensure_publisher_connected: already connected"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let opts = &self.inner.options;
|
||||
let local_engine = opts.local_engine_id;
|
||||
let galaxy = i32::from(opts.galaxy_id);
|
||||
let source_platform = opts.source_platform_id;
|
||||
tracing::debug!(
|
||||
platform_id,
|
||||
engine_id,
|
||||
local_engine,
|
||||
galaxy,
|
||||
source_platform,
|
||||
"ensure_publisher_connected: issuing Connect + AddSubscriberEngine"
|
||||
);
|
||||
{
|
||||
let mut nmx = self.inner.nmx.lock().await;
|
||||
let hr = nmx
|
||||
.connect_engine(local_engine, galaxy, platform_id, engine_id)
|
||||
.await
|
||||
.map_err(map_nmx)?;
|
||||
ensure_hresult_ok(hr)?;
|
||||
let hr = nmx
|
||||
.add_subscriber_engine(engine_id, galaxy, source_platform, local_engine)
|
||||
.await
|
||||
.map_err(map_nmx)?;
|
||||
ensure_hresult_ok(hr)?;
|
||||
}
|
||||
let mut endpoints = self.inner.publisher_endpoints.lock().await;
|
||||
endpoints.insert(key, ());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `subscribe` ordering note: subscribe to the broadcast channel
|
||||
/// FIRST, then issue `AdviseSupervisory`. If we ordered the other
|
||||
/// way, updates that arrive between the advise call and the
|
||||
@@ -2102,6 +2359,15 @@ impl Session {
|
||||
if let Some(exp) = self.inner.callback_exporter.lock().await.take() {
|
||||
exp.shutdown().await;
|
||||
}
|
||||
// F55 / Path A — drop the DCOM-managed sink's IUnknown ref so
|
||||
// CoMarshalInterface's stub manager unregisters our OXID from
|
||||
// RPCSS. Mirrors the .NET reference's `MxNativeSession.Dispose`
|
||||
// path, which lets the `NmxCallbackSink` go out of scope after
|
||||
// unregister.
|
||||
#[cfg(all(windows, feature = "windows-com"))]
|
||||
{
|
||||
self.inner.dcom_sink_holder.lock().await.take();
|
||||
}
|
||||
|
||||
// 3. Wait for the router task. Once the exporter is dropped its
|
||||
// upstream mpsc::Sender closes, the router's recv() returns
|
||||
@@ -2403,10 +2669,16 @@ mod tests {
|
||||
// matches production. Tests don't drive real callbacks through
|
||||
// this path, but keeping the shape symmetric means
|
||||
// shutdown_nmx exercises the full cleanup chain.
|
||||
let (exporter, callback_events) =
|
||||
CallbackExporter::bind("127.0.0.1:0".parse().unwrap(), ExporterIdentities::random())
|
||||
.await
|
||||
.unwrap();
|
||||
// Test-only helper exercises the legacy hand-rolled CallbackExporter
|
||||
// path even when the crate is built with `windows-com`. The DCOM
|
||||
// path needs a real NmxSvc on the wire; this shim talks to a
|
||||
// `loopback_listener::expect_*` peer.
|
||||
let (exporter, callback_events) = CallbackExporter::bind(
|
||||
"127.0.0.1:0".parse().unwrap(),
|
||||
mxaccess_callback::ExporterIdentities::random(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let (callback_tx, _) = broadcast::channel(CALLBACK_BROADCAST_CAPACITY);
|
||||
let (operation_status_tx, _) =
|
||||
broadcast::channel::<Arc<OperationStatus>>(OPERATION_STATUS_BROADCAST_CAPACITY);
|
||||
@@ -2437,6 +2709,10 @@ mod tests {
|
||||
callback_obj_ref: Vec::new(),
|
||||
rebuild_factory: Mutex::new(None),
|
||||
pending_ops,
|
||||
next_item_handle: std::sync::atomic::AtomicI32::new(1),
|
||||
publisher_endpoints: Mutex::new(HashMap::new()),
|
||||
#[cfg(all(windows, feature = "windows-com"))]
|
||||
dcom_sink_holder: Mutex::new(None),
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -2880,18 +3156,31 @@ mod tests {
|
||||
pending_ops,
|
||||
));
|
||||
|
||||
// Build a minimal valid 0x32 SubscriptionStatus body: 23-byte
|
||||
// preamble + 16-byte item_correlation_id, record_count=0 so no
|
||||
// records follow. Total: 39 bytes. Using 0x32 (not 0x33)
|
||||
// because DataUpdate always attempts to parse one record
|
||||
// regardless of record_count, and we'd need a full 38-byte
|
||||
// record body to satisfy that parser.
|
||||
let mut body = vec![0u8; 39];
|
||||
body[0] = 0x32;
|
||||
body[1..3].copy_from_slice(&1u16.to_le_bytes()); // version
|
||||
body[3..7].copy_from_slice(&0i32.to_le_bytes()); // record_count
|
||||
body[7..23].copy_from_slice(&[0xEFu8; 16]); // operation_id
|
||||
body[23..39].copy_from_slice(&[0xCDu8; 16]); // item_correlation_id
|
||||
// Build a minimal valid 0x32 SubscriptionStatus body wrapped
|
||||
// in a `ProcessDataReceived` envelope (header-only form, no
|
||||
// 4-byte total-length prefix): 46-byte header + 39-byte inner.
|
||||
// The header's `inner_length` at offset 2 is `inner_len + 4`
|
||||
// (.NET cs:54-56 — declared length includes the size-of-int).
|
||||
// Using 0x32 (not 0x33) because DataUpdate always attempts
|
||||
// to parse one record regardless of record_count, and we'd
|
||||
// need a full 38-byte record body to satisfy that parser.
|
||||
const HEADER_LEN: usize = 46;
|
||||
const INNER_LEN_OFFSET: usize = 2;
|
||||
let inner_len = 39usize;
|
||||
let mut body = vec![0u8; HEADER_LEN + inner_len];
|
||||
// Inner-length declaration (at INNER_LEN_OFFSET = 2). Flexible
|
||||
// (header-only) form compares `declared == body.len() - HEADER_LEN`
|
||||
// verbatim — no `-4` adjustment (`observed_frame.rs:178`); the
|
||||
// adjustment only applies on the strict path where there's a
|
||||
// 4-byte total-length prefix in front.
|
||||
let declared = inner_len as i32;
|
||||
body[INNER_LEN_OFFSET..INNER_LEN_OFFSET + 4].copy_from_slice(&declared.to_le_bytes());
|
||||
let inner = &mut body[HEADER_LEN..];
|
||||
inner[0] = 0x32;
|
||||
inner[1..3].copy_from_slice(&1u16.to_le_bytes()); // version
|
||||
inner[3..7].copy_from_slice(&0i32.to_le_bytes()); // record_count
|
||||
inner[7..23].copy_from_slice(&[0xEFu8; 16]); // operation_id
|
||||
inner[23..39].copy_from_slice(&[0xCDu8; 16]); // item_correlation_id
|
||||
|
||||
let event = CallbackEvent::CallbackInvoked { opnum: 4, body };
|
||||
event_tx.send(event).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user