463748e395
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m24s
ci / java (push) Successful in 2m32s
ci / nightly-windev (pull_request) Has been skipped
ci / java (pull_request) Successful in 2m38s
ci / portable (push) Failing after 2m52s
ci / portable (pull_request) Failing after 2m53s
ci / windows-x86 (pull_request) Successful in 8m8s
Address review of the #164-#166 fixes: - The HRESULT check could not tell interop from programming errors (a managed ArgumentException already defaults to 0x80070057). Wrap each MXAccess call (AuthenticateUser, AddItem, Advise, Write) in a narrow filter that converts an interop failure to a private marker exception, and catch only that marker, so exceptions from the writer's own code (argument checks, caching, the pump) propagate. - A late OnWriteComplete for a dropped handle recreated the cache entry that DropItem removed. The writer now records completions only for item handles it holds on its live registration. - The item-handle-reuse probe now asserts that handles within one registration strictly increase, so a rerun is a regression check. Refs #164, #165, #166 Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
186 lines
7.4 KiB
C#
186 lines
7.4 KiB
C#
// ItemHandleReuseProbeTests.cs
|
|
//
|
|
// Probe for issue #166: does MXAccess hand out a new item handle number when the
|
|
// same item is removed and re-added on one LMXProxyServer registration? The
|
|
// AlarmAckWriter's Unconfirmed mitigation (drop the item, re-add it, wait on the
|
|
// fresh handle) relies on the number changing, because OnWriteComplete carries
|
|
// only the (server, item) handle pair.
|
|
//
|
|
// The probe only adds, advises and removes items. It never writes, so it does
|
|
// not acknowledge the alarm or create Historian rows.
|
|
//
|
|
// HOW TO RUN (dev rig with MXAccess installed and the DEV Galaxy running):
|
|
// 1. Remove (or set to null) the Skip parameter on the [Fact] below.
|
|
// 2. dotnet test src/ZB.MOM.WW.MxGateway.Worker.Tests/ZB.MOM.WW.MxGateway.Worker.Tests.csproj
|
|
// -p:Platform=x86 --filter FullyQualifiedName~ItemHandleReuseProbeTests
|
|
// --logger "console;verbosity=detailed"
|
|
// 3. Read the logged handles. Results are recorded in docs/AlarmProbeFindings.md.
|
|
//
|
|
// RESULT (windev, 2026-10-20): within one registration item handles increase
|
|
// monotonically and are never reused (1..15 across add/remove/re-add of the same
|
|
// item, with and without Advise, interleaved with another item, and while held
|
|
// twice). Item numbering restarts at 1 after Unregister + Register, and a fresh
|
|
// COM object is handed server handle 1 again.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using ArchestrA.MxAccess;
|
|
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Worker.Tests.Probes;
|
|
|
|
/// <summary>
|
|
/// Live probe that logs the item handles MXAccess returns when the same
|
|
/// <c>AckMsg</c> item is removed and re-added, to decide whether a late
|
|
/// <c>OnWriteComplete</c> for a dropped handle could match a re-added one.
|
|
/// </summary>
|
|
public sealed class ItemHandleReuseProbeTests
|
|
{
|
|
private const string AckItem = "TestMachine_001.TestAlarm001.AckMsg";
|
|
private const string OtherItem = "TestMachine_001.TestAlarm001.Acked";
|
|
private const int Rounds = 5;
|
|
|
|
private readonly ITestOutputHelper output;
|
|
private readonly List<string> log = new List<string>();
|
|
|
|
// Item handles returned on the first registration, in the order they were handed out.
|
|
private readonly List<int> firstRegistrationHandles = new List<int>();
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="ItemHandleReuseProbeTests"/> class.</summary>
|
|
/// <param name="output">Test output helper for logging.</param>
|
|
public ItemHandleReuseProbeTests(ITestOutputHelper output)
|
|
{
|
|
this.output = output;
|
|
}
|
|
|
|
/// <summary>Adds, removes and re-adds the AckMsg item on a fresh registration and logs every handle.</summary>
|
|
[Fact(Skip = "Live dev-rig probe (issue #166) — flip Skip=null on a host with MXAccess and the DEV Galaxy running.")]
|
|
public void RemoveThenReAdd_LogsItemHandles()
|
|
{
|
|
Exception? threadException = null;
|
|
Thread thread = new Thread(() =>
|
|
{
|
|
try { RunProbe(); }
|
|
catch (Exception ex) { threadException = ex; }
|
|
});
|
|
thread.SetApartmentState(ApartmentState.STA);
|
|
thread.Start();
|
|
thread.Join();
|
|
|
|
foreach (string line in log)
|
|
{
|
|
output.WriteLine(line);
|
|
}
|
|
|
|
if (threadException is not null)
|
|
{
|
|
throw new InvalidOperationException("Probe failed: " + threadException.Message, threadException);
|
|
}
|
|
|
|
// The ack writer's drop-and-re-add mitigation depends on this: within one
|
|
// registration a handle number is never handed out twice.
|
|
Assert.Equal((2 * Rounds) + 5, firstRegistrationHandles.Count);
|
|
for (int i = 1; i < firstRegistrationHandles.Count; i++)
|
|
{
|
|
Assert.True(
|
|
firstRegistrationHandles[i] > firstRegistrationHandles[i - 1],
|
|
$"Item handle {firstRegistrationHandles[i]} did not increase past {firstRegistrationHandles[i - 1]}; MXAccess reused or reordered item handles within one registration.");
|
|
}
|
|
}
|
|
|
|
private void RunProbe()
|
|
{
|
|
LMXProxyServerClass com = new LMXProxyServerClass();
|
|
try
|
|
{
|
|
IMxAccessServer mx = new MxAccessComServer(com);
|
|
int server = mx.Register("MxGatewayProbe166");
|
|
Log($"Register -> server handle {server}");
|
|
|
|
// A: the AlarmAckWriter drop path (Advise, then UnAdvise + RemoveItem).
|
|
for (int i = 0; i < Rounds; i++)
|
|
{
|
|
int h = Held(mx.AddItem(server, AckItem));
|
|
mx.Advise(server, h);
|
|
Log($"A{i}: AddItem+Advise {AckItem} -> {h}");
|
|
mx.UnAdvise(server, h);
|
|
mx.RemoveItem(server, h);
|
|
Log($"A{i}: UnAdvise+RemoveItem {h}");
|
|
}
|
|
|
|
// B: RemoveItem without Advise.
|
|
for (int i = 0; i < Rounds; i++)
|
|
{
|
|
int h = Held(mx.AddItem(server, AckItem));
|
|
Log($"B{i}: AddItem {AckItem} -> {h}");
|
|
mx.RemoveItem(server, h);
|
|
Log($"B{i}: RemoveItem {h}");
|
|
}
|
|
|
|
// C: interleave another item to see whether a freed number is handed to any item.
|
|
int c1 = Held(mx.AddItem(server, AckItem));
|
|
Log($"C: AddItem {AckItem} -> {c1}");
|
|
mx.RemoveItem(server, c1);
|
|
Log($"C: RemoveItem {c1}");
|
|
int c2 = Held(mx.AddItem(server, OtherItem));
|
|
Log($"C: AddItem {OtherItem} -> {c2}");
|
|
int c3 = Held(mx.AddItem(server, AckItem));
|
|
Log($"C: AddItem {AckItem} -> {c3}");
|
|
mx.RemoveItem(server, c2);
|
|
mx.RemoveItem(server, c3);
|
|
Log($"C: RemoveItem {c2}, {c3}");
|
|
|
|
// D: the same item added twice while the first is still held.
|
|
int d1 = Held(mx.AddItem(server, AckItem));
|
|
int d2 = Held(mx.AddItem(server, AckItem));
|
|
Log($"D: AddItem {AckItem} twice while held -> {d1}, {d2}");
|
|
mx.RemoveItem(server, d1);
|
|
mx.RemoveItem(server, d2);
|
|
|
|
mx.Unregister(server);
|
|
Log($"Unregister {server}");
|
|
|
|
// E: the TearDownServer path: re-register on the same COM object and add again.
|
|
int server2 = mx.Register("MxGatewayProbe166");
|
|
int e1 = mx.AddItem(server2, AckItem);
|
|
Log($"E: re-Register -> server handle {server2}; AddItem {AckItem} -> {e1}");
|
|
mx.RemoveItem(server2, e1);
|
|
mx.Unregister(server2);
|
|
}
|
|
finally
|
|
{
|
|
Marshal.FinalReleaseComObject(com);
|
|
}
|
|
|
|
// F: a fresh COM object, as AlarmAckWriter creates after a teardown.
|
|
LMXProxyServerClass com2 = new LMXProxyServerClass();
|
|
try
|
|
{
|
|
IMxAccessServer mx2 = new MxAccessComServer(com2);
|
|
int server3 = mx2.Register("MxGatewayProbe166");
|
|
int f1 = mx2.AddItem(server3, AckItem);
|
|
Log($"F: new COM object Register -> server handle {server3}; AddItem {AckItem} -> {f1}");
|
|
mx2.RemoveItem(server3, f1);
|
|
mx2.Unregister(server3);
|
|
}
|
|
finally
|
|
{
|
|
Marshal.FinalReleaseComObject(com2);
|
|
}
|
|
}
|
|
|
|
private int Held(int itemHandle)
|
|
{
|
|
firstRegistrationHandles.Add(itemHandle);
|
|
return itemHandle;
|
|
}
|
|
|
|
private void Log(string line)
|
|
{
|
|
log.Add(DateTime.Now.ToString("HH:mm:ss.fff") + " " + line);
|
|
}
|
|
}
|