graccesscli: correct script-edit docs to TN-537 truth (writes DO persist)
I was wrong. AVEVA Tech Note 537 ("Creating an Application Object Script
Using GRAccess", April 2008) documents the supported pattern:
ConfigurableAttributes[<script>.<field>].SetValue(MxValue) inside a
CheckOut/Save/CheckIn cycle. graccesscli's existing
FindAttributeForMutation already follows this — writes to MxCategoryPackageOnly_Lockable
script-text fields persist correctly.
The earlier "writeback gap" diagnosis was a phantom caused by a reader-side
issue. `object attribute value get` against a script body returns
"Supported: False / Attribute value is not exposed" because
MxValueDetails uses a case-sensitive `ReadProperty(attr, "Value")` lookup
plus an accessor probe (GetBoolean -> GetInteger -> GetFloat -> GetDouble
-> GetString) that can fall through silently for some MxValue shapes. The
COM-side property is exposed as `value` (lowercase), readable as
`attr.value.GetString()` -- which the live probe at
`analysis/ide-edit-investigation/probe_setvalue/` does and confirms the
post-write content matches the marker exactly.
Live verification on $TestMachine.UpdateTestChangingInt.DeclarationsText
and $DelmiaReceiver.ProcessRecipe.{ExecuteText,DeclarationsText}:
=== verdict ===
marker landed on same-proxy ConfigurableAttributes: True
marker landed on same-proxy Attributes : True
marker landed on fresh-proxy ConfigurableAttributes: True
marker landed on fresh-proxy Attributes : True
The probe also confirmed that two earlier graccesscli `object scripts set`
invocations (which I had wrongly believed failed) had persisted -- the
marker text I wrote previously was still on disk in
ProcessRecipe.{ExecuteText,DeclarationsText} when read directly via
attr.value.GetString(). The probe restored both fields to their original
values.
This commit:
- Updates the misleading [Command(...)] / [CommandOption(...)]
descriptions in GRAccessSurfaceCommands.cs back to honest versions
citing TN-537.
- Restores the --file-using examples for `object scripts set` and
`object scripts create` across script-editing.md, llm-integration.md,
usage.md, and zb-testmachine.md.
- Removes the test that asserted the (wrong) EnsureMutableViaSetValue
guard. Re-aims ScriptCommandDescriptions_… at the corrected wording.
- Removes two leftover EnsureMutableViaSetValue calls in the trigger-period
/ trigger-type write paths (both targeted MxCategoryWriteable_C_Lockable
attributes; would never have fired even if the helper still existed).
- Adds analysis/ide-edit-investigation/REPORT.md (replacing the earlier
wrong report) plus the probe sources under probe_setvalue/.
The MxValueDetails reader gap (case-sensitive ReadProperty + accessor
probe) is a real follow-up: `object attribute value get` should
case-insensitively read `value` and try GetString first when the
underlying MxValue.DataType is MxString. Out of scope here -- that's a
separate, smaller fix.
Test count delta: 67 -> 66 (-2 wrong tests, +1 corrected description test).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# How the IDE edits scripts — investigation findings (corrected)
|
||||
|
||||
**Status:** my original report (and the safety-check + docs note that grew out of it) was **wrong**. AVEVA Tech Note 537 ("Creating an Application Object Script Using GRAccess", April 2008) documents the supported pattern, and a live round-trip probe confirmed it works on this Galaxy. graccesscli's existing `FindAttributeForMutation` already follows this pattern correctly; the apparent "writeback gap" was a reader-side gap in `MxValueDetails`.
|
||||
|
||||
This document supersedes the earlier "package-only no-op" claim. The reverts on `master`: commits `4e242ca` (safety check) and `e4e5425` (docs note) plus the corrective commit on top.
|
||||
|
||||
## What's actually true
|
||||
|
||||
Per **AVEVA Tech Note 537** (page 6-7):
|
||||
|
||||
> First we add a new script into the list of scripts. Then we save the object. Saving the object generates all the attributes required to configure the script the conventional way. […] Configurable attributes of the ScriptExtension primitive: ExecuteText, AliasReferences, Aliases, TriggerType, DataChangeDeadband, Expression, DeclarationsText, StartupText, ShutdownText, OnScanText, OffScanText, ExecutionError.Alarmed, TriggerPeriod, ScriptExecutionGroup, ScriptOrder, RunsAsync, State.Historized, ExecuteTimeout.Limit, TriggerOnQualityChange.
|
||||
|
||||
The supported edit sequence is:
|
||||
|
||||
```csharp
|
||||
// 1. Acquire the object.
|
||||
var galaxy = galaxies[GalaxyName];
|
||||
galaxy.Login(user, pwd);
|
||||
var obj = galaxy.QueryObjectsByName(EgObjectIsTemplateOrInstance.gObjectIsTemplate, ref names)[1];
|
||||
|
||||
// 2. Mutation lifecycle.
|
||||
obj.CheckOut();
|
||||
|
||||
// 3. Add the primitive (only when creating a new script).
|
||||
obj.AddExtensionPrimitive("ScriptExtension", scriptName, true);
|
||||
obj.Save(); // generates the script's child attributes
|
||||
|
||||
// 4. Write text via ConfigurableAttributes — NOT Attributes.
|
||||
IAttribute body = obj.ConfigurableAttributes[scriptName + ".ExecuteText"];
|
||||
var v = new MxValueClass();
|
||||
v.PutString("// new script body...");
|
||||
body.SetValue(v);
|
||||
// (repeat for DeclarationsText, TriggerType, Expression, etc.)
|
||||
|
||||
// 5. Persist + release.
|
||||
obj.Save();
|
||||
obj.CheckIn("");
|
||||
```
|
||||
|
||||
graccesscli's `FindAttributeForMutation` (line 988-995) already prefers `ConfigurableAttributes` first and falls back to `Attributes`. `ObjectScriptSet` in the dispatcher writes via that helper. So `object scripts set --field <name> --file <path>` follows the TN-537 pattern.
|
||||
|
||||
## Live verification
|
||||
|
||||
The probe at `analysis/ide-edit-investigation/probe_setvalue/` exercises the TN-537 sequence directly against `$TestMachine.UpdateTestChangingInt.DeclarationsText` and against `$DelmiaReceiver.ProcessRecipe.{ExecuteText,DeclarationsText}` on the local ZB galaxy. Result of the four-way verify after a marker write:
|
||||
|
||||
```
|
||||
=== verdict ===
|
||||
marker landed on same-proxy ConfigurableAttributes: True
|
||||
marker landed on same-proxy Attributes : True
|
||||
marker landed on fresh-proxy ConfigurableAttributes: True
|
||||
marker landed on fresh-proxy Attributes : True
|
||||
```
|
||||
|
||||
Same probe also confirmed two earlier `graccesscli object scripts set` invocations had persisted (the `// roundtrip-test marker (graccesscli scripts set --field DeclarationsText)` text was still in `ProcessRecipe.DeclarationsText` when re-read directly via `attr.value.GetString()`).
|
||||
|
||||
## Why my earlier diagnosis went wrong
|
||||
|
||||
My earlier round-trip test wrote with graccesscli's `object scripts set`, then read back with `object attribute value get` — and the read returned `Supported: False, Value: None, Unavailable: "Attribute value is not exposed by this GRAccess attribute."` I read this as "the write didn't persist." It actually means **the reader couldn't surface the value**, while the write *had* persisted.
|
||||
|
||||
The reader gap is in `GRAccessCommandDispatcher.MxValueDetails` (line 1382-1417) plus `AttributeValueDetails` (line 1340-1380):
|
||||
|
||||
```csharp
|
||||
var direct = MxValueDetails(ReadProperty(attr, "Value"));
|
||||
```
|
||||
|
||||
`ReadProperty(attr, "Value")` uses `Type.InvokeMember(name, BindingFlags.GetProperty, ...)` which is **case-sensitive** by default. The COM-side property is exposed as `value` (lowercase) on `IAttribute` — what the probe successfully reads via `attr.value`. The capital-V `"Value"` lookup may either return `null` or hit a different sibling, and `MxValueDetails(null)` short-circuits to "not exposed." Even when `ReadProperty` does return the MxValue object, `MxValueDetails`'s scalar accessor probe (`GetBoolean` → `GetInteger` → `GetFloat` → `GetDouble` → `GetString`) can fall through silently for some types.
|
||||
|
||||
This is a real gap in `attribute value get` that should be fixed separately — but it's a *reader* problem, not a write-side issue. The script body content does land on disk, and `object scripts get --llm-json` (which uses the package-export fallback) reflects it correctly.
|
||||
|
||||
## What changed in this correction
|
||||
|
||||
- **Reverted** `bd95ace` (the safety check `EnsureMutableViaSetValue`) and `c12fbc5` (the wrong docs note about a "package-only writeback boundary").
|
||||
- **Removed** two leftover `EnsureMutableViaSetValue` call sites the user had added in `87c0124` for the trigger-period / trigger-type write paths (both call sites also targeted `MxCategoryWriteable_C_Lockable` attributes, so they would never have fired even if the helper still existed — removing keeps the dispatcher consistent with the truth).
|
||||
- **Updated** the misleading `[Command(...)]` / `[CommandOption(...)]` descriptions in `GRAccessSurfaceCommands.cs` back to honest versions citing TN-537.
|
||||
- **Restored** the `--file`-using examples for `object scripts set` and `object scripts create` that `87c0124` removed across `script-editing.md`, `llm-integration.md`, `usage.md`, `zb-testmachine.md`.
|
||||
- **Updated** test assertions accordingly: removed `DispatcherScriptSettings_GuardsPackageOnlyNoOpsBeforeSetValue` (asserted the wrong helper) and re-aimed `ScriptCommandDescriptions_…` at the corrected wording.
|
||||
- **Rewrote** `analysis/ide-edit-investigation/REPORT.md` (this file) to document the truth.
|
||||
|
||||
## Open follow-ups (≤2)
|
||||
|
||||
1. **`MxValueDetails` reader gap.** `object attribute value get` against ScriptExtension text fields and similar `IAttribute` shapes returns `Supported: False` despite the value being set. Likely fix: case-insensitive `ReadProperty` lookup *and* try `GetString` first when the underlying `MxValue.DataType` is `MxString`. Out of scope for this corrective commit.
|
||||
2. **The decompiled `IConfigurationAccess2` / `XxGalaxyPackageServer` analysis** under `cac/`, `appcfg/`, `scriptpkg/` is still useful as an *internal IDE map* — but it's not the *required* CLI write path, just an alternative one the IDE happens to use. Keep the artifacts; they document the IDE-internal channel honestly.
|
||||
+1114
File diff suppressed because it is too large
Load Diff
+1072
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Nullable>disable</Nullable>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<Platforms>x86</Platforms>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<RootNamespace>Probe</RootNamespace>
|
||||
<AssemblyName>Probe</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="ArchestrA.GRAccess">
|
||||
<HintPath>C:\Users\dohertj2\Desktop\wwtools\graccesscli\lib\ArchestrA.GRAccess.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using ArchestrA.GRAccess;
|
||||
|
||||
class Program
|
||||
{
|
||||
static int Main(string[] args)
|
||||
{
|
||||
// Probe: write to $TestMachine.UpdateTestChangingInt.DeclarationsText (a
|
||||
// package-only ScriptExtension text field) via the TN-537 pattern using
|
||||
// ConfigurableAttributes. Read back through TWO paths to discriminate
|
||||
// between (a) write-side no-op vs (b) read-side staleness:
|
||||
// 1. Same IgObject proxy via obj.ConfigurableAttributes (in-memory edit view)
|
||||
// 2. Fresh galaxy.QueryObjectsByName proxy via .Attributes (runtime view)
|
||||
//
|
||||
// Restores DeclarationsText to empty when done.
|
||||
|
||||
const string GalaxyName = "ZB";
|
||||
const string Tagname = "$TestMachine";
|
||||
const string ScriptName = "UpdateTestChangingInt";
|
||||
const string FieldName = "DeclarationsText";
|
||||
const string Marker = "// graccesscli probe marker — TN537 ConfigurableAttributes path\n";
|
||||
|
||||
var user = Environment.GetEnvironmentVariable("MX_TEST_USER") ?? "";
|
||||
var pass = Environment.GetEnvironmentVariable("MX_TEST_PASSWORD") ?? "";
|
||||
var node = Environment.MachineName;
|
||||
|
||||
Console.WriteLine($"[probe] connecting to {node} galaxy={GalaxyName} as {user}");
|
||||
var gr = new GRAccessAppClass();
|
||||
var galaxies = gr.QueryGalaxies(node);
|
||||
var galaxy = galaxies[GalaxyName];
|
||||
if (string.IsNullOrEmpty(user))
|
||||
galaxy.Login();
|
||||
else
|
||||
galaxy.Login(user, pass);
|
||||
|
||||
var attrPath = ScriptName + "." + FieldName;
|
||||
|
||||
// Acquire the target template object.
|
||||
var names = new string[] { Tagname };
|
||||
var initial = QueryOne(galaxy, names);
|
||||
|
||||
// Read current value via ConfigurableAttributes (configuration view).
|
||||
var beforeCfg = TryReadString(initial.ConfigurableAttributes, attrPath, "before-cfg");
|
||||
var beforeRt = TryReadString(initial.Attributes, attrPath, "before-rt");
|
||||
Console.WriteLine($"[probe] BEFORE cfg={Quote(beforeCfg)} rt={Quote(beforeRt)}");
|
||||
|
||||
// Mutate via the canonical TN-537 sequence on the same proxy.
|
||||
Console.WriteLine("[probe] CheckOut...");
|
||||
initial.CheckOut();
|
||||
ThrowIfFailed(initial.CommandResult, "CheckOut");
|
||||
|
||||
Console.WriteLine("[probe] ConfigurableAttributes[...].SetValue(MxValue)...");
|
||||
var cfgAttr = initial.ConfigurableAttributes[attrPath];
|
||||
var v = new MxValueClass();
|
||||
v.PutString(Marker);
|
||||
cfgAttr.SetValue(v);
|
||||
|
||||
Console.WriteLine("[probe] Save...");
|
||||
initial.Save();
|
||||
ThrowIfFailed(initial.CommandResult, "Save");
|
||||
|
||||
Console.WriteLine("[probe] CheckIn...");
|
||||
initial.CheckIn("graccesscli probe round-trip test");
|
||||
ThrowIfFailed(initial.CommandResult, "CheckIn");
|
||||
|
||||
// Now read back THREE ways:
|
||||
// A. Same proxy (initial), ConfigurableAttributes — possibly stale.
|
||||
// B. Same proxy (initial), Attributes — possibly stale.
|
||||
// C. Fresh proxy from a fresh QueryObjectsByName call.
|
||||
var afterCfgSame = TryReadString(initial.ConfigurableAttributes, attrPath, "after-cfg-same");
|
||||
var afterRtSame = TryReadString(initial.Attributes, attrPath, "after-rt-same");
|
||||
Console.WriteLine($"[probe] AFTER (same proxy) cfg={Quote(afterCfgSame)} rt={Quote(afterRtSame)}");
|
||||
|
||||
var refreshed = QueryOne(galaxy, names);
|
||||
var afterCfgFresh = TryReadString(refreshed.ConfigurableAttributes, attrPath, "after-cfg-fresh");
|
||||
var afterRtFresh = TryReadString(refreshed.Attributes, attrPath, "after-rt-fresh");
|
||||
Console.WriteLine($"[probe] AFTER (fresh proxy) cfg={Quote(afterCfgFresh)} rt={Quote(afterRtFresh)}");
|
||||
|
||||
// Restore: write the original value back (or empty if there was none).
|
||||
var restoreTo = beforeCfg ?? "";
|
||||
Console.WriteLine($"[probe] Restoring DeclarationsText to {Quote(restoreTo)}...");
|
||||
refreshed.CheckOut();
|
||||
ThrowIfFailed(refreshed.CommandResult, "CheckOut(restore)");
|
||||
var restoreAttr = refreshed.ConfigurableAttributes[attrPath];
|
||||
var rv = new MxValueClass();
|
||||
rv.PutString(restoreTo);
|
||||
restoreAttr.SetValue(rv);
|
||||
refreshed.Save();
|
||||
ThrowIfFailed(refreshed.CommandResult, "Save(restore)");
|
||||
refreshed.CheckIn("graccesscli probe restore");
|
||||
ThrowIfFailed(refreshed.CommandResult, "CheckIn(restore)");
|
||||
Console.WriteLine("[probe] Restored.");
|
||||
|
||||
// Verdict.
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("=== verdict ===");
|
||||
Console.WriteLine($" marker landed on same-proxy ConfigurableAttributes: {(afterCfgSame == Marker)}");
|
||||
Console.WriteLine($" marker landed on same-proxy Attributes : {(afterRtSame == Marker)}");
|
||||
Console.WriteLine($" marker landed on fresh-proxy ConfigurableAttributes: {(afterCfgFresh == Marker)}");
|
||||
Console.WriteLine($" marker landed on fresh-proxy Attributes : {(afterRtFresh == Marker)}");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static IgObject QueryOne(IGalaxy galaxy, string[] names)
|
||||
{
|
||||
var objs = galaxy.QueryObjectsByName(EgObjectIsTemplateOrInstance.gObjectIsTemplate, ref names);
|
||||
ThrowIfFailed(galaxy.CommandResult, "QueryObjectsByName");
|
||||
return objs[1];
|
||||
}
|
||||
|
||||
static string TryReadString(IAttributes attrs, string name, string label)
|
||||
{
|
||||
try
|
||||
{
|
||||
var attr = attrs[name];
|
||||
if (attr == null) return null;
|
||||
var val = attr.value;
|
||||
return val?.GetString();
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"[probe] {label} read threw: {ex.Message}"); return null; }
|
||||
}
|
||||
|
||||
static void ThrowIfFailed(ICommandResult r, string what)
|
||||
{
|
||||
if (r != null && !r.Successful)
|
||||
throw new InvalidOperationException($"{what} failed: ID={r.ID} Text='{r.Text}' Custom='{r.CustomMessage}'");
|
||||
}
|
||||
|
||||
static string Quote(string s) => s == null ? "<null>" : "\"" + s.Replace("\n", "\\n").Replace("\r", "\\r") + "\"";
|
||||
}
|
||||
+595
@@ -0,0 +1,595 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.SymbolStore;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Security;
|
||||
using ArchestrA.QuickScript;
|
||||
using ArchestrA.QuickScript.Digest;
|
||||
using ArchestrA.QuickScript.Emit;
|
||||
using ArchestrA.QuickScript.Model;
|
||||
using ArchestrA.QuickScript.Runtime;
|
||||
|
||||
[assembly: CompilationRelaxations(8)]
|
||||
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
|
||||
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
|
||||
[assembly: AssemblyFileVersion("5800.0038.7005.1")]
|
||||
[assembly: AssemblyTitle("ScriptPackage.Net Module")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("AVEVA Software, LLC")]
|
||||
[assembly: AssemblyProduct("")]
|
||||
[assembly: AssemblyCopyright("Copyright 2020 AVEVA Group plc and its subsidiaries. All rights reserved.")]
|
||||
[assembly: AssemblyTrademark("Refer to: https://sw.aveva.com/legal/trademarks")]
|
||||
[assembly: AssemblyDelaySign(false)]
|
||||
[assembly: AssemblyKeyFile("..\\..\\..\\SharedComponents\\Internal\\MagellanPublic\\Includes\\WWDotNETPrivateKey\\ww.snk")]
|
||||
[assembly: AssemblyKeyName("")]
|
||||
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
|
||||
[assembly: AssemblyVersion("2.0.0.0")]
|
||||
namespace ArchestrA.Scripting
|
||||
{
|
||||
[Guid("85963980-46DF-459d-8A96-2A2AF70F458F")]
|
||||
[ClassInterface(ClassInterfaceType.AutoDual)]
|
||||
public class ScriptPackage
|
||||
{
|
||||
private class NameHierarchy
|
||||
{
|
||||
public Hashtable Hashtable = new Hashtable();
|
||||
|
||||
public readonly string Name;
|
||||
|
||||
public int Index;
|
||||
|
||||
public NameHierarchy(string name)
|
||||
{
|
||||
Name = name;
|
||||
Index = -1;
|
||||
}
|
||||
|
||||
public NameHierarchy()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public void Add(string name, int index)
|
||||
{
|
||||
string text = null;
|
||||
string text2 = null;
|
||||
int num = name.IndexOf('.');
|
||||
if (num < 0)
|
||||
{
|
||||
text = name;
|
||||
}
|
||||
else
|
||||
{
|
||||
text = name.Substring(0, num);
|
||||
text2 = name.Substring(num + 1, name.Length - num - 1);
|
||||
}
|
||||
string key = text.ToLower();
|
||||
if (!Hashtable.ContainsKey(key))
|
||||
{
|
||||
Hashtable[key] = new NameHierarchy(text);
|
||||
}
|
||||
NameHierarchy nameHierarchy = (NameHierarchy)Hashtable[key];
|
||||
if (text2 != null)
|
||||
{
|
||||
nameHierarchy.Add(text2, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
nameHierarchy.Index = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TypeResolveHandler
|
||||
{
|
||||
private Module Module;
|
||||
|
||||
private Hashtable Hashtable;
|
||||
|
||||
public TypeResolveHandler(Module module, Hashtable hashtable)
|
||||
{
|
||||
Module = module;
|
||||
Hashtable = hashtable;
|
||||
}
|
||||
|
||||
public Assembly ResolveEvent(object sender, ResolveEventArgs args)
|
||||
{
|
||||
((TypeBuilder)Hashtable[args.Name]).CreateType();
|
||||
return Module.Assembly;
|
||||
}
|
||||
}
|
||||
|
||||
private string BinFolder;
|
||||
|
||||
private bool DebugEnabled;
|
||||
|
||||
private Parser Parser;
|
||||
|
||||
private string TagName;
|
||||
|
||||
private string ScriptName;
|
||||
|
||||
private Hashtable ReferencedAssemblies;
|
||||
|
||||
private ModuleBuilder ModuleBuilder;
|
||||
|
||||
private TypeBuilder TypeBuilder;
|
||||
|
||||
private ILBuilder ILBuilder;
|
||||
|
||||
private int NestedTypeIndex;
|
||||
|
||||
private string ErrorMessage;
|
||||
|
||||
private int ErrorLine;
|
||||
|
||||
private int ErrorColumn;
|
||||
|
||||
private static readonly FieldInfo ScriptExchange;
|
||||
|
||||
private static readonly Type[] ExpessionParameterTypes;
|
||||
|
||||
private static readonly MethodInfo EnableDebugSupport;
|
||||
|
||||
private static string DebugFolder => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "QuickScriptDebug");
|
||||
|
||||
private static bool DebuggerInfoEnabled => Directory.Exists(DebugFolder);
|
||||
|
||||
public void Compile(string libraryPath, string repositoryPath, string tagName, string scriptName, string compilerAssemblyFile, string compilerTypeName, IntPtr outputAssemblyName, IntPtr aliases, IntPtr declarationsText, IntPtr startupText, IntPtr onScanText, IntPtr expression, IntPtr executeText, IntPtr offScanText, IntPtr shutdownText, IntPtr binary, IntPtr errorMessage, IntPtr errorLine, IntPtr errorColumn, IntPtr libaryDependencies, IntPtr externalReferences, IntPtr externalReferenceFlags, IntPtr aliasReferenceFlags)
|
||||
{
|
||||
IList list = null;
|
||||
byte[] array = null;
|
||||
ErrorMessage = "";
|
||||
BinFolder = Path.GetDirectoryName(Assembly.GetAssembly(GetType()).Location);
|
||||
Parser = new Parser();
|
||||
ILBuilder = new ILBuilder();
|
||||
ReferencedAssemblies = new Hashtable();
|
||||
list = new ArrayList();
|
||||
TagName = tagName;
|
||||
ScriptName = scriptName;
|
||||
DebugEnabled = DebuggerInfoEnabled;
|
||||
NestedTypeIndex = 0;
|
||||
int mxValueElementCount = GetMxValueElementCount(aliases);
|
||||
int i;
|
||||
for (i = 0; i < mxValueElementCount; i++)
|
||||
{
|
||||
ExternalReferences.AddAlias(GetMxValueElementString(aliases, i + 1));
|
||||
}
|
||||
try
|
||||
{
|
||||
string tempPath = Path.GetTempPath();
|
||||
AssemblyName assemblyName = new AssemblyName();
|
||||
assemblyName.Name = GetMxValueString(outputAssemblyName);
|
||||
string text = assemblyName.Name + ".dll";
|
||||
Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
|
||||
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save, tempPath);
|
||||
if (DebugEnabled)
|
||||
{
|
||||
CustomAttributeBuilder customAttribute = new CustomAttributeBuilder(typeof(DebuggableAttribute).GetConstructor(new Type[2]
|
||||
{
|
||||
typeof(bool),
|
||||
typeof(bool)
|
||||
}), new object[2] { true, true });
|
||||
assemblyBuilder.SetCustomAttribute(customAttribute);
|
||||
}
|
||||
ModuleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name, text, DebugEnabled);
|
||||
TypeBuilder = ModuleBuilder.DefineType("ArchestrA.Scripting.Script", TypeAttributes.Public, typeof(ScriptBase), new Type[0]);
|
||||
ILGenerator iLGenerator = TypeBuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, CallingConventions.Standard, new Type[0]).GetILGenerator();
|
||||
ILBuilder.ILGenerator = iLGenerator;
|
||||
iLGenerator.Emit(OpCodes.Ldarg_0);
|
||||
iLGenerator.Emit(OpCodes.Call, typeof(ScriptBase).GetConstructor(new Type[0]));
|
||||
ILBuilder.DebugEnabled = false;
|
||||
ILBuilder.FieldScriptExchange = ScriptExchange;
|
||||
CompileDeclarations(GetMxValueString(declarationsText));
|
||||
ILBuilder.Emit(new Return());
|
||||
if (ErrorMessage.Length == 0 || ErrorMessage.StartsWith("Warning"))
|
||||
{
|
||||
CompileExpression(GetMxValueString(expression));
|
||||
CompileMethod("Startup", GetMxValueString(startupText));
|
||||
CompileMethod("OnScan", GetMxValueString(onScanText));
|
||||
CompileMethod("Execute", GetMxValueString(executeText));
|
||||
CompileMethod("OffScan", GetMxValueString(offScanText));
|
||||
CompileMethod("Shutdown", GetMxValueString(shutdownText));
|
||||
}
|
||||
if (ErrorMessage.Length == 0 || ErrorMessage.StartsWith("Warning"))
|
||||
{
|
||||
if (DebugEnabled)
|
||||
{
|
||||
Hashtable hashtable = new Hashtable();
|
||||
ResolveEventHandler value = new TypeResolveHandler(ModuleBuilder, hashtable).ResolveEvent;
|
||||
AppDomain.CurrentDomain.TypeResolve += value;
|
||||
NameHierarchy nameHierarchy = new NameHierarchy();
|
||||
int num = 0;
|
||||
foreach (ExternalReference item in ExternalReferences.List)
|
||||
{
|
||||
nameHierarchy.Add(item.ReferenceString.Split('[')[0], num++);
|
||||
}
|
||||
Build(nameHierarchy, TypeBuilder, hashtable);
|
||||
RecurseNestedTypes(TypeBuilder.CreateType());
|
||||
AppDomain.CurrentDomain.TypeResolve -= value;
|
||||
}
|
||||
else
|
||||
{
|
||||
TypeBuilder.CreateType();
|
||||
}
|
||||
assemblyBuilder.Save(text);
|
||||
}
|
||||
foreach (Assembly key in ReferencedAssemblies.Keys)
|
||||
{
|
||||
list.Add(key.GetName().Name);
|
||||
}
|
||||
if (ErrorMessage.Length == 0)
|
||||
{
|
||||
string path = Path.Combine(tempPath, text);
|
||||
using (FileStream fileStream = File.OpenRead(path))
|
||||
{
|
||||
array = new byte[fileStream.Length];
|
||||
fileStream.Read(array, 0, (int)fileStream.Length);
|
||||
}
|
||||
if (DebugEnabled)
|
||||
{
|
||||
File.Exists(Path.ChangeExtension(path, "pdb"));
|
||||
}
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Internal QuickScript compiler error: " + ex.Message;
|
||||
}
|
||||
if (ErrorMessage.Length > 0)
|
||||
{
|
||||
PutMxValueString(errorMessage, ErrorMessage);
|
||||
PutMxValueInteger(errorLine, ErrorLine);
|
||||
PutMxValueInteger(errorColumn, ErrorColumn);
|
||||
return;
|
||||
}
|
||||
PutMxValueString(errorMessage, "");
|
||||
PutMxValueInteger(errorLine, 0);
|
||||
PutMxValueInteger(errorColumn, 0);
|
||||
PutMxValueBlob(binary, array, array.Length);
|
||||
i = 1;
|
||||
foreach (string item2 in list)
|
||||
{
|
||||
PutMxValueElementString(libaryDependencies, i++, item2);
|
||||
}
|
||||
i = 1;
|
||||
int index = 1;
|
||||
foreach (ExternalReference item3 in ExternalReferences.List)
|
||||
{
|
||||
if (item3.IsAlias)
|
||||
{
|
||||
PutMxValueElementInteger(aliasReferenceFlags, i++, item3.Flags);
|
||||
continue;
|
||||
}
|
||||
PutMxValueElementString(externalReferences, index, item3.ReferenceString);
|
||||
PutMxValueElementInteger(externalReferenceFlags, index++, item3.Flags);
|
||||
}
|
||||
}
|
||||
|
||||
private void RecurseNestedTypes(Type type)
|
||||
{
|
||||
Type[] nestedTypes = type.GetNestedTypes();
|
||||
foreach (Type type2 in nestedTypes)
|
||||
{
|
||||
RecurseNestedTypes(type2);
|
||||
}
|
||||
}
|
||||
|
||||
private void GetErrorsAndWarnings(string section)
|
||||
{
|
||||
if (ErrorMessage.Length > 0 && !ErrorMessage.StartsWith("Warning"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
IEnumerator enumerator = Parser.Errors.GetEnumerator();
|
||||
try
|
||||
{
|
||||
if (enumerator.MoveNext())
|
||||
{
|
||||
ErrorInfo errorInfo = (ErrorInfo)enumerator.Current;
|
||||
ErrorLine = errorInfo.Line;
|
||||
ErrorColumn = errorInfo.Column;
|
||||
ErrorMessage = $"Script {ScriptName} ({section}): {errorInfo.Message} (Line: {errorInfo.Line}, Col: {errorInfo.Column})";
|
||||
return;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
IDisposable disposable = enumerator as IDisposable;
|
||||
if (disposable != null)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
enumerator = Parser.Warnings.GetEnumerator();
|
||||
try
|
||||
{
|
||||
if (enumerator.MoveNext())
|
||||
{
|
||||
ErrorInfo errorInfo2 = (ErrorInfo)enumerator.Current;
|
||||
ErrorLine = errorInfo2.Line;
|
||||
ErrorColumn = errorInfo2.Column;
|
||||
ErrorMessage = $"Warning: Script {ScriptName} ({section}): {errorInfo2.Message} (Line: {errorInfo2.Line}, Col: {errorInfo2.Column})";
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
IDisposable disposable2 = enumerator as IDisposable;
|
||||
if (disposable2 != null)
|
||||
{
|
||||
disposable2.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CompileDeclarations(string declarationsText)
|
||||
{
|
||||
if (declarationsText.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ILBuilder.DeclarationsAreFields = true;
|
||||
Parser.DeclarationsAreFields = true;
|
||||
Parser.Parse(declarationsText);
|
||||
if (Parser.Tree is Expression)
|
||||
{
|
||||
CodeSpan codeSpan = (Parser.Tree as Expression).CodeSpan;
|
||||
codeSpan = new CodeSpan(codeSpan.End.Line, codeSpan.End.Column + 1, codeSpan.End.Line, codeSpan.End.Column + 1);
|
||||
Parser.Errors.Add(new ErrorInfo("Expected ';'", codeSpan.End.Line, codeSpan.End.Column, codeSpan.End.Line, codeSpan.End.Column));
|
||||
}
|
||||
GetErrorsAndWarnings("Declarations");
|
||||
if (Parser.Errors.Count != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (Statement item in Parser.Tree as IList)
|
||||
{
|
||||
Declaration declaration = item as Declaration;
|
||||
FieldBuilder value = TypeBuilder.DefineField(declaration.VariableName, declaration.Type, FieldAttributes.Public);
|
||||
ILBuilder.Variables[declaration.VariableName] = value;
|
||||
ILBuilder.Emit(declaration);
|
||||
}
|
||||
Parser.GetReferencedAssemblies(ReferencedAssemblies);
|
||||
}
|
||||
|
||||
private void CompileExpression(string expressionText)
|
||||
{
|
||||
if (expressionText.Length != 0)
|
||||
{
|
||||
ILBuilder.DeclarationsAreFields = false;
|
||||
Parser.DeclarationsAreFields = false;
|
||||
Parser.Parse(expressionText);
|
||||
if (Parser.Tree is Statements)
|
||||
{
|
||||
CodeSpan codeSpan = ((Parser.Tree as Statements)[0] as Statement).CodeSpan;
|
||||
Parser.Errors.Add(new ErrorInfo("Not expecting ';'", codeSpan.End.Line, codeSpan.End.Column, codeSpan.End.Line, codeSpan.End.Column));
|
||||
}
|
||||
GetErrorsAndWarnings("Expression");
|
||||
if (Parser.Errors.Count <= 0)
|
||||
{
|
||||
MethodAttributes attributes = MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig;
|
||||
MethodBuilder methodBuilder = TypeBuilder.DefineMethod("Expression", attributes, typeof(void), ExpessionParameterTypes);
|
||||
methodBuilder.DefineParameter(1, ParameterAttributes.Out, null);
|
||||
methodBuilder.DefineParameter(2, ParameterAttributes.Out, null);
|
||||
ILGenerator iLGenerator = methodBuilder.GetILGenerator();
|
||||
ILBuilder.ILGenerator = iLGenerator;
|
||||
ILBuilder.Variables.ClearLocals();
|
||||
ILBuilder.DebugEnabled = false;
|
||||
Expression expression = Parser.Tree as Expression;
|
||||
iLGenerator.Emit(OpCodes.Ldarg_1);
|
||||
ILBuilder.Emit(Coerce.ChangeType(expression, typeof(object)));
|
||||
iLGenerator.Emit(OpCodes.Stind_Ref);
|
||||
iLGenerator.Emit(OpCodes.Ldarg_2);
|
||||
ILBuilder.Emit(Digester.GetQuality(expression));
|
||||
iLGenerator.Emit(OpCodes.Conv_I4);
|
||||
iLGenerator.Emit(OpCodes.Stind_I4);
|
||||
ILBuilder.Emit(new Return());
|
||||
Parser.GetReferencedAssemblies(ReferencedAssemblies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CompileMethod(string methodName, string methodText)
|
||||
{
|
||||
if (methodText.Length == 0)
|
||||
{
|
||||
if (!(methodName == "Startup"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
methodText = ";";
|
||||
}
|
||||
ILBuilder.DeclarationsAreFields = false;
|
||||
Parser.DeclarationsAreFields = false;
|
||||
Parser.Parse(methodText);
|
||||
if (Parser.Tree is Expression)
|
||||
{
|
||||
CodeSpan codeSpan = (Parser.Tree as Expression).CodeSpan;
|
||||
codeSpan = new CodeSpan(codeSpan.End.Line, codeSpan.End.Column + 1, codeSpan.End.Line, codeSpan.End.Column + 1);
|
||||
Parser.Errors.Add(new ErrorInfo("Expected ';'", codeSpan.End.Line, codeSpan.End.Column, codeSpan.End.Line, codeSpan.End.Column));
|
||||
}
|
||||
GetErrorsAndWarnings(methodName);
|
||||
if (Parser.Errors.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ISymbolDocumentWriter symbolDocumentWriter = null;
|
||||
if (DebugEnabled)
|
||||
{
|
||||
string text = DebugFolder + "\\" + TagName + "." + ScriptName + "." + methodName + ".qs";
|
||||
if (methodText.Length > 1)
|
||||
{
|
||||
using StreamWriter streamWriter = File.CreateText(text);
|
||||
streamWriter.Write(methodText);
|
||||
}
|
||||
symbolDocumentWriter = ModuleBuilder.DefineDocument(text, Guid.Empty, Guid.Empty, Guid.Empty);
|
||||
}
|
||||
Type[] parameterTypes = new Type[0];
|
||||
switch (methodName)
|
||||
{
|
||||
case "Startup":
|
||||
parameterTypes = new Type[1] { typeof(IScriptExchange) };
|
||||
break;
|
||||
case "Execute":
|
||||
parameterTypes = new Type[1] { typeof(ScriptTimer) };
|
||||
break;
|
||||
}
|
||||
MethodAttributes attributes = MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig;
|
||||
ILGenerator iLGenerator = TypeBuilder.DefineMethod(methodName, attributes, typeof(void), parameterTypes).GetILGenerator();
|
||||
ILBuilder.ILGenerator = iLGenerator;
|
||||
ILBuilder.Variables.ClearLocals();
|
||||
ILBuilder.SymbolDocumentWriter = symbolDocumentWriter;
|
||||
ILBuilder.DebugEnabled = false;
|
||||
ILBuilder.CheckForTimeout = methodName == "Execute" && !DebugEnabled;
|
||||
if (methodName == "Startup")
|
||||
{
|
||||
iLGenerator.Emit(OpCodes.Ldarg_0);
|
||||
iLGenerator.Emit(OpCodes.Ldarg_1);
|
||||
iLGenerator.Emit(OpCodes.Stfld, ScriptExchange);
|
||||
if (DebugEnabled)
|
||||
{
|
||||
Expression destination = new Field(new This(), typeof(ScriptBase).GetField("TagName"));
|
||||
Expression source = new ConstantString(TagName);
|
||||
ILBuilder.Emit(new Assignment(destination, source));
|
||||
ILBuilder.Emit(new ExpressionStatement(new Method(null, EnableDebugSupport, new Expression[1]
|
||||
{
|
||||
new This()
|
||||
})));
|
||||
}
|
||||
}
|
||||
Statements statements = Parser.Tree as Statements;
|
||||
ILBuilder.DebugEnabled = DebugEnabled;
|
||||
ILBuilder.LineNumber = 0;
|
||||
ILBuilder.Emit(statements);
|
||||
ILBuilder.Emit(new Return());
|
||||
Parser.GetReferencedAssemblies(ReferencedAssemblies);
|
||||
}
|
||||
|
||||
static ScriptPackage()
|
||||
{
|
||||
ScriptExchange = typeof(ScriptBase).GetField("ScriptExchange");
|
||||
ExpessionParameterTypes = null;
|
||||
EnableDebugSupport = typeof(ArchestrA.QuickScript.Runtime.ExternalReference).GetMethod("EnableDebugSupport");
|
||||
ParameterInfo[] parameters = typeof(IScriptObject).GetMethod("Expression").GetParameters();
|
||||
ExpessionParameterTypes = new Type[parameters.Length];
|
||||
int num = 0;
|
||||
ParameterInfo[] array = parameters;
|
||||
foreach (ParameterInfo parameterInfo in array)
|
||||
{
|
||||
ExpessionParameterTypes[num++] = parameterInfo.ParameterType;
|
||||
}
|
||||
}
|
||||
|
||||
private void Build(NameHierarchy nameHierarchy, TypeBuilder typeBuilder, Hashtable hashtable)
|
||||
{
|
||||
Type typeFromHandle = typeof(ArchestrA.QuickScript.Runtime.ExternalReference);
|
||||
typeFromHandle.GetField("Index");
|
||||
if (nameHierarchy.Index == -1)
|
||||
{
|
||||
TypeBuilder typeBuilder2 = null;
|
||||
if (nameHierarchy.Name != null)
|
||||
{
|
||||
typeBuilder2 = typeBuilder.DefineNestedType("Tag" + ++NestedTypeIndex, TypeAttributes.NestedPublic, typeFromHandle);
|
||||
}
|
||||
foreach (NameHierarchy value in nameHierarchy.Hashtable.Values)
|
||||
{
|
||||
Build(value, (typeBuilder2 != null) ? typeBuilder2 : typeBuilder, hashtable);
|
||||
}
|
||||
if (nameHierarchy.Name != null)
|
||||
{
|
||||
hashtable[typeBuilder2.Name] = typeBuilder2;
|
||||
}
|
||||
if (typeBuilder2 != null)
|
||||
{
|
||||
typeBuilder.DefineField(nameHierarchy.Name, typeBuilder2, FieldAttributes.Public);
|
||||
}
|
||||
}
|
||||
else if (nameHierarchy.Name != null)
|
||||
{
|
||||
string name = "T" + nameHierarchy.Index;
|
||||
TypeBuilder typeBuilder3 = typeBuilder.DefineNestedType(name, TypeAttributes.NestedPublic, typeof(ExternalReferenceDebug));
|
||||
typeBuilder.DefineField(nameHierarchy.Name, typeBuilder3, FieldAttributes.Public);
|
||||
hashtable[typeBuilder3.Name] = typeBuilder3;
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("ScriptPackage.dll", CharSet = CharSet.Unicode)]
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[return: MarshalAs(UnmanagedType.BStr)]
|
||||
private static extern string GetMxValueString(IntPtr rawMxValue);
|
||||
|
||||
[DllImport("ScriptPackage.dll")]
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
private static extern void PutMxValueInteger(IntPtr rawMxValue, int value);
|
||||
|
||||
[DllImport("ScriptPackage.dll", CharSet = CharSet.Unicode)]
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
private static extern void PutMxValueString(IntPtr rawMxValue, string value);
|
||||
|
||||
[DllImport("ScriptPackage.dll")]
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
private static extern void PutMxValueBlob(IntPtr rawMxValue, [MarshalAs(UnmanagedType.LPArray)] byte[] value, int size);
|
||||
|
||||
[DllImport("ScriptPackage.dll")]
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
private static extern int GetMxValueElementCount(IntPtr rawMxValue);
|
||||
|
||||
[DllImport("ScriptPackage.dll", CharSet = CharSet.Unicode)]
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[return: MarshalAs(UnmanagedType.BStr)]
|
||||
private static extern string GetMxValueElementString(IntPtr rawMxValue, int index);
|
||||
|
||||
[DllImport("ScriptPackage.dll", CharSet = CharSet.Unicode)]
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
private static extern void PutMxValueElementString(IntPtr rawMxValue, int index, string value);
|
||||
|
||||
[DllImport("ScriptPackage.dll")]
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
private static extern void PutMxValueElementInteger(IntPtr rawMxValue, int index, int value);
|
||||
}
|
||||
}
|
||||
namespace AcmeBuildVersion
|
||||
{
|
||||
internal sealed class Versioning
|
||||
{
|
||||
public const string Company = "AVEVA Software, LLC";
|
||||
|
||||
public const string Product = "";
|
||||
|
||||
public const string Copyright = "Copyright 2020 AVEVA Group plc and its subsidiaries. All rights reserved.";
|
||||
|
||||
public const string Trademark = "Refer to: https://sw.aveva.com/legal/trademarks";
|
||||
|
||||
public const string Configuration = "Release";
|
||||
|
||||
public const string BuildNumber = "5800";
|
||||
|
||||
public const string BuildMaintenanceNumber = "7005";
|
||||
|
||||
public const string BldRevision = "1";
|
||||
|
||||
private Versioning()
|
||||
{
|
||||
}
|
||||
}
|
||||
internal sealed class ComponentVersioning
|
||||
{
|
||||
public const string ComponentVersion = "0038";
|
||||
|
||||
public const string ComponentMaintenanceVersion = "0000";
|
||||
|
||||
public const string ComponentName = "ScriptPrimitive";
|
||||
|
||||
private ComponentVersioning()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
# Package Manager Deep Dive
|
||||
|
||||
Date: 2026-05-05
|
||||
|
||||
## Scope
|
||||
|
||||
This investigation focused on the local System Platform package manager path that sits below GRAccess and appears to be the missing write path for object-level script body fields and some settings.
|
||||
|
||||
Tools used:
|
||||
|
||||
- `TlbImp.exe` against registered COM/type libraries.
|
||||
- .NET reflection over generated interop assemblies.
|
||||
- Registry inspection of `HKCR\TypeLib`.
|
||||
- Binary string extraction from package/runtime DLLs.
|
||||
- `dumpbin.exe` on native package/runtime DLLs.
|
||||
- Existing decompiled IDE wrapper notes under `analysis/ide-edit-investigation`.
|
||||
|
||||
Ghidra was available at `C:\Users\dohertj2\Desktop\focas\tools\ghidra_12.0.4_PUBLIC\support\analyzeHeadless.bat`, but the registered type libraries and embedded primitive XML exposed the actionable surface without needing a full native decompile.
|
||||
|
||||
## Registered components
|
||||
|
||||
### Galaxy package server
|
||||
|
||||
- Type library: `xxGalaxyPackageServer 1.0 Type Library`
|
||||
- TypeLib GUID: `{74B9B2CA-05E6-4BC4-81C3-1CAFF652035D}`
|
||||
- Binary: `C:\Program Files (x86)\ArchestrA\Framework\Bin\xxGalaxyPackageServer.dll`
|
||||
- ProgID: `XxGalaxyPackageServer.GalaxyPackage`
|
||||
- CLSID: `{F6C78E0D-8EA2-48B8-9592-6D9FB507DAD3}`
|
||||
- Threading model: `Apartment`
|
||||
|
||||
### Configuration access component
|
||||
|
||||
- Type library: `ConfigurationAccessComponent 1.0 Type Library`
|
||||
- TypeLib GUID: `{2539619B-EA9B-4035-9AE6-71421DA6C8FD}`
|
||||
- Binary: `C:\Program Files (x86)\ArchestrA\Framework\Bin\ConfigurationAccessComponent.dll`
|
||||
- Main COM server: `ConfigurationAccessComponent.ConfigurationAccessServer`
|
||||
- CLSID from interop: `{55414847-A533-4642-8E92-76B191B24B87}`
|
||||
|
||||
### Script package
|
||||
|
||||
- Type library: `ScriptPackage`
|
||||
- TypeLib GUID: `{8DF25C4D-C021-4080-849D-74A81D97D548}`
|
||||
- Binary: `C:\Program Files (x86)\ArchestrA\Framework\Bin\ScriptPackage.dll`
|
||||
- Package CLSID: `{A261929D-90AB-4217-9234-667A83756FFB}`
|
||||
- Managed compiler helper: `C:\Program Files (x86)\ArchestrA\Framework\Bin\ScriptPackage.Net.dll`
|
||||
|
||||
### Script runtime primitive definition
|
||||
|
||||
The `ScriptExtension` primitive definition is embedded in:
|
||||
|
||||
- `C:\Program Files (x86)\ArchestrA\Framework\Bin\ScriptRuntime.dll`
|
||||
|
||||
Important embedded metadata:
|
||||
|
||||
- `DefaultInternalName`: `ScriptExtension`
|
||||
- `Package_CLSID`: `{A261929D-90AB-4217-9234-667A83756FFB}`
|
||||
- `Runtime_CLSID`: `{2B2616D6-98A0-4e94-ABAD-C75BB022E2D6}`
|
||||
- Default `ExecutionGroup`: `Custom 1`
|
||||
|
||||
## Package manager surface
|
||||
|
||||
The important public COM interfaces are already in the local package server type library.
|
||||
|
||||
### `IGalaxyConfiguration` / `IGalaxyConfigurationV30`
|
||||
|
||||
This is the high-level object lifecycle and package access surface. Important methods:
|
||||
|
||||
- `GetIDFromObjectName(Namespace nSpace, string objectName) -> int`
|
||||
- `GetGObjectInfoFromName(string name, Namespace nSpace)`
|
||||
- `CheckOutObjects(int[] gObjectIds, int hint, IGObjectOperationStatus callback)`
|
||||
- `CheckinObjects(int[] gObjectIds, string comment, int hint, IGObjectOperationStatus callback)`
|
||||
- `UndoCheckoutObjects(int[] gObjectIds, int hint, bool override, IGObjectOperationStatus callback)`
|
||||
- `GetObject(int objectId, ECODEMODULES codeModules, out object package, out ERRORCODE status, out string message, EPACKAGEPROP prop)`
|
||||
- `GetObjectInternal(int objectId, ECODEMODULES code, bool editCheckRequired, out object package, out ERRORCODE status, out string message, EPACKAGEPROP prop)`
|
||||
- `GetGalaxyConfigurationNet() -> object`
|
||||
- `GetIGalaxy() -> object`
|
||||
|
||||
### `IPackageManager`
|
||||
|
||||
This is broader and includes lower-level attribute APIs. Important methods:
|
||||
|
||||
- `GetObject(...)`
|
||||
- `GetObjectInternal(...)`
|
||||
- `GetFullObjectByName(string name) -> object`
|
||||
- `GetIDFromName(string name) -> int`
|
||||
- `ReleaseFsObjectEditsession(int fsObjectId)`
|
||||
- `GetObjectAttribute(Namespace, objectName, attributeName, EATTRIBUTEPROPERTY, MxValue, out result)`
|
||||
- `InternalSetObjectAttributesEx(Namespace, objectName, EPACKAGETYPE, string[] attributeNames, EATTRIBUTEPROPERTY[] properties, MxValue[] values, bool saveObjectEvenErrorOccurs, out status, out statusMsg)`
|
||||
- `InternalGetObjectAttributesEx(...)`
|
||||
|
||||
The `InternalSetObjectAttributesEx` method is a promising direct write path because it accepts object name, package type, attribute names, property ids, and MxValues without needing the IDE configuration access component.
|
||||
|
||||
### `IConfigurationEditorSite2`
|
||||
|
||||
This is the direct package editor site returned by package access calls. Important methods:
|
||||
|
||||
- `GetAttributeCookie(string attributeFullName) -> int`
|
||||
- `GetAttribute(int cookie, short propertyId, MxValue value)`
|
||||
- `SetAttribute(int cookie, short propertyId, MxValue value) -> bool`
|
||||
- `Validate()`
|
||||
- `Commit(out string errorString) -> EPACKAGEOPERATIONSTATUS`
|
||||
- `IsReadOnly()`
|
||||
- `SetSubscriberInfo(object subscription)`
|
||||
- `AddExtensionPrimitive(...)`
|
||||
- `DeleteExtensionPrimitive(...)`
|
||||
- `RenameExtensionPrimitive(...)`
|
||||
- `AddUDA(...)`
|
||||
- `DeleteUDA(...)`
|
||||
- `RenameUDA(...)`
|
||||
- `UpdateUDA(...)`
|
||||
|
||||
For normal value writes, property id `10` is `EATTRIBUTEPROPERTY.idxAttribPropValue`.
|
||||
|
||||
### `IPrimitivePackageSite7`
|
||||
|
||||
The script package uses the generic primitive package site contract:
|
||||
|
||||
- `GetAttributeHandle(string attributeFullName) -> AttributeHandle`
|
||||
- `GetAllCategoryAttributeHandle(string attributeFullName) -> AttributeHandle`
|
||||
- `GetAttribute(AttributeHandle, MxValue)`
|
||||
- `SetAttribute(AttributeHandle, MxValue)`
|
||||
- `GetAttributeHandleEx(...)`
|
||||
- `GetPrimitiveIdByInternalNameEx(...)`
|
||||
- `GetPrimitiveAttributeIds(...)`
|
||||
- dynamic attribute operations.
|
||||
|
||||
The script package itself exposes:
|
||||
|
||||
- `CoScriptPackageClass.Initialize(short primitiveId, IPrimitivePackageSite site)`
|
||||
- `SetHandler(ref AttributeHandle, MxValue) -> string`
|
||||
- `Validate() -> EPACKAGESTATUS`
|
||||
|
||||
There is no script-specific "SetScriptText" API in the script package typelib. It is generic attribute/handler based.
|
||||
|
||||
## ScriptExtension attribute facts
|
||||
|
||||
`ScriptRuntime.dll` embeds the actual script primitive attributes. The important ones:
|
||||
|
||||
- `ExecuteText`: `MxBigString`, `PackageOnly_Lockable`, `CfgSethandler=True`
|
||||
- `DeclarationsText`: `MxBigString`, `PackageOnly_Lockable`, `CfgSethandler=True`
|
||||
- `StartupText`: `MxBigString`, `PackageOnly_Lockable`, `CfgSethandler=True`
|
||||
- `OnScanText`: `MxBigString`, `PackageOnly_Lockable`, `CfgSethandler=True`
|
||||
- `OffScanText`: `MxBigString`, `PackageOnly_Lockable`, `CfgSethandler=True`
|
||||
- `ShutdownText`: `MxBigString`, `PackageOnly_Lockable`, `CfgSethandler=True`
|
||||
- `Expression`: `MxBigString`, `PackageOnly_Lockable`, `CfgSethandler=True`
|
||||
- `TriggerType`: `MxCustomEnum`, `Writeable_C_Lockable`, `CfgSethandler=True`
|
||||
- `_TriggerTypeEnum`: values `WhileTrue`, `WhileFalse`, `OnTrue`, `OnFalse`, `DataChange`, `Periodic`
|
||||
- `TriggerPeriod`: `MxElapsedTime`, `Writeable_C_Lockable`, `CfgSethandler=True`
|
||||
- `DataChangeDeadband`: `MxDouble`, `Writeable_UC_Lockable`, `CfgSethandler=True`
|
||||
- `RunsAsync`: `MxBoolean`, `Writeable_C_Lockable`, `CfgSethandler=True`
|
||||
- `ExecuteTimeout.Limit`: `MxInteger`, `Writeable_C_Lockable`, `CfgSethandler=True`
|
||||
- `ScriptExecutionGroup`: `MxCustomEnum`, `PackageOnly_Lockable`, `CfgSethandler=False`
|
||||
- `ScriptOrder`: `MxInteger`, `PackageOnly_Lockable`, `CfgSethandler=False`
|
||||
|
||||
This confirms the current GRAccess `IAttribute.SetValue` path is not enough for script bodies. The body fields are package-only and cfg-sethandler driven.
|
||||
|
||||
## IDE wrapper behavior
|
||||
|
||||
The decompiled IDE wrapper creates `ConfigurationAccessComponent.ConfigurationAccessServer`, casts it to `IConfigurationEditor`, and initializes it with the object package site:
|
||||
|
||||
1. `configurationEditor.Initialize("", inObjectManage, this, readOnly)`
|
||||
2. `inObjectManage.SetSubscriberInfo(configurationEditor)`
|
||||
3. `inObjectManage.SetSubscriberInfo(this)`
|
||||
4. Subscribes to `DConfigurationAccessEvents`.
|
||||
5. Sets CAS data:
|
||||
- `ww:SupportedLocales`
|
||||
- `ww:IPackageManager`
|
||||
- `ww:Tagname`
|
||||
- `ww:ContainedName`
|
||||
- `ww:DerivedFrom`
|
||||
- `ww:KeepCheckedOut`
|
||||
|
||||
This is a full IDE editor-host path. It is probably more complete than direct attribute writes, but it requires implementing enough editor-site/event plumbing in the CLI.
|
||||
|
||||
## Feasible edit paths
|
||||
|
||||
### Path A: direct package editor site
|
||||
|
||||
Likely flow:
|
||||
|
||||
1. Use existing GRAccess login to obtain `IGalaxy`.
|
||||
2. Call `IGalaxy.GetGalaxyConfiguration()` and cast the returned object to `IGalaxyConfiguration` or `IGalaxyConfigurationV30`.
|
||||
3. Resolve object id with `GetIDFromObjectName(Namespace.AutomationObject, tagName)`.
|
||||
4. Check out the object via existing GRAccess or `CheckOutObjects`.
|
||||
5. Get the editable package with `GetObjectInternal(id, eEditorAndPackageCodeModules, true, out package, ...)` or `GetObject(...)`.
|
||||
6. Cast the package object to `IConfigurationEditorSite2`.
|
||||
7. Resolve `scriptName.ExecuteText` with `GetAttributeCookie`.
|
||||
8. Create `MxValueClass` and call `PutString(newScriptBody)`.
|
||||
9. Call `SetAttribute(cookie, idxAttribPropValue, mxValue)`.
|
||||
10. Call `Validate()`.
|
||||
11. Call `Commit(out error)`.
|
||||
12. Check in or undo checkout and release edit session on failure.
|
||||
|
||||
Pros:
|
||||
|
||||
- Uses typed COM APIs.
|
||||
- Avoids the full IDE configuration access server.
|
||||
- Directly targets the package-only fields.
|
||||
|
||||
Risks:
|
||||
|
||||
- Needs live validation that the object returned by `GetObjectInternal` supports `IConfigurationEditorSite2` in this context.
|
||||
- Needs confirmation that `SetAttribute` invokes the script package cfg set handler and recompiles `_Binary`.
|
||||
- Needs correct custom enum MxValue creation for `TriggerType`.
|
||||
|
||||
### Path B: package manager internal batch set
|
||||
|
||||
Likely flow:
|
||||
|
||||
1. Get `IPackageManager` from the same object returned by `IGalaxy.GetGalaxyConfiguration()` or via package-manager support.
|
||||
2. Check out object.
|
||||
3. Call `InternalSetObjectAttributesEx` with:
|
||||
- `Namespace.AutomationObject`
|
||||
- object tagname
|
||||
- likely `EPACKAGETYPE.eBeingEditedPackage`
|
||||
- attribute names such as `UpdateTestChangingInt.ExecuteText`
|
||||
- property ids `[idxAttribPropValue]`
|
||||
- MxValues for the new data
|
||||
- `saveObjectEvenErrorOccurs=false` initially
|
||||
4. Check status and status message.
|
||||
5. Check in or undo checkout.
|
||||
|
||||
Pros:
|
||||
|
||||
- Smallest API surface.
|
||||
- No editor host required.
|
||||
- Best candidate for a CLI-first package write helper.
|
||||
|
||||
Risks:
|
||||
|
||||
- It is explicitly named `Internal`.
|
||||
- It may require exact package type and checkout state.
|
||||
- Unknown whether it invokes all cfg-sethandler compile side effects for script bodies.
|
||||
|
||||
### Path C: configuration access component
|
||||
|
||||
Likely flow:
|
||||
|
||||
1. Get editable object package as `IConfigurationEditorSite2`.
|
||||
2. Instantiate `ConfigurationAccessComponent.ConfigurationAccessServer`.
|
||||
3. Implement minimal `IBaseEditorSite` and event sink in the CLI.
|
||||
4. Call `IConfigurationEditor.Initialize`.
|
||||
5. Set the IDE wrapper's `ww:*` data entries.
|
||||
6. Set `Data("ScriptName.ExecuteText", "Value") = body`.
|
||||
7. Apply/commit through the editor site.
|
||||
|
||||
Pros:
|
||||
|
||||
- Closest to the IDE behavior.
|
||||
- Most likely to run all validation/dirty/event plumbing.
|
||||
|
||||
Risks:
|
||||
|
||||
- Largest implementation.
|
||||
- Requires COM connection-point/event hosting.
|
||||
- More fragile in a headless CLI.
|
||||
|
||||
## Current CLI implication
|
||||
|
||||
The current `object scripts set` path writes through `IAttribute.SetValue`. That cannot persist the package-only body fields shown above.
|
||||
|
||||
The current `object scripts settings set --trigger-period-ms` should be more viable because `TriggerPeriod` is `Writeable_C_Lockable`, but it still goes through generic GRAccess `IAttribute.SetValue`.
|
||||
|
||||
The current `object scripts settings set --trigger-type` is suspicious because `TriggerType` is `MxCustomEnum`; the CLI currently creates an `MxValue` string for non-explicit data types. The package metadata says the valid values are backed by `_TriggerTypeEnum`, so a package-manager implementation should set `TriggerType` as a custom enum or verify that package `SetAttribute` accepts the string form.
|
||||
|
||||
## Recommended next implementation
|
||||
|
||||
Do not add more typelibs first. The useful typelibs already exist locally:
|
||||
|
||||
- `ArchestrA.GRAccess.dll`
|
||||
- `xxGalaxyPackageServer.dll`
|
||||
- `ConfigurationAccessComponent.dll`
|
||||
- `ScriptPackage.dll`
|
||||
|
||||
The next improvement should add a small, isolated package-manager interop layer and a probe/edit helper:
|
||||
|
||||
1. Add minimal `[ComImport]` declarations for only the needed package-manager interfaces, or add a checked-in interop reference generated from `xxGalaxyPackageServer.dll`.
|
||||
2. Bridge from `IGalaxy.GetGalaxyConfiguration()` to `IGalaxyConfigurationV30` / `IPackageManager`.
|
||||
3. Implement a read-only probe first:
|
||||
- object id resolution
|
||||
- `GetObjectInternal`
|
||||
- cast checks for `IConfigurationEditorSite2`, `IObjectManage`, `IPackageManager`
|
||||
- `GetAttributeCookie` for script fields/settings
|
||||
- `GetAttribute` for `idxAttribPropValue`
|
||||
4. Implement one guarded write path for `ExecuteText`.
|
||||
5. Verify whether `_Binary`, `_ErrorMessage`, `_ErrorLine`, and `_ErrorColumn` update after commit. If they do not, switch from Path A/B to the CAS path.
|
||||
6. Only then wire it into `object scripts set` and package-only script settings.
|
||||
|
||||
## Live validation blocker
|
||||
|
||||
Previous live GRAccess validation against the `ZB` galaxy failed because the local license was unavailable:
|
||||
|
||||
`cmdLicenseUnavailable (105); Feature 'wspdevstudio-iocount'`
|
||||
|
||||
The package-manager write path needs live validation after license access is restored because static metadata cannot prove whether script cfg-sethandler side effects fire on direct package writes.
|
||||
|
||||
Reference in New Issue
Block a user