SendEvent over gRPC: implement + live-validate (was capture-gated)
Captured the native 2023 R2 client's gRPC event send (new capture-send-event harness scenario): it rides HistoryService.AddStreamValues with the SAME "OS" (0x534F) storage-sample buffer the WCF path already uses (HistorianEventWrite- Protocol) — confirming "no distinct RPC", and that it is NOT the historical write's "ON" buffer. Diffed the write-enabled vs read-only v8 Event open: byte- identical apart from per-session crypto, so the existing OpenSession event path is reused unchanged. So SendEvent-over-gRPC was pure assembly of proven parts: - HistorianGrpcEventWriteOrchestrator = v8 Event open + CM_EVENT registration (UpdC3/RegisterTags/EnsureTags) + AddStreamValues(OS buffer). - HistorianClient.SendEventAsync now routes to it for RemoteGrpc (WCF otherwise). Live-validated end-to-end against the 2023 R2 server: pure-managed SDK send → AddStreamValues BSuccess=true → the event reads back from the server (markers confirmed in returned event rows). The native gRPC RegisterTags(24B) + EnsureTags(86B) byte-match our serializers (new GrpcEventSendProtocolTests golden, closing the 83-vs-86 EnsureTags question). Gated live test SendEventAsync_OverGrpc_AcceptsEvent (opt-in HISTORIAN_GRPC_EVENT_SEND=1). 331 offline tests pass. 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:
@@ -92,8 +92,10 @@ namespace AVEVA.Historian.Grpc2023CaptureHarness
|
||||
return DeleteTag(managedDll, args);
|
||||
case "capture-event":
|
||||
return CaptureEvent(managedDll, args);
|
||||
case "capture-send-event":
|
||||
return CaptureSendEvent(managedDll, args);
|
||||
default:
|
||||
Console.Error.WriteLine($"Unknown scenario '{scenario}'. Supported: load-check, connect, capture-write, delete-tag, capture-event.");
|
||||
Console.Error.WriteLine($"Unknown scenario '{scenario}'. Supported: load-check, connect, capture-write, delete-tag, capture-event, capture-send-event.");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -603,6 +605,132 @@ namespace AVEVA.Historian.Grpc2023CaptureHarness
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drives the native 2023 R2 client through an event SEND so the IL-rewritten GrpcClient dumps
|
||||
/// the AddStreamValues.btValues (the event VTQ storage-sample buffer — resolves whether a gRPC
|
||||
/// event send uses the "OS" or "ON" outer signature) AND the Event-connection EnsureTags.btTagInfos
|
||||
/// (the 83-vs-86-byte CM_EVENT registration byte-diff). Opens a WRITE-ENABLED Event connection,
|
||||
/// builds a clearly-marked test HistorianEvent, calls AddStreamedValue, then CloseConnection to
|
||||
/// flush the queued event onto the wire. WRITES a real test event into the server's event history.
|
||||
/// Run with --grpc-rewrite pointing at the instrumented copy and AVEVA_HISTORIAN_RE_CAPTURE set.
|
||||
/// Usage: capture-send-event [--server <host>] [--port 32565] [--cert <host>]
|
||||
/// [--event-type SdkCaptureProbe] [--flush-seconds 6]
|
||||
/// </summary>
|
||||
private static int CaptureSendEvent(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 eventType = Req(asm, "ArchestrA.HistorianEvent");
|
||||
Type propTypeEnum = Req(asm, "ArchestrA.HistorianEventPropertyType");
|
||||
Type certInfoType = Req(asm, "ArchestrA.CertificateInfo");
|
||||
Type secModeType = Req(asm, "ArchestrA.HistorianSecurityMode");
|
||||
|
||||
string server = GetOption(args, "--server") ?? Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST") ?? "localhost";
|
||||
int port = int.TryParse(GetOption(args, "--port"), out int p) ? p : 32565;
|
||||
string certName = GetOption(args, "--cert") ?? server;
|
||||
string evtTypeName = GetOption(args, "--event-type") ?? "SdkCaptureProbe";
|
||||
int flushSeconds = int.TryParse(GetOption(args, "--flush-seconds"), out int fs) ? fs : 6;
|
||||
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;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE")))
|
||||
{
|
||||
string defaultCap = Path.GetFullPath(Path.Combine(
|
||||
"artifacts", "reverse-engineering", "grpc-event-capture", "send-event-capture.ndjson"));
|
||||
Environment.SetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE", defaultCap);
|
||||
}
|
||||
Console.WriteLine($"Capture sink: {Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE")}");
|
||||
|
||||
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, "Event")); // EVENT connection
|
||||
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?[] openArgs = { connArgs, Activator.CreateInstance(errorType) };
|
||||
Console.WriteLine($"OpenConnection: server={server} port={port} type=Event readonly=false");
|
||||
bool opened;
|
||||
try
|
||||
{
|
||||
opened = (bool)accessType.GetMethod("OpenConnection", new[] { connArgsType, errorType.MakeByRefType() })!
|
||||
.Invoke(access, openArgs)!;
|
||||
}
|
||||
catch (TargetInvocationException tie)
|
||||
{
|
||||
Console.Error.WriteLine($"OpenConnection threw: {tie.InnerException?.GetType().Name}: {tie.InnerException?.Message}");
|
||||
return 2;
|
||||
}
|
||||
Console.WriteLine($"OpenConnection returned: {opened} err={DescribeError(openArgs[1])}");
|
||||
if (!opened) { return 2; }
|
||||
|
||||
try
|
||||
{
|
||||
// Build a clearly-marked test event. Required: Type (≤32 chars), Id, EventTime.
|
||||
object evt = Activator.CreateInstance(eventType)!;
|
||||
SetProp(evt, "Type", evtTypeName);
|
||||
TrySetProp(evt, "Id", Guid.NewGuid());
|
||||
TrySetProp(evt, "EventTime", DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc));
|
||||
TrySetProp(evt, "Namespace", "SdkCapture");
|
||||
TrySetProp(evt, "Source", "SdkCaptureProbe");
|
||||
|
||||
// One string property to exercise the property-bag framing.
|
||||
MethodInfo? addProp = eventType.GetMethods().FirstOrDefault(m =>
|
||||
m.Name == "AddProperty" && m.GetParameters().Length == 4);
|
||||
if (addProp != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
object strEnum = Enum.Parse(propTypeEnum, "String", true);
|
||||
object?[] apArgs = { "SdkProbeProp", "SdkProbeValue", strEnum, Activator.CreateInstance(errorType) };
|
||||
addProp.Invoke(evt, apArgs);
|
||||
Console.WriteLine($"AddProperty: err={DescribeError(apArgs[3])}");
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"AddProperty skipped: {ex.GetType().Name}"); }
|
||||
}
|
||||
|
||||
MethodInfo addStreamed = accessType.GetMethods().First(m =>
|
||||
m.Name == "AddStreamedValue" && m.GetParameters().Length == 2
|
||||
&& m.GetParameters()[0].ParameterType == eventType);
|
||||
object?[] asArgs = { evt, Activator.CreateInstance(errorType) };
|
||||
bool sent = (bool)addStreamed.Invoke(access, asArgs)!;
|
||||
Console.WriteLine($"AddStreamedValue({evtTypeName}): {sent} err={DescribeError(asArgs[1])}");
|
||||
|
||||
// Let the native delivery queue flush the event onto the wire (AddStreamValues).
|
||||
System.Threading.Thread.Sleep(flushSeconds * 1000);
|
||||
Console.WriteLine(sent ? "CAPTURE-SEND-EVENT: AddStreamedValue accepted (buffer captured on flush)" : "CAPTURE-SEND-EVENT: AddStreamedValue rejected");
|
||||
return sent ? 0 : 3;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
// CloseConnection flushes any remaining queued values before teardown.
|
||||
MethodInfo? close = accessType.GetMethod("CloseConnection", new[] { errorType.MakeByRefType() });
|
||||
if (close != null) close.Invoke(access, new object?[] { Activator.CreateInstance(errorType) });
|
||||
}
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
|
||||
Reference in New Issue
Block a user