R1.2 GetRuntimeParameter + string-handle wall RESOLVED (handle-format bug)

Execute HCAL roadmap R1.2 (GetRuntimeParameterAsync) end-to-end, and in doing so
discover that the "string-handle wall" blocking R1.1/R1.4/R1.5/R1.6 was a handle
FORMAT bug, not a missing native session/filter registration.

R1.2 (shipped, live-verified):
- Captured native GetRuntimeParameter -> WCF op aa/Stat/GETRP (string-handle op,
  GETHI's shape), via scripts/Capture-RuntimeParam.ps1 + instrument-wcf-{write,read}message.
- HistorianRuntimeParameterProtocol serializes pRequestBuff (54 67 01 00 + uint
  nameCount + per-name uint charCount + UTF-16) and parses pResponseBuff (version +
  uint resultCount + CRetVariant 0x43 VT_BSTR + uint16 len + uint16 charCount + UTF-16).
- IStatusServiceContract2.GetRuntimeParameter (GETRP) op; HistorianWcfStatusClient
  passes the Open2 storage-session GUID as the string handle, UPPERCASE.
- Public HistorianClient.GetRuntimeParameterAsync(name) via the dialect.
- Golden WcfRuntimeParameterProtocolTests + gated live test; returns HistorianVersion.

String-handle wall RESOLVED (proven, public APIs deferred):
- The Open2 storage GUID works as the string handle when sent UPPERCASE
  (ToString("D").ToUpperInvariant()); earlier "blocked" probes used lowercase.
- Live-probed GETHI (R1.4) -> returns data; ExeC (R1.1) -> Retr.GetV prime -> ExeC ->
  GetR returns a BinaryFormatter-serialized .NET DataTable. Gated
  StringHandleProbeDiagnosticTests + scripts/Capture-ExecSql.ps1 + exec-sql harness scenario.
- Docs flipped: wcf-string-handle-wall.md RESOLVED banner; roadmap R1.1/R1.4 reachable,
  R1.5/R1.6 likely; wcf-status-localhost.md GETRP section.
- R1.1/R1.4 public APIs NOT shipped: ExeC needs a GetR paging loop + a BinaryFormatter-
  stream parser (BinaryFormatter is removed from .NET 10); GETHI full-info struct needs
  its own capture.

223 unit tests pass; gated live tests green against the local 2020 Historian.

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:
Joseph Doherty
2026-06-20 22:10:31 -04:00
parent 6d470eab4a
commit 4da5287d01
15 changed files with 953 additions and 16 deletions
@@ -162,7 +162,91 @@ internal static class Program
string? moveTerminalDescription = null;
List<object> rows = [];
if (openSuccess && status.ConnectedToServer && IsEventSendScenario(scenario))
if (openSuccess && status.ConnectedToServer && IsExecSqlScenario(scenario))
{
// R1.1 capture: drive HistorianAccess.ExecuteSqlCommand(sql, option, out retval,
// out DataTable, out error) so instrument-wcf-{write,read}message can observe the
// Retr.ExeC + Retr.GetR wire shape (handle format, command/option encoding, Retr
// priming, result byte stream). Read-only benign query.
string sql = GetArg(args, "--sql") ?? "SELECT 1 AS ProbeValue";
Type sqlOptionType = GetType(assembly, "ArchestrA.HistorianSqlExecuteOption");
object sqlOption = Enum.Parse(sqlOptionType, GetArg(args, "--sql-option") ?? "ExecuteRecord");
MethodInfo execMethod = accessType.GetMethods()
.First(m => m.Name == "ExecuteSqlCommand" && m.GetParameters().Length == 5);
object?[] execArgs = new object?[] { sql, sqlOption, 0, null, Activator.CreateInstance(errorType) };
WriteRuntimeMethodPointerSnapshot(assembly, runtimeMethodPointerOutput, runtimeMethodPointerFilters, repoRoot, scenario, "before-exec-sql");
bool execOk = (bool)execMethod.Invoke(access, execArgs)!;
int rowCount = -1, colCount = -1;
if (execArgs[3] is System.Data.DataTable table)
{
rowCount = table.Rows.Count;
colCount = table.Columns.Count;
}
Console.WriteLine(Serialize(new
{
Scenario = scenario,
Sql = sql,
ExecuteSqlCommandReturned = execOk,
ReturnValue = execArgs[2],
RowCount = rowCount,
ColumnCount = colCount,
Error = SnapshotObject(execArgs[4]!),
}));
return 0;
}
else if (openSuccess && status.ConnectedToServer && IsRuntimeParamScenario(scenario))
{
// R1.2 capture: drive HistorianAccess.GetRuntimeParameter(List<string> names,
// out List<object> results, out error) so instrument-wcf-{write,read}message can
// observe the WCF op name, handle type (uint vs string-handle wall), and the
// btRequest/btResponse buffer format. Pure status read — no write mode needed.
string namesArg = GetArg(args, "--runtime-param-names") ?? "HistorianVersion";
string[] names = namesArg.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
MethodInfo getRtParamMethod = accessType.GetMethods()
.First(m => m.Name == "GetRuntimeParameter" && m.GetParameters().Length == 3);
ParameterInfo[] rtParams = getRtParamMethod.GetParameters();
Type namesListType = rtParams[0].ParameterType; // List<string>
Type resultsListType = rtParams[1].ParameterType.GetElementType()!; // List<...>& -> List<...>
object namesList = Activator.CreateInstance(namesListType)!;
MethodInfo addName = namesListType.GetMethod("Add")!;
foreach (string n in names) addName.Invoke(namesList, new object?[] { n });
object rtError = Activator.CreateInstance(errorType)!;
object?[] rtArgs = new object?[] { namesList, Activator.CreateInstance(resultsListType), rtError };
WriteRuntimeMethodPointerSnapshot(assembly, runtimeMethodPointerOutput, runtimeMethodPointerFilters, repoRoot, scenario, "before-get-runtime-parameter");
bool rtOk = (bool)getRtParamMethod.Invoke(access, rtArgs)!;
object? resultsList = rtArgs[1];
var resultItems = new List<object?>();
if (resultsList is System.Collections.IEnumerable en)
{
foreach (object? item in en)
{
resultItems.Add(new
{
Type = item?.GetType().FullName,
Value = item?.ToString(),
});
}
}
Console.WriteLine(Serialize(new
{
Scenario = scenario,
Names = names,
GetRuntimeParameterReturned = rtOk,
ResultsListType = resultsListType.FullName,
Results = resultItems,
Error = SnapshotObject(rtArgs[2]!),
}));
return 0;
}
else if (openSuccess && status.ConnectedToServer && IsEventSendScenario(scenario))
{
// R2.1 capture: drive AddStreamedValue(HistorianEvent) and let instrument-wcf-*
// observe whether the event delivery rides the WCF MDAS path or the storage-engine
@@ -1407,6 +1491,28 @@ internal static class Program
}
/// <summary>Both event-query and event-send require an Event-type connection.</summary>
/// <summary>
/// Runtime-parameter scenario (R1.2 capture): opens a normal authenticated process
/// connection and calls <c>GetRuntimeParameter</c> so the WCF op + buffer format can be
/// captured. Read-only; not an event or write connection.
/// </summary>
private static bool IsRuntimeParamScenario(string scenario)
{
return scenario.Equals("runtime-param", StringComparison.OrdinalIgnoreCase)
|| scenario.Equals("runtime-parameter", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// SQL-command scenario (R1.1 capture): opens a normal authenticated process connection and
/// calls <c>ExecuteSqlCommand</c> (Retr.ExeC + Retr.GetR) so the string-handle SQL surface
/// can be captured. Read-only benign query.
/// </summary>
private static bool IsExecSqlScenario(string scenario)
{
return scenario.Equals("exec-sql", StringComparison.OrdinalIgnoreCase)
|| scenario.Equals("sql", StringComparison.OrdinalIgnoreCase);
}
private static bool IsEventConnectionScenario(string scenario)
{
return IsEventScenario(scenario) || IsEventSendScenario(scenario);