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:
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="ArchestrA.GRAccess" publicKeyToken="23106a86e706d0ae" culture="neutral" />
|
||||
<publisherPolicy apply="no" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
@@ -0,0 +1,819 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ArchestrA.GRAccess;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Commands
|
||||
{
|
||||
public abstract class RoutedCommandBase : ICommand
|
||||
{
|
||||
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
|
||||
public string GalaxyName { get; init; }
|
||||
|
||||
[CommandOption("node", 'n', Description = "GR node name. Required for one-shot mode; ignored when a session is active.")]
|
||||
public string NodeName { get; init; } = "";
|
||||
|
||||
[CommandOption("json", Description = "Output as JSON")]
|
||||
public bool Json { get; init; }
|
||||
|
||||
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
|
||||
public bool LlmJson { get; init; }
|
||||
|
||||
public abstract string Command { get; }
|
||||
public abstract string Subcommand { get; }
|
||||
|
||||
public virtual Dictionary<string, object> Args() => new Dictionary<string, object>
|
||||
{
|
||||
["json"] = Json,
|
||||
["llm-json"] = LlmJson
|
||||
};
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var args = Args();
|
||||
await CommandRouter.ExecuteAsync(
|
||||
console,
|
||||
GalaxyName,
|
||||
NodeName,
|
||||
Command,
|
||||
Subcommand,
|
||||
args,
|
||||
galaxy => GRAccessCommandDispatcher.Execute(galaxy, Command, Subcommand, args))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ConfirmedRoutedCommandBase : RoutedCommandBase
|
||||
{
|
||||
[CommandOption("confirm", Description = "Required for mutating commands")]
|
||||
public bool Confirm { get; init; }
|
||||
|
||||
[CommandOption("confirm-target", Description = "Required exact target for mutating/destructive commands")]
|
||||
public string ConfirmTarget { get; init; } = "";
|
||||
|
||||
[CommandOption("dry-run", Description = "Validate a mutating command without invoking mutating GRAccess calls")]
|
||||
public bool DryRun { get; init; }
|
||||
|
||||
public override Dictionary<string, object> Args()
|
||||
{
|
||||
var args = base.Args();
|
||||
args["confirm"] = Confirm;
|
||||
args["confirm-target"] = ConfirmTarget;
|
||||
args["dry-run"] = DryRun;
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ObjectCommandBase : ConfirmedRoutedCommandBase
|
||||
{
|
||||
[CommandOption("name", Description = "Object tagname", IsRequired = true)]
|
||||
public string ObjectName { get; init; }
|
||||
|
||||
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
|
||||
public string Type { get; init; } = "all";
|
||||
|
||||
public override Dictionary<string, object> Args()
|
||||
{
|
||||
var args = base.Args();
|
||||
args["name"] = ObjectName;
|
||||
args["type"] = Type;
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BulkObjectsCommandBase : ConfirmedRoutedCommandBase
|
||||
{
|
||||
[CommandOption("name", Description = "Object tagname. May be repeated.")]
|
||||
public IReadOnlyList<string> Names { get; init; } = Array.Empty<string>();
|
||||
|
||||
[CommandOption("pattern", 'p', Description = "GRAccess name pattern. May be repeated. Use % as wildcard.")]
|
||||
public IReadOnlyList<string> Patterns { get; init; } = Array.Empty<string>();
|
||||
|
||||
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
|
||||
public string Type { get; init; } = "instance";
|
||||
|
||||
public override Dictionary<string, object> Args()
|
||||
{
|
||||
var args = base.Args();
|
||||
args["name"] = Names;
|
||||
args["pattern"] = Patterns;
|
||||
args["type"] = Type;
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("galaxy info", Description = "Show galaxy version and status")]
|
||||
public sealed class GalaxyInfoCommand : RoutedCommandBase { public override string Command => "galaxy"; public override string Subcommand => "info"; }
|
||||
|
||||
[Command("galaxy sync", Description = "Synchronize the local client with the galaxy")]
|
||||
public sealed class GalaxySyncCommand : RoutedCommandBase { public override string Command => "galaxy"; public override string Subcommand => "sync"; }
|
||||
|
||||
[Command("galaxy cdi-version", Description = "Show galaxy CDI version")]
|
||||
public sealed class GalaxyCdiVersionCommand : RoutedCommandBase { public override string Command => "galaxy"; public override string Subcommand => "cdi-version"; }
|
||||
|
||||
[Command("galaxy defaults get", Description = "Get a user default")]
|
||||
public sealed class GalaxyDefaultsGetCommand : RoutedCommandBase
|
||||
{
|
||||
public override string Command => "galaxy";
|
||||
public override string Subcommand => "defaults-get";
|
||||
|
||||
[CommandOption("default", Description = "EUserDefault enum name", IsRequired = true)]
|
||||
public string Default { get; init; }
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["default"] = Default; return args; }
|
||||
}
|
||||
|
||||
[Command("galaxy defaults set", Description = "Set a user default")]
|
||||
public sealed class GalaxyDefaultsSetCommand : ConfirmedRoutedCommandBase
|
||||
{
|
||||
public override string Command => "galaxy";
|
||||
public override string Subcommand => "defaults-set";
|
||||
|
||||
[CommandOption("default", Description = "EUserDefault enum name", IsRequired = true)]
|
||||
public string Default { get; init; }
|
||||
|
||||
[CommandOption("value", Description = "Default value", IsRequired = true)]
|
||||
public string Value { get; init; }
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["default"] = Default; args["value"] = Value; return args; }
|
||||
}
|
||||
|
||||
[Command("galaxy backup", Description = "Back up a galaxy")]
|
||||
public sealed class GalaxyBackupCommand : ConfirmedRoutedCommandBase
|
||||
{
|
||||
public override string Command => "galaxy";
|
||||
public override string Subcommand => "backup";
|
||||
|
||||
[CommandOption("file", Description = "Backup file path", IsRequired = true)]
|
||||
public string File { get; init; }
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; args["node"] = NodeName; return args; }
|
||||
}
|
||||
|
||||
[Command("galaxy restore", Description = "Restore a galaxy backup")]
|
||||
public sealed class GalaxyRestoreCommand : ConfirmedRoutedCommandBase
|
||||
{
|
||||
public override string Command => "galaxy";
|
||||
public override string Subcommand => "restore";
|
||||
|
||||
[CommandOption("file", Description = "Backup file path", IsRequired = true)]
|
||||
public string File { get; init; }
|
||||
|
||||
[CommandOption("restore-older", Description = "Allow restoring an older version")]
|
||||
public bool RestoreOlder { get; init; }
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; args["node"] = NodeName; args["restore-older"] = RestoreOlder; return args; }
|
||||
}
|
||||
|
||||
[Command("galaxy migrate", Description = "Migrate or upgrade a galaxy")]
|
||||
public sealed class GalaxyMigrateCommand : ConfirmedRoutedCommandBase { public override string Command => "galaxy"; public override string Subcommand => "migrate"; public override Dictionary<string, object> Args() { var args = base.Args(); args["node"] = NodeName; return args; } }
|
||||
|
||||
[Command("galaxy import-objects", Description = "Import objects from an aaPKG file")]
|
||||
public sealed class GalaxyImportObjectsCommand : ConfirmedRoutedCommandBase
|
||||
{
|
||||
public override string Command => "galaxy";
|
||||
public override string Subcommand => "import-objects";
|
||||
|
||||
[CommandOption("file", Description = "Input file path", IsRequired = true)]
|
||||
public string File { get; init; }
|
||||
|
||||
[CommandOption("overwrite", Description = "Allow overwrites")]
|
||||
public bool Overwrite { get; init; }
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; args["overwrite"] = Overwrite; return args; }
|
||||
}
|
||||
|
||||
[Command("galaxy import-objects-ex", Description = "Import objects with conflict resolution")]
|
||||
public sealed class GalaxyImportObjectsExCommand : ConfirmedRoutedCommandBase
|
||||
{
|
||||
public override string Command => "galaxy";
|
||||
public override string Subcommand => "import-objects-ex";
|
||||
|
||||
[CommandOption("file", Description = "Input file path", IsRequired = true)]
|
||||
public string File { get; init; }
|
||||
|
||||
[CommandOption("version-conflict", Description = "E_RESOLVE_VERSION_CONFLICT_ACTION enum value", IsRequired = true)]
|
||||
public string VersionConflict { get; init; }
|
||||
|
||||
[CommandOption("name-conflict", Description = "E_RESOLVE_NAME_CONFLICT_ACTION enum value", IsRequired = true)]
|
||||
public string NameConflict { get; init; }
|
||||
|
||||
[CommandOption("append-name", Description = "Append suffix for name conflicts")]
|
||||
public string AppendName { get; init; } = "";
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; args["version-conflict"] = VersionConflict; args["name-conflict"] = NameConflict; args["append-name"] = AppendName; return args; }
|
||||
}
|
||||
|
||||
[Command("galaxy import-script-library", Description = "Import a script library")]
|
||||
public sealed class GalaxyImportScriptLibraryCommand : ConfirmedRoutedCommandBase
|
||||
{
|
||||
public override string Command => "galaxy";
|
||||
public override string Subcommand => "import-script-library";
|
||||
|
||||
[CommandOption("path", Description = "Script library path", IsRequired = true)]
|
||||
public string Path { get; init; }
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["path"] = Path; return args; }
|
||||
}
|
||||
|
||||
[Command("galaxy export-all", Description = "Export all objects from a galaxy")]
|
||||
public sealed class GalaxyExportAllCommand : RoutedCommandBase
|
||||
{
|
||||
public override string Command => "galaxy";
|
||||
public override string Subcommand => "export-all";
|
||||
|
||||
[CommandOption("output", Description = "Output file path", IsRequired = true)]
|
||||
public string Output { get; init; }
|
||||
|
||||
[CommandOption("export-type", Description = "EExportType enum value")]
|
||||
public string ExportType { get; init; } = "exportGalaxyDump";
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["output"] = Output; args["export-type"] = ExportType; return args; }
|
||||
}
|
||||
|
||||
[Command("galaxy grload", Description = "Run GRLoad from a CSV file")]
|
||||
public sealed class GalaxyGrloadCommand : ConfirmedRoutedCommandBase
|
||||
{
|
||||
public override string Command => "galaxy";
|
||||
public override string Subcommand => "grload";
|
||||
|
||||
[CommandOption("file", Description = "CSV file path", IsRequired = true)]
|
||||
public string File { get; init; }
|
||||
|
||||
[CommandOption("mode", Description = "GRLoadMode enum value")]
|
||||
public string Mode { get; init; } = "Create";
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; args["mode"] = Mode; return args; }
|
||||
}
|
||||
|
||||
public abstract class PreLoginGalaxyCommandBase : ICommand
|
||||
{
|
||||
[CommandOption("node", 'n', Description = "GR node name. Blank = local node; . = local machine.")]
|
||||
public string NodeName { get; init; } = "";
|
||||
|
||||
[CommandOption("confirm", Description = "Required for mutating commands")]
|
||||
public bool Confirm { get; init; }
|
||||
|
||||
[CommandOption("confirm-target", Description = "Required exact target for destructive commands")]
|
||||
public string ConfirmTarget { get; init; } = "";
|
||||
|
||||
protected IGRAccess CreateApp() => new GRAccessAppClass();
|
||||
protected string Node() => GRAccessDiagnostics.NormalizeNodeName(NodeName);
|
||||
public abstract ValueTask ExecuteAsync(IConsole console);
|
||||
protected void RequireConfirm(string target)
|
||||
{
|
||||
if (!Confirm) throw new CliFx.Exceptions.CommandException("This command requires --confirm.", 1);
|
||||
if (!string.Equals(ConfirmTarget, target, StringComparison.OrdinalIgnoreCase))
|
||||
throw new CliFx.Exceptions.CommandException($"This command requires --confirm-target {target}.", 1);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("galaxy create", Description = "Create a galaxy")]
|
||||
public sealed class GalaxyCreateCommand : PreLoginGalaxyCommandBase
|
||||
{
|
||||
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
|
||||
public string GalaxyName { get; init; }
|
||||
|
||||
[CommandOption("security", Description = "Enable security")]
|
||||
public bool Security { get; init; }
|
||||
|
||||
[CommandOption("auth-mode", Description = "EAuthenticationMode enum value")]
|
||||
public string AuthMode { get; init; } = "aaNoAuthentication";
|
||||
|
||||
[CommandOption("os-user", Description = "OS user name/description")]
|
||||
public string OsUser { get; init; } = "";
|
||||
|
||||
public override ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
RequireConfirm(GalaxyName);
|
||||
var app = CreateApp();
|
||||
app.CreateGalaxy(GalaxyName, Node(), Security, (EAuthenticationMode)Enum.Parse(typeof(EAuthenticationMode), AuthMode), OsUser);
|
||||
console.Output.WriteLine(GRAccessDiagnostics.FormatCommandResult("CreateGalaxy", app.CommandResult));
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("galaxy create-from-template", Description = "Create a galaxy from a template")]
|
||||
public sealed class GalaxyCreateFromTemplateCommand : PreLoginGalaxyCommandBase
|
||||
{
|
||||
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
|
||||
public string GalaxyName { get; init; }
|
||||
|
||||
[CommandOption("template", Description = "Create galaxy template name", IsRequired = true)]
|
||||
public string Template { get; init; }
|
||||
|
||||
public override ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
RequireConfirm(GalaxyName);
|
||||
var app = CreateApp();
|
||||
app.CreateGalaxyFromTemplate(Template, GalaxyName, Node());
|
||||
console.Output.WriteLine(GRAccessDiagnostics.FormatCommandResult("CreateGalaxyFromTemplate", app.CommandResult));
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("galaxy delete", Description = "Delete a galaxy")]
|
||||
public sealed class GalaxyDeleteCommand : PreLoginGalaxyCommandBase
|
||||
{
|
||||
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
|
||||
public string GalaxyName { get; init; }
|
||||
|
||||
public override ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
RequireConfirm(GalaxyName);
|
||||
var app = CreateApp();
|
||||
app.DeleteGalaxy(GalaxyName, Node());
|
||||
console.Output.WriteLine(GRAccessDiagnostics.FormatCommandResult("DeleteGalaxy", app.CommandResult));
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("object get", Description = "Get object details")]
|
||||
public sealed class ObjectGetCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "get"; }
|
||||
|
||||
[Command("object snapshot", Description = "Get a machine-oriented snapshot of an object, attributes, relationships, and script metadata")]
|
||||
public sealed class ObjectSnapshotCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "snapshot"; }
|
||||
|
||||
[Command("object lineage", Description = "Get object inheritance and containment lineage")]
|
||||
public sealed class ObjectLineageCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "lineage"; }
|
||||
|
||||
[Command("object children", Description = "Get contained or derived child objects")]
|
||||
public sealed class ObjectChildrenCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "children"; }
|
||||
|
||||
[Command("object query-name", Description = "Find objects by exact names")]
|
||||
public sealed class ObjectQueryNameCommand : RoutedCommandBase
|
||||
{
|
||||
public override string Command => "object";
|
||||
public override string Subcommand => "query-name";
|
||||
|
||||
[CommandOption("name", Description = "Object tagname. May be repeated.", IsRequired = true)]
|
||||
public IReadOnlyList<string> Names { get; init; }
|
||||
|
||||
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
|
||||
public string Type { get; init; } = "all";
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["name"] = Names; args["type"] = Type; return args; }
|
||||
}
|
||||
|
||||
[Command("object query-condition", Description = "Find objects by one GRAccess condition")]
|
||||
public sealed class ObjectQueryConditionCommand : RoutedCommandBase
|
||||
{
|
||||
public override string Command => "object";
|
||||
public override string Subcommand => "query-condition";
|
||||
|
||||
[CommandOption("condition", Description = "EConditionType enum value")]
|
||||
public string Condition { get; init; } = "namedLike";
|
||||
|
||||
[CommandOption("value", Description = "Condition value")]
|
||||
public string Value { get; init; } = "%";
|
||||
|
||||
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
|
||||
public string Type { get; init; } = "all";
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["condition"] = Condition; args["value"] = Value; args["type"] = Type; return args; }
|
||||
}
|
||||
|
||||
[Command("object query-multi", Description = "Find objects matching any supplied pattern")]
|
||||
public sealed class ObjectQueryMultiCommand : RoutedCommandBase
|
||||
{
|
||||
public override string Command => "object";
|
||||
public override string Subcommand => "query-multi";
|
||||
|
||||
[CommandOption("pattern", 'p', Description = "Name pattern. May be repeated.")]
|
||||
public IReadOnlyList<string> Patterns { get; init; } = Array.Empty<string>();
|
||||
|
||||
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
|
||||
public string Type { get; init; } = "all";
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["pattern"] = Patterns; args["type"] = Type; return args; }
|
||||
}
|
||||
|
||||
[Command("object extended-attributes", Description = "Get extended attributes for an object")]
|
||||
public sealed class ObjectExtendedAttributesCommand : ObjectCommandBase
|
||||
{
|
||||
public override string Command => "object";
|
||||
public override string Subcommand => "extended-attributes";
|
||||
|
||||
[CommandOption("attribute", Description = "Attribute name")]
|
||||
public string Attribute { get; init; } = "";
|
||||
|
||||
[CommandOption("level", Description = "Hierarchy level")]
|
||||
public int Level { get; init; }
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["attribute"] = Attribute; args["level"] = Level; return args; }
|
||||
}
|
||||
|
||||
[Command("object help-url", Description = "Get object help URL")]
|
||||
public sealed class ObjectHelpUrlCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "help-url"; }
|
||||
|
||||
[Command("object scripts list", Description = "List script-like metadata exposed on an object")]
|
||||
public sealed class ObjectScriptsListCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "scripts-list"; }
|
||||
|
||||
[Command("object scripts get", Description = "Get script metadata and body availability for an object script")]
|
||||
public class ObjectScriptsGetCommand : ObjectCommandBase
|
||||
{
|
||||
public override string Command => "object";
|
||||
public override string Subcommand => "scripts-get";
|
||||
|
||||
[CommandOption("script", Description = "Script name", IsRequired = true)]
|
||||
public string Script { get; init; }
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["script"] = Script; return args; }
|
||||
}
|
||||
|
||||
[Command("object scripts set", Description = "Set an object script body when supported by the local adapter")]
|
||||
public sealed class ObjectScriptsSetCommand : ObjectScriptsGetCommand
|
||||
{
|
||||
public override string Subcommand => "scripts-set";
|
||||
|
||||
[CommandOption("file", Description = "Script source file", IsRequired = true)]
|
||||
public string File { get; init; }
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; return args; }
|
||||
}
|
||||
|
||||
public abstract class ObjectScriptSettingsCommandBase : ObjectScriptsGetCommand
|
||||
{
|
||||
[CommandOption("trigger-period-ms", Description = "Periodic script trigger interval in milliseconds")]
|
||||
public string TriggerPeriodMs { get; init; } = "";
|
||||
|
||||
[CommandOption("trigger-type", Description = "Script trigger type value")]
|
||||
public string TriggerType { get; init; } = "";
|
||||
|
||||
[CommandOption("expression", Description = "Script expression value")]
|
||||
public string Expression { get; init; } = "";
|
||||
|
||||
[CommandOption("lock-trigger-period", Description = "Lock TriggerPeriod in this object after setting it")]
|
||||
public bool LockTriggerPeriod { get; init; }
|
||||
|
||||
public override Dictionary<string, object> Args()
|
||||
{
|
||||
var args = base.Args();
|
||||
args["trigger-period-ms"] = TriggerPeriodMs;
|
||||
args["trigger-type"] = TriggerType;
|
||||
args["expression"] = Expression;
|
||||
args["lock-trigger-period"] = LockTriggerPeriod;
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("object scripts create", Description = "Create a ScriptExtension primitive and optionally initialize its body/settings")]
|
||||
public sealed class ObjectScriptsCreateCommand : ObjectScriptSettingsCommandBase
|
||||
{
|
||||
public override string Subcommand => "scripts-create";
|
||||
|
||||
[CommandOption("file", Description = "Optional script source file")]
|
||||
public string File { get; init; } = "";
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["file"] = File; return args; }
|
||||
}
|
||||
|
||||
[Command("object scripts settings set", Description = "Set ScriptExtension settings through ConfigurableAttributes")]
|
||||
public sealed class ObjectScriptsSettingsSetCommand : ObjectScriptSettingsCommandBase
|
||||
{
|
||||
public override string Subcommand => "scripts-settings-set";
|
||||
}
|
||||
|
||||
[Command("object checkout", Description = "Check out an object")]
|
||||
public sealed class ObjectCheckoutCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "checkout"; }
|
||||
|
||||
[Command("object checkin", Description = "Check in an object")]
|
||||
public sealed class ObjectCheckinCommand : ObjectCommandBase
|
||||
{
|
||||
public override string Command => "object";
|
||||
public override string Subcommand => "checkin";
|
||||
|
||||
[CommandOption("comment", Description = "Check-in comment")]
|
||||
public string Comment { get; init; } = "";
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["comment"] = Comment; return args; }
|
||||
}
|
||||
|
||||
[Command("object undo-checkout", Description = "Undo object checkout")]
|
||||
public sealed class ObjectUndoCheckoutCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "undo-checkout"; }
|
||||
|
||||
[Command("object save", Description = "Save an object")]
|
||||
public sealed class ObjectSaveCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "save"; }
|
||||
|
||||
[Command("object unload", Description = "Unload an object from cache")]
|
||||
public sealed class ObjectUnloadCommand : ObjectCommandBase { public override string Command => "object"; public override string Subcommand => "unload"; }
|
||||
|
||||
[Command("object set", Description = "Set an object property")]
|
||||
public sealed class ObjectSetCommand : ObjectCommandBase
|
||||
{
|
||||
public override string Command => "object";
|
||||
public override string Subcommand => "set";
|
||||
|
||||
[CommandOption("property", Description = "tagname, contained-name, area, host, container, toolset, security-group", IsRequired = true)]
|
||||
public string Property { get; init; }
|
||||
|
||||
[CommandOption("value", Description = "New value", IsRequired = true)]
|
||||
public string Value { get; init; }
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["property"] = Property; args["value"] = Value; return args; }
|
||||
}
|
||||
|
||||
[Command("template derive", Description = "Create a derived template")]
|
||||
public sealed class TemplateDeriveCommand : ObjectCommandBase
|
||||
{
|
||||
public override string Command => "template";
|
||||
public override string Subcommand => "derive";
|
||||
[CommandOption("new-name", Description = "New template name", IsRequired = true)] public string NewName { get; init; }
|
||||
[CommandOption("create-contained", Description = "Create contained objects")] public bool CreateContained { get; init; }
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["new-name"] = NewName; args["create-contained"] = CreateContained; return args; }
|
||||
}
|
||||
|
||||
[Command("template instantiate", Description = "Create an instance from a template")]
|
||||
public sealed class TemplateInstantiateCommand : ObjectCommandBase
|
||||
{
|
||||
public override string Command => "template";
|
||||
public override string Subcommand => "instantiate";
|
||||
[CommandOption("new-name", Description = "New instance name", IsRequired = true)] public string NewName { get; init; }
|
||||
[CommandOption("create-contained", Description = "Create contained objects")] public bool CreateContained { get; init; }
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["new-name"] = NewName; args["create-contained"] = CreateContained; return args; }
|
||||
}
|
||||
|
||||
[Command("template delete", Description = "Delete a template")]
|
||||
public sealed class TemplateDeleteCommand : ObjectCommandBase
|
||||
{
|
||||
public override string Command => "template";
|
||||
public override string Subcommand => "delete";
|
||||
[CommandOption("force-option", Description = "EForceDeleteTemplateOption enum value")] public string ForceOption { get; init; } = "dontForceTemplateDelete";
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["force-option"] = ForceOption; return args; }
|
||||
}
|
||||
|
||||
public abstract class InstanceLifecycleCommandBase : ObjectCommandBase
|
||||
{
|
||||
[CommandOption("force-option", Description = "Delete force option")]
|
||||
public string ForceOption { get; init; } = "undeployIfDeployed";
|
||||
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["force-option"] = ForceOption; return args; }
|
||||
}
|
||||
|
||||
[Command("instance delete", Description = "Delete an instance")]
|
||||
public sealed class InstanceDeleteCommand : InstanceLifecycleCommandBase { public override string Command => "instance"; public override string Subcommand => "delete"; }
|
||||
|
||||
[Command("instance deploy", Description = "Deploy an instance")]
|
||||
public sealed class InstanceDeployCommand : ObjectCommandBase { public override string Command => "instance"; public override string Subcommand => "deploy"; }
|
||||
|
||||
[Command("instance undeploy", Description = "Undeploy an instance")]
|
||||
public sealed class InstanceUndeployCommand : ObjectCommandBase { public override string Command => "instance"; public override string Subcommand => "undeploy"; }
|
||||
|
||||
[Command("instance upload", Description = "Upload runtime changes for an instance")]
|
||||
public sealed class InstanceUploadCommand : ObjectCommandBase { public override string Command => "instance"; public override string Subcommand => "upload"; }
|
||||
|
||||
[Command("instance assign-area", Description = "Assign an instance to an area")]
|
||||
public sealed class InstanceAssignAreaCommand : ObjectCommandBase
|
||||
{
|
||||
public override string Command => "instance";
|
||||
public override string Subcommand => "assign-area";
|
||||
[CommandOption("area", Description = "Area instance name", IsRequired = true)] public string Area { get; init; }
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["area"] = Area; return args; }
|
||||
}
|
||||
|
||||
[Command("instance assign-engine", Description = "Assign an instance to an engine")]
|
||||
public sealed class InstanceAssignEngineCommand : ObjectCommandBase
|
||||
{
|
||||
public override string Command => "instance";
|
||||
public override string Subcommand => "assign-engine";
|
||||
[CommandOption("engine", Description = "Engine instance name", IsRequired = true)] public string Engine { get; init; }
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["engine"] = Engine; return args; }
|
||||
}
|
||||
|
||||
[Command("instance assign-container", Description = "Assign an instance to a container")]
|
||||
public sealed class InstanceAssignContainerCommand : ObjectCommandBase
|
||||
{
|
||||
public override string Command => "instance";
|
||||
public override string Subcommand => "assign-container";
|
||||
[CommandOption("container", Description = "Container instance name", IsRequired = true)] public string Container { get; init; }
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["container"] = Container; return args; }
|
||||
}
|
||||
|
||||
[Command("io assign", Description = "Assign an I/O-related instance attribute")]
|
||||
public sealed class IoAssignCommand : ConfirmedRoutedCommandBase
|
||||
{
|
||||
public override string Command => "io";
|
||||
public override string Subcommand => "assign";
|
||||
|
||||
[CommandOption("name", Description = "Instance tagname", IsRequired = true)]
|
||||
public string ObjectName { get; init; }
|
||||
|
||||
[CommandOption("attribute", Description = "Attribute name", IsRequired = true)]
|
||||
public string Attribute { get; init; }
|
||||
|
||||
[CommandOption("value", Description = "Assigned value", IsRequired = true)]
|
||||
public string Value { get; init; }
|
||||
|
||||
[CommandOption("data-type", Description = "string, bool, int, float, double")]
|
||||
public string DataType { get; init; } = "string";
|
||||
|
||||
public override Dictionary<string, object> Args()
|
||||
{
|
||||
var args = base.Args();
|
||||
args["name"] = ObjectName;
|
||||
args["attribute"] = Attribute;
|
||||
args["value"] = Value;
|
||||
args["data-type"] = DataType;
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class AreaEngineCommandBase : ConfirmedRoutedCommandBase
|
||||
{
|
||||
[CommandOption("template", Description = "Template to instantiate", IsRequired = true)]
|
||||
public string Template { get; init; }
|
||||
|
||||
[CommandOption("name", Description = "New instance name", IsRequired = true)]
|
||||
public string Name { get; init; }
|
||||
|
||||
[CommandOption("create-contained", Description = "Create contained objects")]
|
||||
public bool CreateContained { get; init; }
|
||||
|
||||
public override Dictionary<string, object> Args()
|
||||
{
|
||||
var args = base.Args();
|
||||
args["template"] = Template;
|
||||
args["name"] = Name;
|
||||
args["create-contained"] = CreateContained;
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("area list", Description = "List area-like instances")]
|
||||
public sealed class AreaListCommand : RoutedCommandBase
|
||||
{
|
||||
public override string Command => "area";
|
||||
public override string Subcommand => "list";
|
||||
[CommandOption("pattern", 'p', Description = "Name pattern. Use % as wildcard.")] public string Pattern { get; init; } = "%Area%";
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["pattern"] = Pattern; return args; }
|
||||
}
|
||||
|
||||
[Command("area create", Description = "Create an area instance from a template")]
|
||||
public sealed class AreaCreateCommand : AreaEngineCommandBase { public override string Command => "area"; public override string Subcommand => "create"; }
|
||||
|
||||
[Command("engine list", Description = "List engine-like instances")]
|
||||
public sealed class EngineListCommand : RoutedCommandBase
|
||||
{
|
||||
public override string Command => "engine";
|
||||
public override string Subcommand => "list";
|
||||
[CommandOption("pattern", 'p', Description = "Name pattern. Use % as wildcard.")] public string Pattern { get; init; } = "%Engine%";
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["pattern"] = Pattern; return args; }
|
||||
}
|
||||
|
||||
[Command("engine create", Description = "Create an engine instance from a template")]
|
||||
public sealed class EngineCreateCommand : AreaEngineCommandBase { public override string Command => "engine"; public override string Subcommand => "create"; }
|
||||
|
||||
public abstract class AttributeCommandBase : ObjectCommandBase
|
||||
{
|
||||
[CommandOption("attribute", Description = "Attribute name", IsRequired = true)]
|
||||
public string Attribute { get; init; }
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["attribute"] = Attribute; return args; }
|
||||
}
|
||||
|
||||
[Command("object attribute get", Description = "Get one attribute")]
|
||||
public sealed class ObjectAttributeGetCommand : AttributeCommandBase { public override string Command => "object"; public override string Subcommand => "attribute-get"; }
|
||||
|
||||
[Command("object attribute set", Description = "Set one attribute value")]
|
||||
public sealed class ObjectAttributeSetCommand : AttributeCommandBase
|
||||
{
|
||||
public override string Command => "object";
|
||||
public override string Subcommand => "attribute-set";
|
||||
[CommandOption("value", Description = "New value", IsRequired = true)] public string Value { get; init; }
|
||||
[CommandOption("data-type", Description = "string, bool, int, float, double")] public string DataType { get; init; } = "string";
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["value"] = Value; args["data-type"] = DataType; return args; }
|
||||
}
|
||||
|
||||
[Command("object attribute value get", Description = "Read back one attribute value with scalar type metadata")]
|
||||
public sealed class ObjectAttributeValueGetCommand : AttributeCommandBase { public override string Command => "object"; public override string Subcommand => "attribute-value-get"; }
|
||||
|
||||
[Command("object attribute value set", Description = "Set one scalar attribute value")]
|
||||
public sealed class ObjectAttributeValueSetCommand : AttributeCommandBase
|
||||
{
|
||||
public override string Command => "object";
|
||||
public override string Subcommand => "attribute-value-set";
|
||||
[CommandOption("value", Description = "New value", IsRequired = true)] public string Value { get; init; }
|
||||
[CommandOption("data-type", Description = "string, bool, int, float, double")] public string DataType { get; init; } = "string";
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["value"] = Value; args["data-type"] = DataType; return args; }
|
||||
}
|
||||
|
||||
[Command("object attribute lock", Description = "Set attribute locked state")]
|
||||
public sealed class ObjectAttributeLockCommand : AttributeCommandBase
|
||||
{
|
||||
public override string Command => "object"; public override string Subcommand => "attribute-lock";
|
||||
[CommandOption("locked", Description = "MxPropertyLockedEnum value", IsRequired = true)] public string Locked { get; init; }
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["locked"] = Locked; return args; }
|
||||
}
|
||||
|
||||
[Command("object attribute security", Description = "Set attribute security classification")]
|
||||
public sealed class ObjectAttributeSecurityCommand : AttributeCommandBase
|
||||
{
|
||||
public override string Command => "object"; public override string Subcommand => "attribute-security";
|
||||
[CommandOption("security", Description = "MxSecurityClassification value", IsRequired = true)] public string Security { get; init; }
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["security"] = Security; return args; }
|
||||
}
|
||||
|
||||
[Command("object attribute buffer", Description = "Set attribute buffer flag")]
|
||||
public sealed class ObjectAttributeBufferCommand : AttributeCommandBase
|
||||
{
|
||||
public override string Command => "object"; public override string Subcommand => "attribute-buffer";
|
||||
[CommandOption("has-buffer", Description = "Whether the attribute has a buffer")] public bool HasBuffer { get; init; }
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["has-buffer"] = HasBuffer; return args; }
|
||||
}
|
||||
|
||||
public abstract class UdaCommandBase : ObjectCommandBase
|
||||
{
|
||||
[CommandOption("uda", Description = "UDA name", IsRequired = true)] public string Uda { get; init; }
|
||||
[CommandOption("data-type", Description = "MxDataType value")] public string DataType { get; init; } = "MxString";
|
||||
[CommandOption("category", Description = "MxAttributeCategory value")] public string Category { get; init; } = "MxCategoryWriteable_USC";
|
||||
[CommandOption("security", Description = "MxSecurityClassification value")] public string Security { get; init; } = "MxSecurityUndefined";
|
||||
[CommandOption("is-array", Description = "Create/update as array")] public bool IsArray { get; init; }
|
||||
[CommandOption("array-count", Description = "Array element count")] public int ArrayCount { get; init; }
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["uda"] = Uda; args["data-type"] = DataType; args["category"] = Category; args["security"] = Security; args["is-array"] = IsArray; args["array-count"] = ArrayCount; return args; }
|
||||
}
|
||||
|
||||
[Command("object uda add", Description = "Add a UDA")]
|
||||
public sealed class ObjectUdaAddCommand : UdaCommandBase { public override string Command => "object"; public override string Subcommand => "uda-add"; }
|
||||
[Command("object uda delete", Description = "Delete a UDA")]
|
||||
public sealed class ObjectUdaDeleteCommand : UdaCommandBase { public override string Command => "object"; public override string Subcommand => "uda-delete"; }
|
||||
[Command("object uda rename", Description = "Rename a UDA")]
|
||||
public sealed class ObjectUdaRenameCommand : UdaCommandBase { public override string Command => "object"; public override string Subcommand => "uda-rename"; [CommandOption("new-name", Description = "New UDA name", IsRequired = true)] public string NewName { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["new-name"] = NewName; return args; } }
|
||||
[Command("object uda update", Description = "Update a UDA")]
|
||||
public sealed class ObjectUdaUpdateCommand : UdaCommandBase { public override string Command => "object"; public override string Subcommand => "uda-update"; }
|
||||
|
||||
public abstract class ExtensionCommandBase : ObjectCommandBase
|
||||
{
|
||||
[CommandOption("extension-type", Description = "Extension type", IsRequired = true)] public string ExtensionType { get; init; }
|
||||
[CommandOption("primitive", Description = "Primitive name", IsRequired = true)] public string Primitive { get; init; }
|
||||
[CommandOption("object-extension", Description = "Whether this is an object extension")] public bool ObjectExtension { get; init; }
|
||||
public override Dictionary<string, object> Args() { var args = base.Args(); args["extension-type"] = ExtensionType; args["primitive"] = Primitive; args["object-extension"] = ObjectExtension; return args; }
|
||||
}
|
||||
|
||||
[Command("object extension add", Description = "Add an extension primitive")]
|
||||
public sealed class ObjectExtensionAddCommand : ExtensionCommandBase { public override string Command => "object"; public override string Subcommand => "extension-add"; }
|
||||
[Command("object extension delete", Description = "Delete an extension primitive")]
|
||||
public sealed class ObjectExtensionDeleteCommand : ExtensionCommandBase { public override string Command => "object"; public override string Subcommand => "extension-delete"; }
|
||||
[Command("object extension rename", Description = "Rename an extension primitive")]
|
||||
public sealed class ObjectExtensionRenameCommand : ExtensionCommandBase { public override string Command => "object"; public override string Subcommand => "extension-rename"; [CommandOption("new-name", Description = "New primitive name", IsRequired = true)] public string NewName { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["new-name"] = NewName; return args; } }
|
||||
|
||||
[Command("objects checkout", Description = "Check out multiple objects")]
|
||||
public sealed class ObjectsCheckoutCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "checkout"; }
|
||||
[Command("objects checkin", Description = "Check in multiple objects")]
|
||||
public sealed class ObjectsCheckinCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "checkin"; [CommandOption("comment", Description = "Check-in comment")] public string Comment { get; init; } = ""; public override Dictionary<string, object> Args() { var args = base.Args(); args["comment"] = Comment; return args; } }
|
||||
[Command("objects undo-checkout", Description = "Undo checkout for multiple objects")]
|
||||
public sealed class ObjectsUndoCheckoutCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "undo-checkout"; }
|
||||
[Command("objects deploy", Description = "Deploy multiple instances")]
|
||||
public sealed class ObjectsDeployCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "deploy"; }
|
||||
[Command("objects undeploy", Description = "Undeploy multiple instances")]
|
||||
public sealed class ObjectsUndeployCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "undeploy"; }
|
||||
[Command("objects upload", Description = "Upload multiple instances")]
|
||||
public sealed class ObjectsUploadCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "upload"; }
|
||||
[Command("objects delete", Description = "Delete multiple objects")]
|
||||
public sealed class ObjectsDeleteCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "delete"; }
|
||||
[Command("objects export", Description = "Export multiple objects")]
|
||||
public sealed class ObjectsExportCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "export"; [CommandOption("output", Description = "Output file path", IsRequired = true)] public string Output { get; init; } [CommandOption("export-type", Description = "EExportType value")] public string ExportType { get; init; } = "exportGalaxyDump"; public override Dictionary<string, object> Args() { var args = base.Args(); args["output"] = Output; args["export-type"] = ExportType; return args; } }
|
||||
[Command("objects export-protected", Description = "Export multiple objects as protected")]
|
||||
public sealed class ObjectsExportProtectedCommand : BulkObjectsCommandBase { public override string Command => "objects"; public override string Subcommand => "export-protected"; [CommandOption("output", Description = "Output file path", IsRequired = true)] public string Output { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["output"] = Output; return args; } }
|
||||
|
||||
[Command("toolset list", Description = "List toolsets")]
|
||||
public sealed class ToolsetListCommand : RoutedCommandBase { public override string Command => "toolset"; public override string Subcommand => "list"; }
|
||||
[Command("toolset tree", Description = "Show toolset tree")]
|
||||
public sealed class ToolsetTreeCommand : RoutedCommandBase { public override string Command => "toolset"; public override string Subcommand => "tree"; }
|
||||
[Command("toolset add", Description = "Add a toolset")]
|
||||
public class ToolsetAddCommand : ConfirmedRoutedCommandBase { public override string Command => "toolset"; public override string Subcommand => "add"; [CommandOption("name", Description = "Toolset name", IsRequired = true)] public string Name { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["name"] = Name; return args; } }
|
||||
[Command("toolset delete", Description = "Delete a toolset")]
|
||||
public sealed class ToolsetDeleteCommand : ToolsetAddCommand { public override string Subcommand => "delete"; }
|
||||
[Command("toolset rename", Description = "Rename a toolset")]
|
||||
public sealed class ToolsetRenameCommand : ToolsetAddCommand { public override string Subcommand => "rename"; [CommandOption("new-name", Description = "New toolset name", IsRequired = true)] public string NewName { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["new-name"] = NewName; return args; } }
|
||||
[Command("toolset move", Description = "Move a toolset")]
|
||||
public sealed class ToolsetMoveCommand : ToolsetAddCommand { public override string Subcommand => "move"; [CommandOption("parent", Description = "Parent toolset name", IsRequired = true)] public string Parent { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["parent"] = Parent; return args; } }
|
||||
|
||||
[Command("script-library list", Description = "List script libraries")]
|
||||
public sealed class ScriptLibraryListCommand : RoutedCommandBase { public override string Command => "script-library"; public override string Subcommand => "list"; }
|
||||
[Command("script-library add", Description = "Add/import a script library")]
|
||||
public class ScriptLibraryAddCommand : ConfirmedRoutedCommandBase { public override string Command => "script-library"; public override string Subcommand => "add"; [CommandOption("path", Description = "Script library path", IsRequired = true)] public string Path { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["path"] = Path; return args; } }
|
||||
[Command("script-library import", Description = "Import a script library")]
|
||||
public sealed class ScriptLibraryImportCommand : ScriptLibraryAddCommand { public override string Subcommand => "import"; }
|
||||
[Command("script-library export", Description = "Export a script library")]
|
||||
public sealed class ScriptLibraryExportCommand : ConfirmedRoutedCommandBase { public override string Command => "script-library"; public override string Subcommand => "export"; [CommandOption("name", Description = "Script library name", IsRequired = true)] public string Name { get; init; } [CommandOption("output", Description = "Output path", IsRequired = true)] public string Output { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["name"] = Name; args["output"] = Output; return args; } }
|
||||
|
||||
[Command("security info", Description = "Show security settings")]
|
||||
public sealed class SecurityInfoCommand : RoutedCommandBase { public override string Command => "security"; public override string Subcommand => "info"; }
|
||||
[Command("security roles", Description = "List security roles")]
|
||||
public sealed class SecurityRolesCommand : RoutedCommandBase { public override string Command => "security"; public override string Subcommand => "roles"; }
|
||||
[Command("security users", Description = "List security users")]
|
||||
public sealed class SecurityUsersCommand : RoutedCommandBase { public override string Command => "security"; public override string Subcommand => "users"; }
|
||||
[Command("security groups", Description = "List security groups")]
|
||||
public sealed class SecurityGroupsCommand : RoutedCommandBase { public override string Command => "security"; public override string Subcommand => "groups"; }
|
||||
[Command("security permissions", Description = "List permissions for a role")]
|
||||
public sealed class SecurityPermissionsCommand : RoutedCommandBase { public override string Command => "security"; public override string Subcommand => "permissions"; [CommandOption("role", Description = "Role name", IsRequired = true)] public string Role { get; init; } public override Dictionary<string, object> Args() { var args = base.Args(); args["role"] = Role; return args; } }
|
||||
|
||||
[Command("settings locale get", Description = "Get locale settings")]
|
||||
public sealed class SettingsLocaleGetCommand : RoutedCommandBase { public override string Command => "settings"; public override string Subcommand => "locale-get"; }
|
||||
[Command("settings time-master get", Description = "Get time master settings")]
|
||||
public sealed class SettingsTimeMasterGetCommand : RoutedCommandBase { public override string Command => "settings"; public override string Subcommand => "time-master-get"; }
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using ArchestrA.GRAccess;
|
||||
using Newtonsoft.Json;
|
||||
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Protocol;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Session;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Galaxy
|
||||
{
|
||||
[Command("galaxy list", Description = "List available galaxies on a GR node")]
|
||||
public class GalaxyListCommand : ICommand
|
||||
{
|
||||
[CommandOption("node", 'n', Description = "GR node name (blank = local node)")]
|
||||
public string NodeName { get; init; } = "";
|
||||
|
||||
[CommandOption("json", Description = "Output as JSON")]
|
||||
public bool Json { get; init; }
|
||||
|
||||
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
|
||||
public bool LlmJson { get; init; }
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
// This command always runs one-shot — no galaxy login needed,
|
||||
// so no session routing (session is galaxy-specific).
|
||||
var galaxyNames = ListGalaxies(NodeName);
|
||||
|
||||
if (LlmJson)
|
||||
{
|
||||
await console.Output.WriteLineAsync(LlmResponse.Ok("galaxy list", string.Empty, NodeName, galaxyNames)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Json)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(galaxyNames, Formatting.Indented);
|
||||
await console.Output.WriteLineAsync(json).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var name in galaxyNames)
|
||||
await console.Output.WriteLineAsync(name).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
internal static List<string> ListGalaxies(string nodeName)
|
||||
{
|
||||
nodeName = GRAccessDiagnostics.NormalizeNodeName(nodeName);
|
||||
|
||||
var grAccess = new GRAccessAppClass();
|
||||
|
||||
var galaxies = grAccess.QueryGalaxies(nodeName);
|
||||
|
||||
var result = grAccess.CommandResult;
|
||||
if (result != null && !result.Successful)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
GRAccessDiagnostics.FormatCommandResult("QueryGalaxies", result));
|
||||
}
|
||||
|
||||
var names = new List<string>();
|
||||
if (galaxies != null)
|
||||
{
|
||||
for (int i = 1; i <= galaxies.count; i++)
|
||||
{
|
||||
var galaxy = galaxies[i];
|
||||
if (galaxy != null)
|
||||
names.Add(galaxy.Name);
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Protocol;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Session;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Commands
|
||||
{
|
||||
[Command("capabilities", Description = "List machine-readable command capabilities")]
|
||||
public sealed class CapabilitiesCommand : ICommand
|
||||
{
|
||||
[CommandOption("json", Description = "Output as JSON")]
|
||||
public bool Json { get; init; }
|
||||
|
||||
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
|
||||
public bool LlmJson { get; init; }
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var data = new
|
||||
{
|
||||
Version = 1,
|
||||
Commands = CommandCapabilityRegistry.Commands
|
||||
};
|
||||
|
||||
if (LlmJson)
|
||||
{
|
||||
await console.Output.WriteLineAsync(LlmResponse.Ok("capabilities", string.Empty, string.Empty, data)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Json)
|
||||
{
|
||||
await console.Output.WriteLineAsync(JsonConvert.SerializeObject(data, Formatting.Indented)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var command in CommandCapabilityRegistry.Commands)
|
||||
await console.Output.WriteLineAsync($"{command.Name}\tmutates={command.Mutates}\tsession={command.RoutesThroughSession}").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("validate", Description = "Validate a machine command or batch plan JSON file")]
|
||||
public sealed class ValidateCommand : ICommand
|
||||
{
|
||||
[CommandOption("request", Description = "Plan JSON file path", IsRequired = true)]
|
||||
public string Request { get; init; }
|
||||
|
||||
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
|
||||
public bool LlmJson { get; init; }
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var plan = BatchPlan.Load(Request);
|
||||
var result = BatchValidator.Validate(plan, requireMutationConfirmation: true);
|
||||
var output = new { result.Valid, Steps = result.Steps };
|
||||
|
||||
if (LlmJson)
|
||||
await console.Output.WriteLineAsync(LlmResponse.Ok("validate", plan.Galaxy, Request, output)).ConfigureAwait(false);
|
||||
else
|
||||
await console.Output.WriteLineAsync(JsonConvert.SerializeObject(output, Formatting.Indented)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("batch", Description = "Validate or execute a machine command plan")]
|
||||
public sealed class BatchCommand : ICommand
|
||||
{
|
||||
[CommandOption("file", Description = "Plan JSON file path", IsRequired = true)]
|
||||
public string File { get; init; }
|
||||
|
||||
[CommandOption("mode", Description = "validate or execute")]
|
||||
public string Mode { get; init; } = "validate";
|
||||
|
||||
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
|
||||
public bool LlmJson { get; init; }
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var plan = BatchPlan.Load(File);
|
||||
var validation = BatchValidator.Validate(plan, requireMutationConfirmation: true);
|
||||
|
||||
if (!string.Equals(Mode, "execute", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await WriteBatchOutput(console, plan, new { validation.Valid, Steps = validation.Steps }, validation.Valid).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validation.Valid)
|
||||
{
|
||||
await WriteBatchOutput(console, plan, new { validation.Valid, Steps = validation.Steps }, false).ConfigureAwait(false);
|
||||
throw new CommandException("Batch validation failed.", 1);
|
||||
}
|
||||
|
||||
var outputs = new List<object>();
|
||||
for (var i = 0; i < plan.Commands.Count; i++)
|
||||
{
|
||||
var step = plan.Commands[i];
|
||||
try
|
||||
{
|
||||
var output = await ExecuteStepAsync(plan, step).ConfigureAwait(false);
|
||||
outputs.Add(new { Index = i, step.Command, Success = true, Output = ParseOutput(output) });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
outputs.Add(new { Index = i, step.Command, Success = false, Error = ex.Message });
|
||||
await WriteBatchOutput(console, plan, new { Valid = true, Executed = outputs }, false).ConfigureAwait(false);
|
||||
throw new CommandException(ex.Message, 1);
|
||||
}
|
||||
}
|
||||
|
||||
await WriteBatchOutput(console, plan, new { Valid = true, Executed = outputs }, true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task WriteBatchOutput(IConsole console, BatchPlan plan, object data, bool success)
|
||||
{
|
||||
if (LlmJson)
|
||||
{
|
||||
var response = new LlmResponse
|
||||
{
|
||||
Success = success,
|
||||
Command = "batch",
|
||||
Galaxy = plan.Galaxy ?? string.Empty,
|
||||
Target = File ?? string.Empty,
|
||||
Data = data,
|
||||
ExitCode = success ? 0 : 1
|
||||
};
|
||||
await console.Output.WriteLineAsync(LlmResponse.Serialize(response)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await console.Output.WriteLineAsync(JsonConvert.SerializeObject(data, Formatting.Indented)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<string> ExecuteStepAsync(BatchPlan plan, BatchStep step)
|
||||
{
|
||||
var args = step.MaterializedArgs(plan);
|
||||
args["llm-json"] = true;
|
||||
args["json"] = false;
|
||||
|
||||
var split = CommandCapabilityRegistry.SplitCommandName(step.Command);
|
||||
var galaxy = ReadString(args, "galaxy", plan.Galaxy);
|
||||
var node = ReadString(args, "node", plan.Node);
|
||||
|
||||
if (SessionClient.TryConnect(galaxy, out var client))
|
||||
{
|
||||
using (client)
|
||||
{
|
||||
var response = await client.SendCommandAsync(PipeRequest.Execute(split.Command, split.Subcommand, args)).ConfigureAwait(false);
|
||||
if (!response.Success)
|
||||
throw new InvalidOperationException(response.Error);
|
||||
return response.Output;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(node))
|
||||
throw new InvalidOperationException("No active session found. Provide a top-level or step node for one-shot batch execution.");
|
||||
|
||||
using (var connection = new GRAccessConnection(galaxy, node))
|
||||
{
|
||||
connection.Connect();
|
||||
return GRAccessCommandDispatcher.Execute(connection.Galaxy, split.Command, split.Subcommand, args);
|
||||
}
|
||||
}
|
||||
|
||||
private static object ParseOutput(string output)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
return null;
|
||||
|
||||
try { return JToken.Parse(output); }
|
||||
catch { return output; }
|
||||
}
|
||||
|
||||
private static string ReadString(IDictionary<string, object> args, string key, string fallback)
|
||||
{
|
||||
return args.TryGetValue(key, out var value) && value != null
|
||||
? Convert.ToString(value) ?? string.Empty
|
||||
: fallback ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class BatchPlan
|
||||
{
|
||||
public string Galaxy { get; set; } = string.Empty;
|
||||
public string Node { get; set; } = string.Empty;
|
||||
public List<BatchStep> Commands { get; set; } = new List<BatchStep>();
|
||||
|
||||
public static BatchPlan Load(string path)
|
||||
{
|
||||
var root = JObject.Parse(System.IO.File.ReadAllText(path));
|
||||
if (root["commands"] == null && root["command"] != null)
|
||||
root = new JObject { ["commands"] = new JArray(root) };
|
||||
|
||||
var plan = root.ToObject<BatchPlan>() ?? new BatchPlan();
|
||||
if (plan.Commands == null)
|
||||
plan.Commands = new List<BatchStep>();
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class BatchStep
|
||||
{
|
||||
public string Command { get; set; } = string.Empty;
|
||||
public Dictionary<string, object> Args { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
public Dictionary<string, object> MaterializedArgs(BatchPlan plan)
|
||||
{
|
||||
var args = Args == null
|
||||
? new Dictionary<string, object>()
|
||||
: new Dictionary<string, object>(Args, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (!args.ContainsKey("galaxy") && !string.IsNullOrWhiteSpace(plan.Galaxy))
|
||||
args["galaxy"] = plan.Galaxy;
|
||||
if (!args.ContainsKey("node") && !string.IsNullOrWhiteSpace(plan.Node))
|
||||
args["node"] = plan.Node;
|
||||
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class BatchValidationSummary
|
||||
{
|
||||
public bool Valid => Steps.All(s => s.Valid);
|
||||
public List<BatchStepValidation> Steps { get; } = new List<BatchStepValidation>();
|
||||
}
|
||||
|
||||
public sealed class BatchStepValidation
|
||||
{
|
||||
public int Index { get; set; }
|
||||
public string Command { get; set; } = string.Empty;
|
||||
public bool Valid { get; set; }
|
||||
public IReadOnlyList<string> Errors { get; set; } = Array.Empty<string>();
|
||||
public bool Mutates { get; set; }
|
||||
public string ConfirmationTargetRule { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public static class BatchValidator
|
||||
{
|
||||
public static BatchValidationSummary Validate(BatchPlan plan, bool requireMutationConfirmation)
|
||||
{
|
||||
var summary = new BatchValidationSummary();
|
||||
for (var i = 0; i < plan.Commands.Count; i++)
|
||||
{
|
||||
var step = plan.Commands[i];
|
||||
var args = step.MaterializedArgs(plan);
|
||||
var result = CommandCapabilityRegistry.Validate(step.Command, args, requireMutationConfirmation);
|
||||
summary.Steps.Add(new BatchStepValidation
|
||||
{
|
||||
Index = i,
|
||||
Command = step.Command,
|
||||
Valid = result.Valid,
|
||||
Errors = result.Errors,
|
||||
Mutates = result.Capability?.Mutates ?? false,
|
||||
ConfirmationTargetRule = result.Capability?.ConfirmTarget ?? string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
if (plan.Commands.Count == 0)
|
||||
summary.Steps.Add(new BatchStepValidation { Index = -1, Command = string.Empty, Valid = false, Errors = new[] { "Plan contains no commands." }, Mutates = false, ConfirmationTargetRule = string.Empty });
|
||||
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Objects
|
||||
{
|
||||
[Command("instance list", Description = "List instances in a galaxy")]
|
||||
public class InstanceListCommand : ICommand
|
||||
{
|
||||
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
|
||||
public string GalaxyName { get; init; }
|
||||
|
||||
[CommandOption("node", 'n', Description = "GR node name. Blank = local node; . = local machine")]
|
||||
public string NodeName { get; init; } = "";
|
||||
|
||||
[CommandOption("pattern", 'p', Description = "Instance name pattern for GRAccess namedLike query. Use % as wildcard.")]
|
||||
public string Pattern { get; init; } = "%";
|
||||
|
||||
[CommandOption("json", Description = "Output as JSON")]
|
||||
public bool Json { get; init; }
|
||||
|
||||
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
|
||||
public bool LlmJson { get; init; }
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var args = new Dictionary<string, object>
|
||||
{
|
||||
["pattern"] = Pattern,
|
||||
["json"] = Json.ToString(),
|
||||
["llm-json"] = LlmJson
|
||||
};
|
||||
|
||||
await CommandRouter.ExecuteAsync(
|
||||
console,
|
||||
GalaxyName,
|
||||
NodeName,
|
||||
"instance",
|
||||
"list",
|
||||
args,
|
||||
galaxy => GRAccessCommandDispatcher.Execute(galaxy, "instance", "list", args))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Objects
|
||||
{
|
||||
[Command("object attributes", Description = "List attributes for a template or instance")]
|
||||
public class ObjectAttributesCommand : ICommand
|
||||
{
|
||||
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
|
||||
public string GalaxyName { get; init; }
|
||||
|
||||
[CommandOption("node", 'n', Description = "GR node name. Blank = local node; . = local machine")]
|
||||
public string NodeName { get; init; } = "";
|
||||
|
||||
[CommandOption("name", Description = "Template or instance tagname", IsRequired = true)]
|
||||
public string ObjectName { get; init; }
|
||||
|
||||
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
|
||||
public string Type { get; init; } = "all";
|
||||
|
||||
[CommandOption("configurable", Description = "List ConfigurableAttributes instead of all Attributes")]
|
||||
public bool ConfigurableOnly { get; init; }
|
||||
|
||||
[CommandOption("json", Description = "Output as JSON")]
|
||||
public bool Json { get; init; }
|
||||
|
||||
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
|
||||
public bool LlmJson { get; init; }
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var args = new Dictionary<string, object>
|
||||
{
|
||||
["name"] = ObjectName,
|
||||
["type"] = Type,
|
||||
["configurable"] = ConfigurableOnly.ToString(),
|
||||
["json"] = Json.ToString(),
|
||||
["llm-json"] = LlmJson
|
||||
};
|
||||
|
||||
await CommandRouter.ExecuteAsync(
|
||||
console,
|
||||
GalaxyName,
|
||||
NodeName,
|
||||
"object",
|
||||
"attributes",
|
||||
args,
|
||||
galaxy => GRAccessCommandDispatcher.Execute(galaxy, "object", "attributes", args))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Objects
|
||||
{
|
||||
[Command("object list", Description = "List templates and/or instances in a galaxy")]
|
||||
public class ObjectListCommand : ICommand
|
||||
{
|
||||
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
|
||||
public string GalaxyName { get; init; }
|
||||
|
||||
[CommandOption("node", 'n', Description = "GR node name. Blank = local node; . = local machine")]
|
||||
public string NodeName { get; init; } = "";
|
||||
|
||||
[CommandOption("type", 't', Description = "Object type: all, template, instance")]
|
||||
public string Type { get; init; } = "all";
|
||||
|
||||
[CommandOption("pattern", 'p', Description = "Name pattern for GRAccess namedLike query. Use % as wildcard.")]
|
||||
public string Pattern { get; init; } = "%";
|
||||
|
||||
[CommandOption("json", Description = "Output as JSON")]
|
||||
public bool Json { get; init; }
|
||||
|
||||
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
|
||||
public bool LlmJson { get; init; }
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var args = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = Type,
|
||||
["pattern"] = Pattern,
|
||||
["json"] = Json.ToString(),
|
||||
["llm-json"] = LlmJson
|
||||
};
|
||||
|
||||
await CommandRouter.ExecuteAsync(
|
||||
console,
|
||||
GalaxyName,
|
||||
NodeName,
|
||||
"object",
|
||||
"list",
|
||||
args,
|
||||
galaxy => GRAccessCommandDispatcher.Execute(galaxy, "object", "list", args))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal static GRAccessObjectKind ParseKind(string value)
|
||||
{
|
||||
return GRAccessQueryCommandHandler.ParseKind(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Objects
|
||||
{
|
||||
[Command("template list", Description = "List templates in a galaxy")]
|
||||
public class TemplateListCommand : ICommand
|
||||
{
|
||||
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
|
||||
public string GalaxyName { get; init; }
|
||||
|
||||
[CommandOption("node", 'n', Description = "GR node name. Blank = local node; . = local machine")]
|
||||
public string NodeName { get; init; } = "";
|
||||
|
||||
[CommandOption("pattern", 'p', Description = "Template name pattern for GRAccess namedLike query. Use % as wildcard.")]
|
||||
public string Pattern { get; init; } = "%";
|
||||
|
||||
[CommandOption("json", Description = "Output as JSON")]
|
||||
public bool Json { get; init; }
|
||||
|
||||
[CommandOption("llm-json", Description = "Output stable machine JSON envelope")]
|
||||
public bool LlmJson { get; init; }
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var args = new Dictionary<string, object>
|
||||
{
|
||||
["pattern"] = Pattern,
|
||||
["json"] = Json.ToString(),
|
||||
["llm-json"] = LlmJson
|
||||
};
|
||||
|
||||
await CommandRouter.ExecuteAsync(
|
||||
console,
|
||||
GalaxyName,
|
||||
NodeName,
|
||||
"template",
|
||||
"list",
|
||||
args,
|
||||
galaxy => GRAccessCommandDispatcher.Execute(galaxy, "template", "list", args))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Protocol;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Session;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Session
|
||||
{
|
||||
[Command("session start", Description = "Start a background GRAccess session")]
|
||||
public class SessionStartCommand : ICommand
|
||||
{
|
||||
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
|
||||
public string GalaxyName { get; init; }
|
||||
|
||||
[CommandOption("node", 'n', Description = "GR node name", IsRequired = true)]
|
||||
public string NodeName { get; init; }
|
||||
|
||||
[CommandOption("idle-timeout", Description = "Idle timeout in minutes")]
|
||||
public int IdleTimeoutMinutes { get; init; } = 30;
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var nodeName = GRAccessDiagnostics.NormalizeNodeName(NodeName);
|
||||
|
||||
// Check if session already running
|
||||
var existing = SessionInfo.Load(GalaxyName);
|
||||
if (existing != null && existing.IsAlive())
|
||||
{
|
||||
await console.Output.WriteLineAsync(
|
||||
$"Session already running for galaxy '{GalaxyName}' (PID {existing.ProcessId})")
|
||||
.ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up stale session file
|
||||
existing?.Delete();
|
||||
|
||||
// Launch self as background daemon
|
||||
var exePath = Assembly.GetExecutingAssembly().Location;
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
Arguments = $"--daemon --galaxy \"{GalaxyName}\" --node \"{nodeName}\" --idle-timeout {IdleTimeoutMinutes}",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
};
|
||||
var process = Process.Start(startInfo);
|
||||
if (process == null)
|
||||
throw new InvalidOperationException("Failed to start session daemon process.");
|
||||
|
||||
// Wait for daemon to become ready
|
||||
var deadline = DateTime.UtcNow.AddSeconds(90);
|
||||
bool ready = false;
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
if (SessionClient.TryConnect(GalaxyName, out var client))
|
||||
{
|
||||
client.Dispose();
|
||||
ready = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if daemon process exited early (connection failure, etc.)
|
||||
if (process.HasExited)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Daemon process exited with code {process.ExitCode}. Check logs for details.");
|
||||
}
|
||||
}
|
||||
|
||||
if (ready)
|
||||
{
|
||||
await console.Output.WriteLineAsync(
|
||||
$"Session started for galaxy '{GalaxyName}' (PID {process.Id})")
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException(
|
||||
"Session daemon did not become ready within 90 seconds. Check logs for details.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Session;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Session
|
||||
{
|
||||
[Command("session status", Description = "Show GRAccess session status")]
|
||||
public class SessionStatusCommand : ICommand
|
||||
{
|
||||
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
|
||||
public string GalaxyName { get; init; }
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var info = SessionInfo.Load(GalaxyName);
|
||||
if (info == null)
|
||||
{
|
||||
await console.Output.WriteLineAsync("No session found.").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!info.IsAlive())
|
||||
{
|
||||
await console.Output.WriteLineAsync(
|
||||
"Session file exists but daemon is not running (stale). Cleaning up.")
|
||||
.ConfigureAwait(false);
|
||||
info.Delete();
|
||||
return;
|
||||
}
|
||||
|
||||
await console.Output.WriteLineAsync($"Galaxy: {info.GalaxyName}").ConfigureAwait(false);
|
||||
await console.Output.WriteLineAsync($"Node: {info.NodeName}").ConfigureAwait(false);
|
||||
await console.Output.WriteLineAsync($"PID: {info.ProcessId}").ConfigureAwait(false);
|
||||
await console.Output.WriteLineAsync($"Started: {info.StartedAtUtc:u}").ConfigureAwait(false);
|
||||
await console.Output.WriteLineAsync($"Pipe: {info.PipeName}").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Session;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Commands.Session
|
||||
{
|
||||
[Command("session stop", Description = "Stop the background GRAccess session")]
|
||||
public class SessionStopCommand : ICommand
|
||||
{
|
||||
[CommandOption("galaxy", 'g', Description = "Galaxy name", IsRequired = true)]
|
||||
public string GalaxyName { get; init; }
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
if (!SessionClient.TryConnect(GalaxyName, out var client))
|
||||
{
|
||||
await console.Output.WriteLineAsync("No active session found.").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
using (client)
|
||||
{
|
||||
var response = await client.SendShutdownAsync().ConfigureAwait(false);
|
||||
if (response.Success)
|
||||
await console.Output.WriteLineAsync("Session stopped.").ConfigureAwait(false);
|
||||
else
|
||||
await console.Error.WriteLineAsync($"Error stopping session: {response.Error}")
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Infrastructure
|
||||
{
|
||||
public sealed class CommandArgumentCapability
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = "string";
|
||||
public bool Required { get; set; }
|
||||
public IReadOnlyList<string> Values { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public sealed class CommandCapability
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Command { get; set; } = string.Empty;
|
||||
public string Subcommand { get; set; } = string.Empty;
|
||||
public bool Mutates { get; set; }
|
||||
public bool RequiresConfirm { get; set; }
|
||||
public string ConfirmTarget { get; set; } = string.Empty;
|
||||
public bool RoutesThroughSession { get; set; } = true;
|
||||
public bool SupportsJson { get; set; } = true;
|
||||
public bool SupportsLlmJson { get; set; } = true;
|
||||
public string OutputSchema { get; set; } = "Text";
|
||||
public IReadOnlyList<CommandArgumentCapability> Args { get; set; } = Array.Empty<CommandArgumentCapability>();
|
||||
}
|
||||
|
||||
public sealed class CommandValidationResult
|
||||
{
|
||||
public bool Valid => Errors.Count == 0;
|
||||
public List<string> Errors { get; } = new List<string>();
|
||||
public CommandCapability Capability { get; set; }
|
||||
}
|
||||
|
||||
public static class CommandCapabilityRegistry
|
||||
{
|
||||
private static readonly IReadOnlyList<CommandCapability> _commands = Build();
|
||||
|
||||
public static IReadOnlyList<CommandCapability> Commands => _commands;
|
||||
|
||||
public static CommandCapability Find(string name)
|
||||
{
|
||||
return _commands.FirstOrDefault(c => string.Equals(c.Name, NormalizeName(name), StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public static CommandCapability Find(string command, string subcommand)
|
||||
{
|
||||
command = NormalizeToken(command);
|
||||
subcommand = NormalizeToken(subcommand);
|
||||
return _commands.FirstOrDefault(c =>
|
||||
string.Equals(c.Command, command, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(c.Subcommand, subcommand, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public static CommandValidationResult Validate(string commandName, IDictionary<string, object> args, bool requireMutationConfirmation)
|
||||
{
|
||||
var result = new CommandValidationResult();
|
||||
var capability = Find(commandName);
|
||||
if (capability == null)
|
||||
{
|
||||
result.Errors.Add($"Unknown command '{commandName}'.");
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Capability = capability;
|
||||
args = args ?? new Dictionary<string, object>();
|
||||
|
||||
foreach (var arg in capability.Args.Where(a => a.Required))
|
||||
{
|
||||
if (!args.TryGetValue(arg.Name, out var value) || value == null || string.IsNullOrWhiteSpace(Convert.ToString(value)))
|
||||
result.Errors.Add($"Missing required argument '{arg.Name}'.");
|
||||
}
|
||||
|
||||
if (requireMutationConfirmation && capability.RequiresConfirm)
|
||||
{
|
||||
if (!ReadBool(args, "confirm"))
|
||||
result.Errors.Add("Missing required confirm=true.");
|
||||
|
||||
var expected = ExpectedConfirmTarget(capability, args);
|
||||
var actual = ReadString(args, "confirm-target");
|
||||
if (!string.IsNullOrWhiteSpace(expected) && !string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase))
|
||||
result.Errors.Add($"confirm-target must be '{expected}'.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string ExpectedConfirmTarget(CommandCapability capability, IDictionary<string, object> args)
|
||||
{
|
||||
if (capability == null || args == null)
|
||||
return string.Empty;
|
||||
|
||||
switch (capability.ConfirmTarget)
|
||||
{
|
||||
case "name": return ReadString(args, "name");
|
||||
case "galaxy": return ReadString(args, "galaxy");
|
||||
case "file": return ReadString(args, "file");
|
||||
case "path": return ReadString(args, "path");
|
||||
case "output": return ReadString(args, "output");
|
||||
case "default": return ReadString(args, "default");
|
||||
case "bulk":
|
||||
var names = ReadList(args, "name").ToList();
|
||||
if (names.Any()) return string.Join(",", names);
|
||||
return string.Join(",", ReadList(args, "pattern"));
|
||||
case "template":
|
||||
return ReadString(args, "template");
|
||||
default:
|
||||
return capability.ConfirmTarget ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public static (string Command, string Subcommand) SplitCommandName(string commandName)
|
||||
{
|
||||
var normalized = NormalizeName(commandName);
|
||||
var capability = Find(normalized);
|
||||
if (capability != null)
|
||||
return (capability.Command, capability.Subcommand);
|
||||
|
||||
var parts = normalized.Split(new[] { ' ' }, 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
return parts.Length == 1 ? (parts[0], string.Empty) : (parts[0], parts[1].Replace(' ', '-'));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CommandCapability> Build()
|
||||
{
|
||||
var list = new List<CommandCapability>();
|
||||
|
||||
void Add(string name, bool mutates = false, string confirmTarget = "", bool session = true, string schema = "Object", params CommandArgumentCapability[] args)
|
||||
{
|
||||
var split = SplitStatic(name);
|
||||
list.Add(new CommandCapability
|
||||
{
|
||||
Name = name,
|
||||
Command = split.Command,
|
||||
Subcommand = split.Subcommand,
|
||||
Mutates = mutates,
|
||||
RequiresConfirm = mutates,
|
||||
ConfirmTarget = confirmTarget,
|
||||
RoutesThroughSession = session,
|
||||
OutputSchema = schema,
|
||||
Args = args
|
||||
});
|
||||
}
|
||||
|
||||
var galaxy = Arg("galaxy", required: true);
|
||||
var node = Arg("node");
|
||||
var name = Arg("name", required: true);
|
||||
var type = Arg("type", "enum", false, "all", "template", "instance");
|
||||
var json = Arg("json", "bool");
|
||||
var llm = Arg("llm-json", "bool");
|
||||
var confirm = Arg("confirm", "bool");
|
||||
var confirmTarget = Arg("confirm-target");
|
||||
|
||||
Add("capabilities", session: false, schema: "CapabilityList", args: new[] { json, llm });
|
||||
Add("validate", session: false, schema: "ValidationResult", args: new[] { Arg("request", required: true), llm });
|
||||
Add("batch", session: true, schema: "BatchResult", args: new[] { Arg("file", required: true), Arg("mode"), llm });
|
||||
|
||||
Add("galaxy list", session: false, schema: "StringArray", args: new[] { node, json, llm });
|
||||
Add("galaxy info", args: new[] { galaxy, node, json, llm });
|
||||
Add("galaxy sync", args: new[] { galaxy, node, json, llm });
|
||||
Add("galaxy cdi-version", args: new[] { galaxy, node, json, llm });
|
||||
Add("galaxy defaults get", args: new[] { galaxy, node, Arg("default", required: true), json, llm });
|
||||
Add("galaxy defaults set", true, "default", args: new[] { galaxy, node, Arg("default", required: true), Arg("value", required: true), confirm, confirmTarget, llm });
|
||||
Add("galaxy backup", true, "file", args: new[] { galaxy, node, Arg("file", required: true), confirm, confirmTarget, llm });
|
||||
Add("galaxy restore", true, "file", args: new[] { galaxy, node, Arg("file", required: true), Arg("restore-older", "bool"), confirm, confirmTarget, llm });
|
||||
Add("galaxy migrate", true, "galaxy", args: new[] { galaxy, node, confirm, confirmTarget, llm });
|
||||
Add("galaxy import-objects", true, "file", args: new[] { galaxy, node, Arg("file", required: true), Arg("overwrite", "bool"), confirm, confirmTarget, llm });
|
||||
Add("galaxy import-objects-ex", true, "file", args: new[] { galaxy, node, Arg("file", required: true), Arg("version-conflict", required: true), Arg("name-conflict", required: true), Arg("append-name"), confirm, confirmTarget, llm });
|
||||
Add("galaxy import-script-library", true, "path", args: new[] { galaxy, node, Arg("path", required: true), confirm, confirmTarget, llm });
|
||||
Add("galaxy export-all", args: new[] { galaxy, node, Arg("output", required: true), Arg("export-type"), json, llm });
|
||||
Add("galaxy grload", true, "file", args: new[] { galaxy, node, Arg("file", required: true), Arg("mode"), confirm, confirmTarget, llm });
|
||||
Add("galaxy create", true, "galaxy", session: false, args: new[] { Arg("galaxy", required: true), node, confirm, confirmTarget });
|
||||
Add("galaxy create-from-template", true, "galaxy", session: false, args: new[] { Arg("galaxy", required: true), node, Arg("template", required: true), confirm, confirmTarget });
|
||||
Add("galaxy delete", true, "galaxy", session: false, args: new[] { Arg("galaxy", required: true), node, confirm, confirmTarget });
|
||||
|
||||
Add("object list", args: new[] { galaxy, node, type, Arg("pattern"), json, llm });
|
||||
Add("template list", args: new[] { galaxy, node, Arg("pattern"), json, llm });
|
||||
Add("instance list", args: new[] { galaxy, node, Arg("pattern"), json, llm });
|
||||
Add("object get", args: new[] { galaxy, node, name, type, json, llm });
|
||||
Add("object snapshot", schema: "ObjectSnapshot", args: new[] { galaxy, node, name, type, llm });
|
||||
Add("object lineage", schema: "ObjectLineage", args: new[] { galaxy, node, name, type, json, llm });
|
||||
Add("object children", schema: "ObjectChildren", args: new[] { galaxy, node, name, type, json, llm });
|
||||
Add("object query-name", args: new[] { galaxy, node, Arg("name", "array", true), type, json, llm });
|
||||
Add("object query-condition", args: new[] { galaxy, node, Arg("condition"), Arg("value"), type, json, llm });
|
||||
Add("object query-multi", args: new[] { galaxy, node, Arg("pattern", "array"), type, json, llm });
|
||||
Add("object attributes", args: new[] { galaxy, node, name, type, Arg("configurable", "bool"), json, llm });
|
||||
Add("object extended-attributes", args: new[] { galaxy, node, name, type, Arg("attribute"), Arg("level", "int"), json, llm });
|
||||
Add("object help-url", args: new[] { galaxy, node, name, type, json, llm });
|
||||
Add("object checkout", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("object checkin", true, "name", args: new[] { galaxy, node, name, type, Arg("comment"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("object undo-checkout", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("object save", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("object unload", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("object set", true, "name", args: new[] { galaxy, node, name, type, Arg("property", required: true), Arg("value", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
|
||||
Add("object attribute get", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), json, llm });
|
||||
Add("object attribute set", true, "name", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), Arg("value", required: true), Arg("data-type"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("object attribute value get", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), json, llm });
|
||||
Add("object attribute value set", true, "name", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), Arg("value", required: true), Arg("data-type"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("object attribute lock", true, "name", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), Arg("locked", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("object attribute security", true, "name", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), Arg("security", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("object attribute buffer", true, "name", args: new[] { galaxy, node, name, type, Arg("attribute", required: true), Arg("has-buffer", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
|
||||
Add("template derive", true, "name", args: new[] { galaxy, node, name, type, Arg("new-name", required: true), Arg("create-contained", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("template instantiate", true, "name", args: new[] { galaxy, node, name, type, Arg("new-name", required: true), Arg("create-contained", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("template delete", true, "name", args: new[] { galaxy, node, name, type, Arg("force-option"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("instance delete", true, "name", args: new[] { galaxy, node, name, type, Arg("force-option"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("instance deploy", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("instance undeploy", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("instance upload", true, "name", args: new[] { galaxy, node, name, type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("instance assign-area", true, "name", args: new[] { galaxy, node, name, Arg("area", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("instance assign-engine", true, "name", args: new[] { galaxy, node, name, Arg("engine", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("instance assign-container", true, "name", args: new[] { galaxy, node, name, Arg("container", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("io assign", true, "name", args: new[] { galaxy, node, name, Arg("attribute", required: true), Arg("value", required: true), Arg("data-type"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
|
||||
Add("objects checkout", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("objects checkin", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, Arg("comment"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("objects undo-checkout", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("objects deploy", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("objects undeploy", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("objects upload", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("objects delete", true, "bulk", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("objects export", true, "output", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, Arg("output", required: true), Arg("export-type"), confirm, confirmTarget, llm });
|
||||
Add("objects export-protected", true, "output", args: new[] { galaxy, node, Arg("name", "array"), Arg("pattern", "array"), type, Arg("output", required: true), confirm, confirmTarget, llm });
|
||||
|
||||
Add("object scripts list", args: new[] { galaxy, node, name, type, json, llm });
|
||||
Add("object scripts get", args: new[] { galaxy, node, name, type, Arg("script", required: true), json, llm });
|
||||
Add("object scripts set", true, "name", args: new[] { galaxy, node, name, type, Arg("script", required: true), Arg("file", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("object scripts create", true, "name", args: new[] { galaxy, node, name, type, Arg("script", required: true), Arg("file"), Arg("trigger-period-ms"), Arg("trigger-type"), Arg("expression"), Arg("lock-trigger-period", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("object scripts settings set", true, "name", args: new[] { galaxy, node, name, type, Arg("script", required: true), Arg("trigger-period-ms"), Arg("trigger-type"), Arg("expression"), Arg("lock-trigger-period", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
|
||||
Add("area list", args: new[] { galaxy, node, Arg("pattern"), json, llm });
|
||||
Add("area create", true, "template", args: new[] { galaxy, node, Arg("template", required: true), Arg("name", required: true), Arg("create-contained", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("engine list", args: new[] { galaxy, node, Arg("pattern"), json, llm });
|
||||
Add("engine create", true, "template", args: new[] { galaxy, node, Arg("template", required: true), Arg("name", required: true), Arg("create-contained", "bool"), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
|
||||
Add("toolset list", args: new[] { galaxy, node, json, llm });
|
||||
Add("toolset tree", args: new[] { galaxy, node, json, llm });
|
||||
Add("toolset add", true, "name", args: new[] { galaxy, node, name, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("toolset delete", true, "name", args: new[] { galaxy, node, name, confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("toolset rename", true, "name", args: new[] { galaxy, node, name, Arg("new-name", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("toolset move", true, "name", args: new[] { galaxy, node, name, Arg("parent", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("script-library list", args: new[] { galaxy, node, json, llm });
|
||||
Add("script-library add", true, "path", args: new[] { galaxy, node, Arg("path", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("script-library import", true, "path", args: new[] { galaxy, node, Arg("path", required: true), confirm, confirmTarget, llm, Arg("dry-run", "bool") });
|
||||
Add("script-library export", true, "output", args: new[] { galaxy, node, name, Arg("output", required: true), confirm, confirmTarget, llm });
|
||||
Add("security info", args: new[] { galaxy, node, json, llm });
|
||||
Add("security roles", args: new[] { galaxy, node, json, llm });
|
||||
Add("security users", args: new[] { galaxy, node, json, llm });
|
||||
Add("security groups", args: new[] { galaxy, node, json, llm });
|
||||
Add("security permissions", args: new[] { galaxy, node, Arg("role", required: true), json, llm });
|
||||
Add("settings locale get", args: new[] { galaxy, node, json, llm });
|
||||
Add("settings time-master get", args: new[] { galaxy, node, json, llm });
|
||||
|
||||
return list.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
}
|
||||
|
||||
private static CommandArgumentCapability Arg(string name, string type = "string", bool required = false, params string[] values)
|
||||
{
|
||||
return new CommandArgumentCapability { Name = name, Type = type, Required = required, Values = values };
|
||||
}
|
||||
|
||||
private static (string Command, string Subcommand) SplitStatic(string name)
|
||||
{
|
||||
var normalized = NormalizeName(name);
|
||||
var first = normalized.Split(' ')[0];
|
||||
return (first, normalized.Substring(first.Length).Trim().Replace(' ', '-'));
|
||||
}
|
||||
|
||||
private static string NormalizeName(string value)
|
||||
{
|
||||
return string.Join(" ", (value ?? string.Empty).Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
|
||||
private static string NormalizeToken(string value)
|
||||
{
|
||||
return (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static bool ReadBool(IDictionary<string, object> args, string key)
|
||||
{
|
||||
return args.TryGetValue(key, out var value) && value != null && (value is bool b ? b : bool.TryParse(Convert.ToString(value), out var parsed) && parsed);
|
||||
}
|
||||
|
||||
private static string ReadString(IDictionary<string, object> args, string key)
|
||||
{
|
||||
return args.TryGetValue(key, out var value) && value != null ? Convert.ToString(value) ?? string.Empty : string.Empty;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ReadList(IDictionary<string, object> args, string key)
|
||||
{
|
||||
if (!args.TryGetValue(key, out var value) || value == null)
|
||||
yield break;
|
||||
|
||||
if (value is string s)
|
||||
{
|
||||
foreach (var item in s.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
yield return item.Trim();
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (value is System.Collections.IEnumerable enumerable)
|
||||
{
|
||||
foreach (var item in enumerable)
|
||||
if (item != null)
|
||||
yield return Convert.ToString(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Infrastructure;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Protocol;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Session;
|
||||
using ArchestrA.GRAccess;
|
||||
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Infrastructure
|
||||
{
|
||||
public static class CommandRouter
|
||||
{
|
||||
/// <summary>
|
||||
/// Execute a command either via an active session (named pipe) or direct one-shot connection.
|
||||
/// </summary>
|
||||
/// <param name="console">CliFx console for output</param>
|
||||
/// <param name="galaxyName">Galaxy to connect to</param>
|
||||
/// <param name="nodeName">GR node (required for one-shot, ignored if session active)</param>
|
||||
/// <param name="command">Command name (e.g. "query")</param>
|
||||
/// <param name="subcommand">Subcommand name (e.g. "instances")</param>
|
||||
/// <param name="args">Command arguments</param>
|
||||
/// <param name="directExecute">Callback for one-shot execution against a live IGalaxy</param>
|
||||
/// <returns>Exit code</returns>
|
||||
public static async Task<int> ExecuteAsync(
|
||||
IConsole console,
|
||||
string galaxyName,
|
||||
string nodeName,
|
||||
string command,
|
||||
string subcommand,
|
||||
Dictionary<string, object> args,
|
||||
Func<IGalaxy, string> directExecute)
|
||||
{
|
||||
nodeName = nodeName ?? string.Empty;
|
||||
args["galaxy"] = galaxyName;
|
||||
args["node"] = nodeName;
|
||||
|
||||
// 1. Try active session
|
||||
if (SessionClient.TryConnect(galaxyName, out var client))
|
||||
{
|
||||
using (client)
|
||||
{
|
||||
var request = PipeRequest.Execute(command, subcommand, args);
|
||||
var response = await client.SendCommandAsync(request).ConfigureAwait(false);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
if (IsLlmJson(args))
|
||||
{
|
||||
await console.Output.WriteLineAsync(LlmResponse.Fail(
|
||||
DisplayCommand(command, subcommand),
|
||||
galaxyName,
|
||||
Target(args),
|
||||
new InvalidOperationException(response.Error),
|
||||
response.ExitCode)).ConfigureAwait(false);
|
||||
return response.ExitCode;
|
||||
}
|
||||
|
||||
throw new CommandException(response.Error, response.ExitCode);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(response.Output))
|
||||
await console.Output.WriteLineAsync(response.Output).ConfigureAwait(false);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. No session — one-shot mode (already on STA thread via [STAThread])
|
||||
if (string.IsNullOrEmpty(nodeName))
|
||||
{
|
||||
if (IsLlmJson(args))
|
||||
{
|
||||
await console.Output.WriteLineAsync(LlmResponse.Fail(
|
||||
DisplayCommand(command, subcommand),
|
||||
galaxyName,
|
||||
Target(args),
|
||||
new InvalidOperationException("No active session found. Provide --node for one-shot mode, or start a session first."),
|
||||
1)).ConfigureAwait(false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw new CommandException(
|
||||
"No active session found. Provide --node for one-shot mode, or start a session first.",
|
||||
1);
|
||||
}
|
||||
|
||||
using (var connection = new GRAccessConnection(galaxyName, nodeName))
|
||||
{
|
||||
try
|
||||
{
|
||||
connection.Connect();
|
||||
var result = directExecute(connection.Galaxy);
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
await console.Output.WriteLineAsync(result).ConfigureAwait(false);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (IsLlmJson(args))
|
||||
{
|
||||
await console.Output.WriteLineAsync(LlmResponse.Fail(DisplayCommand(command, subcommand), galaxyName, Target(args), ex, 1)).ConfigureAwait(false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw new CommandException($"Error: {ex.Message}", 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsLlmJson(Dictionary<string, object> args)
|
||||
{
|
||||
return args != null
|
||||
&& args.TryGetValue("llm-json", out var value)
|
||||
&& value != null
|
||||
&& (value is bool b ? b : bool.TryParse(Convert.ToString(value), out var parsed) && parsed);
|
||||
}
|
||||
|
||||
private static string DisplayCommand(string command, string subcommand)
|
||||
{
|
||||
return $"{command} {(subcommand ?? string.Empty).Replace('-', ' ')}".Trim();
|
||||
}
|
||||
|
||||
private static string Target(Dictionary<string, object> args)
|
||||
{
|
||||
if (args == null) return string.Empty;
|
||||
foreach (var key in new[] { "name", "output", "file", "path", "template", "galaxy", "pattern", "default" })
|
||||
if (args.TryGetValue(key, out var value) && value != null)
|
||||
return Convert.ToString(value) ?? string.Empty;
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Polyfill for 'init' accessor support on .NET Framework 4.8 (C# 9.0)
|
||||
// See: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/init
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
internal static class IsExternalInit { }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Session;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
[System.STAThread]
|
||||
static async Task<int> Main(string[] args)
|
||||
{
|
||||
// Hidden daemon mode: graccess.exe --daemon --galaxy X --node Y
|
||||
if (args.Length >= 1 && args[0] == "--daemon")
|
||||
return SessionDaemon.Run(args);
|
||||
|
||||
// Normal CLI mode. Machine JSON must remain parseable on stdout.
|
||||
var llmJson = args.Any(arg => arg == "--llm-json");
|
||||
Log.Logger = llmJson
|
||||
? new LoggerConfiguration().CreateLogger()
|
||||
: new LoggerConfiguration()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
|
||||
try
|
||||
{
|
||||
return await new CliApplicationBuilder()
|
||||
.SetTitle("GRAccess CLI")
|
||||
.SetDescription("Aveva System Platform Galaxy management")
|
||||
.AddCommandsFromThisAssembly()
|
||||
.Build()
|
||||
.RunAsync(args);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Protocol
|
||||
{
|
||||
public static class PipeProtocol
|
||||
{
|
||||
public const string PipePrefix = "graccess-session-";
|
||||
|
||||
public static string GetPipeName(string galaxyName)
|
||||
{
|
||||
return PipePrefix + galaxyName.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public static async Task WriteMessageAsync<T>(Stream stream, T message)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(message, Formatting.None);
|
||||
var bytes = Encoding.UTF8.GetBytes(json + "\n");
|
||||
await stream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
await stream.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task<T> ReadMessageAsync<T>(Stream stream)
|
||||
{
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8, false, 4096, leaveOpen: true))
|
||||
{
|
||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
if (line == null)
|
||||
return default;
|
||||
return JsonConvert.DeserializeObject<T>(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Protocol
|
||||
{
|
||||
public class PipeRequest
|
||||
{
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; } = "execute";
|
||||
|
||||
[JsonProperty("command")]
|
||||
public string Command { get; set; }
|
||||
|
||||
[JsonProperty("subcommand")]
|
||||
public string Subcommand { get; set; }
|
||||
|
||||
[JsonProperty("args")]
|
||||
public Dictionary<string, object> Args { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
public static PipeRequest Execute(string command, string subcommand, Dictionary<string, object> args = null)
|
||||
{
|
||||
return new PipeRequest
|
||||
{
|
||||
Type = "execute",
|
||||
Command = command,
|
||||
Subcommand = subcommand,
|
||||
Args = args ?? new Dictionary<string, object>()
|
||||
};
|
||||
}
|
||||
|
||||
public static PipeRequest Shutdown() => new PipeRequest { Type = "shutdown" };
|
||||
|
||||
public static PipeRequest Status() => new PipeRequest { Type = "status" };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Protocol
|
||||
{
|
||||
public class PipeResponse
|
||||
{
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
[JsonProperty("output")]
|
||||
public string Output { get; set; }
|
||||
|
||||
[JsonProperty("error")]
|
||||
public string Error { get; set; }
|
||||
|
||||
[JsonProperty("exitCode")]
|
||||
public int ExitCode { get; set; }
|
||||
|
||||
public static PipeResponse Ok(string output) => new PipeResponse
|
||||
{
|
||||
Success = true,
|
||||
Output = output,
|
||||
ExitCode = 0
|
||||
};
|
||||
|
||||
public static PipeResponse Fail(string error, int exitCode = 1) => new PipeResponse
|
||||
{
|
||||
Success = false,
|
||||
Error = error,
|
||||
ExitCode = exitCode
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.IO.Pipes;
|
||||
using System.Threading.Tasks;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Protocol;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Session
|
||||
{
|
||||
public sealed class SessionClient : IDisposable
|
||||
{
|
||||
private readonly string _pipeName;
|
||||
|
||||
private SessionClient(string pipeName)
|
||||
{
|
||||
_pipeName = pipeName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to connect to a running session for the given galaxy.
|
||||
/// Returns false if no session is available.
|
||||
/// </summary>
|
||||
public static bool TryConnect(string galaxyName, out SessionClient client)
|
||||
{
|
||||
client = null;
|
||||
|
||||
var info = SessionInfo.Load(galaxyName);
|
||||
if (info == null || !info.IsAlive())
|
||||
{
|
||||
// Clean up stale file
|
||||
info?.Delete();
|
||||
return false;
|
||||
}
|
||||
|
||||
client = new SessionClient(info.PipeName);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<PipeResponse> SendCommandAsync(PipeRequest request)
|
||||
{
|
||||
using (var pipe = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut))
|
||||
{
|
||||
pipe.Connect(5000);
|
||||
await PipeProtocol.WriteMessageAsync(pipe, request).ConfigureAwait(false);
|
||||
return await PipeProtocol.ReadMessageAsync<PipeResponse>(pipe).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<PipeResponse> SendShutdownAsync()
|
||||
{
|
||||
return SendCommandAsync(PipeRequest.Shutdown());
|
||||
}
|
||||
|
||||
public Task<PipeResponse> SendStatusAsync()
|
||||
{
|
||||
return SendCommandAsync(PipeRequest.Status());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// No persistent resources to clean up — each call opens/closes its own pipe
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
using System;
|
||||
using System.IO.Pipes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Protocol;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Session
|
||||
{
|
||||
public sealed class SessionDaemon : IDisposable
|
||||
{
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext<SessionDaemon>();
|
||||
|
||||
private readonly string _galaxyName;
|
||||
private readonly string _nodeName;
|
||||
private readonly string _pipeName;
|
||||
private readonly TimeSpan _idleTimeout;
|
||||
private readonly CancellationTokenSource _shutdownCts = new CancellationTokenSource();
|
||||
|
||||
private StaComThread _staThread;
|
||||
private GRAccessConnection _connection;
|
||||
private Mutex _instanceMutex;
|
||||
private DateTime _lastActivity;
|
||||
private Timer _idleTimer;
|
||||
|
||||
public SessionDaemon(string galaxyName, string nodeName, TimeSpan idleTimeout)
|
||||
{
|
||||
_galaxyName = galaxyName;
|
||||
_nodeName = nodeName;
|
||||
_pipeName = PipeProtocol.GetPipeName(galaxyName);
|
||||
_idleTimeout = idleTimeout;
|
||||
_lastActivity = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static entry point called from Program.Main when --daemon flag is present.
|
||||
/// </summary>
|
||||
public static int Run(string[] args)
|
||||
{
|
||||
string galaxyName = null;
|
||||
string nodeName = null;
|
||||
int idleTimeoutMinutes = 30;
|
||||
|
||||
for (int i = 1; i < args.Length; i++)
|
||||
{
|
||||
switch (args[i])
|
||||
{
|
||||
case "--galaxy" when i + 1 < args.Length:
|
||||
galaxyName = args[++i];
|
||||
break;
|
||||
case "--node" when i + 1 < args.Length:
|
||||
nodeName = args[++i];
|
||||
break;
|
||||
case "--idle-timeout" when i + 1 < args.Length:
|
||||
int.TryParse(args[++i], out idleTimeoutMinutes);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(galaxyName) || string.IsNullOrEmpty(nodeName))
|
||||
{
|
||||
Console.Error.WriteLine("--galaxy and --node are required for daemon mode.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Configure daemon logging
|
||||
Serilog.Log.Logger = new LoggerConfiguration()
|
||||
.WriteTo.File(
|
||||
System.IO.Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"ZB.MOM.WW.GRAccess.Cli", "logs", $"daemon-{galaxyName.ToLowerInvariant()}.log"),
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileCountLimit: 7)
|
||||
.CreateLogger();
|
||||
|
||||
try
|
||||
{
|
||||
using (var daemon = new SessionDaemon(galaxyName, nodeName,
|
||||
TimeSpan.FromMinutes(idleTimeoutMinutes)))
|
||||
{
|
||||
return daemon.Start();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Daemon terminated unexpectedly");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Serilog.Log.CloseAndFlush();
|
||||
}
|
||||
}
|
||||
|
||||
public int Start()
|
||||
{
|
||||
// 1. Acquire mutex
|
||||
var mutexName = $"Global\\graccess-session-{_galaxyName.ToLowerInvariant()}";
|
||||
_instanceMutex = new Mutex(true, mutexName, out bool createdNew);
|
||||
if (!createdNew)
|
||||
{
|
||||
Log.Warning("Another daemon is already running for galaxy {Galaxy}", _galaxyName);
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 2. Start STA thread
|
||||
_staThread = new StaComThread();
|
||||
_staThread.Start();
|
||||
|
||||
// 3. Connect to galaxy on STA thread
|
||||
_connection = new GRAccessConnection(_galaxyName, _nodeName);
|
||||
_staThread.RunAsync(() => _connection.Connect()).GetAwaiter().GetResult();
|
||||
|
||||
// 4. Write session info
|
||||
var sessionInfo = new SessionInfo
|
||||
{
|
||||
GalaxyName = _galaxyName,
|
||||
NodeName = _nodeName,
|
||||
PipeName = _pipeName,
|
||||
ProcessId = System.Diagnostics.Process.GetCurrentProcess().Id,
|
||||
StartedAtUtc = DateTime.UtcNow
|
||||
};
|
||||
sessionInfo.Save();
|
||||
|
||||
Log.Information("Daemon started for galaxy {Galaxy} on pipe {Pipe}", _galaxyName, _pipeName);
|
||||
|
||||
// 5. Start idle timer
|
||||
_idleTimer = new Timer(_ => CheckIdleTimeout(), null,
|
||||
TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
|
||||
|
||||
// 6. Accept connections (blocks until shutdown)
|
||||
AcceptConnectionsAsync(_shutdownCts.Token).GetAwaiter().GetResult();
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Daemon startup failed for galaxy {Galaxy} on node {Node}", _galaxyName, _nodeName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 7. Cleanup
|
||||
_idleTimer?.Dispose();
|
||||
|
||||
if (_connection != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_staThread.RunAsync(() => _connection.Disconnect()).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error disconnecting during shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
_staThread?.Dispose();
|
||||
|
||||
var info = SessionInfo.Load(_galaxyName);
|
||||
info?.Delete();
|
||||
|
||||
_instanceMutex.ReleaseMutex();
|
||||
_instanceMutex.Dispose();
|
||||
|
||||
Log.Information("Daemon stopped for galaxy {Galaxy}", _galaxyName);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AcceptConnectionsAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var server = new NamedPipeServerStream(
|
||||
_pipeName,
|
||||
PipeDirection.InOut,
|
||||
NamedPipeServerStream.MaxAllowedServerInstances,
|
||||
PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous))
|
||||
{
|
||||
var waitTask = Task.Factory.FromAsync(
|
||||
server.BeginWaitForConnection, server.EndWaitForConnection, null);
|
||||
|
||||
// Wait for connection or cancellation
|
||||
var completedTask = await Task.WhenAny(waitTask, Task.Delay(-1, ct))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (ct.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
await waitTask.ConfigureAwait(false);
|
||||
|
||||
_lastActivity = DateTime.UtcNow;
|
||||
await HandleClientAsync(server).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error accepting pipe connection");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleClientAsync(NamedPipeServerStream server)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = await PipeProtocol.ReadMessageAsync<PipeRequest>(server).ConfigureAwait(false);
|
||||
if (request == null)
|
||||
return;
|
||||
|
||||
PipeResponse response;
|
||||
|
||||
switch (request.Type)
|
||||
{
|
||||
case "shutdown":
|
||||
response = PipeResponse.Ok("Shutting down");
|
||||
await PipeProtocol.WriteMessageAsync(server, response).ConfigureAwait(false);
|
||||
_shutdownCts.Cancel();
|
||||
return;
|
||||
|
||||
case "status":
|
||||
var status = new
|
||||
{
|
||||
galaxy = _galaxyName,
|
||||
node = _nodeName,
|
||||
connected = _connection?.IsConnected ?? false,
|
||||
uptime = (DateTime.UtcNow - _lastActivity).ToString()
|
||||
};
|
||||
response = PipeResponse.Ok(JsonConvert.SerializeObject(status));
|
||||
break;
|
||||
|
||||
case "execute":
|
||||
response = await ExecuteCommandAsync(request).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
response = PipeResponse.Fail($"Unknown request type: {request.Type}");
|
||||
break;
|
||||
}
|
||||
|
||||
await PipeProtocol.WriteMessageAsync(server, response).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error handling client request");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<PipeResponse> ExecuteCommandAsync(PipeRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _staThread.RunAsync<PipeResponse>(() =>
|
||||
{
|
||||
var output = GRAccessCommandDispatcher.Execute(
|
||||
_connection.Galaxy,
|
||||
request.Command,
|
||||
request.Subcommand,
|
||||
request.Args);
|
||||
|
||||
return PipeResponse.Ok(output);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
return PipeResponse.Fail(ex.Message, 2);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return PipeResponse.Fail($"Execution error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckIdleTimeout()
|
||||
{
|
||||
if (DateTime.UtcNow - _lastActivity > _idleTimeout)
|
||||
{
|
||||
Log.Information("Idle timeout reached ({Timeout} min), shutting down",
|
||||
_idleTimeout.TotalMinutes);
|
||||
_shutdownCts.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_shutdownCts?.Cancel();
|
||||
_idleTimer?.Dispose();
|
||||
_shutdownCts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Session
|
||||
{
|
||||
public class SessionInfo
|
||||
{
|
||||
[JsonProperty("galaxyName")]
|
||||
public string GalaxyName { get; set; }
|
||||
|
||||
[JsonProperty("nodeName")]
|
||||
public string NodeName { get; set; }
|
||||
|
||||
[JsonProperty("pipeName")]
|
||||
public string PipeName { get; set; }
|
||||
|
||||
[JsonProperty("processId")]
|
||||
public int ProcessId { get; set; }
|
||||
|
||||
[JsonProperty("startedAtUtc")]
|
||||
public DateTime StartedAtUtc { get; set; }
|
||||
|
||||
private static string SessionsDir =>
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"ZB.MOM.WW.GRAccess.Cli",
|
||||
"sessions");
|
||||
|
||||
public static string GetSessionFilePath(string galaxyName)
|
||||
{
|
||||
return Path.Combine(SessionsDir, galaxyName.ToLowerInvariant() + ".json");
|
||||
}
|
||||
|
||||
public static SessionInfo Load(string galaxyName)
|
||||
{
|
||||
var path = GetSessionFilePath(galaxyName);
|
||||
if (!File.Exists(path))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
return JsonConvert.DeserializeObject<SessionInfo>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
var path = GetSessionFilePath(GalaxyName);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
|
||||
File.WriteAllText(path, json);
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
var path = GetSessionFilePath(GalaxyName);
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the daemon process is still running.
|
||||
/// </summary>
|
||||
public bool IsAlive()
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = Process.GetProcessById(ProcessId);
|
||||
return !process.HasExited;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Session
|
||||
{
|
||||
/// <summary>
|
||||
/// Dedicated STA thread with a raw Win32 message pump for COM interop.
|
||||
/// All GRAccess COM objects must be created and called on this thread.
|
||||
/// </summary>
|
||||
public sealed class StaComThread : IDisposable
|
||||
{
|
||||
private const uint WM_APP = 0x8000;
|
||||
private const uint PM_NOREMOVE = 0x0000;
|
||||
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext<StaComThread>();
|
||||
|
||||
private readonly Thread _thread;
|
||||
private readonly TaskCompletionSource<bool> _ready = new TaskCompletionSource<bool>();
|
||||
private readonly ConcurrentQueue<Action> _workItems = new ConcurrentQueue<Action>();
|
||||
private volatile uint _nativeThreadId;
|
||||
private bool _disposed;
|
||||
|
||||
public StaComThread()
|
||||
{
|
||||
_thread = new Thread(ThreadEntry)
|
||||
{
|
||||
Name = "GRAccess-STA",
|
||||
IsBackground = true
|
||||
};
|
||||
_thread.SetApartmentState(ApartmentState.STA);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the STA thread and waits until the message pump is running.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
_thread.Start();
|
||||
_ready.Task.GetAwaiter().GetResult();
|
||||
Log.Information("STA COM thread started (ThreadId={ThreadId})", _thread.ManagedThreadId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marshals a synchronous action onto the STA thread and returns a Task
|
||||
/// that completes when the action finishes.
|
||||
/// </summary>
|
||||
public Task RunAsync(Action action)
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(StaComThread));
|
||||
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
_workItems.Enqueue(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
});
|
||||
PostThreadMessage(_nativeThreadId, WM_APP, IntPtr.Zero, IntPtr.Zero);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marshals a synchronous function onto the STA thread and returns
|
||||
/// a Task<T> with the result.
|
||||
/// </summary>
|
||||
public Task<T> RunAsync<T>(Func<T> func)
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(StaComThread));
|
||||
|
||||
var tcs = new TaskCompletionSource<T>();
|
||||
_workItems.Enqueue(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
tcs.TrySetResult(func());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
});
|
||||
PostThreadMessage(_nativeThreadId, WM_APP, IntPtr.Zero, IntPtr.Zero);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
try
|
||||
{
|
||||
if (_nativeThreadId != 0)
|
||||
PostThreadMessage(_nativeThreadId, WM_APP + 1, IntPtr.Zero, IntPtr.Zero);
|
||||
_thread.Join(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error shutting down STA COM thread");
|
||||
}
|
||||
|
||||
Log.Information("STA COM thread stopped");
|
||||
}
|
||||
|
||||
private void ThreadEntry()
|
||||
{
|
||||
try
|
||||
{
|
||||
_nativeThreadId = GetCurrentThreadId();
|
||||
|
||||
// Force message queue creation by peeking
|
||||
MSG msg;
|
||||
PeekMessage(out msg, IntPtr.Zero, 0, 0, PM_NOREMOVE);
|
||||
|
||||
_ready.TrySetResult(true);
|
||||
|
||||
// Run the message loop — blocks until WM_QUIT
|
||||
while (GetMessage(out msg, IntPtr.Zero, 0, 0) > 0)
|
||||
{
|
||||
if (msg.message == WM_APP)
|
||||
{
|
||||
DrainQueue();
|
||||
}
|
||||
else if (msg.message == WM_APP + 1)
|
||||
{
|
||||
// Shutdown signal — drain remaining work then quit
|
||||
DrainQueue();
|
||||
PostQuitMessage(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
TranslateMessage(ref msg);
|
||||
DispatchMessage(ref msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "STA COM thread crashed");
|
||||
_ready.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainQueue()
|
||||
{
|
||||
while (_workItems.TryDequeue(out var workItem))
|
||||
{
|
||||
try
|
||||
{
|
||||
workItem();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Unhandled exception in STA work item");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Win32 PInvoke
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MSG
|
||||
{
|
||||
public IntPtr hwnd;
|
||||
public uint message;
|
||||
public IntPtr wParam;
|
||||
public IntPtr lParam;
|
||||
public uint time;
|
||||
public POINT pt;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct POINT
|
||||
{
|
||||
public int x;
|
||||
public int y;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool TranslateMessage(ref MSG lpMsg);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr DispatchMessage(ref MSG lpMsg);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool PostThreadMessage(uint idThread, uint Msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern void PostQuitMessage(int nExitCode);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern uint GetCurrentThreadId();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>ZB.MOM.WW.GRAccess.Cli</RootNamespace>
|
||||
<AssemblyName>ZB.MOM.WW.GRAccess.Cli</AssemblyName>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<Platforms>x86</Platforms>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliFx" Version="2.3.6" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
<Reference Include="ArchestrA.GRAccess">
|
||||
<HintPath>..\..\lib\ArchestrA.GRAccess.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user