diff --git a/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs b/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs index d6056f4..34fe325 100644 --- a/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs +++ b/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs @@ -502,6 +502,7 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess return AtomicObjectEdit(obj, editObj => { var editAttr = FindAttributeForMutation(editObj, Arg(args, "attribute")); + EnsureMutableViaSetValue(editAttr, Arg(args, "attribute")); editAttr.SetValue(CreateMxValue(Arg(args, "value"), Arg(args, "data-type", "string"))); return CommandSummary(editAttr, "Set attribute value"); }); @@ -1622,11 +1623,40 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess } var attr = FindAttributeForMutation(obj, attributeName); + EnsureMutableViaSetValue(attr, attributeName); var body = File.ReadAllText(file); attr.SetValue(CreateMxValue(body, "string")); return CommandSummary(attr, $"Set script {attributeName}"); } + // Checks whether `attr.SetValue` will actually persist on this attribute, + // or whether the attribute is package-only (in which case SetValue silently + // no-ops despite returning Successful=true). Discovered via investigation + // documented in the writeback-gap notes: ScriptExtension text fields + // (ExecuteText, DeclarationsText, StartupText, ShutdownText, OnScanText, + // OffScanText, Expression) are all `MxCategoryPackageOnly_Lockable` — + // editable only via the package import/export round-trip, never via the + // generic `IAttribute.SetValue` path. Same for any other attribute whose + // category starts with `MxCategoryPackageOnly`. + private static void EnsureMutableViaSetValue(IAttribute attr, string attributeName) + { + string category; + try { category = attr.AttributeCategory.ToString(); } + catch { return; } // best-effort; if we can't read the category, let the write proceed + + if (category != null && category.StartsWith("MxCategoryPackageOnly", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + $"Attribute '{attributeName}' has category '{category}'. " + + "Package-only attributes silently no-op via IAttribute.SetValue and can only be " + + "edited through the package import/export round-trip. " + + "For ScriptExtension bodies (ExecuteText / DeclarationsText / StartupText / etc.), " + + "edit the source object in the System Platform IDE and re-import, " + + "or use a future package-rewrite path. " + + "See docs/script-parsing.md:8."); + } + } + private static string ObjectScriptCreate(IGalaxy galaxy, IgObject obj, IDictionary args) { var scriptName = Arg(args, "script"); @@ -1691,6 +1721,7 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess if (!string.IsNullOrWhiteSpace(expression)) { var attr = FindAttributeForMutation(obj, scriptName + ".Expression"); + EnsureMutableViaSetValue(attr, scriptName + ".Expression"); attr.SetValue(CreateMxValue(expression, "string")); yield return CommandSummary(attr, $"Set script {scriptName}.Expression"); }