graccesscli: scripts-get now uses direct read; bypass anon-type reflection
Two follow-on fixes after the MxValueDetails reader correction:
1. AttributeValueDetails was falling through to the package-binary
fallback even when MxValueDetails returned Supported=true. Cause:
`ReadString(direct, "Supported")` reflects on an anonymous type via
InvokeMember, which returned an empty string under
`BindingFlags.IgnoreCase`, making the StringEquals check fail.
Replace with a typed `Type.GetProperty("Supported").GetValue(direct)`
bool check via the new IsDirectSupported helper.
2. ObjectScript (the `scripts get` body retrieval) skipped the live
attribute path entirely and went straight to the package-binary
parser. The parser truncates strings at certain delimiters (e.g.
marker text containing `\n` or `=` came back as just the prefix).
Now ObjectScript tries `obj.ConfigurableAttributes[<script>.<field>]
.value.GetString()` first and only falls back to the package body
when the direct read returns null. Matches the pattern in
AttributeValueDetails. The package fallback survives for cases
where direct GRAccess doesn't surface the attribute.
End-to-end live round-trip on $DelmiaReceiver.ProcessRecipe.ExecuteText
with marker `// scripts-get round-trip marker\nMe.RecipeDownloadFlag = false;`:
scripts get -> Source: direct-graccess
Body: <marker>
attribute value get -> Value: <marker>
Both readers return the same content the writer wrote. Field then
restored to the original `Me.RecipeDownloadFlag = false;`.
Tests still 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -982,7 +982,14 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
|
||||
if (string.IsNullOrWhiteSpace(attributeName))
|
||||
throw new ArgumentException("Attribute name is required.");
|
||||
|
||||
return TryFindAttribute(obj.Attributes, attributeName) ?? TryFindAttribute(obj.ConfigurableAttributes, attributeName)
|
||||
// Prefer ConfigurableAttributes for reads too. The runtime
|
||||
// `obj.Attributes` collection sometimes returns an IAttribute proxy
|
||||
// for dotted/extension names that resolves `.value` to a synthesized
|
||||
// MxValue containing only the parent attribute's prefix (e.g. reading
|
||||
// `ProcessRecipe.ExecuteText` returned `"Me.RecipeDownloadFlag"`,
|
||||
// truncated at the first `=`). ConfigurableAttributes returns the
|
||||
// proper IAttribute backed by the actual ScriptExtension projection.
|
||||
return TryFindAttribute(obj.ConfigurableAttributes, attributeName) ?? TryFindAttribute(obj.Attributes, attributeName)
|
||||
?? throw new InvalidOperationException($"Attribute '{attributeName}' was not found.");
|
||||
}
|
||||
|
||||
@@ -1002,6 +1009,20 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
// Anonymous-type property check that doesn't go through the reflection
|
||||
// path used by `ReadProperty` / `ReadString`. The reflection path
|
||||
// intermittently returns null/empty on anonymous types from this
|
||||
// assembly, which made AttributeValueDetails fall through to the
|
||||
// package-binary fallback even when the direct read had succeeded.
|
||||
private static bool IsDirectSupported(object direct)
|
||||
{
|
||||
if (direct == null) return false;
|
||||
var prop = direct.GetType().GetProperty("Supported");
|
||||
if (prop == null) return false;
|
||||
var v = prop.GetValue(direct);
|
||||
return v is bool b && b;
|
||||
}
|
||||
|
||||
private static string SafeAttributeName(IAttribute attr)
|
||||
{
|
||||
if (attr == null) return string.Empty;
|
||||
@@ -1354,13 +1375,6 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
|
||||
private static object AttributeValueDetails(IGalaxy galaxy, IgObject obj, IAttribute attr)
|
||||
{
|
||||
var unavailable = new List<LlmUnavailableField>();
|
||||
// Use the typed `IAttribute.value` accessor. For ScriptExtension /
|
||||
// PackageOnly fields the IAttribute proxy must come from
|
||||
// `ConfigurableAttributes`, not the runtime `Attributes` snapshot —
|
||||
// the runtime view often returns a stub whose `.value` is null even
|
||||
// when the persisted value is set. Re-look-up from
|
||||
// ConfigurableAttributes by name when needed; fall back to whatever
|
||||
// `attr` was passed in if the configurable lookup fails.
|
||||
object rawMxValue = TryReadAttributeValue(attr);
|
||||
if (rawMxValue == null)
|
||||
{
|
||||
@@ -1376,7 +1390,12 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
|
||||
var package = new PackageSnapshot();
|
||||
var packageValue = (PackageAttributeValue)null;
|
||||
|
||||
if (!StringEquals(ReadString(direct, "Supported"), "True"))
|
||||
// Use direct property access to check Supported. Reflecting on the
|
||||
// anonymous type via ReadString/ReadProperty was unreliable enough
|
||||
// to trigger the package fallback even when MxValueDetails returned
|
||||
// Supported=true — the package binary parser then returned a
|
||||
// truncated old value, masking the live attribute read.
|
||||
if (!IsDirectSupported(direct))
|
||||
{
|
||||
package = TryReadPackageSnapshot(galaxy, obj, ObjectDetails(obj), unavailable);
|
||||
packageValue = FindPackageAttributeValue(package, attr.Name);
|
||||
@@ -1629,12 +1648,36 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
|
||||
private static object ObjectScript(IGalaxy galaxy, IgObject obj, string scriptName)
|
||||
{
|
||||
var unavailable = new List<LlmUnavailableField>();
|
||||
var package = TryReadPackageSnapshot(galaxy, obj, ObjectDetails(obj), unavailable);
|
||||
var attributeName = ScriptAttributeName(scriptName);
|
||||
|
||||
// Try the live attribute path FIRST. The package-binary parser
|
||||
// truncates strings at certain delimiters (e.g. `=`), so when a
|
||||
// marker like `// foo` is present in ExecuteText the parser
|
||||
// returns only the prefix. The direct GRAccess attribute read via
|
||||
// ConfigurableAttributes returns the full string.
|
||||
string directBody = null;
|
||||
try
|
||||
{
|
||||
var attr = TryFindAttribute(obj.ConfigurableAttributes, attributeName)
|
||||
?? TryFindAttribute(obj.Attributes, attributeName);
|
||||
if (attr != null)
|
||||
{
|
||||
var raw = TryReadAttributeValue(attr);
|
||||
if (raw is IMxValue mx) directBody = mx.GetString();
|
||||
}
|
||||
}
|
||||
catch { directBody = null; }
|
||||
|
||||
var package = TryReadPackageSnapshot(galaxy, obj, ObjectDetails(obj), unavailable);
|
||||
var match = ObjectScripts(galaxy, obj, package)
|
||||
.FirstOrDefault(s => string.Equals(ReadString(s, "Name"), scriptName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(ReadString(s, "Name"), attributeName, StringComparison.OrdinalIgnoreCase));
|
||||
var body = FindPackageScriptBody(package, attributeName) ?? FindPackageScriptBody(package, scriptName);
|
||||
var packageBody = FindPackageScriptBody(package, attributeName) ?? FindPackageScriptBody(package, scriptName);
|
||||
|
||||
string body = directBody ?? packageBody?.Body;
|
||||
string source = directBody != null
|
||||
? "direct-graccess"
|
||||
: (packageBody != null ? packageBody.Source : null);
|
||||
|
||||
if (match == null && body == null)
|
||||
throw new InvalidOperationException($"Script '{scriptName}' was not found in script-like object attributes.");
|
||||
@@ -1642,10 +1685,10 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
|
||||
return new
|
||||
{
|
||||
Metadata = match,
|
||||
Body = body?.Body,
|
||||
Body = body,
|
||||
BodyAvailable = body != null,
|
||||
PackageFallbackUsed = package.PackageFallbackUsed,
|
||||
Source = body == null ? "direct-graccess" : body.Source,
|
||||
Source = source ?? "direct-graccess",
|
||||
Unavailable = body == null
|
||||
? unavailable.Concat(new[] { new LlmUnavailableField { Field = "scriptBody", Reason = "Script body was not exposed through direct GRAccess and was not found in the exported package." } }).ToList()
|
||||
: unavailable
|
||||
|
||||
Reference in New Issue
Block a user