M3 R3.1 decode: AddNonStreamValues reaches server StoreNonStreamValues (storage-engine console pipe)
Empirically decoded the AddNonStreamValues btInput framing against the live 2023 R2 server (grpc-nonstream-decode command + ProbeNonStreamedBuffersAsync driver). Every transaction rolled back (bCommit=false) — no data written. Finding: the btInput is assembled native-C++-side (not in any decompile), so 6 evidence-based framings (44-54B, packed HISTORIAN_VALUE2 variants) were probed. All 6 returned the IDENTICAL server error while an empty buffer returned a different InvalidParameter — so non-empty buffers pass parameter validation into CHistStorageConnection::StoreNonStreamValues, which routes to the \.\pipe\aahStorageEngine\console pipe server-side. Identical-across-framings => the blocker is NOT the btInput layout but a missing storage-engine console session / tag-registration precondition for the connection. Next step (untested): StorageService.OpenStorageConnection + tag registration (RegisterTags/AddTagidPairs/AddShardTagids) before AddNonStreamValues, then commit + read-back on a sandbox tag. Documented in revision-write-path.md (R3.1 decode section); raw artifact gitignored. 272 unit 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:
@@ -75,6 +75,7 @@ try
|
||||
"wcf-add-event-tag" => AddEventTagAndStartQuery(args),
|
||||
"capture-tag-info" => CaptureTagInfo(args),
|
||||
"grpc-revision-probe" => ProbeGrpcRevision(args),
|
||||
"grpc-nonstream-decode" => ProbeGrpcNonStreamedDecode(args),
|
||||
_ => UnknownCommand(args[0])
|
||||
};
|
||||
}
|
||||
@@ -3247,6 +3248,133 @@ static int ProbeGrpcRevision(string[] args)
|
||||
return result.BeginSucceeded ? 0 : 2;
|
||||
}
|
||||
|
||||
static int ProbeGrpcNonStreamedDecode(string[] args)
|
||||
{
|
||||
// Usage: grpc-nonstream-decode <host> [port] [--tls] [--dnsid <name>] [--tag <name>]
|
||||
// Empirically decodes the AddNonStreamValues btInput framing: looks up a real tag key, then
|
||||
// sends evidence-based candidate buffers over a live write-enabled gRPC transaction and reports
|
||||
// the server's accept/reject for each. Every transaction is rolled back (bCommit=false) — no
|
||||
// data is written. Candidates are derived from the decompiled 44-byte packed HISTORIAN_VALUE2.
|
||||
string host = args.Length > 1 ? args[1] : "localhost";
|
||||
int port = args.Length > 2 && int.TryParse(args[2], out int parsedPort)
|
||||
? parsedPort
|
||||
: HistorianClientOptions.DefaultGrpcPort;
|
||||
bool tls = HasOption(args, "--tls");
|
||||
string? dnsId = GetOption(args, "--dnsid");
|
||||
string tagName = GetOption(args, "--tag") ?? "SysTimeSec";
|
||||
|
||||
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");
|
||||
string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD");
|
||||
bool explicitCreds = !string.IsNullOrEmpty(user);
|
||||
|
||||
var options = new HistorianClientOptions
|
||||
{
|
||||
Host = host,
|
||||
Port = port,
|
||||
Transport = HistorianTransport.RemoteGrpc,
|
||||
GrpcUseTls = tls,
|
||||
AllowUntrustedServerCertificate = tls,
|
||||
ServerDnsIdentity = dnsId,
|
||||
IntegratedSecurity = !explicitCreds,
|
||||
UserName = user ?? string.Empty,
|
||||
Password = password ?? string.Empty,
|
||||
};
|
||||
|
||||
var client = new HistorianClient(options);
|
||||
AVEVA.Historian.Client.Models.HistorianTagMetadata? metadata =
|
||||
client.GetTagMetadataAsync(tagName, CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (metadata is null)
|
||||
{
|
||||
Console.Error.WriteLine($"Tag '{tagName}' not found on the server; cannot resolve a tag key.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (metadata.Key is not uint tagKey)
|
||||
{
|
||||
Console.Error.WriteLine($"Tag '{tagName}' metadata has no tag key.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// A historical timestamp ~2 hours in the past (non-streamed = backfill of past data).
|
||||
long fileTime = DateTime.UtcNow.AddHours(-2).ToFileTimeUtc();
|
||||
const short opcQualityGood = 192;
|
||||
double sampleValue = 123.0;
|
||||
|
||||
byte[] BuildHistorianValue2(byte[] value8)
|
||||
{
|
||||
byte[] v = new byte[44];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(v.AsSpan(0, 4), tagKey);
|
||||
BinaryPrimitives.WriteInt64LittleEndian(v.AsSpan(4, 8), fileTime);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(v.AsSpan(20, 2), opcQualityGood);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(v.AsSpan(24, 4), 7); // Type = numeric
|
||||
// @28 u32 MaxLength = 0 (numeric); @32 ApplyScaling = 0
|
||||
value8.AsSpan(0, 8).CopyTo(v.AsSpan(33, 8)); // @33 value (8 bytes, unaligned)
|
||||
// @41 bVersioned = 0; @42 VersionStatus = 0
|
||||
return v;
|
||||
}
|
||||
|
||||
byte[] valueAsDoubleBits = BitConverter.GetBytes(sampleValue); // 8 bytes, double
|
||||
byte[] valueAsFloatLow = new byte[8];
|
||||
BitConverter.GetBytes((float)sampleValue).CopyTo(valueAsFloatLow, 0); // float in low 4
|
||||
|
||||
byte[] structDouble = BuildHistorianValue2(valueAsDoubleBits);
|
||||
byte[] structFloat = BuildHistorianValue2(valueAsFloatLow);
|
||||
|
||||
byte[] WithCountU32(byte[] body, uint count)
|
||||
{
|
||||
byte[] b = new byte[4 + body.Length];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0, 4), count);
|
||||
body.CopyTo(b.AsSpan(4));
|
||||
return b;
|
||||
}
|
||||
|
||||
byte[] WithVersionAndCount(byte[] body, ushort version, uint count)
|
||||
{
|
||||
byte[] b = new byte[2 + 4 + body.Length];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(b.AsSpan(0, 2), version);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(2, 4), count);
|
||||
body.CopyTo(b.AsSpan(6));
|
||||
return b;
|
||||
}
|
||||
|
||||
// "OS"-style storage-sample header (as AddS2 uses), wrapping the packed struct as the blob.
|
||||
byte[] OsWrap(byte[] body)
|
||||
{
|
||||
byte[] b = new byte[10 + body.Length];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(b.AsSpan(0, 2), 0x534F); // "OS"
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(b.AsSpan(2, 2), 1); // sampleCount
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(4, 4), (uint)(body.Length + 1));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(b.AsSpan(8, 2), (ushort)body.Length);
|
||||
body.CopyTo(b.AsSpan(10));
|
||||
return b;
|
||||
}
|
||||
|
||||
var candidates = new List<(string Label, byte[] Buffer)>
|
||||
{
|
||||
("count(u32)+struct[double@33]", WithCountU32(structDouble, 1)),
|
||||
("count(u32)+struct[float@33]", WithCountU32(structFloat, 1)),
|
||||
("struct-only[double@33]", structDouble),
|
||||
("ver(u16=0)+count(u32)+struct[double]", WithVersionAndCount(structDouble, 0, 1)),
|
||||
("ver(u16=2)+count(u32)+struct[double]", WithVersionAndCount(structDouble, 2, 1)),
|
||||
("OS-wrap(struct[double])", OsWrap(structDouble)),
|
||||
("empty", Array.Empty<byte>()),
|
||||
};
|
||||
|
||||
var probe = new HistorianGrpcRevisionProbe(options);
|
||||
IReadOnlyList<HistorianGrpcNonStreamedCandidateResult> results =
|
||||
probe.ProbeNonStreamedBuffersAsync(candidates, CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
Console.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
Tag = tagName,
|
||||
TagKey = tagKey,
|
||||
FileTimeUtc = DateTime.FromFileTimeUtc(fileTime).ToString("o"),
|
||||
Candidates = results,
|
||||
}, CreateJsonOptions()));
|
||||
|
||||
return results.Any(static r => r.AddSucceeded) ? 0 : 2;
|
||||
}
|
||||
|
||||
static int ProbeWcf(string[] args)
|
||||
{
|
||||
string host = args.Length > 1 ? args[1] : "localhost";
|
||||
|
||||
Reference in New Issue
Block a user