/** * @author Martin Karkowski * @email m.karkowski@zema.de * @create date 2021-07-27 15:45:00 * @modify date 2021-07-27 15:45:00 * @desc [description] */ import { ArgumentParser } from "argparse"; import * as inquirer from "inquirer"; import "reflect-metadata"; import { getLayer, layerDefaultParameters, validLayers, } from "../communication/getLayer.nodejs"; import { getDispatcher } from "../dispatcher/getDispatcher"; import { dynamicSort } from "../helpers/arrayMethods"; import { objectToMap } from "../helpers/objectMethods"; import { convertPath } from "../helpers/path"; import { padString } from "../helpers/stringMethods"; import { getNopeLogger } from "../logger/index.browser"; import { LoggerLevels } from "../logger/nopeLogger"; import { ICommunicationBridge, INopeModuleDescription, INopeObserver, } from "../types/nope"; inquirer.registerPrompt("search-list", require("inquirer-search-list")); /** * Helper to parse JSON-Input. * * @author M.Karkowski * @return {*} */ async function getJsonInput() { let data: any = null; while (data == null) { data = ( await inquirer.prompt([ { type: "input", message: "Please enter valid JSON.", name: "data", }, ]) ).data; try { return JSON.parse(data); } catch (e) { data = null; } } return JSON.parse(data); } export async function interact( additionalArguments: { help: string; type: "string" | "number"; name: string | string; defaultValue?: any; }[] = [] ): Promise { const parser = new ArgumentParser({ // version: "1.0.0", add_help: true, description: "Command Line interface, to analyze the runtime.", }); for (const arg of additionalArguments) { parser.add_argument(arg.name, { help: arg.help, defaultValue: arg.defaultValue, type: arg.type, }); } parser.add_argument("-s", "--server", { help: "The Server to Use.", defaultValue: "localhost", type: "str", dest: "uri", }); parser.add_argument("-p", "--port", { help: "The Port the Connector", defaultValue: 7000, type: "int", dest: "port", }); parser.add_argument("-c", "--channel", { help: "The Communication Channel, which should be used. Possible Values are: " + // Display all Options: Object.getOwnPropertyNames(validLayers) .map((item) => '"' + item + '"') .join(", "), defaultValue: "event", type: "str", dest: "channel", }); parser.add_argument(["-l", "--log"], { help: 'Specify the Logger Level. Defaults to "info". Valid values are: ' + LoggerLevels.join(", "), defaultValue: "info", type: "str", dest: "log", }); const args = parser.parse_args(); if (!Object.getOwnPropertyNames(validLayers).includes(args.channel)) { console.error( "Invalid Channel. Please use the following values. " + Object.getOwnPropertyNames(validLayers) .map((item) => '"' + item + '"') .join(", ") ); return; } if (args.channel === "io-client") { args.params = "http://" + args.uri + ":" + args.port.toString(); } else { // Assign the Default Setting for the Channel. args.params = layerDefaultParameters[args.channel]; } // Define a Logger const logger = getNopeLogger("nope-cli-tool"); try { logger.info( "Connecting to http://" + args.uri + ":" + args.port.toString() ); const dispatcher = getDispatcher({ communicator: getLayer( args.channel, args.params, args.log ) as ICommunicationBridge, logger, id: "nope-cli-tool", }); await dispatcher.ready.waitFor((value) => value); logger.info("Connected to http://" + args.uri + ":" + args.port.toString()); // Now we wait for the User to Select an Action: let exit = false; const choices = [ { name: "inspect - Operations to inspect the NoPE-System", value: "inspect", type: "menu", items: [ { name: "instances - Show available Instances", value: "showInstances", }, { name: "types - Show available Types", value: "showTypes", }, { name: "services - Show available Services", value: "showServices", }, { name: "hosts - Show available Hosts", value: "showHosts", }, { name: "events - Show subscribed Events", value: "showSubscribedEvents", }, { name: "events - Show published Events", value: "showPublishEvents", }, { name: "data-hooks - Show subscribed data listeners", value: "showSubscribedProps", }, { name: "data-changes - Show published data emitters", value: "showPublishProps", }, { name: "back - Back to the main menu", value: "back", }, { name: "exit - Exit the programm", value: "exit", }, ], }, { name: "execute - Operations to execute a service of the NoPE-System", value: "execute", items: [ { name: "repl - switch to console", value: "repl", }, { name: "execute-service - Execute a service", value: "execute-service", }, { name: "execute-instance - Execute a service on a instance", value: "execute-instance", }, { name: "back - Back to the main menu", value: "back", }, { name: "exit - Exit the programm", value: "exit", }, ], }, { name: "data - Operations to work with data of the NoPE-System", value: "data", items: [ { name: "show-data - show current data", value: "show-data", }, { name: "set-available-data - manipulate data for a given path.", value: "set-available-data", }, { name: "set-data - manipulate data manually", value: "set-data", }, { name: "back - Back to the main menu", value: "back", }, { name: "exit - Exit the programm", value: "exit", }, ], }, { name: "exit - Exit the programm", value: "exit", }, ]; let question = choices; let subscriptions: { [index: string]: INopeObserver } = {}; while (!exit) { const result = ( await inquirer.prompt([ { type: "search-list", message: "Select the operation to perform", name: "option", choices: question, }, ]) ).option; switch (result) { case "showInstances": // Show the Instances if ( dispatcher.instanceManager.instances.data.getContent().length > 0 ) { console.log("The following instances are available:"); const length = dispatcher.instanceManager.instances.data.getContent().length; for (const [ idx, instance, ] of dispatcher.instanceManager.instances.data .getContent() .sort(dynamicSort("identifier")) .entries()) { console.log( ` ${padString(idx + 1, length + 1, true)}.\t${ instance.identifier }<${instance.type}>` ); } } else { console.log("No instances has been found."); } break; case "showHosts": // Show the hosts: console.log("The following host has been found:"); for (const host of dispatcher.connectivityManager .getAllHosts() .sort()) { console.log(" *\t", host); } break; case "showTypes": // Filter the hosts: if ( dispatcher.instanceManager.instances.data.getContent().length > 0 ) { const types = new Set(); const typesAmount: { [index: string]: number } = {}; dispatcher.instanceManager.instances.data .getContent() .map((item) => { types.add(item.type); typesAmount[item.type] = (typesAmount[item.type] || 0) + 1; }); console.log("The following types has been found:"); for (const [idx, t] of [...types].sort().entries()) { console.log( ` ${padString(idx + 1, types.size + 1, true)}.\t<${t}>:${ typesAmount[t] }` ); } } else { console.log("No instance available"); } break; case "showSubscribedEvents": // Filter the hosts: if (dispatcher.eventDistributor.emitters.subscribers.length > 0) { console.log("The following events are susbcribed:"); const subscribers = dispatcher.eventDistributor.emitters.subscribers; const length = subscribers.length; for (const [idx, events] of subscribers.sort().entries()) { console.log( ` ${padString(idx + 1, length + 1, true)}.\t${events}` ); } } else { console.log("No event has been subscribed has been subscribed."); } break; case "showPublishEvents": // Filter the hosts: if (dispatcher.eventDistributor.emitters.publishers.length > 0) { console.log("The following event emitters are registered:"); const publishers = dispatcher.eventDistributor.emitters.publishers; const length = publishers.length; for (const [idx, events] of publishers.sort().entries()) { console.log( ` ${padString(idx + 1, length + 1, true)}.\t${events}` ); } } else { console.log("No known event emitter are registered."); } break; case "showSubscribedProps": // Filter the hosts: if (dispatcher.dataDistributor.emitters.subscribers.length > 0) { console.log("The following events are susbcribed:"); const subscribers = dispatcher.eventDistributor.emitters.subscribers; const length = subscribers.length; for (const [idx, events] of subscribers.sort().entries()) { console.log( ` ${padString(idx + 1, length + 1, true)}.\t${events}` ); } } else { console.log("No known data-hooks are known."); } break; case "showPublishProps": // Filter the hosts: if (dispatcher.dataDistributor.emitters.publishers.length > 0) { console.log("The following event emitters are registered:"); const publishers = dispatcher.eventDistributor.emitters.publishers; const length = publishers.length; for (const [idx, events] of publishers.sort().entries()) { console.log( ` ${padString(idx + 1, length + 1, true)}.\t${events}` ); } } else { console.log("No known publishing properties are registered."); } break; case "showServices": if (dispatcher.rpcManager.services.data.getContent().length > 0) { console.log("The following services are available:"); const length = dispatcher.rpcManager.services.data.getContent().length; for (const [idx, service] of dispatcher.rpcManager.services.data .getContent() .sort() .entries()) { console.log( ` ${padString(idx + 1, length + 1, true)}.\t${service}` ); } } else { console.log("No services are available."); } break; case "execute-service": if (dispatcher.rpcManager.services.data.getContent().length > 0) { const service = ( await inquirer.prompt([ { type: "search-list", message: "Select the operation service to perform", name: "service", choices: dispatcher.rpcManager.services.data .getContent() .sort(), }, ]) ).service; await dispatcher.rpcManager.performCall(service, [], { timeout: 5000, }); } else { console.log("No services are available."); } break; case "execute-instance": if ( dispatcher.instanceManager.instances.data.getContent().length > 0 ) { const instanceName = ( await inquirer.prompt([ { type: "search-list", message: "Select the instance", name: "instance", choices: dispatcher.instanceManager.instances.data .getContent() .sort(dynamicSort("identifier")) .map((item) => { return { name: item.identifier + "<" + item.type + ">", value: item.identifier, }; }), }, ]) ).instance; const instanceDescription = dispatcher.instanceManager.getInstanceDescription( instanceName ) as INopeModuleDescription; const service = ( await inquirer.prompt([ { type: "search-list", message: "Select the instance", name: "service", choices: Object.keys(instanceDescription.functions).sort(), }, ]) ).service; try { // No Parameters are required logger.info("Generating Accessor"); const instance = await dispatcher.instanceManager.createInstance({ identifier: instanceDescription.identifier, type: instanceDescription.type, params: [], }); logger.info("Accessor Generated"); // Now we know, which service we are trying to call. if ( instanceDescription.functions[service].schema?.inputs?.length == 0 ) { try { // Now we execute the service const serviceResult = await instance[service](); logger.info("executed: ", instanceName + "." + service); logger.info("result=", serviceResult); } catch (e) { logger.error( "Failed to execute ", instanceName + "." + service ); logger.error(e); } } else { console.log("parameters are required"); } } catch (e) { logger.error("Failed to create an accessor"); } } else { console.log("No instance are available."); } break; case "show-data": { const data = dispatcher.dataDistributor.pullData("", {}); const pathes = objectToMap(data); // Filter the hosts: if (pathes.size > 0) { // Show all Elements / Properties const dataPath = ( await inquirer.prompt([ { type: "search-list", message: "Select the data to show.", name: "dataPath", choices: Array.from(pathes.keys()).sort(), }, ]) ).dataPath; // Now render the content logger.info("content=", pathes.get(dataPath)); } else { logger.warn("No data present."); } break; } case "set-available-data": { const data = dispatcher.dataDistributor.pullData("", {}); const pathes = objectToMap(data); if (pathes.size > 0) { // Show all Elements / Properties const dataPath = ( await inquirer.prompt([ { type: "search-list", message: "Select the data to show.", name: "dataPath", choices: Array.from(pathes.keys()).sort(), }, ]) ).dataPath; const data = await getJsonInput(); try { // Dont gets an update. await dispatcher.dataDistributor.pushData(dataPath, data, { sender: "nope-cli", }); } catch (e) { logger.warn("failed to update the data"); logger.error(e); } } else { logger.warn("No Preset data available use 'set-data'"); } break; } case "set-data": { // Show all Elements / Properties let dataPath = ( await inquirer.prompt([ { type: "input", name: "dataPath", message: "Enter the data path. Please use '/' as seperator or valid js-notation", }, ]) ).dataPath; dataPath = convertPath(dataPath); const data = await getJsonInput(); try { // Dont gets an update. dispatcher.dataDistributor.patternBasedPush(dataPath, data, { sender: "nope-cli", }); } catch (e) { logger.warn("failed to update the data"); logger.error(e); } break; } case "exit": exit = true; break; case "back": question = choices; break; default: for (const choice of choices) { if (choice.value === result) { question = choice.items || [ { name: "back - Back to the main menu", value: "back", }, ]; } } break; } } process.exit(1); } catch (e) { logger.error("Something went wrong"); logger.error(e); } } // If requested As Main => Perform the Operation. if (require.main === module) { interact(); }