M3 R3.1 capture: instrument-grpc-nonstream IL-rewrite + harness --grpc-rewrite loading
Adds the dnlib instrument command + harness wiring to capture the two non-streamed-write buffers from the native 2023 R2 client: - `instrument-grpc-nonstream <GrpcClient.dll> [out]` injects CaptureLogger.LogByteArray at the entry of GrpcHistoryClient.RegisterTags (byte[] tagInfos) and AddNonStreamValues (byte[] inBuff), writing the rewrite to docs/reverse-engineering/dnlib-write-copy/grpc2023 (gitignored — derived AVEVA binary). dnlib preserves the AVEVA public-key identity so aahClientManaged still binds the rewritten copy under the LoadFrom context (no SN re-verification). - harness `--grpc-rewrite <dir>` probes that dir first, so the instrumented GrpcClient.dll + ReverseInstrumentation.dll load ahead of the originals. load-check confirms the rewritten strong-named copy binds (HistorianConnectionMode.Historian=2; GrpcHistoryClient RegisterTags + AddNonStreamValues present). Next: capture-write scenario (open write-enabled -> sandbox tag -> read-prime -> AddNonStreamedValue), which dumps tagInfos + inBuff to the capture NDJSON. Prod write — confirm before running. 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:
@@ -32,3 +32,6 @@ coverage.cobertura.xml
|
|||||||
|
|
||||||
# Live 2023 R2 server credentials — never commit
|
# Live 2023 R2 server credentials — never commit
|
||||||
wonder-sql-vd03.txt
|
wonder-sql-vd03.txt
|
||||||
|
|
||||||
|
# Reverse-engineering IL-rewrite output: derived AVEVA binaries, never commit
|
||||||
|
docs/reverse-engineering/dnlib-write-copy/
|
||||||
|
|||||||
@@ -40,8 +40,15 @@ namespace AVEVA.Historian.Grpc2023CaptureHarness
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve siblings from both the core bin dir and the gRPC-runtime msi-extract dir.
|
// Resolve siblings from: (optional) IL-rewrite dir FIRST (so the instrumented
|
||||||
string[] probeDirs = Directory.Exists(msiX64) ? new[] { binDir, msiX64 } : new[] { binDir };
|
// 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) =>
|
AppDomain.CurrentDomain.AssemblyResolve += (_, e) =>
|
||||||
{
|
{
|
||||||
string simpleName = new AssemblyName(e.Name).Name + ".dll";
|
string simpleName = new AssemblyName(e.Name).Name + ".dll";
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ try
|
|||||||
"instrument-wcf-auth-context" => InstrumentWcfAuthContext(args),
|
"instrument-wcf-auth-context" => InstrumentWcfAuthContext(args),
|
||||||
"instrument-wcf-writemessage" => InstrumentWcfWriteMessage(args),
|
"instrument-wcf-writemessage" => InstrumentWcfWriteMessage(args),
|
||||||
"instrument-wcf-readmessage" => InstrumentWcfReadMessage(args),
|
"instrument-wcf-readmessage" => InstrumentWcfReadMessage(args),
|
||||||
|
"instrument-grpc-nonstream" => InstrumentGrpcNonStream(args),
|
||||||
"mark" => WriteMarker(args),
|
"mark" => WriteMarker(args),
|
||||||
"wcf-probe" => ProbeWcf(args),
|
"wcf-probe" => ProbeWcf(args),
|
||||||
"wcf-cert-probe" => ProbeWcfCertificate(args),
|
"wcf-cert-probe" => ProbeWcfCertificate(args),
|
||||||
@@ -1363,6 +1364,92 @@ static Instruction[] CreateCClientBaseOpenConnectionLogInstructions(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int InstrumentGrpcNonStream(string[] args)
|
||||||
|
{
|
||||||
|
// Usage: instrument-grpc-nonstream <Archestra.Historian.GrpcClient.dll> [output.dll]
|
||||||
|
// M3 R3.1 capture: injects CaptureLogger.LogByteArray at the entry of
|
||||||
|
// GrpcHistoryClient.RegisterTags (the byte[] tagInfos input) and AddNonStreamValues (the
|
||||||
|
// byte[] inBuff) so a write driven through the native 2023 R2 client dumps both buffers.
|
||||||
|
// Strong-name note: the input is signed (AVEVA key); dnlib preserves the public-key identity
|
||||||
|
// so aahClientManaged still binds to the rewritten copy under the LoadFrom context (which
|
||||||
|
// does not re-verify the SN signature). Write the rewrite to a copy — never the bin original.
|
||||||
|
if (args.Length < 2)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Usage: instrument-grpc-nonstream <Archestra.Historian.GrpcClient.dll> [output.dll]");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
string sourcePath = args[1];
|
||||||
|
string outputPath = args.Length > 2
|
||||||
|
? args[2]
|
||||||
|
: Path.Combine("docs", "reverse-engineering", "dnlib-write-copy", "grpc2023", "Archestra.Historian.GrpcClient.dll");
|
||||||
|
|
||||||
|
ModuleDefMD module = ModuleDefMD.Load(sourcePath);
|
||||||
|
TypeDef historyClient = module.GetTypes().FirstOrDefault(t => t.Name == "GrpcHistoryClient")
|
||||||
|
?? throw new InvalidOperationException("GrpcHistoryClient type not found in the module.");
|
||||||
|
|
||||||
|
MemberRefUser logByteArray = CreateLogByteArrayRef(module);
|
||||||
|
|
||||||
|
var instrumented = new List<object>();
|
||||||
|
foreach ((string methodName, string phase) in new[]
|
||||||
|
{
|
||||||
|
("RegisterTags", "Grpc.RegisterTags.tagInfos"),
|
||||||
|
("AddNonStreamValues", "Grpc.AddNonStreamValues.inBuff"),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// The input byte[] is "System.Byte[]"; the out byte[] params are "System.Byte[]&".
|
||||||
|
MethodDef method = historyClient.Methods.FirstOrDefault(m =>
|
||||||
|
m.Name == methodName && m.HasBody
|
||||||
|
&& m.Parameters.Any(p => !p.IsHiddenThisParameter && p.Type.FullName == "System.Byte[]"))
|
||||||
|
?? throw new InvalidOperationException($"{methodName} (with a byte[] input param) not found.");
|
||||||
|
|
||||||
|
dnlib.DotNet.Parameter bufParam = method.Parameters
|
||||||
|
.First(p => !p.IsHiddenThisParameter && p.Type.FullName == "System.Byte[]");
|
||||||
|
|
||||||
|
Instruction[] injected =
|
||||||
|
[
|
||||||
|
Instruction.Create(OpCodes.Ldstr, phase),
|
||||||
|
Instruction.Create(OpCodes.Ldarg, bufParam),
|
||||||
|
Instruction.Create(OpCodes.Call, logByteArray),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (Instruction instruction in injected.Reverse())
|
||||||
|
{
|
||||||
|
method.Body.Instructions.Insert(0, instruction);
|
||||||
|
}
|
||||||
|
|
||||||
|
method.Body.MaxStack = (ushort)Math.Max((int)method.Body.MaxStack, 8);
|
||||||
|
instrumented.Add(new
|
||||||
|
{
|
||||||
|
Method = methodName,
|
||||||
|
Phase = phase,
|
||||||
|
Param = bufParam.Name,
|
||||||
|
Token = "0x" + method.MDToken.Raw.ToString("X8"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPath))!);
|
||||||
|
if (module.IsILOnly)
|
||||||
|
{
|
||||||
|
module.Write(outputPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
module.NativeWrite(outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
Source = Path.GetFullPath(sourcePath),
|
||||||
|
Output = Path.GetFullPath(outputPath),
|
||||||
|
Type = historyClient.FullName,
|
||||||
|
Instrumented = instrumented,
|
||||||
|
LoggerMethod = "LogByteArray",
|
||||||
|
}, CreateJsonOptions()));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int InstrumentWcfWriteMessage(string[] args)
|
static int InstrumentWcfWriteMessage(string[] args)
|
||||||
{
|
{
|
||||||
string sourcePath = args.Length > 1 ? args[1] : Path.Combine("current", "aahClientManaged.dll");
|
string sourcePath = args.Length > 1 ? args[1] : Path.Combine("current", "aahClientManaged.dll");
|
||||||
|
|||||||
Reference in New Issue
Block a user