M3 capture harness: add delete-tag scenario (sandbox cleanup)

delete-tag drives the native client's DeleteTags (the clean-delete path, unlike the SDK's WCF
DelT which can leave the row). Primes the write session with AddTag first (DeleteTags on a fresh
connection returns UnknownClient(51) until the client is registered). Used to remove the capture
sandbox tag SdkM3CaptureSandbox from the live server (DeleteTags returned success).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
Joseph Doherty
2026-06-21 21:34:38 -04:00
parent 3cc02e3ed0
commit d527784def
@@ -88,12 +88,109 @@ namespace AVEVA.Historian.Grpc2023CaptureHarness
return Connect(managedDll, args); return Connect(managedDll, args);
case "capture-write": case "capture-write":
return CaptureWrite(managedDll, args); return CaptureWrite(managedDll, args);
case "delete-tag":
return DeleteTag(managedDll, args);
default: default:
Console.Error.WriteLine($"Unknown scenario '{scenario}'. Supported: load-check, connect, capture-write."); Console.Error.WriteLine($"Unknown scenario '{scenario}'. Supported: load-check, connect, capture-write, delete-tag.");
return 1; return 1;
} }
} }
/// <summary>
/// Deletes a tag via the native client's <c>DeleteTags</c> (the path that removes the tag
/// cleanly, unlike the SDK's WCF DelT). Used to clean up the capture sandbox tag.
/// Usage: delete-tag --tag SdkM3CaptureSandbox [--server …] [--port …] [--cert …]
/// </summary>
private static int DeleteTag(string managedDll, string[] args)
{
Assembly asm = Assembly.LoadFrom(managedDll);
Type accessType = Req(asm, "ArchestrA.HistorianAccess");
Type connArgsType = Req(asm, "ArchestrA.HistorianConnectionArgs");
Type connModeType = Req(asm, "ArchestrA.HistorianConnectionMode");
Type connTypeType = Req(asm, "ArchestrA.HistorianConnectionType");
Type errorType = Req(asm, "ArchestrA.HistorianAccessError");
Type certInfoType = Req(asm, "ArchestrA.CertificateInfo");
Type secModeType = Req(asm, "ArchestrA.HistorianSecurityMode");
Type tagStatusType = Req(asm, "ArchestrA.HistorianTagStatus");
Type tagStatusListType = Req(asm, "ArchestrA.HistorianTagStatusList");
string server = GetOption(args, "--server") ?? "WONDER-SQL-VD03";
int port = int.TryParse(GetOption(args, "--port"), out int p) ? p : 32565;
string certName = GetOption(args, "--cert") ?? server;
string tagName = GetOption(args, "--tag") ?? "SdkM3CaptureSandbox";
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");
string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD");
if (string.IsNullOrEmpty(user))
{
Console.Error.WriteLine("Set HISTORIAN_USER/HISTORIAN_PASSWORD.");
return 1;
}
object connArgs = Activator.CreateInstance(connArgsType)!;
SetProp(connArgs, "ServerName", server);
SetProp(connArgs, "TcpPort", checked((ushort)port));
SetProp(connArgs, "ConnectionMode", Enum.Parse(connModeType, "Historian"));
SetProp(connArgs, "ConnectionType", Enum.Parse(connTypeType, "Process"));
SetProp(connArgs, "ReadOnly", false);
SetProp(connArgs, "IntegratedSecurity", false);
SetProp(connArgs, "AllowUnTrustedConnection", true);
SetProp(connArgs, "UserName", user);
SetProp(connArgs, "Password", password ?? string.Empty);
object certInfo = Activator.CreateInstance(certInfoType)!;
TrySetProp(certInfo, "CertificateName", certName);
TrySetProp(certInfo, "SecurityMode", Enum.Parse(secModeType, "TransportCertificate"));
TrySetProp(connArgs, "SecurityInfo", certInfo);
object access = Activator.CreateInstance(accessType)!;
object openErr = Activator.CreateInstance(errorType)!;
object?[] openArgs = { connArgs, openErr };
bool opened = (bool)accessType.GetMethod("OpenConnection", new[] { connArgsType, errorType.MakeByRefType() })!
.Invoke(access, openArgs)!;
Console.WriteLine($"OpenConnection: {opened} err={DescribeError(openArgs[1])}");
if (!opened) { return 2; }
try
{
// Prime the write session exactly as the capture flow does — DeleteTags on a fresh
// connection returns UnknownClient(51) until AddTag has registered the client
// (UpdateClientStatus). AddTag on the existing tag is idempotent here.
Type tagType = Req(asm, "ArchestrA.HistorianTag");
Type tagDataTypeEnum = Req(asm, "ArchestrA.HistorianDataType");
Type tagStorageEnum = Req(asm, "ArchestrA.HistorianStorageType");
object primeTag = Activator.CreateInstance(tagType)!;
SetProp(primeTag, "TagName", tagName);
TrySetProp(primeTag, "TagDataType", Enum.Parse(tagDataTypeEnum, "Float", true));
TrySetProp(primeTag, "TagStorageType", Enum.Parse(tagStorageEnum, "Cyclic", true));
object primeErr = Activator.CreateInstance(errorType)!;
object?[] primeArgs = { primeTag, 0u, primeErr };
bool primed = (bool)accessType.GetMethod("AddTag", new[] { tagType, typeof(uint).MakeByRefType(), errorType.MakeByRefType() })!
.Invoke(access, primeArgs)!;
Console.WriteLine($"Prime AddTag({tagName}): {primed} err={DescribeError(primeArgs[2])}");
System.Threading.Thread.Sleep(2000);
object list = Activator.CreateInstance(tagStatusListType)!;
object status = Activator.CreateInstance(tagStatusType)!;
SetProp(status, "TagName", tagName);
tagStatusListType.GetMethods().First(m => m.Name == "Add" && m.GetParameters().Length == 1).Invoke(list, new[] { status });
MethodInfo delete = accessType.GetMethods().First(m => m.Name == "DeleteTags" && m.GetParameters().Length == 2);
object delErr = Activator.CreateInstance(errorType)!;
object?[] delArgs = { list, delErr };
bool ok = (bool)delete.Invoke(access, delArgs)!;
Console.WriteLine($"DeleteTags({tagName}): {ok} err={DescribeError(delArgs[1])}");
return ok ? 0 : 3;
}
finally
{
try
{
MethodInfo? close = accessType.GetMethod("CloseConnection", new[] { errorType.MakeByRefType() });
if (close != null) close.Invoke(access, new object?[] { Activator.CreateInstance(errorType) });
}
catch { /* best-effort */ }
}
}
/// <summary> /// <summary>
/// Drives the native 2023 R2 client through a non-streamed (historical backfill) write so the /// Drives the native 2023 R2 client through a non-streamed (historical backfill) write so the
/// IL-rewritten GrpcHistoryClient dumps the two buffers (RegisterTags.tagInfos + /// IL-rewritten GrpcHistoryClient dumps the two buffers (RegisterTags.tagInfos +