diff --git a/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs b/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs
index c4220f6..df43ac7 100644
--- a/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs
+++ b/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs
@@ -88,12 +88,109 @@ namespace AVEVA.Historian.Grpc2023CaptureHarness
return Connect(managedDll, args);
case "capture-write":
return CaptureWrite(managedDll, args);
+ case "delete-tag":
+ return DeleteTag(managedDll, args);
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;
}
}
+ ///
+ /// Deletes a tag via the native client's DeleteTags (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 …]
+ ///
+ 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 */ }
+ }
+ }
+
///
/// Drives the native 2023 R2 client through a non-streamed (historical backfill) write so the
/// IL-rewritten GrpcHistoryClient dumps the two buffers (RegisterTags.tagInfos +