using CliFx.Attributes; using CliFx.Exceptions; using CliFx.Infrastructure; using Opc.Ua; using ZB.MOM.WW.OtOpcUa.Client.CLI.Helpers; using ZB.MOM.WW.OtOpcUa.Client.Shared; namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Commands; [Command("browse", Description = "Browse the OPC UA address space")] public class BrowseCommand : CommandBase { /// /// Creates the browse command used to inspect the server address space from the terminal. /// /// The factory that creates the shared client service for the command run. public BrowseCommand(IOpcUaClientServiceFactory factory) : base(factory) { } /// /// Gets the optional starting node for the browse, defaulting to the Objects folder. /// [CommandOption("node", 'n', Description = "Node ID to browse (default: Objects folder)")] public string? NodeId { get; init; } /// /// Gets the maximum browse depth the command should traverse. /// [CommandOption("depth", 'd', Description = "Maximum browse depth")] public int Depth { get; init; } = 1; /// /// Gets a value indicating whether child nodes should be traversed recursively. /// [CommandOption("recursive", 'r', Description = "Browse recursively (uses --depth as max depth)")] public bool Recursive { get; init; } /// public override async ValueTask ExecuteAsync(IConsole console) { ConfigureLogging(); if (Depth <= 0) throw new CommandException($"--depth must be greater than 0 (was {Depth})."); NodeId? startNode; try { startNode = NodeIdParser.Parse(NodeId); } catch (Exception ex) when (ex is FormatException or ArgumentException) { throw new CommandException($"Invalid --node value: {ex.Message}"); } IOpcUaClientService? service = null; try { var ct = console.RegisterCancellationHandler(); (service, _) = await CreateServiceAndConnectAsync(ct); var maxDepth = Recursive ? Depth : 1; await BrowseNodeAsync(service, console, startNode, maxDepth, 0, ct); } finally { if (service != null) { await service.DisconnectAsync(); service.Dispose(); } } } private static async Task BrowseNodeAsync( IOpcUaClientService service, IConsole console, NodeId? nodeId, int maxDepth, int currentDepth, CancellationToken ct) { var indent = new string(' ', currentDepth * 2); var results = await service.BrowseAsync(nodeId, ct); foreach (var result in results) { var marker = result.NodeClass switch { "Object" => "[Object]", "Variable" => "[Variable]", "Method" => "[Method]", _ => $"[{result.NodeClass}]" }; await console.Output.WriteLineAsync( $"{indent}{marker} {result.DisplayName} (NodeId: {result.NodeId})"); if (currentDepth + 1 < maxDepth && result.HasChildren) { var childNodeId = NodeIdParser.Parse(result.NodeId); await BrowseNodeAsync(service, console, childNodeId, maxDepth, currentDepth + 1, ct); } } } }