R1.10 RenameTagsAsync: async tag rename via History StartJob (StJb)
Tag rename has no dedicated WCF op — the (old,new) name batch rides the generic History StartJob (StJb) job buffer; the server returns a job id and applies renames asynchronously. Handle is the uppercase storage-session GUID, Open2 in write mode; reuses the write orchestrator's open+priming chain. jobBuffer layout (decoded + server-validated): byte[7] zero prefix + uint32 pairCount + per pair (uint32 oldCharCount + UTF-16 oldName + uint32 newCharCount + UTF-16 newName), order (old,new). The raw instrument capture mangles the final byte with MDAS chunk markers (the R1.1 lesson), so the golden fixture pins the CLEAN byte[] the SDK handed the channel (dumped via AVEVA_HISTORIAN_RENAME_DUMP) — the exact buffer the live server accepted and renamed with. Gated server-side by the AllowRenameTags system parameter (default 0): when disabled the native client rejects pre-wire (err 132); the managed SDK surfaces it as StartJob=false -> Accepted=false. Enabling needs a Historian config reload, not just a storage-engine restart. Shipped: HistorianClient.RenameTagAsync/RenameTagsAsync -> HistorianTagRenameResult; HistorianTagRenameProtocol; orchestrator RenameTags/SendStartJobRename; golden WcfTagRenameProtocolTests (4, pins server-accepted buffer); gated live test RenameTagsAsync_AgainstLocalHistorian_RenamesSandboxTag (passed end-to-end). Native-harness `rename` scenario + Capture-RenameTags.ps1 + decode-rename-capture.py. Doc: docs/reverse-engineering/wcf-rename-tags.md. 213 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:
@@ -93,7 +93,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));
|
||||
SetProperty(connectionArgs, "ReadOnly", !(IsWriteScenario(scenario) || IsRenameScenario(scenario)));
|
||||
SetProperty(connectionArgs, "IntegratedSecurity", integratedSecurity);
|
||||
SetProperty(connectionArgs, "ConnectionType", Enum.Parse(connectionType, IsEventScenario(scenario) ? "Event" : "Process"));
|
||||
if (directConnection)
|
||||
@@ -683,6 +683,104 @@ internal static class Program
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (openSuccess && status.ConnectedToServer && IsRenameScenario(scenario))
|
||||
{
|
||||
// R1.10 capture: drive HistorianAccess.RenameTags(Tuple<string,string>[] pairs, ref
|
||||
// HistorianTagRenameStatus, out err) so instrument-wcf-{write,read}message can observe
|
||||
// the StJb (StartJob) jobBuffer that encodes the rename request + the GtJb
|
||||
// (GetJobStatus) response. Sandbox-guarded: both names must start with RetestSdkWrite.
|
||||
string renameFrom = GetArg(args, "--rename-from") ?? "RetestSdkWriteRenameSrc";
|
||||
string renameTo = GetArg(args, "--rename-to") ?? "RetestSdkWriteRenameDst";
|
||||
if (!renameFrom.StartsWith("RetestSdkWrite", StringComparison.Ordinal)
|
||||
|| !renameTo.StartsWith("RetestSdkWrite", StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Rename scenario refuses names that don't start with 'RetestSdkWrite'. Pass --rename-from/--rename-to RetestSdkWrite...");
|
||||
}
|
||||
|
||||
var renameRows = new List<object>();
|
||||
|
||||
// 1) Ensure the source sandbox tag exists (AddTag), unless --rename-skip-create.
|
||||
if (!HasFlag(args, "--rename-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", renameFrom);
|
||||
SetProperty(tag, "TagDescription", "SDK rename-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() })
|
||||
?? throw new MissingMethodException("HistorianAccess.AddTag");
|
||||
object?[] addTagArgs = [tag, 0u, addError];
|
||||
bool addOk = (bool)addTagMethod.Invoke(access, addTagArgs)!;
|
||||
renameRows.Add(new
|
||||
{
|
||||
Kind = "AddTag",
|
||||
Success = addOk,
|
||||
ErrorDescription = GetPropertyText(addTagArgs[2]!, "ErrorDescription"),
|
||||
});
|
||||
}
|
||||
|
||||
// 2) RenameTags([(from, to)]) — batch of (old, new) name pairs.
|
||||
Type renameStatusType = GetType(assembly, "ArchestrA.HistorianTagRenameStatus");
|
||||
MethodInfo renameMethod = accessType.GetMethods()
|
||||
.First(m => m.Name == "RenameTags" && m.GetParameters().Length == 3);
|
||||
Array pairs = Array.CreateInstance(typeof(Tuple<string, string>), 1);
|
||||
pairs.SetValue(Tuple.Create(renameFrom, renameTo), 0);
|
||||
object renameStatus = Activator.CreateInstance(renameStatusType)!;
|
||||
object renameError = Activator.CreateInstance(errorType)!;
|
||||
object?[] renameArgs = [pairs, renameStatus, renameError];
|
||||
WriteRuntimeMethodPointerSnapshot(assembly, runtimeMethodPointerOutput, runtimeMethodPointerFilters, repoRoot, scenario, "before-rename-tags");
|
||||
bool renameOk = (bool)renameMethod.Invoke(access, renameArgs)!;
|
||||
renameStatus = renameArgs[1]!;
|
||||
renameError = renameArgs[2]!;
|
||||
renameRows.Add(new
|
||||
{
|
||||
Kind = "RenameTags",
|
||||
Success = renameOk,
|
||||
Status = SnapshotObject(renameStatus),
|
||||
ErrorDescription = GetPropertyText(renameError, "ErrorDescription"),
|
||||
ErrorCode = GetPropertyText(renameError, "ErrorCode"),
|
||||
ErrorType = GetPropertyText(renameError, "ErrorType"),
|
||||
});
|
||||
|
||||
// 3) Poll GetTagRenameStatus(ref status) a few times (job is async server-side).
|
||||
MethodInfo? statusMethod = accessType.GetMethods()
|
||||
.FirstOrDefault(m => m.Name == "GetTagRenameStatus" && m.GetParameters().Length == 1);
|
||||
if (renameOk && statusMethod is not null)
|
||||
{
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
object?[] stArgs = [renameStatus];
|
||||
object? stRet = statusMethod.Invoke(access, stArgs);
|
||||
renameStatus = stArgs[0]!;
|
||||
renameRows.Add(new { Kind = "GetTagRenameStatus", Iteration = i, Returned = stRet?.ToString(), Status = SnapshotObject(renameStatus) });
|
||||
bool pending = GetPropertyValue(renameStatus, "Pending") as bool? ?? false;
|
||||
if (!pending) break;
|
||||
System.Threading.Thread.Sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine(Serialize(new
|
||||
{
|
||||
Scenario = scenario,
|
||||
RenameFrom = renameFrom,
|
||||
RenameTo = renameTo,
|
||||
RenameTagsReturned = renameOk,
|
||||
Rows = renameRows,
|
||||
}));
|
||||
return 0;
|
||||
}
|
||||
else if (openSuccess && status.ConnectedToServer && IsTagScenario(scenario))
|
||||
{
|
||||
object query = accessType.GetMethod("CreateTagQuery", Type.EmptyTypes)!.Invoke(access, Array.Empty<object>())!;
|
||||
@@ -1245,6 +1343,20 @@ internal static class Program
|
||||
|| scenario.Equals("tag-write", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tag-rename scenario (R1.10 capture): opens a write-enabled connection and calls
|
||||
/// <c>RenameTags(Tuple<string,string>[] pairs, ref HistorianTagRenameStatus, out err)</c>
|
||||
/// so instrument-wcf-{write,read}message can observe the <c>StJb</c> (StartJob) jobBuffer that
|
||||
/// encodes the rename request and the <c>GtJb</c> (GetJobStatus) response. Sandbox-guarded:
|
||||
/// both names must start with <c>RetestSdkWrite</c>.
|
||||
/// </summary>
|
||||
private static bool IsRenameScenario(string scenario)
|
||||
{
|
||||
return scenario.Equals("rename", StringComparison.OrdinalIgnoreCase)
|
||||
|| scenario.Equals("rename-tag", StringComparison.OrdinalIgnoreCase)
|
||||
|| scenario.Equals("rename-tags", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> SnapshotObject(object target)
|
||||
{
|
||||
Dictionary<string, object?> snapshot = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
Reference in New Issue
Block a user