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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user