Initial commit: Wonderware / System Platform tools and reference
Five tools under one repo, all docs organized per DOCS-GUIDE.md: - aalogcli: .NET 4.8 / x86 CliFx CLI for reading System Platform binary logs (*.aaLGX) for LLM debugging, built on aaOpenSource/aaLog. Commands: last, tail, range, unread, fields. Stable JSON envelope under --llm-json. Build template under lib/build/ for rebuilding aaLogReader.dll. - aot: ArchestrA Object Toolkit 2014 v4.0 reference material. Dev guide (Markdown converted from CHM), API reference for the ArchestrA.Toolkit namespace, and the Monitor / Watchdog VS sample solutions. - graccesscli: .NET 4.8 / x86 CliFx CLI that automates Galaxy configuration via the ArchestrA GRAccess COM interop. Includes session daemon, IPC protocol, and llm-json envelope contract. - grdb: SQL/DDL exploration of the Galaxy Repository database. DDL captures, reusable queries, hierarchy / contained-name <-> tag-name translation notes. - histdb: LLM-oriented reference for AVEVA Historian retrieval. INSQL linked-server, extension tables, every wwXxx time-domain extension, every retrieval mode, alarm/event SQL recipes, REST API. Distilled from the 243-page Historian Retrieval Guide. Root contains: - CLAUDE.md: thin index pointing into each tool's README. - DOCS-GUIDE.md: doctrine for organizing docs for LLM consumption. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using ArchestrA.GRAccess;
|
||||
using Serilog;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
|
||||
{
|
||||
public sealed class GRAccessConnection : IDisposable
|
||||
{
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext<GRAccessConnection>();
|
||||
|
||||
private GRAccessApp _grAccessApp;
|
||||
|
||||
public string GalaxyName { get; }
|
||||
public string NodeName { get; }
|
||||
public IGalaxy Galaxy { get; private set; }
|
||||
public bool IsConnected { get; private set; }
|
||||
|
||||
public GRAccessConnection(string galaxyName, string nodeName)
|
||||
{
|
||||
GalaxyName = galaxyName ?? throw new ArgumentNullException(nameof(galaxyName));
|
||||
NodeName = GRAccessDiagnostics.NormalizeNodeName(
|
||||
nodeName ?? throw new ArgumentNullException(nameof(nodeName)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Must be called on an STA thread.
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
Log.Information("Connecting to galaxy {Galaxy} on {Node}", GalaxyName, NodeName);
|
||||
|
||||
_grAccessApp = new GRAccessAppClass();
|
||||
|
||||
var galaxies = _grAccessApp.QueryGalaxies(NodeName);
|
||||
GRAccessDiagnostics.ThrowIfFailed(_grAccessApp.CommandResult, "QueryGalaxies");
|
||||
|
||||
if (galaxies == null || galaxies.count == 0)
|
||||
throw new InvalidOperationException($"No galaxies found on node '{NodeName}'.");
|
||||
|
||||
Galaxy = galaxies[GalaxyName];
|
||||
if (Galaxy == null)
|
||||
throw new InvalidOperationException($"Galaxy '{GalaxyName}' not found on node '{NodeName}'.");
|
||||
|
||||
Galaxy.Login("", "");
|
||||
GRAccessDiagnostics.ThrowIfFailed(Galaxy.CommandResult, "Login");
|
||||
|
||||
IsConnected = true;
|
||||
Log.Information("Connected to galaxy {Galaxy}", GalaxyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Must be called on an STA thread.
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
if (!IsConnected && Galaxy == null && _grAccessApp == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (IsConnected)
|
||||
{
|
||||
Galaxy?.Logout();
|
||||
Log.Information("Disconnected from galaxy {Galaxy}", GalaxyName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error during galaxy logout");
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReleaseComObject(Galaxy);
|
||||
ReleaseComObject(_grAccessApp);
|
||||
Galaxy = null;
|
||||
_grAccessApp = null;
|
||||
IsConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
private static void ReleaseComObject(object value)
|
||||
{
|
||||
if (value == null || !Marshal.IsComObject(value))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Marshal.FinalReleaseComObject(value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error releasing GRAccess COM object");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using ArchestrA.GRAccess;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
|
||||
{
|
||||
internal static class GRAccessDiagnostics
|
||||
{
|
||||
public static string NormalizeNodeName(string nodeName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(nodeName))
|
||||
return string.Empty;
|
||||
|
||||
var trimmed = nodeName.Trim();
|
||||
return trimmed == "." ? Environment.MachineName : trimmed;
|
||||
}
|
||||
|
||||
public static string FormatCommandResult(string operation, ICommandResult result)
|
||||
{
|
||||
if (result == null)
|
||||
return $"{operation} failed: no GRAccess command result was returned.";
|
||||
|
||||
if (result.Successful)
|
||||
return $"{operation}: OK";
|
||||
|
||||
return $"{operation} failed: ID={result.ID} ({(int)result.ID}); Text='{result.Text}'; CustomMessage='{result.CustomMessage}'";
|
||||
}
|
||||
|
||||
public static void ThrowIfFailed(ICommandResult result, string operation)
|
||||
{
|
||||
if (result != null && !result.Successful)
|
||||
throw new InvalidOperationException(FormatCommandResult(operation, result));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ArchestrA.GRAccess;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
|
||||
{
|
||||
internal static class GRAccessQueryCommandHandler
|
||||
{
|
||||
public static string Execute(
|
||||
IGalaxy galaxy,
|
||||
string command,
|
||||
string subcommand,
|
||||
IDictionary<string, string> args)
|
||||
{
|
||||
command = (command ?? string.Empty).Trim().ToLowerInvariant();
|
||||
subcommand = (subcommand ?? string.Empty).Trim().ToLowerInvariant();
|
||||
args = args ?? new Dictionary<string, string>();
|
||||
|
||||
if (command == "object" && subcommand == "list")
|
||||
return ExecuteObjectList(galaxy, args);
|
||||
|
||||
if (command == "template" && subcommand == "list")
|
||||
return ExecuteTypedList(galaxy, args, GRAccessObjectKind.Template);
|
||||
|
||||
if (command == "instance" && subcommand == "list")
|
||||
return ExecuteTypedList(galaxy, args, GRAccessObjectKind.Instance);
|
||||
|
||||
if (command == "object" && subcommand == "attributes")
|
||||
return ExecuteObjectAttributes(galaxy, args);
|
||||
|
||||
throw new NotSupportedException($"Command '{command} {subcommand}' is not implemented.");
|
||||
}
|
||||
|
||||
public static string ExecuteObjectList(IGalaxy galaxy, IDictionary<string, string> args)
|
||||
{
|
||||
var kind = ParseKind(GetArg(args, "type", "all"));
|
||||
var pattern = GetArg(args, "pattern", "%");
|
||||
var json = GetBoolArg(args, "json");
|
||||
var objects = GRAccessQueryService.QueryObjects(galaxy, kind, pattern)
|
||||
.OrderBy(o => o.Kind)
|
||||
.ThenBy(o => o.Tagname)
|
||||
.ToList();
|
||||
|
||||
if (json)
|
||||
return JsonConvert.SerializeObject(objects, Formatting.Indented);
|
||||
|
||||
return string.Join(
|
||||
Environment.NewLine,
|
||||
objects.Select(o => $"{o.Kind}\t{o.Tagname}\t{o.HierarchicalName}"));
|
||||
}
|
||||
|
||||
public static string ExecuteTypedList(
|
||||
IGalaxy galaxy,
|
||||
IDictionary<string, string> args,
|
||||
GRAccessObjectKind kind)
|
||||
{
|
||||
var pattern = GetArg(args, "pattern", "%");
|
||||
var json = GetBoolArg(args, "json");
|
||||
var objects = GRAccessQueryService.QueryObjects(galaxy, kind, pattern)
|
||||
.OrderBy(o => o.Tagname)
|
||||
.ToList();
|
||||
|
||||
if (json)
|
||||
return JsonConvert.SerializeObject(objects, Formatting.Indented);
|
||||
|
||||
return string.Join(Environment.NewLine, objects.Select(o => o.Tagname));
|
||||
}
|
||||
|
||||
public static string ExecuteObjectAttributes(IGalaxy galaxy, IDictionary<string, string> args)
|
||||
{
|
||||
var kind = ParseKind(GetArg(args, "type", "all"));
|
||||
var objectName = GetArg(args, "name", string.Empty);
|
||||
var configurableOnly = GetBoolArg(args, "configurable");
|
||||
var json = GetBoolArg(args, "json");
|
||||
var attributes = GRAccessQueryService.QueryAttributes(
|
||||
galaxy,
|
||||
kind,
|
||||
objectName,
|
||||
configurableOnly)
|
||||
.ToList();
|
||||
|
||||
if (json)
|
||||
return JsonConvert.SerializeObject(attributes, Formatting.Indented);
|
||||
|
||||
return string.Join(
|
||||
Environment.NewLine,
|
||||
attributes.Select(a => $"{a.Name}\t{a.DataType}\t{a.Category}"));
|
||||
}
|
||||
|
||||
public static GRAccessObjectKind ParseKind(string value)
|
||||
{
|
||||
switch ((value ?? string.Empty).Trim().ToLowerInvariant())
|
||||
{
|
||||
case "":
|
||||
case "all":
|
||||
return GRAccessObjectKind.All;
|
||||
case "template":
|
||||
case "templates":
|
||||
return GRAccessObjectKind.Template;
|
||||
case "instance":
|
||||
case "instances":
|
||||
return GRAccessObjectKind.Instance;
|
||||
default:
|
||||
throw new ArgumentException("Object type must be one of: all, template, instance.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetArg(IDictionary<string, string> args, string key, string defaultValue)
|
||||
{
|
||||
return args != null && args.TryGetValue(key, out var value) ? value : defaultValue;
|
||||
}
|
||||
|
||||
private static bool GetBoolArg(IDictionary<string, string> args, string key)
|
||||
{
|
||||
return args != null
|
||||
&& args.TryGetValue(key, out var value)
|
||||
&& bool.TryParse(value, out var parsed)
|
||||
&& parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ArchestrA.GRAccess;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
|
||||
{
|
||||
internal static class GRAccessQueryService
|
||||
{
|
||||
public static IReadOnlyList<GRAccessObjectInfo> QueryObjects(
|
||||
IGalaxy galaxy,
|
||||
GRAccessObjectKind kind,
|
||||
string pattern)
|
||||
{
|
||||
if (galaxy == null) throw new ArgumentNullException(nameof(galaxy));
|
||||
if (string.IsNullOrWhiteSpace(pattern)) pattern = "%";
|
||||
|
||||
var results = new List<GRAccessObjectInfo>();
|
||||
|
||||
if (kind == GRAccessObjectKind.All || kind == GRAccessObjectKind.Template)
|
||||
results.AddRange(QueryObjects(galaxy, EgObjectIsTemplateOrInstance.gObjectIsTemplate, "template", pattern));
|
||||
|
||||
if (kind == GRAccessObjectKind.All || kind == GRAccessObjectKind.Instance)
|
||||
results.AddRange(QueryObjects(galaxy, EgObjectIsTemplateOrInstance.gObjectIsInstance, "instance", pattern));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<GRAccessAttributeInfo> QueryAttributes(
|
||||
IGalaxy galaxy,
|
||||
GRAccessObjectKind kind,
|
||||
string objectName,
|
||||
bool configurableOnly)
|
||||
{
|
||||
if (galaxy == null) throw new ArgumentNullException(nameof(galaxy));
|
||||
if (string.IsNullOrWhiteSpace(objectName))
|
||||
throw new ArgumentException("Object name is required.", nameof(objectName));
|
||||
|
||||
var obj = FindSingleObject(galaxy, kind, objectName);
|
||||
var attrs = configurableOnly ? obj.ConfigurableAttributes : obj.Attributes;
|
||||
|
||||
var results = new List<GRAccessAttributeInfo>();
|
||||
if (attrs == null)
|
||||
return results;
|
||||
|
||||
for (var i = 1; i <= attrs.count; i++)
|
||||
{
|
||||
var attr = attrs[i];
|
||||
if (attr == null)
|
||||
continue;
|
||||
|
||||
results.Add(new GRAccessAttributeInfo
|
||||
{
|
||||
Name = TryRead(() => attr.Name),
|
||||
DataType = TryRead(() => attr.DataType.ToString()),
|
||||
Category = TryRead(() => attr.AttributeCategory.ToString()),
|
||||
SecurityClassification = TryRead(() => attr.SecurityClassification.ToString()),
|
||||
Locked = TryRead(() => attr.Locked.ToString()),
|
||||
UpperBoundDim1 = TryReadNullableShort(() => attr.UpperBoundDim1),
|
||||
HasBuffer = TryReadNullableBool(() => attr.HasBuffer),
|
||||
RuntimeSetHandler = TryReadNullableBool(() => attr.RtSethandler),
|
||||
ConfigSetHandler = TryReadNullableBool(() => attr.CfgSethandler)
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static IEnumerable<GRAccessObjectInfo> QueryObjects(
|
||||
IGalaxy galaxy,
|
||||
EgObjectIsTemplateOrInstance templateOrInstance,
|
||||
string kind,
|
||||
string pattern)
|
||||
{
|
||||
var objects = galaxy.QueryObjects(
|
||||
templateOrInstance,
|
||||
EConditionType.namedLike,
|
||||
pattern,
|
||||
EMatch.MatchCondition);
|
||||
|
||||
GRAccessDiagnostics.ThrowIfFailed(galaxy.CommandResult, "QueryObjects");
|
||||
|
||||
if (objects == null)
|
||||
yield break;
|
||||
|
||||
for (var i = 1; i <= objects.count; i++)
|
||||
{
|
||||
var obj = objects[i];
|
||||
if (obj == null)
|
||||
continue;
|
||||
|
||||
yield return new GRAccessObjectInfo
|
||||
{
|
||||
Kind = kind,
|
||||
Tagname = obj.Tagname,
|
||||
ContainedName = obj.ContainedName,
|
||||
HierarchicalName = obj.HierarchicalName,
|
||||
CheckoutStatus = TryRead(() => obj.CheckoutStatus.ToString())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static IgObject FindSingleObject(IGalaxy galaxy, GRAccessObjectKind kind, string objectName)
|
||||
{
|
||||
if (kind == GRAccessObjectKind.All)
|
||||
{
|
||||
var template = FindSingleObjectOrNull(galaxy, EgObjectIsTemplateOrInstance.gObjectIsTemplate, objectName);
|
||||
if (template != null)
|
||||
return template;
|
||||
|
||||
var instance = FindSingleObjectOrNull(galaxy, EgObjectIsTemplateOrInstance.gObjectIsInstance, objectName);
|
||||
if (instance != null)
|
||||
return instance;
|
||||
}
|
||||
else
|
||||
{
|
||||
var templateOrInstance = kind == GRAccessObjectKind.Template
|
||||
? EgObjectIsTemplateOrInstance.gObjectIsTemplate
|
||||
: EgObjectIsTemplateOrInstance.gObjectIsInstance;
|
||||
|
||||
var obj = FindSingleObjectOrNull(galaxy, templateOrInstance, objectName);
|
||||
if (obj != null)
|
||||
return obj;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Object '{objectName}' was not found.");
|
||||
}
|
||||
|
||||
private static IgObject FindSingleObjectOrNull(
|
||||
IGalaxy galaxy,
|
||||
EgObjectIsTemplateOrInstance templateOrInstance,
|
||||
string objectName)
|
||||
{
|
||||
var names = new[] { objectName };
|
||||
var objects = galaxy.QueryObjectsByName(templateOrInstance, ref names);
|
||||
GRAccessDiagnostics.ThrowIfFailed(galaxy.CommandResult, "QueryObjectsByName");
|
||||
|
||||
if (objects == null || objects.count == 0)
|
||||
return null;
|
||||
|
||||
return objects[1];
|
||||
}
|
||||
|
||||
private static string TryRead(Func<string> read)
|
||||
{
|
||||
try
|
||||
{
|
||||
return read() ?? string.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool? TryReadNullableBool(Func<bool> read)
|
||||
{
|
||||
try
|
||||
{
|
||||
return read();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static short? TryReadNullableShort(Func<short> read)
|
||||
{
|
||||
try
|
||||
{
|
||||
return read();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum GRAccessObjectKind
|
||||
{
|
||||
All,
|
||||
Template,
|
||||
Instance
|
||||
}
|
||||
|
||||
public sealed class GRAccessObjectInfo
|
||||
{
|
||||
public string Kind { get; set; } = string.Empty;
|
||||
public string Tagname { get; set; } = string.Empty;
|
||||
public string ContainedName { get; set; } = string.Empty;
|
||||
public string HierarchicalName { get; set; } = string.Empty;
|
||||
public string CheckoutStatus { get; set; } = string.Empty;
|
||||
public string DerivedFrom { get; set; } = string.Empty;
|
||||
public string BasedOn { get; set; } = string.Empty;
|
||||
public string Area { get; set; } = string.Empty;
|
||||
public string Host { get; set; } = string.Empty;
|
||||
public string Container { get; set; } = string.Empty;
|
||||
public string Toolset { get; set; } = string.Empty;
|
||||
public string SecurityGroup { get; set; } = string.Empty;
|
||||
public string ConfigVersion { get; set; } = string.Empty;
|
||||
public string DeploymentStatus { get; set; } = string.Empty;
|
||||
public string DeployedVersion { get; set; } = string.Empty;
|
||||
public string ValidationErrors { get; set; } = string.Empty;
|
||||
public string ValidationWarnings { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
internal sealed class GRAccessAttributeInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string DataType { get; set; } = string.Empty;
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public string SecurityClassification { get; set; } = string.Empty;
|
||||
public string Locked { get; set; } = string.Empty;
|
||||
public short? UpperBoundDim1 { get; set; }
|
||||
public bool? HasBuffer { get; set; }
|
||||
public bool? RuntimeSetHandler { get; set; }
|
||||
public bool? ConfigSetHandler { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
|
||||
{
|
||||
public sealed class LlmUnavailableField
|
||||
{
|
||||
[JsonProperty("field")]
|
||||
public string Field { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("reason")]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class LlmError
|
||||
{
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class LlmResponse
|
||||
{
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
[JsonProperty("command")]
|
||||
public string Command { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("galaxy")]
|
||||
public string Galaxy { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("target")]
|
||||
public string Target { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("data")]
|
||||
public object Data { get; set; }
|
||||
|
||||
[JsonProperty("commandResult")]
|
||||
public object CommandResult { get; set; }
|
||||
|
||||
[JsonProperty("warnings")]
|
||||
public List<string> Warnings { get; set; } = new List<string>();
|
||||
|
||||
[JsonProperty("unavailable")]
|
||||
public List<LlmUnavailableField> Unavailable { get; set; } = new List<LlmUnavailableField>();
|
||||
|
||||
[JsonProperty("error")]
|
||||
public LlmError Error { get; set; }
|
||||
|
||||
[JsonProperty("exitCode")]
|
||||
public int ExitCode { get; set; }
|
||||
|
||||
public static string Ok(string command, string galaxy, string target, object data, object commandResult = null, IEnumerable<string> warnings = null, IEnumerable<LlmUnavailableField> unavailable = null)
|
||||
{
|
||||
return Serialize(new LlmResponse
|
||||
{
|
||||
Success = true,
|
||||
Command = command,
|
||||
Galaxy = galaxy ?? string.Empty,
|
||||
Target = target ?? string.Empty,
|
||||
Data = data,
|
||||
CommandResult = commandResult,
|
||||
Warnings = warnings == null ? new List<string>() : new List<string>(warnings),
|
||||
Unavailable = unavailable == null ? new List<LlmUnavailableField>() : new List<LlmUnavailableField>(unavailable),
|
||||
ExitCode = 0
|
||||
});
|
||||
}
|
||||
|
||||
public static string Fail(string command, string galaxy, string target, Exception exception, int exitCode = 1)
|
||||
{
|
||||
return Serialize(new LlmResponse
|
||||
{
|
||||
Success = false,
|
||||
Command = command ?? string.Empty,
|
||||
Galaxy = galaxy ?? string.Empty,
|
||||
Target = target ?? string.Empty,
|
||||
Error = new LlmError
|
||||
{
|
||||
Message = exception?.Message ?? "Unknown error",
|
||||
Type = exception?.GetType().Name ?? "Error"
|
||||
},
|
||||
ExitCode = exitCode
|
||||
});
|
||||
}
|
||||
|
||||
public static string Serialize(object value)
|
||||
{
|
||||
return JsonConvert.SerializeObject(value, Formatting.Indented, new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Include
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
|
||||
{
|
||||
public sealed class PackageAttributeValue
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string DataType { get; set; } = string.Empty;
|
||||
public object Value { get; set; }
|
||||
public string Source { get; set; } = "package";
|
||||
}
|
||||
|
||||
public sealed class PackageScriptBody
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Body { get; set; } = string.Empty;
|
||||
public string Source { get; set; } = "package";
|
||||
}
|
||||
|
||||
public sealed class PackageSnapshot
|
||||
{
|
||||
public List<string> Lineage { get; } = new List<string>();
|
||||
public List<GRAccessObjectInfo> Children { get; } = new List<GRAccessObjectInfo>();
|
||||
public List<GRAccessObjectInfo> ContainedObjects { get; } = new List<GRAccessObjectInfo>();
|
||||
public List<PackageAttributeValue> AttributeValues { get; } = new List<PackageAttributeValue>();
|
||||
public List<PackageScriptBody> ScriptBodies { get; } = new List<PackageScriptBody>();
|
||||
public List<LlmUnavailableField> Unavailable { get; } = new List<LlmUnavailableField>();
|
||||
public bool PackageFallbackUsed { get; set; }
|
||||
public string Source { get; set; } = "direct-graccess";
|
||||
}
|
||||
|
||||
public static class PackageSnapshotParser
|
||||
{
|
||||
private static readonly Regex LineageRegex = new Regex(@"Lineage\s*[:=]\s*(?<value>.+)$", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
|
||||
private static readonly Regex DerivedRegex = new Regex(@"DerivedFrom\s*[:=]\s*(?<value>[^\r\n<]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex BasedOnRegex = new Regex(@"BasedOn\s*[:=]\s*(?<value>[^\r\n<]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex XmlValueRegex = new Regex(@"<(?<tag>DerivedFrom|BasedOn)>\s*(?<value>[^<]+)\s*</\k<tag>>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex AttributeRegex = new Regex(@"AttributeValue\s+Name=""(?<name>[^""]+)""(?:\s+DataType=""(?<type>[^""]*)"")?\s+Value=""(?<value>[^""]*)""", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex AttributeKeyRegex = new Regex(@"AttributeValues\.(?<name>[^=\r\n]+)\s*=\s*(?<value>[^\r\n]*)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex ScriptRegex = new Regex(@"ScriptBody\s+Name=""(?<name>[^""]+)""\s*>\s*(?<body>.*?)\s*</ScriptBody>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
|
||||
private static readonly Regex ScriptKeyRegex = new Regex(@"ScriptBodies\.(?<name>[^=\r\n]+)\s*=\s*(?<value>[\s\S]*?)(?=\r?\n[A-Za-z0-9_.-]+\s*=|\z)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
public static PackageSnapshot Parse(string path)
|
||||
{
|
||||
var snapshot = new PackageSnapshot { PackageFallbackUsed = true, Source = "export-package" };
|
||||
var entries = ReadTextEntries(path).ToList();
|
||||
if (!entries.Any() && File.Exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
entries.Add(File.ReadAllText(path));
|
||||
}
|
||||
catch
|
||||
{
|
||||
snapshot.Unavailable.Add(new LlmUnavailableField { Field = "package", Reason = "Package file could not be read as text." });
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var text in entries)
|
||||
ParseText(text, snapshot);
|
||||
|
||||
if (File.Exists(path) && !LooksLikeZip(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(path);
|
||||
ParseText(text, snapshot);
|
||||
ParseLineageLines(text, snapshot);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Directory and archive package paths are handled above.
|
||||
}
|
||||
}
|
||||
|
||||
Deduplicate(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ReadTextEntries(string path)
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
||||
foreach (var text in ReadPackageFile(file, 0))
|
||||
yield return text;
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!File.Exists(path))
|
||||
yield break;
|
||||
|
||||
foreach (var text in ReadPackageFile(path, 0))
|
||||
yield return text;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ReadPackageFile(string path, int depth)
|
||||
{
|
||||
byte[] bytes;
|
||||
try
|
||||
{
|
||||
bytes = File.ReadAllBytes(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var text in ReadPackageBytes(bytes, path, depth))
|
||||
yield return text;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ReadPackageBytes(byte[] bytes, string name, int depth)
|
||||
{
|
||||
if (bytes == null || bytes.Length == 0 || depth > 6)
|
||||
yield break;
|
||||
|
||||
if (LooksLikeZip(bytes))
|
||||
{
|
||||
using (var stream = new MemoryStream(bytes))
|
||||
using (var archive = new ZipArchive(stream, ZipArchiveMode.Read))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
if (entry.Length == 0)
|
||||
continue;
|
||||
|
||||
byte[] entryBytes;
|
||||
using (var entryStream = entry.Open())
|
||||
using (var copy = new MemoryStream())
|
||||
{
|
||||
entryStream.CopyTo(copy);
|
||||
entryBytes = copy.ToArray();
|
||||
}
|
||||
|
||||
foreach (var text in ReadPackageBytes(entryBytes, $"{name}!{entry.FullName}", depth + 1))
|
||||
yield return text;
|
||||
}
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (LooksTextual(name))
|
||||
{
|
||||
var utf8 = ReadText(bytes, Encoding.UTF8);
|
||||
if (!string.IsNullOrWhiteSpace(utf8))
|
||||
yield return utf8;
|
||||
}
|
||||
|
||||
var utf16Strings = ExtractUtf16Strings(bytes);
|
||||
if (!string.IsNullOrWhiteSpace(utf16Strings))
|
||||
yield return utf16Strings;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ReadTextFile(string path)
|
||||
{
|
||||
string text;
|
||||
try
|
||||
{
|
||||
text = File.ReadAllText(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
yield return text;
|
||||
}
|
||||
|
||||
private static bool LooksLikeZip(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = File.OpenRead(path))
|
||||
{
|
||||
if (stream.Length < 4)
|
||||
return false;
|
||||
|
||||
return stream.ReadByte() == 0x50 && stream.ReadByte() == 0x4b;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool LooksLikeZip(byte[] bytes)
|
||||
{
|
||||
return bytes.Length >= 4 && bytes[0] == 0x50 && bytes[1] == 0x4b;
|
||||
}
|
||||
|
||||
private static bool LooksTextual(string path)
|
||||
{
|
||||
var ext = Path.GetExtension(path).ToLowerInvariant();
|
||||
return ext == ".xml" || ext == ".json" || ext == ".txt" || ext == ".csv" || ext == ".ini" || ext == ".config" || ext == ".pkg" || ext == ".aapkg";
|
||||
}
|
||||
|
||||
private static string ReadText(byte[] bytes, Encoding fallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = new MemoryStream(bytes))
|
||||
using (var reader = new StreamReader(stream, fallback, true))
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractUtf16Strings(byte[] bytes)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
var builder = new StringBuilder();
|
||||
for (var i = 0; i < bytes.Length - 1;)
|
||||
{
|
||||
var start = i;
|
||||
builder.Clear();
|
||||
while (i < bytes.Length - 1)
|
||||
{
|
||||
var lo = bytes[i];
|
||||
var hi = bytes[i + 1];
|
||||
if (hi == 0 && IsPackageTextByte(lo))
|
||||
{
|
||||
builder.Append((char)lo);
|
||||
i += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (builder.Length >= 4)
|
||||
lines.Add(builder.ToString());
|
||||
|
||||
if (i == start)
|
||||
i++;
|
||||
}
|
||||
|
||||
return string.Join("\n", lines);
|
||||
}
|
||||
|
||||
private static bool IsPackageTextByte(byte value)
|
||||
{
|
||||
return (value >= 32 && value <= 126) || value == 9 || value == 10 || value == 13;
|
||||
}
|
||||
|
||||
private static void ParseText(string text, PackageSnapshot snapshot)
|
||||
{
|
||||
TryParseJson(text, snapshot);
|
||||
ParseLineage(text, snapshot);
|
||||
ParseAttributes(text, snapshot);
|
||||
ParseScripts(text, snapshot);
|
||||
}
|
||||
|
||||
private static void TryParseJson(string text, PackageSnapshot snapshot)
|
||||
{
|
||||
try
|
||||
{
|
||||
var token = JToken.Parse(text);
|
||||
foreach (var item in token.SelectTokens("$..Lineage[*]").Values<string>())
|
||||
AddLineage(snapshot, item);
|
||||
foreach (var item in token.SelectTokens("$..Children[*]"))
|
||||
AddObject(snapshot.Children, item);
|
||||
foreach (var item in token.SelectTokens("$..ContainedObjects[*]"))
|
||||
AddObject(snapshot.ContainedObjects, item);
|
||||
foreach (var item in token.SelectTokens("$..AttributeValues[*]"))
|
||||
AddAttribute(snapshot, item);
|
||||
foreach (var item in token.SelectTokens("$..ScriptBodies[*]"))
|
||||
AddScript(snapshot, item);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Non-JSON package entries are parsed by the text matchers.
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseLineage(string text, PackageSnapshot snapshot)
|
||||
{
|
||||
ParseLineageLines(text, snapshot);
|
||||
|
||||
foreach (Match match in LineageRegex.Matches(text))
|
||||
foreach (var item in match.Groups["value"].Value.Split(new[] { "->", "|", "," }, StringSplitOptions.RemoveEmptyEntries))
|
||||
AddLineage(snapshot, item);
|
||||
|
||||
foreach (Match match in DerivedRegex.Matches(text))
|
||||
AddLineage(snapshot, match.Groups["value"].Value);
|
||||
foreach (Match match in BasedOnRegex.Matches(text))
|
||||
AddLineage(snapshot, match.Groups["value"].Value);
|
||||
foreach (Match match in XmlValueRegex.Matches(text))
|
||||
AddLineage(snapshot, match.Groups["value"].Value);
|
||||
}
|
||||
|
||||
private static void ParseLineageLines(string text, PackageSnapshot snapshot)
|
||||
{
|
||||
foreach (var line in text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None))
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (!trimmed.StartsWith("Lineage:", StringComparison.OrdinalIgnoreCase) && !trimmed.StartsWith("Lineage=", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var separator = trimmed.IndexOfAny(new[] { ':', '=' });
|
||||
if (separator < 0)
|
||||
continue;
|
||||
|
||||
var value = trimmed.Substring(separator + 1);
|
||||
foreach (var item in value.Split(new[] { "->", "|", "," }, StringSplitOptions.RemoveEmptyEntries))
|
||||
AddLineage(snapshot, item);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseAttributes(string text, PackageSnapshot snapshot)
|
||||
{
|
||||
foreach (Match match in AttributeRegex.Matches(text))
|
||||
{
|
||||
snapshot.AttributeValues.Add(new PackageAttributeValue
|
||||
{
|
||||
Name = match.Groups["name"].Value.Trim(),
|
||||
DataType = match.Groups["type"].Value.Trim(),
|
||||
Value = match.Groups["value"].Value
|
||||
});
|
||||
}
|
||||
|
||||
foreach (Match match in AttributeKeyRegex.Matches(text))
|
||||
{
|
||||
snapshot.AttributeValues.Add(new PackageAttributeValue
|
||||
{
|
||||
Name = match.Groups["name"].Value.Trim(),
|
||||
Value = match.Groups["value"].Value.Trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseScripts(string text, PackageSnapshot snapshot)
|
||||
{
|
||||
foreach (Match match in ScriptRegex.Matches(text))
|
||||
snapshot.ScriptBodies.Add(new PackageScriptBody { Name = match.Groups["name"].Value.Trim(), Body = match.Groups["body"].Value });
|
||||
|
||||
foreach (Match match in ScriptKeyRegex.Matches(text))
|
||||
snapshot.ScriptBodies.Add(new PackageScriptBody { Name = match.Groups["name"].Value.Trim(), Body = match.Groups["value"].Value.Trim() });
|
||||
|
||||
ParseBinaryScriptRecords(text, snapshot);
|
||||
}
|
||||
|
||||
private static void ParseBinaryScriptRecords(string text, PackageSnapshot snapshot)
|
||||
{
|
||||
var lines = text
|
||||
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)
|
||||
.Select(line => line.Trim())
|
||||
.Where(line => !string.IsNullOrWhiteSpace(line))
|
||||
.ToList();
|
||||
|
||||
for (var i = 0; i < lines.Count; i++)
|
||||
{
|
||||
var marker = lines[i];
|
||||
if (!marker.EndsWith("_ScriptExtension", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var baseName = marker.Substring(0, marker.Length - "_ScriptExtension".Length);
|
||||
if (string.IsNullOrWhiteSpace(baseName))
|
||||
continue;
|
||||
|
||||
for (var j = i + 1; j < Math.Min(lines.Count, i + 12); j++)
|
||||
{
|
||||
var body = lines[j];
|
||||
if (!LooksLikeScriptBody(body))
|
||||
continue;
|
||||
|
||||
AddScriptBody(snapshot, $"{baseName}.ExecuteText", body, "export-package:binary");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool LooksLikeScriptBody(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return false;
|
||||
|
||||
var trimmed = value.Trim();
|
||||
if (trimmed.StartsWith("{", StringComparison.Ordinal) && trimmed.EndsWith("}", StringComparison.Ordinal))
|
||||
return false;
|
||||
if (trimmed.IndexOf("_ScriptExtension", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return false;
|
||||
|
||||
return trimmed.IndexOf("Me.", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| trimmed.IndexOf("System.", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| trimmed.IndexOf("ScriptExchange", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| trimmed.IndexOf(";", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| trimmed.IndexOf(":=", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static void AddScriptBody(PackageSnapshot snapshot, string name, string body, string source)
|
||||
{
|
||||
snapshot.ScriptBodies.Add(new PackageScriptBody { Name = name, Body = body, Source = source });
|
||||
snapshot.AttributeValues.Add(new PackageAttributeValue
|
||||
{
|
||||
Name = name,
|
||||
DataType = "MxBigString",
|
||||
Value = body,
|
||||
Source = source
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddObject(List<GRAccessObjectInfo> target, JToken token)
|
||||
{
|
||||
target.Add(new GRAccessObjectInfo
|
||||
{
|
||||
Kind = Value(token, "Kind"),
|
||||
Tagname = Value(token, "Tagname", "Name"),
|
||||
ContainedName = Value(token, "ContainedName"),
|
||||
HierarchicalName = Value(token, "HierarchicalName"),
|
||||
CheckoutStatus = Value(token, "CheckoutStatus"),
|
||||
DerivedFrom = Value(token, "DerivedFrom"),
|
||||
BasedOn = Value(token, "BasedOn"),
|
||||
Container = Value(token, "Container")
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddAttribute(PackageSnapshot snapshot, JToken token)
|
||||
{
|
||||
snapshot.AttributeValues.Add(new PackageAttributeValue
|
||||
{
|
||||
Name = Value(token, "Name", "Attribute"),
|
||||
DataType = Value(token, "DataType"),
|
||||
Value = token["Value"]?.ToObject<object>()
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddScript(PackageSnapshot snapshot, JToken token)
|
||||
{
|
||||
snapshot.ScriptBodies.Add(new PackageScriptBody
|
||||
{
|
||||
Name = Value(token, "Name", "Script"),
|
||||
Body = Value(token, "Body", "Text")
|
||||
});
|
||||
}
|
||||
|
||||
private static string Value(JToken token, params string[] names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
var value = token[name] ?? token[name.ToLowerInvariant()];
|
||||
if (value != null)
|
||||
return value.Type == JTokenType.Null ? string.Empty : value.ToString();
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static void AddLineage(PackageSnapshot snapshot, string value)
|
||||
{
|
||||
value = (value ?? string.Empty).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
snapshot.Lineage.Add(value);
|
||||
}
|
||||
|
||||
private static void Deduplicate(PackageSnapshot snapshot)
|
||||
{
|
||||
Replace(snapshot.Lineage, snapshot.Lineage.Where(s => !string.IsNullOrWhiteSpace(s)).Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
Replace(snapshot.Children, snapshot.Children.GroupBy(o => $"{o.Kind}:{o.Tagname}", StringComparer.OrdinalIgnoreCase).Select(g => g.First()));
|
||||
Replace(snapshot.ContainedObjects, snapshot.ContainedObjects.GroupBy(o => $"{o.Kind}:{o.Tagname}", StringComparer.OrdinalIgnoreCase).Select(g => g.First()));
|
||||
Replace(snapshot.AttributeValues, snapshot.AttributeValues.Where(a => !string.IsNullOrWhiteSpace(a.Name)).GroupBy(a => a.Name, StringComparer.OrdinalIgnoreCase).Select(g => g.First()));
|
||||
Replace(snapshot.ScriptBodies, snapshot.ScriptBodies.Where(s => !string.IsNullOrWhiteSpace(s.Name)).GroupBy(s => s.Name, StringComparer.OrdinalIgnoreCase).Select(g => g.First()));
|
||||
}
|
||||
|
||||
private static void Replace<T>(List<T> list, IEnumerable<T> values)
|
||||
{
|
||||
var materialized = values.ToList();
|
||||
list.Clear();
|
||||
list.AddRange(materialized);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user