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:
@@ -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 +
|
||||||
|
|||||||
Reference in New Issue
Block a user