R1.11 AddTagExtendedPropertiesAsync: extended-property write via AddTEx
Adds user-defined extended properties to an existing tag via the 2020 WCF
AddTEx (AddTagExtendedProperties) op. Write-enabled connection + uppercase
storage-session GUID handle; reuses the write orchestrator open/priming chain.
The AddTEx inBuff is the exact inverse of the R1.5 GetTepByNm read-response
framing, so the serializer mirrors the read parser:
uint32 groupCount + 0x01(group) + [0x09+u16+ASCII tag] + uint32 propCount
+ per prop{ 0x02 + [0x09+u16+ASCII name] + 0x43 VT_BSTR + u16 payloadLen
+ u16 charCount + UTF-16 value } + 0x01(group trailer) + 0x00(terminator).
The trailing 0x00 is required — without it inBuff is one byte short and the
server throws SErrorException in CHistStorage::AddTagExtendedProperties. The
golden fixture pins the clean inBuff the live server accepted (dumped via
AVEVA_HISTORIAN_TEP_DUMP); read-back verified via R1.5. String (0x43) values only.
Delete (DelTep) is deferred: the native DeleteTagExtendedPropertiesByName does a
client-side sync check and returns err 229 for a just-added property, so the
DelTep request never reaches the wire and its inBuff can't be captured yet.
Shipped: HistorianClient.AddTagExtendedPropertiesAsync/AddTagExtendedPropertyAsync;
HistorianTagExtendedPropertyProtocol.SerializeAddRequest; orchestrator path;
golden WcfTagExtendedPropertyWriteProtocolTests (4); gated live write/read-back test;
native-harness `add-tep` scenario + Capture-AddTagExtendedProperties.ps1 +
decode-add-tep-capture.py. Doc: wcf-add-tag-extended-properties.md. 233 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:
@@ -131,7 +131,7 @@ internal static class Program
|
||||
object connectionArgs = Activator.CreateInstance(connectionArgsType)!;
|
||||
SetProperty(connectionArgs, "ServerName", serverName);
|
||||
SetProperty(connectionArgs, "TcpPort", checked((ushort)tcpPort));
|
||||
SetProperty(connectionArgs, "ReadOnly", !(IsWriteScenario(scenario) || IsEventSendScenario(scenario)));
|
||||
SetProperty(connectionArgs, "ReadOnly", !(IsWriteScenario(scenario) || IsEventSendScenario(scenario) || IsAddTagExtendedPropertiesScenario(scenario)));
|
||||
SetProperty(connectionArgs, "IntegratedSecurity", integratedSecurity);
|
||||
SetProperty(connectionArgs, "ConnectionType", Enum.Parse(connectionType, IsEventConnectionScenario(scenario) ? "Event" : "Process"));
|
||||
if (directConnection)
|
||||
@@ -323,6 +323,120 @@ internal static class Program
|
||||
}));
|
||||
return 0;
|
||||
}
|
||||
else if (openSuccess && status.ConnectedToServer && IsAddTagExtendedPropertiesScenario(scenario))
|
||||
{
|
||||
// R1.11 capture: drive AddTagExtendedProperties(TagExtendedPropertyGroupList, out err)
|
||||
// — and optionally DeleteTagExtendedPropertiesByName — so instrument-wcf-writemessage can
|
||||
// observe the AddTEx / DelTep inBuff (tag + property name/value framing). Sandbox-guarded.
|
||||
string tepTag = GetArg(args, "--tep-tag") ?? "RetestSdkWriteTepTag";
|
||||
if (!tepTag.StartsWith("RetestSdkWrite", StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"add-tep scenario refuses tags that don't start with 'RetestSdkWrite'. Pass --tep-tag RetestSdkWrite...");
|
||||
}
|
||||
string propName = GetArg(args, "--tep-name") ?? "SdkTestProp";
|
||||
string propValue = GetArg(args, "--tep-value") ?? "SdkTestValue";
|
||||
|
||||
var tepRows = new List<object>();
|
||||
|
||||
// 1) Ensure the tag exists (AddTag) unless --tep-skip-create.
|
||||
if (!HasFlag(args, "--tep-skip-create"))
|
||||
{
|
||||
Type tagDefType = GetType(assembly, "ArchestrA.HistorianTag");
|
||||
Type tagDataTypeEnum = GetType(assembly, "ArchestrA.HistorianDataType");
|
||||
Type tagStorageTypeEnum = GetType(assembly, "ArchestrA.HistorianStorageType");
|
||||
object tag = Activator.CreateInstance(tagDefType)!;
|
||||
SetProperty(tag, "TagName", tepTag);
|
||||
SetProperty(tag, "TagDescription", "SDK ext-property write RE sandbox tag");
|
||||
SetProperty(tag, "EngineeringUnit", "test");
|
||||
SetProperty(tag, "TagDataType", Enum.Parse(tagDataTypeEnum, "Float", ignoreCase: true));
|
||||
SetProperty(tag, "TagStorageType", Enum.Parse(tagStorageTypeEnum, "Cyclic", ignoreCase: true));
|
||||
SetProperty(tag, "MinEU", 0.0);
|
||||
SetProperty(tag, "MaxEU", 100.0);
|
||||
SetProperty(tag, "MinRaw", 0.0);
|
||||
SetProperty(tag, "MaxRaw", 100.0);
|
||||
SetProperty(tag, "StorageRate", 1000u);
|
||||
SetProperty(tag, "ApplyScaling", false);
|
||||
object addError = Activator.CreateInstance(errorType)!;
|
||||
MethodInfo addTagMethod = accessType.GetMethod("AddTag", new[] { tagDefType, typeof(uint).MakeByRefType(), errorType.MakeByRefType() })!;
|
||||
object?[] addTagArgs = [tag, 0u, addError];
|
||||
bool addOk = (bool)addTagMethod.Invoke(access, addTagArgs)!;
|
||||
tepRows.Add(new { Kind = "AddTag", Success = addOk, ErrorDescription = GetPropertyText(addTagArgs[2]!, "ErrorDescription") });
|
||||
}
|
||||
|
||||
// Prime the tag identity (same reason as the read scenario — server-side resolution).
|
||||
MethodInfo? getTagInfoByName = accessType.GetMethods()
|
||||
.FirstOrDefault(m => m.Name == "GetTagInfoByName" && m.GetParameters().Length == 4);
|
||||
if (getTagInfoByName is not null)
|
||||
{
|
||||
object tibError = Activator.CreateInstance(errorType)!;
|
||||
object?[] tibArgs = new object?[] { tepTag, true, null, tibError };
|
||||
try { getTagInfoByName.Invoke(access, tibArgs); } catch { }
|
||||
}
|
||||
|
||||
// 2) Build TagExtendedPropertyGroupList { TagExtendedPropertyGroup { TagName, [TagExtendedProperty] } }
|
||||
Type listType = GetType(assembly, "ArchestrA.TagExtendedPropertyGroupList");
|
||||
Type groupType = GetType(assembly, "ArchestrA.TagExtendedPropertyGroup");
|
||||
Type propType = GetType(assembly, "ArchestrA.TagExtendedProperty");
|
||||
Type propDataTypeEnum = GetType(assembly, "ArchestrA.TagExtendedPropertyDataType");
|
||||
|
||||
object list = Activator.CreateInstance(listType)!;
|
||||
object group = Activator.CreateInstance(groupType)!;
|
||||
SetProperty(group, "TagName", tepTag);
|
||||
object prop = Activator.CreateInstance(propType)!;
|
||||
SetProperty(prop, "PropertyName", propName);
|
||||
SetProperty(prop, "Type", Enum.Parse(propDataTypeEnum, "String", ignoreCase: true));
|
||||
SetProperty(prop, "Value", propValue);
|
||||
groupType.GetMethod("Add", new[] { propType })!.Invoke(group, [prop]);
|
||||
listType.GetMethod("Add", new[] { groupType })!.Invoke(list, [group]);
|
||||
|
||||
MethodInfo addTepMethod = accessType.GetMethods()
|
||||
.First(m => m.Name == "AddTagExtendedProperties" && m.GetParameters().Length == 2);
|
||||
object addTepError = Activator.CreateInstance(errorType)!;
|
||||
object?[] addTepArgs = [list, addTepError];
|
||||
WriteRuntimeMethodPointerSnapshot(assembly, runtimeMethodPointerOutput, runtimeMethodPointerFilters, repoRoot, scenario, "before-add-tep");
|
||||
bool addTepOk = false;
|
||||
string? addTepEx = null;
|
||||
try { addTepOk = (bool)addTepMethod.Invoke(access, addTepArgs)!; }
|
||||
catch (TargetInvocationException ex) { addTepEx = FormatException(ex.InnerException ?? ex); }
|
||||
tepRows.Add(new
|
||||
{
|
||||
Kind = "AddTagExtendedProperties",
|
||||
Success = addTepOk,
|
||||
Exception = addTepEx,
|
||||
ErrorDescription = GetPropertyText(addTepArgs[1]!, "ErrorDescription"),
|
||||
ErrorCode = GetPropertyText(addTepArgs[1]!, "ErrorCode"),
|
||||
});
|
||||
|
||||
// 3) Optional delete (DelTep) to capture its inBuff too.
|
||||
if (HasFlag(args, "--tep-delete"))
|
||||
{
|
||||
MethodInfo? delTepMethod = accessType.GetMethods()
|
||||
.FirstOrDefault(m => m.Name == "DeleteTagExtendedPropertiesByName" && m.GetParameters().Length == 4);
|
||||
if (delTepMethod is not null)
|
||||
{
|
||||
Type namesColType = delTepMethod.GetParameters()[1].ParameterType; // StringCollection
|
||||
object names = Activator.CreateInstance(namesColType)!;
|
||||
namesColType.GetMethod("Add", new[] { typeof(string) })!.Invoke(names, [propName]);
|
||||
object delErr = Activator.CreateInstance(errorType)!;
|
||||
object?[] delArgs = [tepTag, names, true, delErr];
|
||||
bool delOk = false; string? delEx = null;
|
||||
try { delOk = (bool)delTepMethod.Invoke(access, delArgs)!; }
|
||||
catch (TargetInvocationException ex) { delEx = FormatException(ex.InnerException ?? ex); }
|
||||
tepRows.Add(new { Kind = "DeleteTagExtendedPropertiesByName", Success = delOk, Exception = delEx, ErrorDescription = GetPropertyText(delArgs[3]!, "ErrorDescription") });
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine(Serialize(new
|
||||
{
|
||||
Scenario = scenario,
|
||||
TepTag = tepTag,
|
||||
PropertyName = propName,
|
||||
PropertyValue = propValue,
|
||||
Rows = tepRows,
|
||||
}));
|
||||
return 0;
|
||||
}
|
||||
else if (openSuccess && status.ConnectedToServer && IsEventSendScenario(scenario))
|
||||
{
|
||||
// R2.1 capture: drive AddStreamedValue(HistorianEvent) and let instrument-wcf-*
|
||||
@@ -1638,6 +1752,19 @@ internal static class Program
|
||||
|| scenario.Equals("tag-tep", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extended-property WRITE scenario (R1.11 capture): opens a write-enabled connection and calls
|
||||
/// <c>AddTagExtendedProperties(TagExtendedPropertyGroupList, out err)</c> (and optionally
|
||||
/// <c>DeleteTagExtendedPropertiesByName</c>) so instrument-wcf-writemessage can observe the
|
||||
/// <c>AddTEx</c>/<c>DelTep</c> inBuff. Sandbox-guarded: the tag must start with RetestSdkWrite.
|
||||
/// </summary>
|
||||
private static bool IsAddTagExtendedPropertiesScenario(string scenario)
|
||||
{
|
||||
return scenario.Equals("add-tep", StringComparison.OrdinalIgnoreCase)
|
||||
|| scenario.Equals("tep-write", StringComparison.OrdinalIgnoreCase)
|
||||
|| scenario.Equals("extended-property-write", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsEventConnectionScenario(string scenario)
|
||||
{
|
||||
return IsEventScenario(scenario) || IsEventSendScenario(scenario);
|
||||
|
||||
Reference in New Issue
Block a user