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:
Joseph Doherty
2026-06-21 01:18:41 -04:00
parent 362fcb0ef4
commit bc353df8c4
12 changed files with 794 additions and 3 deletions
@@ -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&lt;string,string&gt;[] 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);