nope/lib/cli/cli.ts

637 lines
19 KiB
TypeScript

/**
* @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<void> {
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<string>();
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();
}