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 +