R1.5 GetTagExtendedPropertiesAsync (GetTepByNm) + R1.6 closed (no op)
Ship tag extended-property reads over the 2020 WCF aa/Retr/GetTepByNm op: HistorianClient.GetTagExtendedPropertiesAsync(tag) -> name/value pairs. String-handle op reached with the Open2 storage-session GUID formatted uppercase (same format that unlocked GETRP/GETHI/ExeC). Routed via the name-based native path (GetTagExtendedPropertiesByName, server-fetch flag), not the index-based TagQuery path. Evidence-backed findings from the capture: - GetTepByNm (and GetTgByNm) succeed with the uppercase handle -- further validates the resolved string-handle wall. - QTB (StartTagQuery) does NOT punch through: captured uppercase, it still fails server-side (CMdServer::StartActiveTagnamesQuery over the aahMetadataServer pipe) -- a metadata-server blocker, not handle format. - R1.6 (localized properties) has NO distinct op (only error-message/UI-text localization in the managed client); collapses into R1.5. Closed, not throwing. Wire format (golden-pinned, synthetic bytes -- no dev tag names committed): - request tagNames = uint count + per-name(uint charCount + UTF-16) - response = uint tagCount + per-tag(marker + compact-ASCII name + uint propCount + per-prop(marker + compact-ASCII name + 0x43 VT_BSTR value) + trailer); sequence-paged. Adds: HistorianTagExtendedProperty model, HistorianTagExtendedPropertyProtocol (codec), HistorianWcfTagExtendedPropertyClient (orchestration), dialect + public API; golden WcfTagExtendedPropertyProtocolTests (4) + gated live test (HISTORIAN_TEP_TAG). Tooling: Capture-TagExtendedProperties.ps1, decode-tag-properties-capture.py, harness tag-extended-properties scenario. Docs: wcf-tag-extended-properties.md; roadmap R1.5 DONE / R1.6 collapsed; wall doc + memory updated with the QTB-server-side nuance. 228 tests green. 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:
@@ -43,6 +43,12 @@ internal static class Program
|
||||
string? aggregationTypeName = GetArg(args, "--aggregation-type");
|
||||
uint maxStates = uint.TryParse(GetArg(args, "--max-states"), out uint parsedMaxStates) ? parsedMaxStates : 0;
|
||||
string? historyFilter = GetArg(args, "--filter");
|
||||
// R1.5 capture: flip the TagQueryArgs flag that makes the native client retrieve extended
|
||||
// properties in the index-based TagQuery path. (The dedicated tag-extended-properties
|
||||
// scenario, which drives the name-based GetTagExtendedPropertiesByName, is the reliable
|
||||
// GetTepByNm capture path; the TagQuery path is gated behind QTB, which fails server-side
|
||||
// here.) Off by default so the normal tag-query scenario is unchanged.
|
||||
bool retrieveExtendedProperties = HasFlag(args, "--retrieve-extended-properties");
|
||||
DateTime endUtc = TryParseUtc(GetArg(args, "--end-utc")) ?? DateTime.UtcNow;
|
||||
DateTime startUtc = TryParseUtc(GetArg(args, "--start-utc")) ?? endUtc.AddMinutes(-lookbackMinutes);
|
||||
|
||||
@@ -246,6 +252,77 @@ internal static class Program
|
||||
}));
|
||||
return 0;
|
||||
}
|
||||
else if (openSuccess && status.ConnectedToServer && IsTagExtendedPropertiesScenario(scenario))
|
||||
{
|
||||
// R1.5 capture: drive HistorianAccess.GetTagExtendedPropertiesByName(string tagName,
|
||||
// bool fetchFromServer, out TagExtendedPropertyGroup, out error) directly. This is the
|
||||
// NAME-based entry point that issues the GetTepByNm WCF op WITHOUT a prior
|
||||
// StartTagQuery (QTB) — the index-based TagQuery.GetTagExtendedPropertyInfo path is
|
||||
// blocked here because QTB fails server-side (CMdServer StartActiveTagnamesQuery).
|
||||
// The second GetTagExtendedPropertiesByName arg forces a server fetch (issues GetTepByNm)
|
||||
// when true; when false the C++ client reads its local cache and returns err 41 if the
|
||||
// tag's properties were never fetched. Default true so the scenario captures GetTepByNm;
|
||||
// pass --tep-cache-only to exercise the cache-read (no WCF op) path.
|
||||
bool fetchFromServer = !HasFlag(args, "--tep-cache-only");
|
||||
|
||||
// Prime the tag identity table first (ProcessTagNameIdentity inside
|
||||
// GetTagExtendedPropertiesByName fails with err 41 if the tag was never resolved on
|
||||
// this connection). GetTagInfoByName(tagName, cache, out HistorianTag, out err) is the
|
||||
// proven uint-handle metadata path that registers the tag.
|
||||
string? primeResult = null;
|
||||
MethodInfo? getTagInfoByName = accessType.GetMethods()
|
||||
.FirstOrDefault(m => m.Name == "GetTagInfoByName" && m.GetParameters().Length == 4);
|
||||
if (getTagInfoByName is not null)
|
||||
{
|
||||
ParameterInfo[] tibParams = getTagInfoByName.GetParameters();
|
||||
Type tagOutType = tibParams[2].ParameterType.GetElementType()!;
|
||||
object tibError = Activator.CreateInstance(errorType)!;
|
||||
object?[] tibArgs = new object?[] { tagName, true, null, tibError };
|
||||
try
|
||||
{
|
||||
bool tibOk = (bool)getTagInfoByName.Invoke(access, tibArgs)!;
|
||||
primeResult = $"GetTagInfoByName={tibOk} err={GetPropertyText(tibArgs[3], "ErrorDescription")}";
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
primeResult = "GetTagInfoByName threw: " + FormatException(ex.InnerException ?? ex);
|
||||
}
|
||||
}
|
||||
|
||||
MethodInfo getTepByName = accessType.GetMethods()
|
||||
.First(m => m.Name == "GetTagExtendedPropertiesByName" && m.GetParameters().Length == 4);
|
||||
ParameterInfo[] tepParams = getTepByName.GetParameters();
|
||||
Type groupType = tepParams[2].ParameterType.GetElementType()!; // TagExtendedPropertyGroup& -> TagExtendedPropertyGroup
|
||||
|
||||
object tepError = Activator.CreateInstance(errorType)!;
|
||||
object?[] tepArgs = new object?[] { tagName, fetchFromServer, null, tepError };
|
||||
WriteRuntimeMethodPointerSnapshot(assembly, runtimeMethodPointerOutput, runtimeMethodPointerFilters, repoRoot, scenario, "before-get-tag-extended-properties");
|
||||
bool tepOk = false;
|
||||
string? tepException = null;
|
||||
try
|
||||
{
|
||||
tepOk = (bool)getTepByName.Invoke(access, tepArgs)!;
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
tepException = FormatException(ex.InnerException ?? ex);
|
||||
}
|
||||
|
||||
Console.WriteLine(Serialize(new
|
||||
{
|
||||
Scenario = scenario,
|
||||
TagName = tagName,
|
||||
FetchFromServer = fetchFromServer,
|
||||
Prime = primeResult,
|
||||
GroupType = groupType.FullName,
|
||||
GetTagExtendedPropertiesByNameReturned = tepOk,
|
||||
Exception = tepException,
|
||||
Group = ToSerializableValue(tepArgs[2]),
|
||||
GroupSnapshot = tepArgs[2] is null ? null : SnapshotObject(tepArgs[2]!),
|
||||
Error = SnapshotObject(tepArgs[3]!),
|
||||
}));
|
||||
return 0;
|
||||
}
|
||||
else if (openSuccess && status.ConnectedToServer && IsEventSendScenario(scenario))
|
||||
{
|
||||
// R2.1 capture: drive AddStreamedValue(HistorianEvent) and let instrument-wcf-*
|
||||
@@ -916,7 +993,7 @@ internal static class Program
|
||||
object queryArgs = Activator.CreateInstance(tagQueryArgsType)!;
|
||||
SetProperty(queryArgs, "TagFilter", tagName);
|
||||
SetProperty(queryArgs, "CacheTagInfo", true);
|
||||
SetProperty(queryArgs, "RetrieveTagExtendedPropertyInfo", false);
|
||||
SetProperty(queryArgs, "RetrieveTagExtendedPropertyInfo", retrieveExtendedProperties);
|
||||
snapshots["TagQueryArgsBeforeStart"] = SnapshotObject(queryArgs);
|
||||
|
||||
startError = Activator.CreateInstance(errorType)!;
|
||||
@@ -980,6 +1057,42 @@ internal static class Program
|
||||
Tags = SummarizeTagList(tagInfoArgs[2])
|
||||
});
|
||||
}
|
||||
|
||||
// R1.5 capture: explicitly pull extended properties so the native client issues
|
||||
// the GetTepByNm WCF op (only fires when --retrieve-extended-properties is set,
|
||||
// which flips RetrieveTagExtendedPropertyInfo on the query args above).
|
||||
if (retrieveExtendedProperties)
|
||||
{
|
||||
MethodInfo? getTepMethod = queryType.GetMethods().FirstOrDefault(method =>
|
||||
method.Name == "GetTagExtendedPropertyInfo" && method.GetParameters().Length == 4);
|
||||
if (getTepMethod is not null)
|
||||
{
|
||||
object tepError = Activator.CreateInstance(errorType)!;
|
||||
object?[] tepArgs = [0u, requestedRows, null, tepError];
|
||||
bool tepSuccess = false;
|
||||
try
|
||||
{
|
||||
tepSuccess = (bool)getTepMethod.Invoke(query, tepArgs)!;
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
rows.Add(new { Kind = "TagExtendedPropertyException", Detail = FormatException(ex.InnerException ?? ex) });
|
||||
}
|
||||
tepError = tepArgs[3]!;
|
||||
rows.Add(new
|
||||
{
|
||||
Kind = "TagExtendedProperties",
|
||||
Success = tepSuccess,
|
||||
ErrorDescription = GetPropertyText(tepError, "ErrorDescription"),
|
||||
ErrorCode = GetPropertyText(tepError, "ErrorCode"),
|
||||
Groups = ToSerializableValue(tepArgs[2])
|
||||
});
|
||||
if (tepArgs[2] is not null)
|
||||
{
|
||||
snapshots["TagExtendedPropertyGroups"] = SnapshotObject(tepArgs[2]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MethodInfo? endMethod = queryType.GetMethod("EndQuery", new[] { errorType.MakeByRefType() });
|
||||
@@ -1513,6 +1626,18 @@ internal static class Program
|
||||
|| scenario.Equals("sql", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tag extended-properties scenario (R1.5 capture): opens a normal authenticated process
|
||||
/// connection and calls the NAME-based <c>GetTagExtendedPropertiesByName</c> so the GetTepByNm
|
||||
/// WCF op + tagNames request / extended-property response buffers can be captured. This
|
||||
/// bypasses the QTB (StartTagQuery) path, which fails server-side here.
|
||||
/// </summary>
|
||||
private static bool IsTagExtendedPropertiesScenario(string scenario)
|
||||
{
|
||||
return scenario.Equals("tag-extended-properties", StringComparison.OrdinalIgnoreCase)
|
||||
|| scenario.Equals("tag-tep", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsEventConnectionScenario(string scenario)
|
||||
{
|
||||
return IsEventScenario(scenario) || IsEventSendScenario(scenario);
|
||||
|
||||
Reference in New Issue
Block a user