d527784def
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
680 lines
36 KiB
C#
680 lines
36 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
|
|
namespace AVEVA.Historian.Grpc2023CaptureHarness
|
|
{
|
|
/// <summary>
|
|
/// Capture harness for the M3 R3.1 follow-up. Loads the 2023 R2 mixed-mode
|
|
/// <c>aahClientManaged.dll</c> by path and drives it over gRPC to emit the two uncaptured
|
|
/// non-streamed-write buffers (regular-tag <c>RegisterTags</c> <c>btTagInfos</c> +
|
|
/// <c>AddNonStreamValues</c> <c>btInput</c>) — see <c>docs/plans/revision-write-path.md</c>
|
|
/// §"R3.1 capture plan". The byte[] payloads are captured by IL-rewriting
|
|
/// <c>Archestra.Historian.GrpcClient.dll</c>'s <c>GrpcHistoryClient.RegisterTags</c> /
|
|
/// <c>AddNonStreamValues</c> (separate dnlib step).
|
|
///
|
|
/// This file currently implements only the <c>load-check</c> scenario: a local, no-network
|
|
/// feasibility probe that confirms the mixed-mode assembly loads in this net481 x64 process and
|
|
/// that the connection API is reflectable (notably the <c>HistorianConnectionMode</c> enum, whose
|
|
/// gRPC value the live-connect step will need). Live scenarios (open/read/write) are added once
|
|
/// load-check passes.
|
|
/// </summary>
|
|
internal static class Program
|
|
{
|
|
private static int Main(string[] args)
|
|
{
|
|
string scenario = args.FirstOrDefault(a => !a.StartsWith("--", StringComparison.Ordinal)) ?? "load-check";
|
|
|
|
// Default to the sibling analysis tree; overridable with --bin <dir>.
|
|
string repoRoot = FindRepoRoot();
|
|
string defaultBin = Path.GetFullPath(Path.Combine(repoRoot, "..", "histsdk-2023r2-analysis", "bin"));
|
|
string binDir = GetOption(args, "--bin") ?? defaultBin;
|
|
string msiX64 = Path.GetFullPath(Path.Combine(binDir, "..", "msi-extract", "ArchestrA", "Toolkits", "Bin", "x64"));
|
|
|
|
string managedDll = Path.Combine(binDir, "aahClientManaged.dll");
|
|
if (!File.Exists(managedDll))
|
|
{
|
|
Console.Error.WriteLine($"aahClientManaged.dll not found at: {managedDll}");
|
|
Console.Error.WriteLine("Pass --bin <dir> pointing at histsdk-2023r2-analysis/bin.");
|
|
return 1;
|
|
}
|
|
|
|
// Resolve siblings from: (optional) IL-rewrite dir FIRST (so the instrumented
|
|
// Archestra.Historian.GrpcClient.dll + ReverseInstrumentation.dll win), then the core
|
|
// bin dir, then the gRPC-runtime msi-extract dir.
|
|
string? rewriteDir = GetOption(args, "--grpc-rewrite");
|
|
var probeList = new System.Collections.Generic.List<string>();
|
|
if (!string.IsNullOrEmpty(rewriteDir) && Directory.Exists(rewriteDir)) probeList.Add(rewriteDir!);
|
|
probeList.Add(binDir);
|
|
if (Directory.Exists(msiX64)) probeList.Add(msiX64);
|
|
string[] probeDirs = probeList.ToArray();
|
|
AppDomain.CurrentDomain.AssemblyResolve += (_, e) =>
|
|
{
|
|
string simpleName = new AssemblyName(e.Name).Name + ".dll";
|
|
foreach (string dir in probeDirs)
|
|
{
|
|
string candidate = Path.Combine(dir, simpleName);
|
|
if (File.Exists(candidate))
|
|
{
|
|
return Assembly.LoadFrom(candidate);
|
|
}
|
|
}
|
|
return null!;
|
|
};
|
|
|
|
// Pre-load the instrumented GrpcClient (+ logger) from the rewrite dir BEFORE anything
|
|
// touches aahClientManaged. The CLR reuses an already-loaded assembly (matched by
|
|
// identity) before probing the filesystem, so this wins over aahClientManaged's
|
|
// LoadFrom-context sibling probing (which would otherwise pick the original next to it).
|
|
if (!string.IsNullOrEmpty(rewriteDir) && Directory.Exists(rewriteDir))
|
|
{
|
|
foreach (string dll in new[] { "AVEVA.Historian.ReverseInstrumentation.dll", "Archestra.Historian.GrpcClient.dll" })
|
|
{
|
|
string path = Path.Combine(rewriteDir!, dll);
|
|
if (File.Exists(path))
|
|
{
|
|
Assembly pre = Assembly.LoadFrom(path);
|
|
Console.WriteLine($"Pre-loaded {pre.GetName().Name} from {pre.Location}");
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (scenario)
|
|
{
|
|
case "load-check":
|
|
return LoadCheck(managedDll, probeDirs);
|
|
case "connect":
|
|
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, delete-tag.");
|
|
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>
|
|
/// Drives the native 2023 R2 client through a non-streamed (historical backfill) write so the
|
|
/// IL-rewritten GrpcHistoryClient dumps the two buffers (RegisterTags.tagInfos +
|
|
/// AddNonStreamValues.inBuff) to the capture NDJSON. Sequence: open write-enabled gRPC ->
|
|
/// (optional) AddTag sandbox -> GetTagInfoByName (real TagKey + primes the per-connection
|
|
/// cache, the gate mitigation) -> CreateHistorianDataValueList(NonStreamedOriginal) ->
|
|
/// NonStreamedValuesBegin -> AddNonStreamedValue -> AddNonStreamedValuesEnd -> SendValues.
|
|
/// SendValues (the actual wire push) only runs with --commit. Run with --grpc-rewrite pointing
|
|
/// at the instrumented copy and AVEVA_HISTORIAN_RE_CAPTURE set to the output file.
|
|
/// Usage: capture-write --tag SdkM3CaptureSandbox [--create] [--commit]
|
|
/// [--server WONDER-SQL-VD03] [--port 32565] [--cert WONDER-SQL-VD03] [--value 123.0]
|
|
/// </summary>
|
|
private static int CaptureWrite(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 tagType = Req(asm, "ArchestrA.HistorianTag");
|
|
Type tagDataTypeEnum = Req(asm, "ArchestrA.HistorianDataType");
|
|
Type tagStorageEnum = Req(asm, "ArchestrA.HistorianStorageType");
|
|
Type valueType = Req(asm, "ArchestrA.HistorianDataValue");
|
|
Type valueDataTypeEnum = Req(asm, "ArchestrA.HistorianDataType");
|
|
Type listType = Req(asm, "ArchestrA.HistorianDataValueList");
|
|
Type categoryEnum = Req(asm, "ArchestrA.HistorianDataCategory");
|
|
|
|
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";
|
|
bool create = args.Contains("--create");
|
|
bool commit = args.Contains("--commit");
|
|
float sampleValue = float.TryParse(GetOption(args, "--value"), out float fv) ? fv : 123.0f;
|
|
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;
|
|
}
|
|
|
|
// Default capture sink if the caller didn't set one.
|
|
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE")))
|
|
{
|
|
string defaultCap = Path.GetFullPath(Path.Combine(
|
|
"artifacts", "reverse-engineering", "grpc-nonstream-capture", "capture.ndjson"));
|
|
Environment.SetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE", defaultCap);
|
|
}
|
|
string capPath = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE")!;
|
|
Console.WriteLine($"Capture sink: {capPath}");
|
|
|
|
// --- open write-enabled gRPC connection ---
|
|
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); // write-enabled
|
|
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(write-enabled): {opened} err={DescribeError(openArgs[1])}");
|
|
if (!opened) { return 2; }
|
|
|
|
try
|
|
{
|
|
// --- (optional) create the sandbox tag ---
|
|
if (create)
|
|
{
|
|
object tag = Activator.CreateInstance(tagType)!;
|
|
SetProp(tag, "TagName", tagName);
|
|
TrySetProp(tag, "TagDescription", "histsdk M3 non-streamed-write capture sandbox");
|
|
TrySetProp(tag, "TagDataType", Enum.Parse(tagDataTypeEnum, "Float", true));
|
|
TrySetProp(tag, "TagStorageType", Enum.Parse(tagStorageEnum, "Cyclic", true));
|
|
object addErr = Activator.CreateInstance(errorType)!;
|
|
object?[] addArgs = { tag, 0u, addErr };
|
|
bool addOk = (bool)accessType.GetMethod("AddTag", new[] { tagType, typeof(uint).MakeByRefType(), errorType.MakeByRefType() })!
|
|
.Invoke(access, addArgs)!;
|
|
Console.WriteLine($"AddTag({tagName}): {addOk} synthKey={addArgs[1]} err={DescribeError(addArgs[2])}");
|
|
System.Threading.Thread.Sleep(3000); // let the server pick up the new tag
|
|
}
|
|
|
|
// --- resolve real TagKey + prime the per-connection cache (gate mitigation) ---
|
|
// AddTag returns a synthetic key (10000000); the real wwTagKey is assigned server-side,
|
|
// so force a server fetch (cache:false) after a resync wait. GetTagInfoByName also
|
|
// primes the per-connection tag cache that AddNonStreamedValue's gate checks.
|
|
uint tagKey = 0;
|
|
if (uint.TryParse(GetOption(args, "--tag-key"), out uint overrideKey) && overrideKey != 0)
|
|
{
|
|
tagKey = overrideKey;
|
|
Console.WriteLine($"TagKey override: {tagKey}");
|
|
}
|
|
|
|
MethodInfo? getInfo = accessType.GetMethods().FirstOrDefault(m => m.Name == "GetTagInfoByName" && m.GetParameters().Length == 4);
|
|
int resyncWait = int.TryParse(GetOption(args, "--resync-wait"), out int rw2) ? rw2 : 10;
|
|
if (getInfo != null)
|
|
{
|
|
if (create && resyncWait > 0)
|
|
{
|
|
Console.WriteLine($"Waiting {resyncWait}s for server tag-key assignment...");
|
|
System.Threading.Thread.Sleep(resyncWait * 1000);
|
|
}
|
|
// Try server-fetch (cache:false) first for the real key, then cache:true as a prime.
|
|
foreach (bool useCache in new[] { false, true })
|
|
{
|
|
object infoErr = Activator.CreateInstance(errorType)!;
|
|
object?[] infoArgs = { tagName, useCache, null, infoErr };
|
|
bool infoOk = (bool)getInfo.Invoke(access, infoArgs)!;
|
|
object? tagInfo = infoArgs[2];
|
|
uint k = 0;
|
|
if (tagInfo != null && tagInfo.GetType().GetProperty("TagKey")?.GetValue(tagInfo) is { } kv) k = Convert.ToUInt32(kv);
|
|
Console.WriteLine($"GetTagInfoByName({tagName}, cache={useCache}): {infoOk} TagKey={k} err={DescribeError(infoArgs[3])}");
|
|
// Prefer a real (non-synthetic) server key.
|
|
if (k != 0 && (tagKey == 0 || (k != 10000000 && tagKey == 10000000))) tagKey = k;
|
|
}
|
|
}
|
|
if (tagKey == 0)
|
|
{
|
|
Console.Error.WriteLine("Could not resolve a TagKey — aborting before the write. Pass --tag-key <wwTagKey>.");
|
|
return 3;
|
|
}
|
|
Console.WriteLine($"Using TagKey={tagKey}");
|
|
|
|
// --- build the historical (backfill) value ---
|
|
object value = Activator.CreateInstance(valueType)!;
|
|
SetProp(value, "TagKey", tagKey);
|
|
TrySetProp(value, "DataValueType", Enum.Parse(valueDataTypeEnum, "Float", true));
|
|
TrySetProp(value, "OpcQuality", (ushort)192);
|
|
TrySetProp(value, "Value", sampleValue);
|
|
DateTime ts = DateTime.UtcNow.AddHours(-2); // backfill = past timestamp
|
|
TrySetProp(value, "StartDateTime", ts);
|
|
TrySetProp(value, "EndDateTime", ts);
|
|
TrySetProp(value, "ApplyScaling", false);
|
|
|
|
const BindingFlags allInstance = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
|
|
|
// --- non-streamed write sequence ---
|
|
MethodInfo createList = accessType.GetMethods().Where(m => m.Name == "CreateHistorianDataValueList").OrderBy(m => m.GetParameters().Length).First();
|
|
object?[] clArgs = createList.GetParameters().Select(pi =>
|
|
{
|
|
if (pi.ParameterType == categoryEnum) return Enum.Parse(categoryEnum, "NonStreamedOriginal");
|
|
if (pi.ParameterType.Name == "ConnectionIndex") return Enum.ToObject(pi.ParameterType, 0);
|
|
return pi.ParameterType.IsValueType ? Activator.CreateInstance(pi.ParameterType) : null;
|
|
}).ToArray();
|
|
object list = createList.Invoke(access, clArgs)!;
|
|
|
|
Invoke0(list, listType.GetMethods(allInstance).First(m => m.Name == "NonStreamedValuesBegin"), out object? beginRes);
|
|
Console.WriteLine($"NonStreamedValuesBegin: {beginRes}");
|
|
|
|
MethodInfo addVal = listType.GetMethods(allInstance).Where(m => m.Name == "AddNonStreamedValue").OrderBy(m => m.GetParameters().Length).First();
|
|
object addValErr = Activator.CreateInstance(errorType)!;
|
|
object?[] addValArgs = new object?[addVal.GetParameters().Length];
|
|
addValArgs[0] = value;
|
|
for (int i = 1; i < addValArgs.Length - 1; i++)
|
|
{
|
|
Type pt = addVal.GetParameters()[i].ParameterType;
|
|
addValArgs[i] = pt == typeof(bool) ? true : pt.IsValueType ? Activator.CreateInstance(pt) : null;
|
|
}
|
|
addValArgs[addValArgs.Length - 1] = addValErr;
|
|
object addValRes = addVal.Invoke(list, addValArgs)!;
|
|
Console.WriteLine($"AddNonStreamedValue: {addValRes} err={DescribeError(addValArgs[addValArgs.Length - 1])}");
|
|
|
|
// SendValues reads GetBatchID(); call it BEFORE AddNonStreamedValuesEnd, which
|
|
// resets the batch (End-before-Send yielded error 160 InvalidBatchId).
|
|
if (commit)
|
|
{
|
|
MethodInfo send = accessType.GetMethods(allInstance).Where(m => m.Name == "SendValues").OrderBy(m => m.GetParameters().Length).First();
|
|
object sendErr = Activator.CreateInstance(errorType)!;
|
|
object?[] sendArgs = new object?[send.GetParameters().Length];
|
|
sendArgs[0] = list;
|
|
for (int i = 1; i < sendArgs.Length - 1; i++)
|
|
{
|
|
Type pt = send.GetParameters()[i].ParameterType;
|
|
sendArgs[i] = pt.IsValueType ? Activator.CreateInstance(pt) : null;
|
|
}
|
|
sendArgs[sendArgs.Length - 1] = sendErr;
|
|
bool sendOk = (bool)send.Invoke(access, sendArgs)!;
|
|
Console.WriteLine($"SendValues: {sendOk} err={DescribeError(sendArgs[sendArgs.Length - 1])}");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("SendValues SKIPPED (pass --commit to push + capture btInput).");
|
|
}
|
|
|
|
Invoke0(list, listType.GetMethods(allInstance).First(m => m.Name == "AddNonStreamedValuesEnd"), out object? endRes);
|
|
Console.WriteLine($"AddNonStreamedValuesEnd: {endRes}");
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
MethodInfo? close = accessType.GetMethod("CloseConnection", new[] { errorType.MakeByRefType() });
|
|
if (close != null) close.Invoke(access, new object?[] { Activator.CreateInstance(errorType) });
|
|
}
|
|
catch { /* best-effort */ }
|
|
}
|
|
|
|
foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name == "Archestra.Historian.GrpcClient"))
|
|
{
|
|
Console.WriteLine($"Loaded GrpcClient from: {a.Location}");
|
|
}
|
|
|
|
int capLines = File.Exists(capPath) ? File.ReadAllLines(capPath).Length : 0;
|
|
Console.WriteLine($"Capture file lines: {capLines} ({capPath})");
|
|
return 0;
|
|
}
|
|
|
|
private static void Invoke0(object target, MethodInfo m, out object? result)
|
|
{
|
|
object?[] a = m.GetParameters().Select(pi => pi.ParameterType.IsValueType ? Activator.CreateInstance(pi.ParameterType) : null).ToArray();
|
|
result = m.Invoke(target, a);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read-only gRPC connect probe: opens a 2023 R2 Historian (mode=Historian) connection via the
|
|
/// native client and reports the resulting connection status. Proves the mixed-mode client can
|
|
/// reach the live server over gRPC from this box — the foundation for the write-capture step.
|
|
/// Reads creds from HISTORIAN_USER / HISTORIAN_PASSWORD (explicit) or uses IntegratedSecurity.
|
|
/// Usage: connect --server WONDER-SQL-VD03 [--port 32565] [--cert WONDER-SQL-VD03] [--integrated]
|
|
/// </summary>
|
|
private static int Connect(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 statusType = Req(asm, "ArchestrA.HistorianConnectionStatus");
|
|
Type certInfoType = Req(asm, "ArchestrA.CertificateInfo");
|
|
Type secModeType = Req(asm, "ArchestrA.HistorianSecurityMode");
|
|
|
|
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;
|
|
bool integrated = args.Contains("--integrated");
|
|
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");
|
|
string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD");
|
|
if (!integrated && string.IsNullOrEmpty(user))
|
|
{
|
|
Console.Error.WriteLine("Set HISTORIAN_USER/HISTORIAN_PASSWORD or pass --integrated.");
|
|
return 1;
|
|
}
|
|
|
|
object connArgs = Activator.CreateInstance(connArgsType)!;
|
|
SetProp(connArgs, "ServerName", server);
|
|
SetProp(connArgs, "TcpPort", checked((ushort)port));
|
|
SetProp(connArgs, "ConnectionMode", Enum.Parse(connModeType, "Historian")); // 2 = gRPC
|
|
SetProp(connArgs, "ConnectionType", Enum.Parse(connTypeType, "Process"));
|
|
SetProp(connArgs, "ReadOnly", true);
|
|
SetProp(connArgs, "IntegratedSecurity", integrated);
|
|
SetProp(connArgs, "AllowUnTrustedConnection", true);
|
|
if (!integrated)
|
|
{
|
|
SetProp(connArgs, "UserName", user!);
|
|
SetProp(connArgs, "Password", password ?? string.Empty);
|
|
}
|
|
|
|
// TLS transport: SecurityInfo = CertificateInfo { SecurityMode=TransportCertificate,
|
|
// CertificateName=<dns/cert name used as the https:// host> }. AllowUnTrustedConnection
|
|
// skips chain validation (the box reaches the server cert CN over the loopback tunnel).
|
|
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 error = Activator.CreateInstance(errorType)!;
|
|
MethodInfo open = accessType.GetMethod("OpenConnection", new[] { connArgsType, errorType.MakeByRefType() })
|
|
?? throw new MissingMethodException("OpenConnection");
|
|
Console.WriteLine($"OpenConnection: server={server} port={port} mode=Historian cert={certName} integrated={integrated} readonly=true");
|
|
object?[] openArgs = { connArgs, error };
|
|
bool ok;
|
|
try
|
|
{
|
|
ok = (bool)open.Invoke(access, openArgs)!;
|
|
}
|
|
catch (TargetInvocationException tie)
|
|
{
|
|
Console.Error.WriteLine($"OpenConnection threw: {tie.InnerException?.GetType().Name}: {tie.InnerException?.Message}");
|
|
return 2;
|
|
}
|
|
error = openArgs[1]!;
|
|
Console.WriteLine($"OpenConnection returned: {ok}");
|
|
Console.WriteLine($" error: {DescribeError(error)}");
|
|
|
|
// Poll connection status for a few seconds.
|
|
MethodInfo getStatus = accessType.GetMethod("GetConnectionStatus", new[] { statusType.MakeByRefType() })
|
|
?? accessType.GetMethods().First(m => m.Name == "GetConnectionStatus" && m.GetParameters().Length == 1);
|
|
object? status = null;
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
object?[] sArgs = { null };
|
|
getStatus.Invoke(access, sArgs);
|
|
status = sArgs[0];
|
|
bool connected = ReadBoolProp(status, "ConnectedToServer");
|
|
bool pending = ReadBoolProp(status, "Pending");
|
|
if (connected || !pending)
|
|
{
|
|
break;
|
|
}
|
|
System.Threading.Thread.Sleep(500);
|
|
}
|
|
|
|
Console.WriteLine("ConnectionStatus:");
|
|
DumpProps(status, " ");
|
|
|
|
bool connectedToServer = ReadBoolProp(status, "ConnectedToServer");
|
|
|
|
// Always close cleanly.
|
|
try
|
|
{
|
|
MethodInfo? close = accessType.GetMethod("CloseConnection", new[] { errorType.MakeByRefType() });
|
|
if (close != null)
|
|
{
|
|
object?[] cArgs = { Activator.CreateInstance(errorType) };
|
|
close.Invoke(access, cArgs);
|
|
}
|
|
}
|
|
catch { /* close best-effort */ }
|
|
|
|
Console.WriteLine(connectedToServer ? "CONNECT: PASS (ConnectedToServer)" : "CONNECT: FAIL (not connected)");
|
|
return connectedToServer ? 0 : 3;
|
|
}
|
|
|
|
private static int LoadCheck(string managedDll, string[] probeDirs)
|
|
{
|
|
Console.WriteLine($"Process: {(Environment.Is64BitProcess ? "x64" : "x86")}, CLR {Environment.Version}");
|
|
Console.WriteLine($"Probe dirs:");
|
|
foreach (string d in probeDirs)
|
|
{
|
|
Console.WriteLine($" {d} (exists={Directory.Exists(d)})");
|
|
}
|
|
|
|
Assembly asm;
|
|
try
|
|
{
|
|
asm = Assembly.LoadFrom(managedDll);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.Error.WriteLine($"LoadFrom FAILED: {ex.GetType().Name}: {ex.Message}");
|
|
if (ex is BadImageFormatException)
|
|
{
|
|
Console.Error.WriteLine(" -> likely an x86/x64 mismatch or missing VC++ runtime (MSVCP140/VCRUNTIME140_1).");
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
Console.WriteLine($"Loaded: {asm.FullName}");
|
|
|
|
Type? access = asm.GetType("ArchestrA.HistorianAccess");
|
|
Type? connArgs = asm.GetType("ArchestrA.HistorianConnectionArgs");
|
|
Type? connMode = asm.GetType("ArchestrA.HistorianConnectionMode");
|
|
Console.WriteLine($"HistorianAccess resolved: {access != null}");
|
|
Console.WriteLine($"HistorianConnectionArgs resolved:{connArgs != null}");
|
|
Console.WriteLine($"HistorianConnectionMode resolved:{connMode != null}");
|
|
|
|
if (connMode != null && connMode.IsEnum)
|
|
{
|
|
Console.WriteLine("HistorianConnectionMode values (the gRPC vs legacy selector):");
|
|
foreach (object v in Enum.GetValues(connMode))
|
|
{
|
|
Console.WriteLine($" {v} = {Convert.ToInt64(v)}");
|
|
}
|
|
}
|
|
|
|
// Confirm the managed gRPC client (IL-rewrite capture target) is reachable too.
|
|
try
|
|
{
|
|
Assembly grpc = Assembly.Load("Archestra.Historian.GrpcClient");
|
|
Type? historyClient = grpc.GetType("Archestra.Historian.GrpcClient.GrpcHistoryClient");
|
|
bool hasRegister = historyClient?.GetMethod("RegisterTags") != null;
|
|
bool hasAddNonStream = historyClient?.GetMethod("AddNonStreamValues") != null;
|
|
Console.WriteLine($"GrpcHistoryClient resolved: {historyClient != null} (RegisterTags={hasRegister}, AddNonStreamValues={hasAddNonStream})");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"GrpcHistoryClient load note: {ex.GetType().Name}: {ex.Message}");
|
|
}
|
|
|
|
bool ok = access != null && connArgs != null && connMode != null;
|
|
Console.WriteLine(ok ? "LOAD-CHECK: PASS" : "LOAD-CHECK: PARTIAL (some types unresolved)");
|
|
return ok ? 0 : 3;
|
|
}
|
|
|
|
private static string? GetOption(string[] args, string name)
|
|
{
|
|
int i = Array.IndexOf(args, name);
|
|
return i >= 0 && i + 1 < args.Length ? args[i + 1] : null;
|
|
}
|
|
|
|
private static Type Req(Assembly asm, string name) =>
|
|
asm.GetType(name) ?? throw new TypeLoadException($"Type not found: {name}");
|
|
|
|
private static void SetProp(object target, string name, object value)
|
|
{
|
|
PropertyInfo prop = target.GetType().GetProperty(name)
|
|
?? throw new MissingMemberException(target.GetType().FullName, name);
|
|
prop.SetValue(target, value);
|
|
}
|
|
|
|
private static void TrySetProp(object target, string name, object value)
|
|
{
|
|
try
|
|
{
|
|
PropertyInfo? prop = target.GetType().GetProperty(name);
|
|
if (prop != null && prop.CanWrite)
|
|
{
|
|
prop.SetValue(target, value);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($" (note: could not set {name}: {ex.GetType().Name})");
|
|
}
|
|
}
|
|
|
|
private static bool ReadBoolProp(object? target, string name)
|
|
{
|
|
if (target == null) return false;
|
|
PropertyInfo? prop = target.GetType().GetProperty(name);
|
|
return prop != null && prop.PropertyType == typeof(bool) && (bool)(prop.GetValue(target) ?? false);
|
|
}
|
|
|
|
private static void DumpProps(object? target, string indent)
|
|
{
|
|
if (target == null) { Console.WriteLine($"{indent}<null>"); return; }
|
|
foreach (PropertyInfo prop in target.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
|
{
|
|
if (prop.GetIndexParameters().Length != 0) continue;
|
|
object? val;
|
|
try { val = prop.GetValue(target); }
|
|
catch { val = "<throw>"; }
|
|
Console.WriteLine($"{indent}{prop.Name} = {val}");
|
|
}
|
|
}
|
|
|
|
private static string DescribeError(object? error)
|
|
{
|
|
if (error == null) return "<null>";
|
|
var sb = new System.Text.StringBuilder();
|
|
foreach (PropertyInfo prop in error.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
|
{
|
|
if (prop.GetIndexParameters().Length != 0) continue;
|
|
object? val;
|
|
try { val = prop.GetValue(error); }
|
|
catch { continue; }
|
|
if (val != null && !string.IsNullOrEmpty(val.ToString()))
|
|
{
|
|
sb.Append($"{prop.Name}={val} ");
|
|
}
|
|
}
|
|
return sb.Length == 0 ? "<empty>" : sb.ToString().Trim();
|
|
}
|
|
|
|
private static string FindRepoRoot()
|
|
{
|
|
string dir = AppDomain.CurrentDomain.BaseDirectory;
|
|
for (int i = 0; i < 8 && dir != null; i++)
|
|
{
|
|
if (File.Exists(Path.Combine(dir, "Histsdk.slnx")))
|
|
{
|
|
return dir;
|
|
}
|
|
dir = Path.GetDirectoryName(dir.TrimEnd(Path.DirectorySeparatorChar))!;
|
|
}
|
|
return AppDomain.CurrentDomain.BaseDirectory;
|
|
}
|
|
}
|
|
}
|